Layer 0 — Host OS Hardening

Lock down the bare-metal machine before anything else runs on it.

ATTACKER PROTECTED CORE L0 Hardened Host OS Ubuntu/Debian · Secure & measured boot · kernel lockdown · LUKS2 · nftables · auditd CIS L1/L2 L1 Immutable VM (KVM) Talos / Flatcar · read-only root · dm-verity · no shell · API-only · sVirt isolation VM SANDBOX L2 CIS-Hardened k3s Audit logging · PSA restricted · RBAC least-priv · secrets encryption · etcd kube-bench L3 Sealed Runtime gVisor / Kata sandbox · seccomp · drop ALL caps · userns · read-only rootfs · distroless PER-POD L4 Zero-Trust Network Cilium eBPF · default-deny · WireGuard encryption · Hubble · FQDN egress · L7 policy DEFAULT DENY L5 Encrypted Storage LUKS2 USB · Garage S3 object · Longhorn/TopoLVM persistent · encrypted ephemeral AT REST L6 Always-On Observability Prometheus · Loki · Tetragon/Falco eBPF detection & enforcement · Trivy · Kubescape WATCHING Your Workload — encrypted, sandboxed, watched
The hardened stack — Layer 0 is the host the whole fortress stands on.

This chapter covers the host operating system — the Linux you install on bare metal before any virtual machine or k3s cluster exists. Every other layer runs on top of this one. If the host OS is compromised, nothing above it can be trusted.

Supported platforms:

Platform Status
Ubuntu 24.04 LTS (x86_64) Primary — best CIS tooling, TPM2 support
Debian 13 “Trixie” (x86_64) Supported — same steps, Lynis instead of USG
Raspberry Pi OS 64-bit Supported with caveats — no TPM2, no genuine Secure Boot

Think of the host OS as the foundation of a building. You can build the most secure penthouse suite (your Kubernetes cluster) on top, but if the foundation has cracks, none of that matters. This chapter fills those cracks before we build anything else.

In plain English

1. Fresh Minimal Install

Start from the smallest possible surface. Every extra package is code that can have bugs, and bugs on a host OS are serious.

Ubuntu 24.04 Server: Choose “Ubuntu Server (minimized)” in the installer. Deselect all tasksel groups except OpenSSH server. The subiquity installer supports full-disk encryption (LUKS2) natively — enable it during install, not after.

Debian 13 / Raspberry Pi OS: Run the netinstall and uncheck every optional package group (“Desktop”, “Web server”, etc.). Only “SSH server” and “standard system utilities” are needed.

Do not add the host to any graphical desktop environment. A display manager and browser on a hypervisor host is a large and unnecessary attack surface.

Important

After install, update everything before touching hardening:

On the host (root)
apt-get update && apt-get upgrade -y

2. Full-Disk Encryption with LUKS2

LUKS2 (Linux Unified Key Setup version 2) encrypts everything on the disk. Without the unlock key, the data is unreadable — even if someone removes the drive and puts it in another machine.

Imagine your entire hard drive is a locked safe. Even if a burglar steals the safe, they can’t read what’s inside without the combination. LUKS2 is that lock. “Full-disk encryption” means the operating system, your configuration files, and all data are locked — not just one folder.

In plain English

2.1 Format at Install Time (the Right Way)

Enable encryption in the installer before you install anything. Ubuntu’s subiquity installer prompts for LUKS2 in the disk partitioning screen. Use Argon2id as the key-derivation function — it is the modern, memory-hard standard that makes brute-force attacks slow even on GPUs.

Manual LUKS2 format (if not set up at install)
# Only run this on a new, empty partition — it destroys existing data.
cryptsetup luksFormat \
  --type luks2 \
  --pbkdf argon2id \
  --cipher aes-xts-plain64 \
  --key-size 512 \
  /dev/sda3        # replace with your actual partition

Running luksFormat on a partition with existing data destroys it permanently. Set up LUKS2 at install time, not after.

Caution

2.2 Keep a Passphrase Slot as Fallback

