Agent Deployment

Deploying a Wazuh Agent DaemonSet

Deploy a real Wazuh agent into Kubernetes using a DaemonSet pattern, write the agent configuration with init containers, connect it to the Wazuh manager services, validate enrollment, and document monitoring coverage.

Domain alignment

Kubernetes Security SIEM Operations Platform Operations

What this lesson covers

  • Why the DaemonSet model is a good first Kubernetes agent lab
  • Why the agent needs a real ossec.conf, not just environment variables
  • How the agent reaches the Wazuh manager services inside the cluster
  • How to create an agent namespace
  • How to copy the Wazuh enrollment password into the agent namespace
  • Where to create the DaemonSet manifest file
  • How to write agent configuration through init containers
  • How to validate scheduling, startup, configuration, and enrollment
  • How to verify the agent in the dashboard
  • How to document monitoring coverage and blind spots
Lab note: Wazuh supports Kubernetes agent deployment patterns such as DaemonSet and sidecar. This first lab uses the DaemonSet model because it is the clearest way to monitor cluster nodes in a small Kubernetes SOC lab.

Step 1: Confirm the Wazuh platform is healthy

Do not deploy agents until the Wazuh manager and indexer are healthy. Agent enrollment depends on the Wazuh manager, and alert flow depends on the manager and indexer working together.

kubectl get pods -n wazuh
kubectl get svc -n wazuh
kubectl get pvc -n wazuh

You want the indexer, manager master, manager worker, and dashboard pods to be Running and Ready, and the PVCs to be Bound.

Do not continue yet: If a Wazuh manager pod is in CrashLoopBackOff, or if kubectl describe pod shows Reason: OOMKilled with Exit Code: 137, go back to the troubleshooting lesson and fix manager stability before deploying agents.

Step 2: Identify the manager services the agent will use

kubectl get svc -n wazuh

In this lab, the agent uses two in-cluster Wazuh service paths:

  • wazuh.wazuh.svc.cluster.local for registration/enrollment on port 1515.
  • wazuh-workers.wazuh.svc.cluster.local for agent event traffic on port 1514.
Local lab note: Services may show EXTERNAL-IP <pending> in a local cluster. That does not block in-cluster communication. Pods can still use Kubernetes DNS service names.

Step 3: Decide the in-cluster manager addresses

This step defines values that will be written into the agent's ossec.conf file later in Step 6. You are not setting these values globally in Kubernetes, and you are not running a command yet. You are deciding what the DaemonSet manifest will write into the agent configuration.

Agent event manager:
wazuh-workers.wazuh.svc.cluster.local

Agent enrollment / registration manager:
wazuh.wazuh.svc.cluster.local

In the DaemonSet manifest, these appear in the write-ossec-config init container:

- name: WAZUH_MANAGER
  value: "wazuh-workers.wazuh.svc.cluster.local"

- name: WAZUH_REGISTRATION_SERVER
  value: "wazuh.wazuh.svc.cluster.local"

The init container then writes those values into /var/ossec/etc/ossec.conf before the main Wazuh agent container starts.

Important correction: Setting WAZUH_MANAGER as an environment variable by itself may not be enough. If the agent starts with an empty <address> in ossec.conf, the logs may show Invalid server address found: '' and No client configured. This lab writes the config file explicitly to avoid that problem.

Optional DNS test

Before deploying the agent, you can confirm Kubernetes DNS can resolve the Wazuh manager service names.

kubectl create namespace wazuh-agents --dry-run=client -o yaml | kubectl apply -f -

kubectl run dns-test -n wazuh-agents --image=busybox:latest --restart=Never -- sleep 3600
kubectl exec -n wazuh-agents dns-test -- nslookup wazuh.wazuh.svc.cluster.local
kubectl exec -n wazuh-agents dns-test -- nslookup wazuh-workers.wazuh.svc.cluster.local
kubectl delete pod -n wazuh-agents dns-test

Step 4: Create an agent namespace

Keep the agent deployment separate from the Wazuh platform namespace. This makes troubleshooting and cleanup easier.

kubectl create namespace wazuh-agents --dry-run=client -o yaml | kubectl apply -f -

Confirm the namespace exists:

kubectl get namespace wazuh-agents

Step 5: Copy the enrollment password secret

The Wazuh Kubernetes deployment creates a secret named wazuh-authd-pass in the wazuh namespace. The agent DaemonSet runs in wazuh-agents, so it needs access to the same enrollment password in its own namespace.

kubectl get secret -n wazuh wazuh-authd-pass

Use PowerShell to copy the secret value into the agent namespace:

$EncodedAuthdPass = kubectl get secret wazuh-authd-pass -n wazuh -o jsonpath="{.data.authd\.pass}"
$AuthdPass = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($EncodedAuthdPass))

