CR-2026-06-25: Implementation
Implementation
Phase 0: Pre-Flight
Verify connectivity, back up current security configs, and document the baseline posture before any changes.
Set shell variables
DOMAIN="inside.domusdigitalis.dev"
WAZUH_MGR="wazuh.${DOMAIN}"
WAZUH_WORKERS="wazuh-workers.${DOMAIN}"
WAZUH_REG_PORT=1515
WAZUH_EVT_PORT=1514
WAZUH_API_PORT=55000
Verify connectivity
nc -zv "${WAZUH_MGR}" "${WAZUH_REG_PORT}" 2>&1
nc -zv "${WAZUH_WORKERS}" "${WAZUH_EVT_PORT}" 2>&1
curl -sk -o /dev/null -w '%{http_code}' "https://${WAZUH_MGR}:${WAZUH_API_PORT}/"
Expected: succeeded for both nc checks, 401 for API (unauthenticated).
Back up current security configs
BACKUP_DIR="/tmp/pre-wazuh-backup-$(date +%Y%m%d)"
mkdir -p "${BACKUP_DIR}"
sudo cp -a /etc/apparmor.d/ "${BACKUP_DIR}/apparmor.d/"
sudo cp -a /etc/nftables.conf "${BACKUP_DIR}/nftables.conf"
sudo aa-status > "${BACKUP_DIR}/apparmor-status.txt" 2>&1
sudo nft list ruleset > "${BACKUP_DIR}/nftables-ruleset.txt" 2>&1
Document baseline with fortress-verification
Paste and run the fortress verification script block from partials/worklog/daily-notes/YYYY-MM-DD/fortress-verification/fortress-script.adoc (or the equivalent sub-partial for the current day’s daily notes):
# Run the fortress verification script block and capture output
bash /tmp/fortress-verify.sh > "${BACKUP_DIR}/fortress-baseline.txt" 2>&1
fortress-verification is not a standalone command β it is the script block from the fortress-verification/fortress-script.adoc sub-partial. Copy the script to /tmp/fortress-verify.sh and execute it.
|
Phase 1: Install Wazuh Agent
NOT the AUR β a security agent should come from the vendor, not a community PKGBUILD. The official install script (wazuh-install.sh) only supports RPM/DEB distros. On Arch, extract the agent from the official RPM package.
Step 1.1 β Import Wazuh GPG key
gpg --list-keys | grep -i wazuh
curl -s https://packages.wazuh.com/key/GPG-KEY-WAZUH -o /tmp/wazuh-gpg-key.asc
gpg --import /tmp/wazuh-gpg-key.asc
gpg --list-keys | grep -i wazuh
Step 1.2 β Determine manager version
The agent version MUST match the manager. Version mismatch causes silent failures.
ssh k3s-master-01 'sudo /usr/local/bin/k3s kubectl exec -n wazuh wazuh-manager-master-0 -- /var/ossec/bin/wazuh-control info 2>/dev/null | head -5'
Result: WAZUH_VERSION="v4.14.3" β agent must be 4.14.3.
Step 1.3 β Install script investigation (failed β documented for audit)
The 4.x path on Wazuh’s CDN returns AccessDenied as of 2026-06-25. The 4.14 path serves the script, but it only supports apt/yum/dnf/zypper β no pacman/Arch support.
# 4.x path β AccessDenied on both script and signature
curl -sI https://packages.wazuh.com/4.x/wazuh-install.sh | head -3
curl -sI https://packages.wazuh.com/4.x/wazuh-install.sh.asc | head -3
# 4.14 path β script exists, signature does not
curl -sI https://packages.wazuh.com/4.14/wazuh-install.sh | head -3
# Download and check Arch support
curl -sO https://packages.wazuh.com/4.14/wazuh-install.sh
grep -i 'arch\|pacman\|apt\|yum\|dnf\|zypper' wazuh-install.sh | head -20
Result: Script references only apt/yum/dnf/zypper. No Arch/pacman path. The .asc GPG signature file returns AccessDenied from S3 β GPG verification not available for the install script.
Step 1.4 β RPM extraction method (used)
Download the official RPM and extract with bsdtar (available on Arch by default).
cd /tmp
curl -sO "https://packages.wazuh.com/4.x/yum/wazuh-agent-4.14.3-1.x86_64.rpm"
file wazuh-agent-4.14.3-1.x86_64.rpm
Expected: RPM v3.0 bin i386/x86_64
mkdir -p /tmp/wazuh-staging
cd /tmp/wazuh-staging
bsdtar -xf /tmp/wazuh-agent-4.14.3-1.x86_64.rpm
find . -type f | head -30
Key files: ./usr/lib/systemd/system/wazuh-agent.service (systemd unit) and ./var/ossec/ (entire agent tree).
Step 1.5 β Install agent from staging
ls -la /var/ossec 2>&1
sudo cp -a ./var/ossec /var/ossec
sudo cp ./usr/lib/systemd/system/wazuh-agent.service /usr/lib/systemd/system/
sudo groupadd -r wazuh 2>/dev/null
sudo useradd -r -g wazuh -d /var/ossec -s /sbin/nologin wazuh 2>/dev/null
sudo chown -R root:wazuh /var/ossec
sudo chown -R wazuh:wazuh /var/ossec/queue /var/ossec/var /var/ossec/logs /var/ossec/tmp
sudo chmod -R 770 /var/ossec/queue /var/ossec/var /var/ossec/logs /var/ossec/tmp
sudo chmod +x /var/ossec/bin/*
sudo chmod +x /var/ossec/active-response/bin/*
sudo systemctl daemon-reload
ls -la /var/ossec/bin/wazuh-agentd
/var/ossec/bin/wazuh-agentd -V
systemctl status wazuh-agent 2>&1 | head -5
Phase 2: Configure Agent
Configure ossec.conf with manager address, enrollment settings, and log collection paths appropriate for a hardened Arch Linux workstation.
Back up default config
sudo ls -la /var/ossec/etc/
RPM extraction does not include a default ossec.conf. The /var/ossec/etc/ directory contains client.keys, internal_options.conf, local_internal_options.conf, localtime, shared/, and wpk_root.pem β but no agent config. Skip backup and write the config fresh.
|
Configure manager connection and log collection
sudo tee /var/ossec/etc/ossec.conf > /dev/null << 'OSSEC_EOF'
<ossec_config>
<client>
<server>
<address>wazuh-workers.inside.domusdigitalis.dev</address>
<port>1514</port>
<protocol>tcp</protocol>
</server>
<enrollment>
<enabled>yes</enabled>
<manager_address>wazuh.inside.domusdigitalis.dev</manager_address>
<port>1515</port>
<agent_name>modestus-p16g</agent_name>
</enrollment>
<notify_time>10</notify_time>
<time-reconnect>60</time-reconnect>
</client>
<!-- Systemd journal (primary log source on Arch β covers sshd, PAM, sudo, AppArmor, nftables) -->
<localfile>
<log_format>journald</log_format>
</localfile>
<!-- Audit log (auditd β age_key_read, ssh_key_read, authorized_keys_mod, gpg_key_read rules) -->
<localfile>
<log_format>audit</log_format>
<location>/var/log/audit/audit.log</location>
</localfile>
<!-- Pacman log (package installs/updates/removals) -->
<localfile>
<log_format>syslog</log_format>
<location>/var/log/pacman.log</location>
</localfile>
<!-- NOTE: Arch uses journald exclusively. No /var/log/auth.log, /var/log/syslog, or /var/log/nftables.log.
AppArmor denials, nftables drops, and sshd events are captured via the journald localfile above. -->
<!-- File integrity monitoring -->
<syscheck>
<disabled>no</disabled>
<frequency>43200</frequency>
<directories check_all="yes" realtime="yes">/etc,/usr/bin,/usr/sbin</directories>
<directories check_all="yes">/boot</directories>
<ignore>/etc/mtab</ignore>
<ignore>/etc/hosts.deny</ignore>
<ignore>/etc/mail/statistics</ignore>
<ignore>/etc/random-seed</ignore>
<ignore>/etc/adjtime</ignore>
<ignore>/etc/resolv.conf</ignore>
</syscheck>
<!-- Rootcheck -->
<rootcheck>
<disabled>no</disabled>
<check_files>yes</check_files>
<check_trojans>yes</check_trojans>
<check_dev>yes</check_dev>
<check_sys>yes</check_sys>
<check_pids>yes</check_pids>
<check_ports>yes</check_ports>
<check_if>yes</check_if>
<frequency>43200</frequency>
</rootcheck>
<!-- Active response -->
<active-response>
<disabled>no</disabled>
</active-response>
<logging>
<log_format>plain</log_format>
</logging>
</ossec_config>
OSSEC_EOF
Fix ownership (wazuh group, not ossec)
Wazuh 4.14.x expects group wazuh, not ossec. If Step 1.5 was run with ossec, re-run ownership fix here.
|
sudo ls -la /var/ossec/etc/ossec.conf
sudo groupadd -r wazuh 2>/dev/null
sudo useradd -r -g wazuh -d /var/ossec -s /sbin/nologin wazuh 2>/dev/null
sudo chown -R root:wazuh /var/ossec
sudo ls -la /var/ossec/etc/ossec.conf
sudo /var/ossec/bin/wazuh-logcollector -t 2>&1 | head -5
sudo wc -l /var/ossec/etc/ossec.conf
Phase 3: AppArmor Profile for ossec
Create an AppArmor profile for the Wazuh agent binaries. Deploy in complain mode first — enforce after 48 hours of clean operation.
Create the profile
sudo aa-status 2>&1 | grep -i ossec
ls /etc/apparmor.d/ | grep -i ossec
sudo tee /etc/apparmor.d/usr.local.wazuh-agent > /dev/null << 'AA_EOF'
# AppArmor profile for Wazuh agent
# Mode: complain (48h observation before enforce)
#include <tunables/global>
/var/ossec/bin/wazuh-agentd flags=(complain) {
#include <abstractions/base>
#include <abstractions/nameservice>
# Agent binaries
/var/ossec/bin/* rix,
/var/ossec/agentless/* rix,
# Configuration
/var/ossec/etc/** r,
/var/ossec/etc/ossec.conf r,
/var/ossec/etc/internal_options.conf r,
/var/ossec/etc/local_internal_options.conf r,
# Logs (read for collection, write for agent logs)
/var/log/audit/audit.log r,
/var/log/pacman.log r,
/var/ossec/logs/** rw,
/var/ossec/logs/ossec.log rw,
# Journald access (primary log source on Arch)
/run/log/journal/** r,
/var/log/journal/** r,
# Queue and state
/var/ossec/queue/** rw,
/var/ossec/var/** rw,
/var/ossec/tmp/** rw,
# FIM: read monitored directories
/etc/** r,
/usr/bin/** r,
/usr/sbin/** r,
/boot/** r,
# System information
/proc/** r,
/sys/** r,
@{PROC}/[0-9]*/status r,
@{PROC}/[0-9]*/fd/ r,
# Network (agent -> manager)
network tcp,
# Deny everything else
deny /home/** rw,
deny /root/** rw,
}
AA_EOF
sudo apparmor_parser -r /etc/apparmor.d/usr.local.wazuh-agent
sudo aa-status 2>&1 | grep -i 'wazuh\|ossec'
Expected: /var/ossec/bin/wazuh-agentd appears in the output (complain mode).
Phase 4: nftables Egress Rules β SKIPPED
sudo nft list ruleset | grep -A5 'chain output'
sudo nft list tables
Result: Tables are ip filter, ip nat, ip6 filter, ip6 nat, ip raw β all managed by iptables-nft (Docker/libvirt). No custom egress filtering chain exists. No inet egress or inet filter table.
Decision: Skip Phase 4. No egress filtering is active β all outbound traffic is allowed. The Wazuh agent can reach the manager (1515) and workers (1514) without any nftables changes.
Future action: When egress filtering is enabled (via security-hardening-carryover/nftables-egress-enforce.adoc), add Wazuh rules at that time. Do NOT add inet rules to an iptables-nft managed ruleset β they are separate subsystems and will conflict.
Phase 5: Start and Verify Agent
Enable and start the service (auto-enrollment β failed)
systemctl status wazuh-agent 2>&1 | head -5
sudo systemctl enable wazuh-agent
sudo systemctl start wazuh-agent
Auto-enrollment via systemctl start created duplicate agents due to a race condition. The enrollment section in ossec.conf triggers registration on every service start, and timing issues with the manager caused duplicate entries.
|
Manual registration with agent-auth (used)
Auto-enrollment failed. Use agent-auth for a one-shot manual registration instead.
Cleanup stale/duplicate agents
# Stop the agent
sudo systemctl stop wazuh-agent
# Authenticate to the API
DOMAIN="inside.domusdigitalis.dev"
TOKEN=$(curl -sk -u "wazuh-wui:$(gopass show -o v3/domains/d000/k3s/wazuh/api)" \
-X POST "https://wazuh.${DOMAIN}:55000/security/user/authenticate" \
| jq -r '.data.token')
# List agents to find stale/duplicate IDs
curl -sk -H "Authorization: Bearer ${TOKEN}" \
"https://wazuh.${DOMAIN}:55000/agents" \
| jq '.data.affected_items[] | {id, name, status, ip}' | tee /tmp/wazuh-agents-stale.json
# Delete stale agent(s) by ID (replace AGENT_ID with actual ID from above)
curl -sk -H "Authorization: Bearer ${TOKEN}" \
-X DELETE "https://wazuh.${DOMAIN}:55000/agents?agents_list=AGENT_ID&status=all&older_than=0s" \
| jq '.' | tee /tmp/wazuh-agent-delete.json
# Truncate client.keys to clear local registration state
sudo truncate -s 0 /var/ossec/etc/client.keys
Register with agent-auth
sudo /var/ossec/bin/agent-auth \
-m wazuh.inside.domusdigitalis.dev \
-p 1515 \
-P password \
-A modestus-p16g
The -P flag takes the registration password as a string, not a file path. The authd.pass file approach failed because -P expects the literal password value, not a path to a file containing it.
|
The registration password is literally password (the Wazuh default). This requires immediate rotation β see Phase 7.
|
Start agent after manual registration
sudo systemctl start wazuh-agent
systemctl status wazuh-agent 2>&1 | head -10
Verify agent registered with manager
sudo tail -30 /var/ossec/logs/ossec.log | grep -E 'Connected|Enrolled|ERROR'
Expected: Connected to the server ([wazuh-workers.inside.domusdigitalis.dev]:1514/tcp). The agent connects to workers for event delivery, not the manager directly. No ERROR lines.
Verify on manager side
DOMAIN="inside.domusdigitalis.dev"
TOKEN=$(curl -sk -u "wazuh-wui:$(gopass show -o v3/domains/d000/k3s/wazuh/api)" \
-X POST "https://wazuh.${DOMAIN}:55000/security/user/authenticate" \
| jq -r '.data.token')
curl -sk -H "Authorization: Bearer ${TOKEN}" \
"https://wazuh.${DOMAIN}:55000/agents" \
| jq '.data.affected_items[] | {id, name, status, ip}' | tee /tmp/wazuh-agents.json
Expected: modestus-p16g appears with status active.
Rootcheck warnings (non-critical)
The RPM extraction method does not include rootcheck definition files (rootcheck_files, rootcheck_trojans). The agent logs will show warnings:
rootcheck: WARN: File '/var/ossec/etc/shared/rootcheck_files' not found rootcheck: WARN: File '/var/ossec/etc/shared/rootcheck_trojans' not found
These are non-critical. To resolve, either:
-
Disable rootcheck in
ossec.confby setting<disabled>yes</disabled>in the<rootcheck>block. -
Download definition files from the Wazuh GitHub repository:
sudo curl -sL "https://raw.githubusercontent.com/wazuh/wazuh/v4.14.3/etc/shared/rootkit_files.txt" \ -o /var/ossec/etc/shared/rootcheck_files sudo curl -sL "https://raw.githubusercontent.com/wazuh/wazuh/v4.14.3/etc/shared/rootkit_trojans.txt" \ -o /var/ossec/etc/shared/rootcheck_trojans sudo chown root:wazuh /var/ossec/etc/shared/rootcheck_files /var/ossec/etc/shared/rootcheck_trojans sudo chmod 640 /var/ossec/etc/shared/rootcheck_files /var/ossec/etc/shared/rootcheck_trojans
Phase 6: Validate Security Posture
Confirm that the agent installation introduced no regressions in the hardening stack.
# Run the fortress verification script block from the daily notes sub-partial
# (partials/worklog/daily-notes/YYYY-MM-DD/fortress-verification/fortress-script.adoc)
bash /tmp/fortress-verify.sh | tee /tmp/fortress-post-wazuh.txt
fortress-verification is not a standalone command. Copy the script block from the fortress-verification/fortress-script.adoc sub-partial to /tmp/fortress-verify.sh and execute it.
|
diff "${BACKUP_DIR}/fortress-baseline.txt" /tmp/fortress-post-wazuh.txt
Expected: No regressions. New entries for the wazuh-agent service are acceptable. Any regression in AppArmor, nftables, auditd, LUKS, USBGuard, or fail2ban status requires investigation before proceeding.
Phase 7: Follow-up Actions
Post-enrollment items that require action but are not blockers for the initial deployment.
7.1 β Rotate registration password
The Wazuh authd registration password is the factory default (password). Rotate immediately.
# Generate strong password and store in gopass
gopass generate v3/domains/d000/k3s/wazuh/authd-password 32
# Update on manager (k3s)
ssh k3s-master-01 'sudo /usr/local/bin/k3s kubectl exec -n wazuh wazuh-manager-master-0 -- \
bash -c "echo $(gopass show -o v3/domains/d000/k3s/wazuh/authd-password) > /var/ossec/etc/authd.pass"'
7.2 β Issue proper TLS certificate for wazuh-dashboard
The current dashboard certificate CN is wazuh, not wazuh-dashboard. Browser warnings and API clients may reject the mismatch. Issue a cert with the correct CN via Vault or ACME.
7.3 β Download rootcheck definition files
See the rootcheck warnings section in Phase 5. Download from the Wazuh GitHub repository and place in /var/ossec/etc/shared/.
7.4 β Move AppArmor profile from complain to enforce
After 48 hours of clean operation with no AppArmor denials from the wazuh-agent:
sudo aa-enforce /etc/apparmor.d/usr.local.wazuh-agent
sudo aa-status 2>&1 | grep -B1 -A1 wazuh
7.5 β Create wazuh-agent API scripts
Create reusable API query scripts in data/d000/infra/ for agent management, status checks, and alert queries. These replace the inline curl commands used during enrollment.