Always keep at least one passphrase in key slot 0. The TPM2 auto-unlock (next section) will be in a separate slot. If the TPM slot stops working (firmware update, hardware swap), your passphrase is your recovery path.

2.3 Raspberry Pi: LUKS2 With a Passphrase

The Pi 5’s embedded security chip does not expose a standard TPM2 interface to Linux — systemd-cryptenroll --tpm2-device=list reports nothing. Use a strong passphrase or a USB key (via crypttab with keyscript) instead. The passphrase is not as convenient as TPM2 auto-unlock, but it is genuinely secure.


3. TPM2 Auto-Unlock (x86_64 Only)

A TPM2 chip (Trusted Platform Module) is a small security co-processor soldered onto modern motherboards. It can store a secret that it only reveals if the machine is in a specific, trusted state. We use it to store the LUKS2 unlock key — so the machine boots unattended, but only when the firmware, bootloader, and Secure Boot policy are exactly as expected.

The TPM is a tiny vault inside your motherboard. You tell it: “only hand over the disk unlock key if the machine looks exactly like it did the day I set this up — same UEFI firmware, same bootloader, same Secure Boot settings.” If anything changes (someone tampers with the bootloader, flashes rogue firmware), the TPM refuses, and the disk stays locked.

In plain English

The TPM tracks system state via PCR registers (Platform Configuration Registers). Each PCR measures a specific component:

PCR What it measures
0 Core UEFI firmware code
2 Option ROMs and UEFI drivers
4 Bootloader (GRUB) code and configuration
7 Secure Boot state (enabled/disabled, enrolled keys)

We bind the LUKS2 key to PCRs 0+2+4+7. If any of those four things changes, the TPM refuses to release the key.

PCR 9 (initrd/initramfs measurement) is intentionally excluded. Ubuntu uses initramfs-tools and the initrd is not cryptographically signed; including PCR 9 would break auto-unlock after every kernel update. Systems using Unified Kernel Images (UKIs) — like Talos Linux — can safely include PCR 9.

Note

Run the enrolment script after a successful first boot:

On the host (root)
bash scripts/host/12-tpm-luks-enroll.sh /dev/sda3

The script validates that LUKS2 and TPM2 are present, enrolls the new key slot, patches /etc/crypttab, and rebuilds the initramfs.

Store your passphrase somewhere safe. You will need it after any UEFI firmware update, Secure Boot key change, or motherboard replacement — the TPM slot will be rejected and the disk will ask for the passphrase.

Warning

4. UEFI Secure Boot

Secure Boot is a UEFI feature that checks a cryptographic signature on every program in the boot chain (bootloader → kernel → kernel modules) before running it. If anything in the chain is unsigned or has been tampered with, the machine refuses to boot.

Secure Boot is like a bouncer who checks ID at every door in the building. The UEFI firmware, bootloader, and kernel each have to show a signed certificate. A bootkitattacker who tries to slip in a fake bootloader gets turned away at the door.

In plain English

Ubuntu 24.04 has supported Secure Boot since 18.04. The chain is: signed shim → signed GRUB → signed kernel. Module signing is enforced by kernel lockdown (next section).

Enable Secure Boot in your UEFI/BIOS settings. Ubuntu will work with the Microsoft-signed shim out of the box. Debian requires shim-signed and grub-efi-amd64-signed.

Raspberry Pi: Genuine Secure Boot is not available on Pi. The first-stage bootloader lives in a closed-source ROM on the SoC — you cannot replace it with a signed open-source loader, and an attacker with root can modify the firmware partition to inject their own keys. Do not rely on Secure Boot as a security control on Raspberry Pi.

Note

5. Automatic Security Updates

Security vulnerabilities in the OS are patched daily by the distribution’s security team. “Unattended upgrades” applies those patches automatically, without waiting for a human to remember.

Think of this like automatic Windows Update, but only for security patches — not version upgrades. The OS checks every night, downloads any security fixes, installs them, and reboots at 3am if needed. You don’t have to remember.

In plain English
On the host (root)
apt-get install unattended-upgrades

