Layer 0 — Host OS Hardening
Lock down the bare-metal machine before anything else runs on it.
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.
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.
After install, update everything before touching hardening:
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.
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.
# 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.
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.
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.
Run the enrolment script after a successful first boot:
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.
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.
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.
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.
apt-get install unattended-upgrades
The config file (scripts/config/host/unattended-upgrades.conf) is deployed by the hardening script. Key settings:
- Only the
*-securitypocket is auto-installed. New features still require manualapt 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.
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.
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:
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.
Two levels exist:
integrity— prevents writing to kernel memory and loading unsigned modules.confidentiality— additionally prevents reading kernel memory (includesintegrity).
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.
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.
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.
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.
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):
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.
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_modulesyscalls - 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.
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).
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.
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.
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.
After replacing the placeholder:
# 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.
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.
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.
# 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.
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.
16. Running the Hardening Scripts
All of the above is automated by three scripts. Run them in order:
# 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.
After running the scripts, you must:
- Verify
/etc/nftables.confhas the correct WireGuard port (51820by default). If you chose a different port in Layer 4, update theudp dportrule and runnft -f /etc/nftables.conf. - Edit
/etc/ssh/sshd_config.d/90-fk-hardening.confand set your actual username inAllowUsers. - 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.