kubectl create secret generic wazuh-authd-pass -n wazuh-agents --from-literal=authd.pass=$AuthdPass --dry-run=client -o yaml | kubectl apply -f -

Confirm the secret exists in the agent namespace:

kubectl get secret -n wazuh-agents wazuh-authd-pass
Expected warning: If you previously created this secret without kubectl apply, Kubernetes may warn that the secret is missing the last-applied-configuration annotation. In this lab, that warning is not a failure. Kubernetes patches the annotation and continues.

Step 6: Create the Wazuh agent DaemonSet manifest

Create the manifest inside your Wazuh Kubernetes working directory so the lab files stay together.

cd C:\k8s\wazuh-kubernetes
New-Item -ItemType Directory -Force .\lab-manifests
notepad .\lab-manifests\wazuh-agent-daemonset.yaml

In Notepad, paste the full manifest below and save the file.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: wazuh-agent
  namespace: wazuh-agents
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: wazuh-agent
  namespace: wazuh-agents
  labels:
    app: wazuh-agent
spec:
  selector:
    matchLabels:
      app: wazuh-agent
  template:
    metadata:
      labels:
        app: wazuh-agent
    spec:
      serviceAccountName: wazuh-agent
      terminationGracePeriodSeconds: 20
      tolerations:
        - operator: Exists

      initContainers:
        - name: cleanup-ossec-stale
          image: busybox:1.36
          imagePullPolicy: IfNotPresent
          command: ["/bin/sh", "-lc"]
          args:
            - |
              set -e
              echo "[init] Cleaning stale Wazuh agent pid and lock files..."
              mkdir -p /agent/var/run /agent/queue/ossec
              rm -f /agent/var/run/*.pid || true
              rm -f /agent/queue/ossec/*.lock || true
          volumeMounts:
            - name: ossec-data
              mountPath: /agent

        - name: seed-ossec-tree
          image: wazuh/wazuh-agent:4.14.5
          imagePullPolicy: IfNotPresent
          command: ["/bin/sh", "-lc"]
          args:
            - |
              set -e
              echo "[init] Checking whether /var/ossec needs to be seeded..."
              if [ ! -d /agent/bin ]; then
                echo "[init] Seeding /var/ossec into the agent hostPath..."
                tar -C /var/ossec -cf - . | tar -C /agent -xpf -
              else
                echo "[init] Existing Wazuh agent data found. Skipping seed."
              fi
          volumeMounts:
            - name: ossec-data
              mountPath: /agent

        - name: fix-permissions
          image: busybox:1.36
          imagePullPolicy: IfNotPresent
          command: ["/bin/sh", "-lc"]
          args:
            - |
              set -e
              echo "[init] Fixing Wazuh agent file permissions..."
              for d in etc logs queue var rids tmp active-response; do
                [ -d "/agent/$d" ] && chown -R 999:999 "/agent/$d"
              done
              chown -R 0:0 /agent/bin /agent/lib || true
              find /agent/bin -type f -exec chmod 0755 {} \; || true
          volumeMounts:
            - name: ossec-data
              mountPath: /agent

        - name: write-ossec-config
          image: busybox:1.36
          imagePullPolicy: IfNotPresent
          env:
            - name: WAZUH_MANAGER
              value: "wazuh-workers.wazuh.svc.cluster.local"
            - name: WAZUH_PORT
              value: "1514"
            - name: WAZUH_PROTOCOL
              value: "tcp"
            - name: WAZUH_REGISTRATION_SERVER
              value: "wazuh.wazuh.svc.cluster.local"
            - name: WAZUH_REGISTRATION_PORT
              value: "1515"
            - name: NODE_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
          command: ["/bin/sh", "-lc"]
          args:
            - |
              set -e
              echo "[init] Writing Wazuh agent ossec.conf..."
              mkdir -p /agent/etc
              cat > /agent/etc/ossec.conf <<'AGENTCONF'
              <ossec_config>
                <client>
                  <server>
                    <address>${WAZUH_MANAGER}</address>
                    <port>${WAZUH_PORT}</port>
                    <protocol>${WAZUH_PROTOCOL}</protocol>
                  </server>
                  <enrollment>
                    <enabled>yes</enabled>
                    <agent_name>${NODE_NAME}</agent_name>
                    <manager_address>${WAZUH_REGISTRATION_SERVER}</manager_address>
                    <port>${WAZUH_REGISTRATION_PORT}</port>
                    <authorization_pass_path>/var/ossec/etc/authd.pass</authorization_pass_path>
                  </enrollment>
                </client>
              </ossec_config>
AGENTCONF
              sed -i "s|\${WAZUH_MANAGER}|${WAZUH_MANAGER}|g" /agent/etc/ossec.conf
              sed -i "s|\${WAZUH_PORT}|${WAZUH_PORT}|g" /agent/etc/ossec.conf
              sed -i "s|\${WAZUH_PROTOCOL}|${WAZUH_PROTOCOL}|g" /agent/etc/ossec.conf
              sed -i "s|\${NODE_NAME}|${NODE_NAME}|g" /agent/etc/ossec.conf
              sed -i "s|\${WAZUH_REGISTRATION_SERVER}|${WAZUH_REGISTRATION_SERVER}|g" /agent/etc/ossec.conf
              sed -i "s|\${WAZUH_REGISTRATION_PORT}|${WAZUH_REGISTRATION_PORT}|g" /agent/etc/ossec.conf
              chown 999:999 /agent/etc/ossec.conf
              chmod 0640 /agent/etc/ossec.conf
              echo "[init] ossec.conf created:"
              cat /agent/etc/ossec.conf
          volumeMounts:
            - name: ossec-data
              mountPath: /agent

        - name: copy-authd-pass
          image: busybox:1.36
          imagePullPolicy: IfNotPresent
          command: ["/bin/sh", "-lc"]
          args:
            - |
              set -e
              echo "[init] Copying authd.pass from Kubernetes Secret..."
              mkdir -p /agent/etc
              cp /secret/authd.pass /agent/etc/authd.pass
              chown 0:999 /agent/etc/authd.pass
              chmod 0640 /agent/etc/authd.pass
              ls -l /agent/etc/authd.pass
          volumeMounts:
            - name: ossec-data
              mountPath: /agent
            - name: wazuh-authd-pass
              mountPath: /secret/authd.pass
              subPath: authd.pass
              readOnly: true

      containers:
        - name: wazuh-agent
          image: wazuh/wazuh-agent:4.14.5
          imagePullPolicy: IfNotPresent
          command: ["/bin/sh", "-lc"]
          args:
            - |
              set -e
              ln -sf /var/ossec/etc/ossec.conf /etc/ossec.conf || true
              exec /init
          env:
            - name: WAZUH_MANAGER
              value: "wazuh-workers.wazuh.svc.cluster.local"
            - name: NODE_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
          securityContext:
            runAsUser: 0
            allowPrivilegeEscalation: true
            capabilities:
              add:
                - SETGID
                - SETUID
          volumeMounts:
            - name: varlog
              mountPath: /var/log
              readOnly: true
            - name: ossec-data
              mountPath: /var/ossec

      volumes:
        - name: varlog
          hostPath:
            path: /var/log
            type: Directory
        - name: ossec-data
          hostPath:
            path: /var/lib/wazuh-agent
            type: DirectoryOrCreate
        - name: wazuh-authd-pass
          secret:
            secretName: wazuh-authd-pass
Best-practice note: This lab manifest is intentionally limited and practical for a local SOC lab. It uses a DaemonSet, a dedicated namespace, a Kubernetes Secret, read-only access to /var/log, and a local hostPath for Wazuh agent state. A production deployment requires deeper review of privileges, hostPath mounts, update strategy, node selectors, tolerations, resource limits, RBAC, network policy, and secret management.

Confirm the file exists

Get-Item .\lab-manifests\wazuh-agent-daemonset.yaml

Validate the manifest before applying it

kubectl apply --dry-run=client -f .\lab-manifests\wazuh-agent-daemonset.yaml

Step 7: Apply the DaemonSet

Apply the manifest from the same directory where you created it.

kubectl apply -f .\lab-manifests\wazuh-agent-daemonset.yaml

Step 8: Verify the DaemonSet exists

kubectl get daemonset -n wazuh-agents
kubectl get pods -n wazuh-agents -o wide

In a single-node lab, expect one Wazuh agent pod. In a multi-node lab, expect one agent pod per schedulable node.

Step 9: Watch the agent logs

First capture the agent pod name into a PowerShell variable:

$AgentPod = kubectl get pods -n wazuh-agents -l app=wazuh-agent -o jsonpath="{.items[0].metadata.name}"

Check the init container that writes ossec.conf:

kubectl logs -n wazuh-agents $AgentPod -c write-ossec-config

You should see the generated manager values:

<address>wazuh-workers.wazuh.svc.cluster.local</address>
<manager_address>wazuh.wazuh.svc.cluster.local</manager_address>

Then check the main agent logs:

kubectl logs -n wazuh-agents -l app=wazuh-agent --tail=100

The agent should no longer fail with an empty manager address.

Step 10: Troubleshoot agent startup

Agent says Invalid server address found: ''

This means the agent started without a valid manager address in ossec.conf.

wazuh-agentd: ERROR: (4112): Invalid server address found: ''
wazuh-agentd: ERROR: (1215): No client configured. Exiting.

Fix this by confirming the write-ossec-config init container exists and wrote the manager address into the config file.

$AgentPod = kubectl get pods -n wazuh-agents -l app=wazuh-agent -o jsonpath="{.items[0].metadata.name}"
kubectl logs -n wazuh-agents $AgentPod -c write-ossec-config

Image pull failure

kubectl describe pod -n wazuh-agents <agent-pod-name>

Confirm the image tag exists and the node can pull from the registry.

Secret error

kubectl get secret -n wazuh-agents wazuh-authd-pass
kubectl describe pod -n wazuh-agents <agent-pod-name>

If the secret or key name is wrong, the pod may fail during the copy-authd-pass init container.

Manager connection or DNS error

kubectl run dns-test -n wazuh-agents --image=busybox:latest --restart=Never -- sleep 3600
kubectl exec -n wazuh-agents dns-test -- nslookup wazuh.wazuh.svc.cluster.local
kubectl exec -n wazuh-agents dns-test -- nslookup wazuh-workers.wazuh.svc.cluster.local
kubectl delete pod -n wazuh-agents dns-test

If DNS fails, troubleshoot cluster DNS or service naming before troubleshooting Wazuh enrollment.

Step 11: Verify the agent in the Wazuh dashboard

Open the Wazuh dashboard and navigate to agent management or the agent summary view. Confirm the Kubernetes node agent appears.

Expected evidence:
- Agent visible in Wazuh
- Agent name matches the Kubernetes node name or configured agent name
- Agent status is active
- Agent group is kubernetes, if configured
- Recent keepalive or event timestamp is present

Step 12: Generate a safe node-level test event

Generate a simple log event on the node through a lab pod. Keep this benign and documented.

kubectl run soc-agent-test -n wazuh-agents --image=busybox:latest --restart=Never -- sh -c "echo Wazuh agent DaemonSet lab test && sleep 30"

kubectl logs -n wazuh-agents soc-agent-test

kubectl delete pod -n wazuh-agents soc-agent-test

Then search the Wazuh dashboard around the timestamp of the test. Depending on agent configuration and collected log paths, this may show directly as an event or may support timing correlation.

Step 13: Document monitoring coverage

Agent deployment model:
Namespace:
DaemonSet name:
Agent image/tag:
Manager event service address:
Manager registration service address:
Nodes covered:
Nodes not covered:
Host paths mounted:
Secrets used:
Agent visible in dashboard:
Known blind spots:
Next tuning action:
Bottom line: Deploying an agent is not complete until you can prove it is running, configured, enrolled, visible in Wazuh, and documented with clear coverage boundaries.

Cleanup note

If you need to remove the lab agent deployment, delete the DaemonSet and then decide whether to keep or remove the namespace and secret.

kubectl delete daemonset wazuh-agent -n wazuh-agents
kubectl delete serviceaccount wazuh-agent -n wazuh-agents

To fully remove the agent lab namespace:

kubectl delete namespace wazuh-agents

Check your understanding

  1. Why is a DaemonSet useful for Kubernetes agent deployment?
  2. Why should agents be deployed after Wazuh manager/indexer health is confirmed?
  3. Why use a separate namespace for agents?
  4. Why does the agent need ossec.conf to contain a valid manager address?
  5. Where is WAZUH_MANAGER set in this lab?
  6. Why is the registration service different from the event service?
  7. Why use a Kubernetes Secret for the enrollment password?
  8. What does one agent pod per node tell you?
  9. Why is EXTERNAL-IP <pending> not a blocker for in-cluster agent communication?
  10. What should you check if the agent cannot resolve the manager service name?
  11. Why document hostPath mounts?
  12. What proves the agent is enrolled?
  13. Why should monitoring coverage include blind spots?
Answer key
  1. It schedules one agent on each node, which is useful for node-level monitoring.
  2. Agents must connect to a working manager and the platform must be able to process data.
  3. It separates monitoring agents from the Wazuh platform and simplifies cleanup/troubleshooting.
  4. Without a valid <client> server address, the Wazuh agent has no manager to connect to and exits.
  5. It is set in the DaemonSet manifest, specifically in the write-ossec-config init container, which writes the value into ossec.conf.
  6. The master service handles enrollment/registration on port 1515, while the worker service receives agent event traffic on port 1514.
  7. It avoids putting the enrollment password directly in the manifest.
  8. The DaemonSet is scheduling as expected across available nodes.
  9. Pods can communicate through ClusterIP/DNS service names inside the cluster.
  10. Cluster DNS, service name, namespace, and service existence.
  11. Host mounts affect security boundaries and visibility.
  12. Agent visible and active in the Wazuh dashboard with recent communication.
  13. Because no alert may mean no collection, not no issue.
← Previous: Accessing and Validating the Wazuh Dashboard Series index Next: Installing a Wazuh Agent on the Windows Host →