The config file (scripts/config/host/unattended-upgrades.conf) is deployed by the hardening script. Key settings:

  • Only the *-security pocket is auto-installed. New features still require manual apt upgrade.
  • Automatic reboot at 03:00 when required (to activate kernel patches).
  • Orphaned dependencies are cleaned up automatically.

On Ubuntu, enrol Ubuntu Pro (free for personal use on up to 5 machines) to also receive ESM patches — extended security fixes for packages after their normal support window ends. Run pro attach <token> then add the ESM origins in the config.

Important

6. Kernel Hardening (sysctl)

The Linux kernel exposes hundreds of tunable parameters via sysctl. The defaults are optimised for broad compatibility, not security. The hardening config (scripts/config/host/90-hardening.sysctl.conf) tightens the important ones.

sysctl is a control panel for the Linux kernel — dials and switches that change how the kernel behaves. Most defaults are “permissive” (broadly compatible). We turn the security dials to “strict”: reject forged network packets, hide kernel memory addresses, prevent processes from spying on each other.

In plain English

Key parameters applied:

Setting What it does
net.ipv4.conf.all.accept_source_route = 0 Reject packets that tell your machine how to route them — an old firewall-bypass trick
net.ipv4.conf.all.accept_redirects = 0 Reject ICMP redirects — an attacker on your LAN can redirect traffic through themselves
net.ipv4.tcp_syncookies = 1 Defend against SYN flood attacks
net.ipv4.conf.all.rp_filter = 1 Drop packets whose return path doesn’t match where they came from (stops IP spoofing)
kernel.kptr_restrict = 2 Never expose raw kernel memory addresses — closes a common step in kernel exploits
kernel.dmesg_restrict = 1 Restrict the kernel’s boot log to root — it often contains useful exploit hints
kernel.unprivileged_bpf_disabled = 1 Normal users cannot run BPF programs in the kernel
net.core.bpf_jit_harden = 2 Harden the BPF JIT compiler against side-channel leaks
kernel.yama.ptrace_scope = 2 A process can only be traced (debugged) by its direct parent — stops injection attacks
fs.protected_symlinks = 1 Block symlink tricks in sticky /tmp directories — a classic privilege-escalation path
kernel.unprivileged_userns_clone = 1 Keep unprivileged user namespaces enabled — required by Layer 3 pod-level user-namespace isolation (hostUsers: false); see Layer 3

The 90-kubelet.sysctl.conf config sets four values that k3s’s --protect-kernel-defaults flag enforces on startup:

scripts/config/host/90-kubelet.sysctl.conf (summary)
vm.panic_on_oom = 0               # k3s requires exactly 0
vm.overcommit_memory = 1          # k3s requires exactly 1
kernel.panic = 10                 # reboot 10s after kernel panic
kernel.panic_on_oops = 1          # treat oops as panic
kernel.keys.root_maxkeys = 1000000   # k3s protect-kernel-defaults check
kernel.keys.root_maxbytes = 25000000 # k3s protect-kernel-defaults check

The hardening script deploys both files and runs sysctl --system to activate them without rebooting.


7. Kernel Lockdown Mode

Kernel lockdown mode prevents even the root user from modifying the running kernel at runtime — no writing to /dev/mem, no loading of unsigned kernel modules, no reading raw kernel memory.

Normally, the root user is all-powerful — they can do anything, including reaching inside the running kernel and changing its code. Lockdown mode adds a new level above root: even root can’t touch the kernel’s insides. Think of root as a hotel manager who can enter any room, but the hotel vault has a separate combination that not even the manager knows.

In plain English

Two levels exist:

  • integrity — prevents writing to kernel memory and loading unsigned modules.
  • confidentiality — additionally prevents reading kernel memory (includes integrity).

We set lockdown=confidentiality in the kernel command line.

When UEFI Secure Boot is active, Ubuntu automatically sets lockdown to integrity via the shim/GRUB chain. Setting confidentiality explicitly is strictly stronger.

Note

The hardening script adds lockdown=confidentiality to GRUB_CMDLINE_LINUX in /etc/default/grub and runs update-grub. This takes effect after the next reboot.


