$ xmrhost-cli docs show --topic=kernel-hardening-checklist
[$ ] doc: kernel-hardening-checklist
// Kernel hardening checklist for a XMRHost VPS — sysctl, KSPP, AppArmor, module blacklist
// published=2026-04-29 · updated=2026-04-29 · diff=advanced · read=23min · tags=[kernel, hardening, kspp, apparmor, sysctl]
// ABSTRACT
abstract
The brand's kernel hardening pass against a freshly-provisioned Debian 12 VPS. Covers the sysctl set the Kernel Self-Protection Project recommends, the kernel-cmdline parameters that lock down memory and entropy initialisation, the kernel-module blacklist that closes the legacy-filesystem and obscure-network attack surfaces, the AppArmor profile state baseline, and the four post-reboot checks that confirm the changes actually landed (sysctl readback, lockdown mode, dmesg taint flags, AppArmor enforce mode).
Scope and assumptions
This is the brand’s kernel-hardening checklist for a freshly-provisioned Debian 12 (bookworm) VPS. It captures the Kernel Self-Protection Project’s recommendations, [linux-doc: admin-guide/sysctl/kernel.rst] the relevant Linux Foundation hardening guide entries, and the brand’s accumulated set of “the time we got bitten by exactly this on a customer box” rules. None of it is exotic. All of it is the kind of change an operator does once, reboots, and leaves alone.
Baseline assumptions:
- Debian 12 (bookworm) on a XMRHost VPS, kernel 6.1.x or newer (the brand procurement spec demands ≥ 6.1; older kernels are missing several of the controls below).
- The brand’s
hardened-by-defaultbaseline is in place — seeharden-sshdat minimum. - You can reboot the box. Most of the kernel-cmdline + sysctl changes apply only at boot; a hot-apply via
sysctl -pcovers the runtime knobs but not the boot-time ones. - Console / VNC access from the VPS provider is available in case a typo in
/etc/default/grubproduces a non-booting box. This has happened to every operator at least once.
This is NOT a checklist for hand-rolled kernels (no custom-compiled kernel, no make menuconfig). It assumes the Debian-shipped kernel; deviations from that baseline are out of scope.
Step 1 — sysctl baseline
Drop the brand sysctl into /etc/sysctl.d/00-xmrhost.conf. The directives are grouped by surface; comments inline explain the threat each closes.
# /etc/sysctl.d/00-xmrhost.conf — XMRHost kernel sysctl baseline.
# References: KSPP recommendations, ANSSI BP-028, Linux kernel docs
# Documentation/admin-guide/sysctl/.
# ---------- network ip-stack -----------------------------------------------
# IP forwarding OFF unless the box is a router. Tor relays / VPN gateways
# need this ON; toggle there. The baseline is OFF.
net.ipv4.ip_forward = 0
net.ipv4.conf.all.forwarding = 0
net.ipv6.conf.all.forwarding = 0
# ICMP redirects — drop received, do not send. Redirects are a known route-
# poisoning vector on shared subnets.
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
# Source-routed packets — drop. Source-routing is universally a smell.
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
net.ipv6.conf.default.accept_source_route = 0
# Reverse-path filter — strict mode. Drops packets whose source IP couldn't
# return to us via the same interface. Catches simple spoofing.
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# Log martian packets (impossible-source-IP). Useful for forensics; trades
# some logspace for visibility into nuisance traffic.
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1
# SYN-flood mitigations.
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_max_syn_backlog = 4096
# Reject ICMP echo requests to broadcast (Smurf-attack mitigation, mostly
# historical but cheap to keep).
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
# Disable IPv6 router-advertisement acceptance (we configure addresses
# statically; an RA-injection attack on the LAN can otherwise rewrite the
# route table). Comment out if your VPS provider uses SLAAC.
net.ipv6.conf.all.accept_ra = 0
net.ipv6.conf.default.accept_ra = 0
# ---------- kernel exposure controls --------------------------------------
# Hide kernel pointers from /proc — kASLR is meaningless if /proc/kallsyms
# leaks the slide.
kernel.kptr_restrict = 2
# Restrict access to dmesg to root only. Userspace dmesg leaks boot-time
# entropy and module-load addresses that aid local exploitation.
kernel.dmesg_restrict = 1
# Restrict access to perf events to root only. Perf is a documented info-
# leak surface for unprivileged users.
kernel.perf_event_paranoid = 3
# Restrict ptrace — only direct children, never arbitrary processes.
kernel.yama.ptrace_scope = 2
# Disable unprivileged BPF (Linux 5.15+ honours this directive). Unprivileged
# eBPF has been a recurring source of LPE CVEs.
kernel.unprivileged_bpf_disabled = 1
net.core.bpf_jit_harden = 2
# Disable kexec_load — kexec replaces the running kernel. Threat: a
# compromised root user can otherwise load a malicious kernel at runtime
# and survive the next reboot transparently.
kernel.kexec_load_disabled = 1
# Restrict use of the user namespaces by unprivileged users (Debian default
# is unrestricted; the brand baseline restricts).
kernel.unprivileged_userns_clone = 0
# Hardlink + symlink protections on /tmp-style sticky dirs. Prevents the
# classic "follow symlink to /etc/passwd" race.
fs.protected_hardlinks = 1
fs.protected_symlinks = 1
fs.protected_fifos = 2
fs.protected_regular = 2
# Disable SysRq except for the safe subset (signalling, reboot). Default 176
# allows reboot + signalling but not the panic/crashdump combo.
kernel.sysrq = 176
# Disable the legacy core-dump handler — core-dumps with setuid programs
# leak privileged memory.
fs.suid_dumpable = 0
# Magic-number for randomize-va-space — full ASLR (mmap, stack, brk).
# Default on modern Debian is 2; pin explicitly.
kernel.randomize_va_space = 2
Apply runtime:
sysctl --system
Verify a few key directives stuck:
kernel.kptr_restrict = 2 kernel.dmesg_restrict = 1 kernel.unprivileged_bpf_disabled = 1
If any read back as the default (often 0 or 1 depending on the directive), check /etc/sysctl.d/ for a higher-numbered file overriding the brand file — sysctl --system applies in lexicographic order across /etc/sysctl.d/*.conf and /usr/lib/sysctl.d/*.conf.
Step 2 — kernel-cmdline parameters
The kernel-cmdline parameters apply at boot only — they live in /etc/default/grub under GRUB_CMDLINE_LINUX_DEFAULT (for parameters that go on the running kernel) or GRUB_CMDLINE_LINUX (for parameters that go on EVERY kernel including recovery). Brand baseline: parameters go on GRUB_CMDLINE_LINUX_DEFAULT.
Edit /etc/default/grub and set:
# /etc/default/grub — append the brand cmdline.
GRUB_CMDLINE_LINUX_DEFAULT="quiet \
slab_nomerge \
init_on_alloc=1 init_on_free=1 \
page_alloc.shuffle=1 \
vsyscall=none \
module.sig_enforce=1 \
lockdown=confidentiality \
random.trust_cpu=off random.trust_bootloader=off \
pti=on \
spectre_v2=on spec_store_bypass_disable=on \
l1tf=full,force mds=full,nosmt tsx=off tsx_async_abort=full,nosmt \
mmio_stale_data=full,nosmt retbleed=auto,nosmt \
iommu=force iommu.passthrough=0 iommu.strict=1 \
audit=1 audit_backlog_limit=8192"
Each parameter, briefly:
slab_nomerge— disable slab cache merging. Merging makes some heap-overflow exploits easier by widening the set of objects co-located.init_on_alloc=1 init_on_free=1— zero memory at allocate AND free. Mitigates use-after-free + uninitialised-memory disclosures.page_alloc.shuffle=1— randomise the page allocator’s free-list order. Mitigates predictable heap-spray.vsyscall=none— disable the legacy vsyscall page (a fixed-address jumpgate that breaks ASLR for the few syscalls that go through it).module.sig_enforce=1— refuse to load unsigned kernel modules. The Debian-shipped modules are signed; a third-party.kowon’t load. [linux-doc: admin-guide/module-signing.rst]lockdown=confidentiality— kernel lockdown mode at the strict level. Prohibits even root from reading/dev/mem, modifying running kernel memory, kexec-loading a non-signed kernel, and similar privileged-but-dangerous operations. Required for full benefit of secure boot if used. [linux-doc: security/lock-down.rst]random.trust_cpu=off random.trust_bootloader=off— do NOT trust the CPU’s RDRAND or the bootloader’s entropy seed for the random pool initialisation. Forces the entropy pool to fill from genuine kernel-observed events.pti=on spectre_v2=on ...— full set of speculative-execution mitigations. Performance hit on modern CPUs is 1-3%; threat model says yes.iommu=force iommu.passthrough=0 iommu.strict=1— force the IOMMU on (DMA protection); strict mode flushes IOTLB on every unmap. Defeats DMA attacks from a compromised peripheral; cheap on a VPS where peripherals are paravirtualised and the host hypervisor is trusted (or compromised, in which case nothing matters anyway).audit=1 audit_backlog_limit=8192— kernel audit subsystem on, larger backlog so a burst of events doesn’t cause auditd to drop.
Then update grub and reboot:
update-grub
reboot
After reboot, verify the cmdline took:
BOOT_IMAGE=/boot/vmlinuz-6.1.0-15-amd64 root=UUID=... ro quiet slab_nomerge init_on_alloc=1 init_on_free=1 ... lockdown=confidentiality ...
If lockdown=confidentiality is missing from /proc/cmdline, the grub regen didn’t pick it up — re-check /etc/default/grub and re-run update-grub.
Step 3 — kernel-module blacklist
The legacy filesystem and obscure-network kernel modules are an attack surface no offshore VPS needs. Block them via modprobe blacklist + install-line override (the install-line override prevents modprobe -i from loading them either):
cat > /etc/modprobe.d/xmrhost-blacklist.conf <<'EOF'
# XMRHost kernel-module blacklist.
# References: KSPP recommendations on attack-surface reduction.
# ---- legacy filesystems ---------------------------------------------------
# These haven't been actively maintained and have a steady trickle of CVEs
# from fuzzing campaigns. The brand's threat model never mounts them.
install cramfs /bin/false
install freevxfs /bin/false
install jffs2 /bin/false
install hfs /bin/false
install hfsplus /bin/false
install squashfs /bin/false
install udf /bin/false
install vfat /bin/false # FAT — yes, including this. Disable on a VPS.
# ---- obscure network protocols --------------------------------------------
# Not required for the workloads the brand catalog targets. SCTP / DCCP /
# RDS / TIPC have all had remote LPE CVEs in the last 5 years.
install dccp /bin/false
install sctp /bin/false
install rds /bin/false
install tipc /bin/false
install n-hdlc /bin/false
# ---- bluetooth, firewire, thunderbolt -------------------------------------
# A VPS doesn't have any of these. Block to make sure a hot-plug emulation
# can't load them either.
install bluetooth /bin/false
install firewire-core /bin/false
install thunderbolt /bin/false
# ---- USB storage on a server box ------------------------------------------
# Comment out if you actually attach USB to this machine (e.g. a hardware
# token). Most VPSes have no USB.
install usb-storage /bin/false
# ---- atm / appletalk / decnet / ipx / x25 / netrom / rose / af-802154 ----
# All legacy. None of these is in any plausible workload of an offshore VPS.
install atm /bin/false
install appletalk /bin/false
install decnet /bin/false
install ipx /bin/false
install netrom /bin/false
install rose /bin/false
install x25 /bin/false
install af_802154 /bin/false
EOF
Apply (the next reboot will refuse the modules; for runtime you can manually unload any that are currently loaded):
# Confirm none of these are in the running kernel.
lsmod | grep -E '^(cramfs|freevxfs|jffs2|hfs|hfsplus|udf|vfat|dccp|sctp|rds|tipc|n-hdlc|bluetooth|firewire-core|thunderbolt|usb-storage|atm|appletalk|decnet|ipx|netrom|rose|x25|af_802154) '
# If any are loaded:
modprobe -r <module-name>
Empty output from lsmod | grep -E ... is the success state.
Step 4 — AppArmor in enforce mode
Debian 12 ships with AppArmor enabled by default but with most profiles in complain mode (logs but does not enforce). Switch the relevant profiles to enforce and add the brand profile for the daemons that ship without one.
Confirm AppArmor is active:
apparmor module is loaded. 40 profiles are loaded. 35 profiles are in enforce mode. /usr/sbin/sshd /usr/sbin/nginx ... 5 profiles are in complain mode. ... 0 profiles are in kill mode. 0 processes have profiles defined.
Switch the complain-mode profiles to enforce (review them first — aa-status | grep '^ ' lists each):
apt install -y apparmor-utils
aa-enforce /etc/apparmor.d/*
systemctl reload apparmor
aa-status
After reload, every profile should be in enforce mode. The aa-enforce step is non-destructive: a profile that is satisfiable by the daemon’s actual behaviour just becomes enforced; one that’s NOT satisfiable causes the daemon to fail (visible in syslog as apparmor="DENIED").
Step 5 — confirm lockdown is active
The lockdown=confidentiality cmdline parameter requires kernel CONFIG_SECURITY_LOCKDOWN_LSM to be compiled in (Debian’s stock kernel has it). Confirm at runtime:
none integrity [confidentiality]
The bracketed value is the active level. If [none] is bracketed instead, the cmdline parameter didn’t take — verify /proc/cmdline (step 2) and the kernel config (grep CONFIG_SECURITY_LOCKDOWN /boot/config-$(uname -r) should show =y).
Step 6 — module signing verification
module.sig_enforce=1 from the cmdline causes the kernel to refuse unsigned modules. Confirm by trying to load an unsigned out-of-tree module would fail; cheaper, just verify the kernel taint-flags are clean:
0
Tainted = 0 means no proprietary modules, no unsigned modules, no live-patched kernel. Any non-zero value means at least one module fell outside the signing policy — decode the bitfield with cat /proc/sys/kernel/tainted and look up the bits in Documentation/admin-guide/tainted-kernels.rst. [linux-doc: admin-guide/tainted-kernels.rst]
Step 7 — four post-reboot checks
Check 1 — sysctl readback
for k in kernel.kptr_restrict kernel.dmesg_restrict kernel.unprivileged_bpf_disabled \
kernel.unprivileged_userns_clone kernel.kexec_load_disabled \
net.ipv4.tcp_syncookies fs.protected_hardlinks fs.protected_symlinks; do
printf '%-50s = %s\n' "$k" "$(sysctl -n $k)"
done
Each value should match the brand baseline (mostly 1 or 2). If any read back as 0, look for a later-numbered file in /etc/sysctl.d/ that’s overriding.
Check 2 — lockdown active
grep -o 'lockdown=[a-z]*' /proc/cmdline
cat /sys/kernel/security/lockdown
Both should agree on confidentiality.
Check 3 — kernel taint clean
cat /proc/sys/kernel/tainted
0.
Check 4 — AppArmor 100% enforce
aa-status --json | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d["profiles"]["enforce"], "enforce,", d["profiles"]["complain"], "complain,", d["profiles"]["unconfined"], "unconfined")'
Expected: 0 in complain and 0 in unconfined.
Closing — what to do next
The kernel is hardened. Order of next steps:
- Read
harden-sshdfor the network-side lockdown if not already applied. - Configure unattended-upgrades to apply security patches automatically — kernel updates require a reboot, so set
Unattended-Upgrade::Automatic-Reboot "true"ANDUnattended-Upgrade::Automatic-Reboot-Time "03:00"(or the operator’s preferred low-traffic window). - Subscribe operator email to
debian-security-announce@lists.debian.org— the canonical channel for kernel CVE notifications. - Add a node-exporter scrape for the
node_kernel_*metrics: kernel version, taint flags, available entropy. The brand monitoring stack alerts on tainted kernel and on entropy < 256 bits.
Most of the controls above are “cheap, set and forget”. The expensive ones (init_on_alloc/init_on_free, the speculation mitigations, IOMMU strict mode) trade a couple of percent of CPU for measurable closure of well-understood attack classes. The brand’s working assumption is that operators of offshore VPSes are not running latency-sensitive HFT workloads; the trade is favourable.
// END OF DOC
$ cd /docs # back to the manual