8. Kernel Module Blacklisting

Kernel modules extend the kernel with new capabilities (drivers, filesystems, network protocols). Each loaded module is kernel code, reachable by anyone who can trigger the relevant subsystem. Modules we don’t use are attack surface we don’t need.

A kernel module is like a plugin for the kernel. Some plugins handle unusual filesystem formats (HFS+, JFFS2), old network protocols (DECnet, IPX), or hardware with DMA access (Thunderbolt, FireWire). We don’t use any of those. Using “install /bin/false” instead of just “blacklist” means even if something tries to load the module automatically, the kernel runs /bin/false instead — and nothing loads.

In plain English

The blacklist (scripts/config/host/modprobe-blacklist.conf) covers:

  • DMA attack vectors: FireWire, Thunderbolt (physical port attacks)
  • Uncommon filesystems: cramfs, freevxfs, jffs2, HFS/HFS+, squashfs, UDF
  • Unused network protocols: DCCP, SCTP, RDS, TIPC, AX.25, X.25, DECnet, ATM, IPX, and more

After the script deploys the blacklist, it rebuilds the initramfs so the blacklist is active during early boot.

Verify a module is blocked
modprobe dccp   # should return: modprobe: ERROR: could not insert 'dccp': ...

9. AppArmor

AppArmor is a Mandatory Access Control (MAC) system. Instead of trusting a process to do only what it should, AppArmor attaches a profile to each program that lists exactly what files, sockets, and capabilities it is allowed to use. If the process tries to do something not in its profile, the kernel blocks it.

Linux normally says “if you’re root, you can do anything.” AppArmor adds a second layer: “even if you’re root running the SSH daemon, you can only touch these specific files and these specific network ports — nothing else.” If malicious code hijacks the SSH daemon, it’s still stuck inside the SSH daemon’s allowed list and can’t reach the rest of the system.

In plain English

Ubuntu 24.04 ships AppArmor enabled by default with profiles for key system daemons. When KVM is installed, libvirt automatically applies a per-VM AppArmor profile to each QEMU process (via the sVirt framework). This means each virtual machine runs in its own MAC jail — a compromised VM’s QEMU process cannot read another VM’s disk files.

The hardening script ensures all installed profiles are in enforce mode (not just complain mode):

Verify AppArmor status
aa-status    # should show profiles in "enforce" mode, including libvirt entries

10. auditd

auditd is the Linux kernel’s audit subsystem. It watches specific files, system calls, and commands, writing a tamper-evident log of what happened. When something goes wrong, audit logs answer: who ran what, and when.

auditd is a security camera for the kernel. Every time someone modifies /etc/passwd, runs sudo, loads a kernel module, or changes the firewall rules, auditd writes a timestamped, signed record to /var/log/audit/audit.log. If an attacker covers their tracks by deleting files, the kernel-level audit log is harder to tamper with than regular log files.

In plain English

The ruleset (scripts/config/host/audit.rules) monitors:

  • Identity files: /etc/passwd, /etc/shadow, /etc/sudoers
  • SSH config: any write to /etc/ssh/sshd_config.d/
  • Privileged commands: sudo, su
  • Kernel modules: init_module, finit_module, delete_module syscalls
  • Network config changes: sethostname, /etc/hosts, /etc/netplan/
  • Firewall and sysctl: writes to /etc/nftables.conf, /etc/sysctl.d/
  • Time changes: adjtimex, clock_settime, /etc/localtime
  • User management commands: useradd, usermod, passwd, etc.

To query audit logs, use ausearch -k <key>. For example: ausearch -k identity shows every change to password/user files. aureport --summary gives a high-level digest.

Tip

11. SSH Hardening

SSH is the primary remote access path to the host. The defaults are broadly compatible; the hardened config (scripts/config/host/sshd-hardening.conf) closes the gaps.

SSH lets you log into the machine remotely over an encrypted connection. By default it allows password login, which is vulnerable to brute-force attacks. Hardening means: only allow your specific keys (not passwords), only allow your specific admin account (not root), and only accept connections from your home network (not the whole internet).

In plain English

Key settings applied as an sshd_config.d/ drop-in:

Setting Value Why
PermitRootLogin no Never log in as root directly. Log in as a named user and use sudo.
PasswordAuthentication no Keys only. A 4096-bit key cannot be brute-forced; a password can.
AllowUsers adminuser Whitelist: only this account may log in at all.
LoginGraceTime 30 Drop unauthenticated connections after 30 seconds.
MaxAuthTries 3 Three strikes.
AllowTcpForwarding no Dedicated hypervisor — no tunnelling needed.
HostKeyAlgorithms Ed25519, RSA-SHA2 only Ban DSA and legacy RSA-SHA1.
KexAlgorithms Curve25519, large-group DH No NIST curves (potential NSA-influenced constants).
Ciphers ChaCha20-Poly1305, AES-GCM Authenticated encryption, no legacy CBC ciphers.
MACs ETM variants only Encrypt-then-MAC. Blocks padding-oracle attacks.

Edit /etc/ssh/sshd_config.d/90-fk-hardening.conf and replace adminuser on the AllowUsers line with your actual username before reloading sshd. If you lock out adminuser and it doesn’t match a real account, you will be unable to log in remotely.

Important

The firewall (next section) restricts SSH to your management LAN/VPN — the two controls work together.


12. nftables Host Firewall

The host firewall’s job is simple: default-deny everything inbound and forward, allow only what is explicitly needed.

A firewall is a doorman. Without one, any program on any computer on the internet can try to knock on any port of your machine. The nftables firewall slams the door shut on all ports by default, then opens specific, named doors: “only my home network can use SSH,” “WireGuard VPN gets one UDP port,” “VM traffic stays on the internal bridge.” Everything else is silently dropped.

In plain English

Rules deployed (scripts/config/host/nftables.conf):

  • Default DROP on all inbound and forward traffic.
  • Accept established/related — return traffic for connections you initiated.
  • Drop invalid state packets immediately.
  • Loopback always allowed.
  • ICMP ping and neighbour-discovery allowed; everything else ICMP is dropped.
  • SSH (TCP 22) from your LAN/management subnet only (192.168.1.0/24 — replace with yours).
  • WireGuard UDP port 51820 — default admin VPN port (Layer 4); update the rule if you choose a different port.
  • libvirt bridge (virbr*) — VM traffic on the internal virtual network is allowed.

The firewall config uses 51820 as the WireGuard UDP port (the default used by scripts/host/14-wireguard.sh). If you choose a different port in Layer 4, update udp dport 51820 accept in scripts/config/host/nftables.conf before or after loading the firewall.

Important

After replacing the placeholder:

Load and activate the firewall
# Validate (dry run — no changes):
nft -c -f /etc/nftables.conf

# Load for real:
nft -f /etc/nftables.conf

# Persist across reboots:
systemctl enable --now nftables

13. NTP / Time Synchronisation

Accurate system time is required for: audit log timestamps, TLS certificate validation (a clock more than a few minutes off causes certificate errors), and Kubernetes leader election (nodes must agree on time).

systemd-timesyncd is built into every systemd-based distro and is sufficient for a homelab. It is enabled by the hardening script.

Verify time sync is active
timedatectl status
# "System clock synchronized: yes" confirms it is working

14. Disabling Unused Services

Every running service is a daemon with network sockets, file handles, and privileges. Services you don’t use are attack surface you don’t need.

The hardening script stops and disables:

Service What it is Why disabled
avahi-daemon Zeroconf/mDNS auto-discovery Not needed; announces the machine to the LAN
bluetooth Bluetooth daemon No Bluetooth needed on a wired hypervisor
cups Printing No printing on a server
ModemManager Mobile modem management No modems
iscsid iSCSI network storage Not using iSCSI
multipathd Multipath block device manager Single-path local disks only
rpcbind RPC portmapper (NFS prerequisite) Not serving NFS

15. Running a CIS Audit

A CIS (Center for Internet Security) benchmark is a checklist of hundreds of specific security settings. Running an automated audit tells you how the current state compares to the benchmark and highlights anything you missed.

Think of a CIS audit as a home inspection for your OS. A professional inspector goes through a list of hundreds of specific things: “Is this door locked? Is this window latched? Is this electrical panel grounded?” The audit tool does the same for Linux settings, and hands you a report showing what passed and what needs attention.

In plain English

Ubuntu 24.04: Ubuntu Security Guide (USG)

USG is Ubuntu’s official CIS automation tool, built on OpenSCAP. It is free for personal use on up to 5 machines via Ubuntu Pro.

Ubuntu: CIS Level 1 audit and auto-remediation
# Attach Ubuntu Pro (free ≤5 personal machines):
pro attach <your-token>

apt install usg

# Audit — shows compliance status without changing anything:
usg audit cis_level1_server

# Auto-remediate — applies recommended fixes:
usg fix cis_level1_server

Debian 13 / Raspberry Pi OS: Lynis

Lynis performs 2000+ checks and scores the host 0–100. It does not auto-fix, but its output is a prioritised action list.

Debian / Pi: Lynis system audit
apt install lynis
lynis audit system --quick

# Full report (with detailed findings):
lynis audit system
cat /var/log/lynis.log

Run Lynis before and after the hardening script to see the score improve. A freshly hardened Ubuntu 24.04 typically scores in the 70–80 range with USG CIS Level 1 applied.

Tip

16. Running the Hardening Scripts

All of the above is automated by three scripts. Run them in order:

Layer 0: full hardening run (root)
# Step 1: OS hardening (sysctl, AppArmor, auditd, SSH, firewall, NTP, updates)
ASSUME_YES=1 bash scripts/host/10-host-harden.sh

# Step 2: KVM/libvirt installation and hardening (x86_64 only; Pi exits gracefully)
ASSUME_YES=1 bash scripts/host/11-kvm-libvirt.sh

# Step 3: TPM2 LUKS2 auto-unlock enrolment (x86_64 with TPM2 only)
# Requires: LUKS2 at install time, UEFI Secure Boot enabled
bash scripts/host/12-tpm-luks-enroll.sh /dev/sda3   # replace partition

All three scripts are idempotent — safe to re-run. If a step is already complete, it prints “already done” and skips it. Use ASSUME_YES=1 in front of the command to suppress confirmation prompts for unattended runs.

Note

After running the scripts, you must:

  1. Verify /etc/nftables.conf has the correct WireGuard port (51820 by default). If you chose a different port in Layer 4, update the udp dport rule and run nft -f /etc/nftables.conf.
  2. Edit /etc/ssh/sshd_config.d/90-fk-hardening.conf and set your actual username in AllowUsers.
  3. Reboot to activate: kernel lockdown, updated initramfs (module blacklist), and AppArmor cmdline flags.

What This Layer Bought You

Control What it prevents
LUKS2 full-disk encryption Physical theft of the machine — attacker cannot read the disk
TPM2 auto-unlock (PCRs 0+2+4+7) Booting a tampered system — firmware/bootloader/SB changes break auto-unlock
UEFI Secure Boot Bootkit and rootkit injection into the boot chain
Unattended upgrades Known-CVE exploitation — patches arrive within 24 hours of release
sysctl hardening Network spoofing, kernel pointer leaks, BPF exploits, ptrace injection
Kernel lockdown (confidentiality) Root-level kernel modification at runtime — even a root shell cannot patch the kernel
Module blacklist Exploitation of unused kernel code via auto-loaded modules
AppArmor Process privilege escalation — compromised daemon is confined to its allowed file/socket list
auditd Forensics gap — all privileged actions leave a kernel-level audit trail
SSH hardening Brute-force, password spraying, weak cipher attacks
nftables (default drop) Unexpected inbound connections from the network
Unused service removal Reduced daemon count = reduced attack surface

The host OS is now a hardened, encrypted, audited foundation. The virtual machine layer (Layer 1) sits on top of this and inherits these protections — a compromised VM process must first escape the VM, then defeat AppArmor/sVirt, then get past the auditd watchlist, before it can do meaningful damage to the host.