Introduction: Security by default
Most Linux distributions follow a default-permit model: everything is allowed until the administrator restricts it. This post inverts that model — we build a system that is born restricted and only opens up what is strictly necessary.
This post is the evolution of our Kernel Hardening implementation with KSPP. We start from that foundation and take it to the next level: a Debian 13 (Trixie) that is secure by default at the kernel, syscall, network, filesystem and process level. An operating system that is secure before installing anything.
| Principle | Implementation |
|---|---|
| Deny-all syscalls | Seccomp-BPF per-service via systemd (sshd excluded) |
| Deny-all network | nftables with DROP policy on input, output and forward |
| Deny-all filesystem | Module blacklist + noexec partitions |
| Deny-all modules | Aggressive blacklist + module.sig_enforce |
| Deny-all capabilities | Minimal CapabilityBoundingSet per service |
| Immutable kernel | lockdown=confidentiality + immutable auditd (-e 2) |
| Irrevocable telemetry | Real-time shipping to external SIEM (Wazuh/Elastic/Splunk) |
The result is a Debian with military-grade restrictions, while keeping the flexibility of the Debian ecosystem for production environments. The entire guide is applicable both on bare-metal (laptop, physical server) and on any enterprise hypervisor (VMware, Proxmox, Hyper-V, Xen, KVM). All the configuration has been validated in a real environment (Debian 13, kernel 6.12.88).
Defense-in-depth architecture
┌─────────────────────────────────────────────────────┐
│ HARDWARE / HYPERVISOR │
│ Disable: clipboard, USB passthrough, audio │
│ Applies to: VMware, Proxmox, Hyper-V, KVM, bare │
├─────────────────────────────────────────────────────┤
│ LAYER 1: KERNEL (KSPP + lockdown) │
│ - lockdown=confidentiality │
│ - module.sig_enforce=1, audit=1 │
│ - init_on_alloc/free, slab_nomerge, pti=on │
│ - Spectre/MDS/TSX mitigations enforced │
├─────────────────────────────────────────────────────┤
│ LAYER 2: SYSCTL (60+ parameters) │
│ - ptrace_scope=3, kptr_restrict=2 │
│ - kexec_load_disabled=1, io_uring_disabled=2 │
│ - IPv6 disabled, ICMP blocked │
├─────────────────────────────────────────────────────┤
│ LAYER 3: SECCOMP-BPF (per-service) │
│ - SystemCallFilter per service via systemd │
│ - sshd/auditd excluded (privilege separation) │
│ - fail2ban, cron, timesyncd: full filtering │
├─────────────────────────────────────────────────────┤
│ LAYER 4: NETWORK (nftables deny-all) │
│ - Input: only SSH from management network │
│ - Output: only DNS + apt + NTP + SIEM │
│ - Forward: full DROP │
│ - Rate-limiting on all rules │
├─────────────────────────────────────────────────────┤
│ LAYER 5: FILESYSTEM │
│ - /tmp noexec,nosuid,nodev │
│ - /proc hidepid=2 │
│ - Kernel modules blacklisted (USB, BT, wireless) │
├─────────────────────────────────────────────────────┤
│ LAYER 6: AUDITING (immutable auditd) │
│ - MITRE ATT&CK rules (-e 2 immutable) │
│ - AIDE/Wazuh syscheck every 15 min │
│ - Fail2ban + centralized logging │
├─────────────────────────────────────────────────────┤
│ LAYER 7: TELEMETRY (SIEM + EDR + DFIR) │
│ - Wazuh Agent (FIM + rootkit + log collection) │
│ - Velociraptor Client (live threat hunting) │
│ - Send to external SIEM (locally irrevocable) │
└─────────────────────────────────────────────────────┘Before hardening: Decision Framework
Every control has a cost: complexity, performance, operability. Applying "maximum hardening" by default is as dangerous as applying none at all. Before implementing any restriction, answer these 4 questions:
1. What are we protecting (Asset Criticality)
| Level | Example | Implication |
|---|---|---|
| Critical | Production bastion, payment systems | Maximum hardening, even with operational impact |
| High | Application servers with sensitive data | Adapted hardening, prioritizing detection |
| Medium | Staging environments, internal tools | Base hardening + monitoring |
| Low | Labs, ephemeral VMs | Minimal hardening, focus on network isolation |
2. Who could attack us (Threat Modeling)
| Actor | Motivation | Priority controls |
|---|---|---|
| Script kiddie / Botnet | Opportunism, cryptomining | Strict firewall, automatic updates |
| Competitor / Espionage | IP theft | Advanced telemetry, FIM, immutable auditing |
| APT / Nation-state | Persistence in critical infrastructure | Kernel hardening, remote attestation, Zero Trust |
| Malicious insider | Sabotage, exfiltration | Segregation of duties, session logging, MFA |
3. What can we afford to lose (Risk Tolerance)
- Is 15 minutes of downtime acceptable to patch the kernel?
- Can we block a syscall and break an application?
- Do we have recovery capability if hardening causes an incident?
Golden rule: If you cannot answer these questions, you are not ready for advanced hardening. Start with detection controls.
4. How will we know it works (Success Metrics)
Define metrics before implementing:
- Mean time to detect anomalies (MTTD) — target: < 5 min
- False positives per day — target: < 5 per bastion
- Performance impact — target: < 10% degradation
- Recovery time from a configuration incident (MTTR) — target: < 15 min
Decision Matrix: What to apply depending on the service
Not all servers need the same level of restriction. This matrix guides the decision:
| Control | Bastion | Web App | Database | Container Host |
|---|---|---|---|---|
lockdown=confidentiality | Yes | Integrity | Integrity | Integrity |
io_uring_disabled=2 | Yes | Yes | No (*) | Yes |
user.max_user_namespaces=0 | Yes | Yes | Yes | No (**) |
| Seccomp per-service | Aggressive | Adapted | Minimal | Container-native |
| nftables output deny-all | Yes | FQDN allowlist | DB peers allowlist | Registry allowlist |
ptrace_scope=3 | Yes | Scope=2 | Scope=2 | Scope=2 |
| FIM every 15 min | Yes | Every 1h | Configs only | Binaries only |
| Mandatory MFA | Yes | Yes | Yes | Admin only |
(*) PostgreSQL 14+ uses io_uring; disabling it = -30% throughput under write-intensive load.
(**) Docker/Podman require user namespaces for container isolation.
Philosophy by service type:
- Bastion: "Deny-all by default" — minimal surface, direct human access
- Web/DB: "Detect-and-respond" — allow functionality, detect anomalies
- Containers: "Isolate-by-design" — trust runtime isolation, harden the host
Everything that follows in this post applies the Bastion profile (maximum restriction level). Adapt according to your risk matrix.
Profile-specific parameters
| Control | Bastion | Web Server | Database | Container host |
|---|---|---|---|---|
user.max_user_namespaces | 0 | 0 | 0 | 1024 (Docker needs it) |
lockdown | confidentiality | integrity | integrity | integrity |
slub_debug=FZP | Yes | No (perf impact ~5-15%) | No | No |
init_on_free=1 | Yes | Yes | No (I/O impact) | Yes |
io_uring_disabled | 2 | 2 | 0 (PostgreSQL uses io_uring) | 2 |
module.sig_enforce | 1 | 1 | 0 (DKMS/drivers) | 0 (overlay fs) |
| nftables output | Only DNS/APT/NTP/SIEM | +HTTP/HTTPS to backends | +DB ports | +registry |
| Seccomp per-service | Yes (aggressive) | Yes (adapted) | No (performance) | Yes (containerd) |
Real performance impact:
slub_debug=FZP+init_on_free=1+page_poison=1can reduce throughput by 5-15% under memory-intensive workloads. On a bastion it is irrelevant. On a database server with thousands of transactions/second, it is unacceptable. Always measure before deploying.
Known incompatibilities with enterprise tools
| Tool | Conflicting control | Solution |
|---|---|---|
| Docker/Podman | user.max_user_namespaces=0 | Raise to 1024+ on container hosts |
| DKMS / proprietary drivers (NVIDIA, etc.) | module.sig_enforce=1 | Disable or sign modules with MOK |
| eBPF tools (Cilium, Falco, bpftrace) | lockdown=confidentiality | Use lockdown=integrity or exclude |
| Backup agents (Veeam, NetBackup) | Seccomp @system-service | Specific drop-in without SystemCallFilter |
| Monitoring with perf/flamegraph | kernel.yama.ptrace_scope=3 | Reduce to 2 in profiling environments |
| Enterprise antivirus (CrowdStrike, SentinelOne) | lockdown=confidentiality + module.sig | Requires kernel exceptions or signing |
Phase 1: Minimalist base installation
Hardware/VM requirements
- CPU: 2 cores minimum
- RAM: 2GB minimum, 4GB recommended
- Disk: 20GB minimum (with LUKS, encryption adds overhead)
- Network: Dedicated interface for management (separated from production traffic)
Installing Debian 13 (Trixie)
Regardless of the hypervisor or physical hardware, the installation must be minimalist. Use the netinst ISO and, at the software selection step (tasksel), check only:
- "Standard system utilities"
Uncheck everything else: desktop, web server, SSH server (we install it hardened afterwards), print server, etc. The goal is to start from a system with the smallest possible attack surface.
💡 Enterprise consideration: In environments with dozens of bastions, this installation is automated with Packer + Ansible. See the "Scaling hardening" section.
Considerations by hypervisor
In virtualized environments, reduce the attack surface of the hypervisor itself:
| Hypervisor | Recommended actions |
|---|---|
| VMware ESXi/Workstation | Disable: copy-paste, drag-drop, shared folders, 3D acceleration. Enable Secure Boot on the VM |
| Proxmox/KVM | Use machine type q35, disable USB passthrough, tablet device and audio. Enable cpu host |
| Hyper-V | Disable unnecessary Integration Services, enable Secure Boot, use Generation 2 |
| Xen | Use HVM mode with IOMMU, disable unnecessary PV drivers |
| Bare-metal | BIOS/UEFI: disable USB boot, enable Secure Boot, BIOS password, disable Thunderbolt/FireWire if unused |
Initial post-installation hardening
Once the base system is installed and you have root access:
# Update and clean
apt update && apt full-upgrade -y && apt autoremove --purge -y
# Install hardening tools
apt install -y \
apparmor apparmor-utils apparmor-profiles apparmor-profiles-extra \
libpam-apparmor \
aide aide-common \
auditd audispd-plugins \
fail2ban \
nftables \
haveged rng-tools5 \
lynis \
unattended-upgrades \
needrestart \
debsums \
systemd-coredump \
cryptsetup-bin \
libseccomp-dev libseccomp2 seccomp \
openssh-server \
jq curl gnupg2
# Remove unnecessary packages (minimalism)
apt purge -y \
telnet rsh-client xinetd nis tftp \
avahi-daemon cups 2>/dev/null || true
apt autoremove --purge -yCreate the administration user
Access to the bastion is done exclusively via SSH with public key. No password login, no direct root access:
# Create group and user
groupadd bastion-ssh
useradd -m -s /bin/bash -G bastion-ssh,sudo bastion-admin
# Configure SSH key
mkdir -p /home/bastion-admin/.ssh
chmod 700 /home/bastion-admin/.ssh
# Copy your public key (from your local machine)
# echo "ssh-ed25519 AAAA..." > /home/bastion-admin/.ssh/authorized_keys
chmod 600 /home/bastion-admin/.ssh/authorized_keys
chown -R bastion-admin:bastion-admin /home/bastion-admin/.ssh
# Lock the root password
passwd -l rootPassword policy (PAM + pwquality)
Although SSH access is exclusively by public key, local passwords remain relevant for sudo, physical console and privilege escalation. A robust policy prevents weak passwords that an attacker could exploit after compromising a session:
# /etc/login.defs - lifecycle policy
PASS_MAX_DAYS 90 # Rotate every 90 days
PASS_MIN_DAYS 7 # Minimum 7 days between changes (avoid history bypass)
PASS_WARN_AGE 14 # Warn 14 days before expiration
PASS_MIN_LEN 14 # Minimum length 14 characters# /etc/security/pwquality.conf - complexity
minlen = 14 # Minimum length
dcredit = -1 # At least 1 digit
ucredit = -1 # At least 1 uppercase
ocredit = -1 # At least 1 special character
lcredit = -1 # At least 1 lowercase
maxrepeat = 3 # Max 3 identical consecutive characters
maxclassrepeat = 4 # Max 4 consecutive of the same class
usercheck = 1 # Cannot contain the username
dictcheck = 1 # Check against dictionary
minclass = 4 # Minimum 4 character classes
difok = 8 # Minimum 8 characters different from the previous one
palindrome = 1 # Reject palindromes# PAM: history + account lockout
# /etc/pam.d/common-password (add remember=12)
password requisite pam_pwquality.so retry=3
password [success=1 default=ignore] pam_unix.so obscure use_authtok try_first_pass yescrypt remember=12
# Lockout after failed attempts (pam_faillock)
# /etc/pam.d/common-auth
auth required pam_faillock.so preauth silent audit deny=5 unlock_time=900 fail_interval=900
auth [default=die] pam_faillock.so authfail audit deny=5 unlock_time=900 fail_interval=900
auth sufficient pam_faillock.so authsucc audit deny=5 unlock_time=900 fail_interval=900This establishes: passwords of at least 14 characters with mandatory complexity, rotation every 90 days, a history of 12 passwords (non-reusable), and account lockout for 15 minutes after 5 failed attempts.
Sudo hardening
Sudo is the most common privilege escalation vector. The default configuration is too permissive:
# /etc/sudoers.d/00-security
Defaults requiretty # Only from a real terminal (no remote scripts)
Defaults use_pty # Force pseudo-terminal
Defaults logfile="/var/log/sudo.log"
Defaults log_input, log_output # Record the ENTIRE sudo session
Defaults iolog_dir="/var/log/sudo-io"
Defaults passwd_timeout=1 # 1 minute to enter password
Defaults timestamp_timeout=5 # 5 min cache (not indefinite)
Defaults passwd_tries=3 # Max 3 attempts
Defaults env_reset # Clear environment variables
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
Defaults !visiblepw # Do not show password on screen
Defaults always_set_home # Always set HOME
Defaults log_denied # Log denied attempts# /etc/sudoers.d/bastion - granular permissions (principle of least privilege)
bastion-admin ALL=(root) NOPASSWD: /usr/local/bin/security-test-suite.sh
bastion-admin ALL=(root) NOPASSWD: /usr/local/bin/bastion-emergency.sh audit-mode
bastion-admin ALL=(root) NOPASSWD: /usr/bin/journalctl
bastion-admin ALL=(root) NOPASSWD: /usr/bin/systemctl status *
bastion-admin ALL=(root) NOPASSWD: /usr/sbin/aide --check
bastion-admin ALL=(root) NOPASSWD: /usr/bin/lynis audit system *
bastion-admin ALL=(root) NOPASSWD: /usr/bin/aa-statusNote on
requiretty: This parameter prevents running sudo from non-interactive scripts (such as cron or CI/CD pipelines). On a bastion this is desirable — all privileged access must be interactive and auditable. On other server types, it may require exceptions.
Multi-factor authentication (2FA/MFA)
The SSH public key protects against brute-force attacks, but if an attacker steals the private key they get full access. The second factor (TOTP) mitigates this risk:
# Install
apt install -y libpam-google-authenticator
# Configure for SSH: publickey + mandatory TOTP
# /etc/ssh/sshd_config.d/hardening.conf (add)
AuthenticationMethods publickey,keyboard-interactive
# Configure PAM for SSH
# /etc/pam.d/sshd (add at the end)
auth required pam_google_authenticator.so nullok
# Enable challenge-response in SSH
# /etc/ssh/sshd_config.d/hardening.conf (add)
KbdInteractiveAuthentication yesEach user configures their TOTP:
# The user runs (NOT root):
google-authenticator -t -d -f -r 3 -R 30 -w 3 -Q UTF8
# This generates:
# - A QR code to scan with Google Authenticator/Authy/FreeOTP
# - Emergency codes (store offline)
# - File ~/.google_authenticator
# Recommended options:
# -t = Time-based (TOTP)
# -d = Do not allow token reuse
# -f = Force writing the file
# -r 3 -R 30 = Rate limiting: 3 attempts every 30 sec
# -w 3 = Window of 3 tokens (90 sec margin)
nullokin PAM: Allows login without 2FA if the user has not yet configured~/.google_authenticator. This is useful during the transition. Once all users have 2FA, change toauth required pam_google_authenticator.so(without nullok) to make it mandatory.
Resulting authentication flow:
- Client presents SSH public key → verified against
authorized_keys - Server requests TOTP code via keyboard-interactive
- User enters the 6-digit code from their app
- Access granted only if both factors are valid
Integration with enterprise identity managers
In corporate environments, local accounts do not scale. Integration with a centralized IdP enables:
- Centralized access management (automatic onboarding/offboarding)
- Unified password policies
- Centrally managed MFA
- Centralized access auditing
FreeIPA / Red Hat IdM
# Install the FreeIPA client
apt install -y freeipa-client
# Enroll the bastion in the domain
ipa-client-install \
--server=ipa.example.com \
--domain=example.com \
--realm=EXAMPLE.COM \
--mkhomedir \
--ssh-trust-dns \
--force-ntpd
# Configure SSSD to control access by group
cat >> /etc/sssd/sssd.conf << 'EOF'
[domain/example.com]
access_provider = ipa
ipa_hbac_refresh = 60
# Only allow a specific group
simple_allow_groups = bastion-admins, security-team
EOF
# HBAC (Host-Based Access Control) in FreeIPA
# Create a rule that only allows access to the bastion from the authorized group
# ipa hbacrule-add bastion-access
# ipa hbacrule-add-host bastion-access --hosts=bastion.example.com
# ipa hbacrule-add-user bastion-access --groups=bastion-admins
# Centralized sudo via FreeIPA
# ipa sudorule-add bastion-sudo
# ipa sudorule-add-host bastion-sudo --hosts=bastion.example.com
# ipa sudorule-add-user bastion-sudo --groups=bastion-admins
# ipa sudorule-add-runasuser bastion-sudo --users=root
# ipa sudorule-add-allow-command bastion-sudo --sudocmds="/usr/bin/journalctl"Microsoft Entra ID (Azure AD) + SSSD
# For Microsoft environments, integrate via SSSD + Kerberos
apt install -y sssd sssd-tools sssd-krb5 krb5-user realmd adcli
# Join the domain
realm join --user=admin EXAMPLE.ONMICROSOFT.COM
# /etc/sssd/sssd.conf
[sssd]
domains = example.onmicrosoft.com
services = nss, pam, ssh, sudo
[domain/example.onmicrosoft.com]
id_provider = ad
auth_provider = ad
access_provider = ad
ad_access_filter = memberOf=CN=Bastion-Admins,OU=Security,DC=example,DC=com
sudo_provider = ad
ldap_sudo_search_base = OU=Sudoers,DC=example,DC=com
# MFA via Entra ID Conditional Access
# Managed from the Azure Portal:
# - Conditional Access Policy → Require MFA for SSH access
# - Device compliance required
# - Location-based restrictionsGeneric LDAP (OpenLDAP, 389ds)
# /etc/sssd/sssd.conf for generic LDAP
[domain/ldap.example.com]
id_provider = ldap
auth_provider = ldap
ldap_uri = ldaps://ldap.example.com
ldap_search_base = dc=example,dc=com
ldap_tls_reqcert = demand
ldap_tls_cacert = /etc/ssl/certs/ca-ldap.pem
access_provider = simple
simple_allow_groups = bastion-sshSSH Certificate Authority (no passwords, no static keys)
The most secure solution eliminates authorized_keys entirely. A CA signs short-lived SSH certificates:
# In sshd_config:
TrustedUserCAKeys /etc/ssh/ca-user.pub # CA that signs user certificates
AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u
# The user obtains a temporary certificate (via Vault, Teleport, etc.)
# $ vault write ssh/sign/bastion public_key=@~/.ssh/id_ed25519.pub
# Generates a certificate valid for 8 hours with specific principals
# Advantages:
# - No authorized_keys to manage
# - Certificates expire automatically
# - Immediate revocation via CRL
# - Audit of who signed what and whenPhase 2: Full KSPP - Kernel parameters
The bootloader is configured with all the KSPP parameters plus additional extensions that go beyond the standard recommendations:
💡 Enterprise consideration: KSPP parameters vary by service profile (bastion vs webserver vs database). See the Decision Matrix.
GRUB_CMDLINE_LINUX_DEFAULT="quiet \
slab_nomerge \
slub_debug=FZP \
page_alloc.shuffle=1 \
page_poison=1 \
vsyscall=none \
init_on_alloc=1 \
init_on_free=1 \
pti=on \
randomize_kstack_offset=on \
spectre_v2=on \
spec_store_bypass_disable=seccomp \
l1tf=full,force \
mds=full,nosmt \
tsx=off \
tsx_async_abort=full,nosmt \
mmio_stale_data=full,nosmt \
retbleed=auto,nosmt \
kvm.nx_huge_pages=force \
lockdown=confidentiality \
module.sig_enforce=1 \
oops=panic \
debugfs=off \
lsm=landlock,lockdown,yama,integrity,apparmor,bpf \
extra_latent_entropy \
iommu=force \
intel_iommu=on \
efi=disable_early_pci_dma \
audit=1 \
audit_backlog_limit=8192"Explanation of advanced key parameters
| Parameter | Purpose | What's new |
|---|---|---|
slub_debug=FZP | Free/Zero/Poison checking in the allocator | Detects use-after-free |
randomize_kstack_offset=on | Randomizes the kernel stack offset | Anti-ROP in the kernel |
oops=panic | Immediate panic on oops | Avoids inconsistent state |
lsm=landlock,lockdown,yama,integrity,apparmor,bpf | Full LSM stack | Multiple MAC layers |
iommu=force | Force IOMMU for DMA | Anti-DMA attacks |
efi=disable_early_pci_dma | Block pre-boot DMA | Evil-maid protection |
mmio_stale_data=full,nosmt | MMIO mitigation | New CPU vulns |
retbleed=auto,nosmt | Retbleed mitigation | AMD/Intel 2022+ |
Phase 3: Sysctl - 60+ hardening parameters
The file /etc/sysctl.d/99-bastion-hardening.conf implements restrictions that go significantly beyond the CIS Benchmark:
# Kernel information protection
kernel.kptr_restrict = 2 # Hide kernel pointers completely
kernel.dmesg_restrict = 1 # Only root can read dmesg
kernel.yama.ptrace_scope = 3 # NEVER ptrace (not even root)
kernel.kexec_load_disabled = 1 # Impossible to load an alternative kernel
kernel.io_uring_disabled = 2 # Disable io_uring (huge attack surface)
kernel.sysrq = 0 # No magic sysrq
# Maximum ASLR
vm.mmap_rnd_bits = 32 # 32 bits of entropy in mmap
vm.mmap_min_addr = 65536 # Prevent null-pointer exploits
# Core dumps eliminated
fs.suid_dumpable = 0
kernel.core_pattern = |/bin/false
# Network: everything blocked by default
net.ipv4.icmp_echo_ignore_all = 1 # Do not respond to pings
net.ipv4.tcp_timestamps = 0 # No timestamps (fingerprinting)
net.ipv4.tcp_sack = 0 # Disable SACK (CVE-2019-11477)
net.ipv6.conf.all.disable_ipv6 = 1 # IPv6 unnecessary = disabled
# TTY hardening
dev.tty.ldisc_autoload = 0 # Do not autoload line disciplines
user.max_user_namespaces = 0 # No user namespaces (container escape)Difference from the CIS Benchmark
CIS recommends kernel.yama.ptrace_scope = 1. We use 3 (no one can use ptrace, not even root). On a bastion there is no reason for debugging in production.
CIS does not mention kernel.io_uring_disabled. io_uring has been the source of multiple critical CVEs (CVE-2022-29582, CVE-2023-2598) and is not needed on a bastion.
Phase 4: System-Wide Seccomp-BPF - The heart of the hardening
Concept: Per-service syscall restriction
In Linux we can restrict the syscalls a process can use by combining:
- Seccomp-BPF → filtering of system calls
- Systemd sandboxing (ProtectHome, PrivateTmp) → filesystem isolation
- AppArmor → additional reinforcement of paths and capabilities
💡 Enterprise consideration: The Seccomp profile generator allows creating per-service tailored profiles, exportable to Ansible for mass deployment.
Seccomp profile generator
We create a generator that produces Seccomp profiles organized by privilege levels:
#!/bin/bash
# seccomp-profile-generator.sh <service> <level>
# Levels: minimal | standard | network | permissive
# STDIO: basic I/O and memory operations
STDIO_SYSCALLS=(
"brk" "mmap" "mprotect" "munmap" "mremap" "madvise"
"clone" "exit" "exit_group" "getpid" "gettid"
"rt_sigaction" "rt_sigprocmask" "rt_sigreturn"
"futex" "nanosleep" "clock_gettime"
"close" "read" "write" "lseek" "fstat"
"arch_prctl" "prctl" "getrandom"
)
# RPATH: filesystem reading
RPATH_SYSCALLS=(
"openat" "access" "faccessat2" "getcwd"
"readlink" "stat" "getdents64"
)
# INET: network operations
INET_SYSCALLS=(
"socket" "bind" "connect" "listen" "accept4"
"sendto" "recvfrom" "epoll_create1" "epoll_ctl" "epoll_wait"
)
# ALWAYS DENIED (immediate KILL)
ALWAYS_DENY=(
"kexec_load" "init_module" "finit_module" "delete_module"
"bpf" "perf_event_open" "userfaultfd" "ptrace"
"mount" "umount2" "pivot_root" "chroot"
"io_uring_setup" "io_uring_enter" "io_uring_register"
)The minimal level only allows stdio + file reading. The network level adds sockets. A service like auditd uses minimal, SSH uses network.
Application via systemd (per service)
Lesson learned in production: A global drop-in in
/etc/systemd/system/service.d/looks appealing but breaks critical services like sshd, which needssetuid/setgid/sys_chrootfor its internal privilege separation. The aggressiveSystemCallFiltercauses SIGSYS (signal 31) and kills the process immediately. The same happens withProtectSystem=strict, which mounts/as read-only and prevents basic operations.
The correct strategy is to apply individual per-service drop-ins, explicitly excluding sshd and auditd which require special privileges:
# /etc/systemd/system/fail2ban.service.d/hardening.conf
# Also apply to: cron, systemd-timesyncd, and other non-privileged services
[Service]
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~@mount @swap @reboot @raw-io @debug @obsolete
SystemCallFilter=~io_uring_setup io_uring_enter io_uring_register
SystemCallFilter=~kexec_load kexec_file_load
SystemCallErrorNumber=EPERM
NoNewPrivileges=yes
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
MemoryDenyWriteExecute=true
RestrictNamespaces=true
LockPersonality=true
ProtectClock=true
ProtectHostname=trueFor sshd we use a more conservative drop-in — filesystem sandboxing without syscall filtering:
# /etc/systemd/system/ssh.service.d/hardening.conf
[Service]
ProtectHome=read-only
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
RestrictNamespaces=true
RestrictAddressFamilies=AF_INET AF_UNIX
NoNewPrivileges=no
LockPersonality=true
ProtectClock=true
ProtectHostname=true
SystemCallArchitectures=nativeCritical note: Do not include
SystemCallFilter,MemoryDenyWriteExecute=trueorProtectSystem=strictin the sshd drop-in. OpenSSH 9.8+ separatessshd(listener) fromsshd-session(session), and the session needs privileged syscalls for PAM, PTY allocation and privilege separation. Forcing Seccomp here causes immediate core-dumps.
Why per-service and not global
A global drop-in in service.d/ would seem more secure (deny-all), but in practice:
- Breaks critical services: sshd, auditd, and systemd internals need
@privileged - Unrecoverable remotely: If sshd dies from SIGSYS, you can only recover via physical console
- False security: The global drop-in is inherited but cannot be "subtracted" — you can only add exceptions that often nullify the entire filter
The correct approach is:
- Individual drop-ins for services that do not need privileges (fail2ban, cron, timesyncd)
- Conservative sandboxing for sshd (filesystem + namespaces, no syscall filter)
- AppArmor as a reinforcement layer for paths and capabilities (see the note on sshd below)
- Auditd without syscall sandboxing — it needs full access to the netlink audit socket
Phase 5: nftables firewall - Bidirectional deny-all
Unlike typical configurations that only filter INPUT, our bastion also controls outbound traffic:
💡 Enterprise consideration: In large fleets, the nftables rules are generated from Jinja2 templates with per-environment variables (management IPs, SIEM ranges).
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
iif "lo" accept
ct state established,related accept
ct state invalid drop
# Only SSH from the management network, rate-limited
tcp dport 22 ip saddr 192.168.1.0/24 \
ct state new limit rate 10/minute burst 20 packets accept
# Log + drop everything else
limit rate 10/minute log prefix "NFT-DROP: " level warn
counter drop
}
chain output {
type filter hook output priority 0; policy drop;
oif "lo" accept
ct state established,related accept
# Only DNS, apt and NTP allowed
tcp dport 53 accept
udp dport 53 accept
tcp dport { 80, 443 } accept
udp dport 123 accept
# Everything else logged and dropped
limit rate 5/minute log prefix "NFT-OUT-DROP: " level warn
counter drop
}
chain forward {
type filter hook forward priority 0; policy drop;
counter drop
}
}This means that if an attacker compromises a service, they cannot establish outbound connections (reverse shells, exfiltration) except to very specific ports.
Phase 6: Auditing with MITRE ATT&CK mapping
Critical requirement: The Debian 13 kernel has
CONFIG_AUDIT=ybut audit initializes as disabled by default. It is mandatory to addaudit=1 audit_backlog_limit=8192to the GRUB parameters. Without this, auditctl cannot open the netlink socket and all rules fail silently.
The auditd rules use tags that map directly to MITRE ATT&CK techniques for immediate correlation with SIEMs:
# /etc/audit/rules.d/bastion.rules
-D
-b 8192
-f 2
## CREDENTIAL ACCESS (T1003)
-w /etc/shadow -p wa -k T1003_credential_access
-w /etc/gshadow -p wa -k T1003_credential_access
-w /etc/passwd -p wa -k T1003_credential_access
## PERSISTENCE (T1547/T1053/T1543)
-a always,exit -F arch=b64 -S init_module -S finit_module -S delete_module -k T1547_kernel_module
-w /etc/crontab -p wa -k T1053_cron_persistence
-w /etc/cron.d/ -p wa -k T1053_cron_persistence
-w /var/spool/cron/ -p wa -k T1053_cron_persistence
-w /etc/systemd/system/ -p wa -k T1543_systemd_persistence
-w /usr/lib/systemd/system/ -p wa -k T1543_systemd_persistence
## PRIVILEGE ESCALATION (T1548)
-a always,exit -F arch=b64 -S setuid -S setgid -S setreuid -S setregid -k T1548_privilege_change
-w /etc/sudoers -p wa -k T1548_sudo_modify
-w /etc/sudoers.d/ -p wa -k T1548_sudo_modify
-a always,exit -F arch=b64 -S execve -F euid=0 -F auid!=0 -F auid!=-1 -k T1548_priv_exec
## DEFENSE EVASION (T1562/T1070)
-w /etc/apparmor.d/ -p wa -k T1562_modify_apparmor
-w /usr/sbin/auditctl -p x -k T1562_tamper_audit
-w /etc/audit/ -p wa -k T1562_tamper_audit
-w /var/log/ -p wa -k T1070_log_tampering
## LATERAL MOVEMENT (T1021)
-w /etc/ssh/sshd_config -p wa -k T1021_ssh_config
-w /etc/ssh/sshd_config.d/ -p wa -k T1021_ssh_config
## EXECUTION (T1059)
-a always,exit -F arch=b64 -S execve -k T1059_execution
## IMMUTABLE RULES after boot (requires reboot to modify)
-e 2The -e 2 flag is critical: once the rules are loaded, they cannot be modified without rebooting. An attacker who gains root cannot silence the auditing.
Phase 7: AIDE with real-time verification
AIDE runs every 15 minutes via a systemd timer and alerts immediately if it detects changes:
# Timer: every 15 min with 2 min jitter
[Timer]
OnBootSec=10min
OnUnitActiveSec=15min
RandomizedDelaySec=2min
Persistent=trueThe monitored attributes include SHA-256 + SHA-512 for critical security files, making it extremely difficult to create collisions:
SecurityFiles = p+u+g+s+b+m+c+sha256+sha512
/etc/apparmor.d SecurityFiles
/etc/seccomp SecurityFiles
/etc/ssh/sshd_config SecurityFiles
/boot BinlibPhase 8: Centralized telemetry - SIEM, EDR and Threat Hunting
A bastion without centralized telemetry is a blind spot. The auditd and AIDE rules generate local alerts, but if the attacker has root they can tamper with local logs (despite -e 2). The solution is to exfiltrate telemetry in real time to an external system.
💡 Enterprise consideration: The choice of SIEM depends on your existing stack. The telemetry section includes a comparison matrix to help decide.
Telemetry architecture
┌─────────────────────────┐
│ BASTION │
│ auditd → audisp-remote ─────┐
│ Wazuh Agent ─────────────────┼──→ SIEM (Wazuh/Elastic/Splunk)
│ Velociraptor Client ─────────┼──→ Velociraptor Server (DFIR)
│ journald → remote ───────────┘
└─────────────────────────┘Option 1: Wazuh Agent
Wazuh acts as an EDR + SIEM agent in a single component. It monitors file integrity (replaces AIDE), collects auditd logs, detects rootkits and sends everything to the manager:
# Install Wazuh Agent
curl -s https://packages.wazuh.com/key/GPG-KEY-WAZUH | gpg --dearmor -o /usr/share/keyrings/wazuh.gpg
echo "deb [signed-by=/usr/share/keyrings/wazuh.gpg] https://packages.wazuh.com/4.x/apt/ stable main" > /etc/apt/sources.list.d/wazuh.list
apt update && apt install -y wazuh-agent
# Configure the manager
cat > /var/ossec/etc/ossec.conf << 'EOF'
<ossec_config>
<client>
<server>
<address>WAZUH_MANAGER_IP</address>
<port>1514</port>
<protocol>tcp</protocol>
</server>
<enrollment>
<enabled>yes</enabled>
<agent_name>bastion-prod</agent_name>
<groups>linux,hardened,bastion</groups>
</enrollment>
</client>
<!-- Monitor auditd logs -->
<localfile>
<log_format>audit</log_format>
<location>/var/log/audit/audit.log</location>
</localfile>
<!-- Integrity of critical files (syscheck) -->
<syscheck>
<frequency>900</frequency>
<directories check_all="yes" realtime="yes">/etc/ssh,/etc/apparmor.d,/etc/audit,/etc/nftables.conf,/etc/sysctl.d</directories>
<directories check_all="yes">/boot,/usr/sbin/sshd,/usr/sbin/auditd</directories>
</syscheck>
<!-- Rootkit detection -->
<rootcheck>
<frequency>3600</frequency>
</rootcheck>
</ossec_config>
EOF
systemctl enable wazuh-agent
systemctl start wazuh-agentOption 2: Elastic Agent (Elastic SIEM)
# Download and install Elastic Agent
curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent-8.x-linux-x86_64.tar.gz
tar xzf elastic-agent-*.tar.gz
cd elastic-agent-*
# Enroll with the Fleet Server
./elastic-agent install \
--url=https://FLEET_SERVER:8220 \
--enrollment-token=TOKEN \
--insecureIn Kibana, configure the Auditd Manager + System integrations to automatically collect auditd events with the MITRE ATT&CK tags.
Option 3: Splunk Universal Forwarder
# Install the forwarder
dpkg -i splunkforwarder-*.deb
# Configure outputs
cat > /opt/splunkforwarder/etc/system/local/outputs.conf << 'EOF'
[tcpout]
defaultGroup = splunk_indexers
[tcpout:splunk_indexers]
server = SPLUNK_INDEXER:9997
sslCertPath = /opt/splunkforwarder/etc/certs/server.pem
sslPassword = ********
sslVerifyServerCert = true
EOF
# Monitor the audit log
cat > /opt/splunkforwarder/etc/system/local/inputs.conf << 'EOF'
[monitor:///var/log/audit/audit.log]
sourcetype = linux:audit
index = security
[monitor:///var/log/auth.log]
sourcetype = linux_secure
index = security
EOFOption 4: QRadar (shipping via syslog/LEEF)
# audisp-remote to ship audit logs via syslog
cat > /etc/audit/plugins.d/syslog.conf << 'EOF'
active = yes
direction = out
path = /sbin/audisp-syslog
type = always
args = LOG_LOCAL6
format = string
EOF
# rsyslog to send to QRadar
cat > /etc/rsyslog.d/qradar.conf << 'EOF'
local6.* @@QRADAR_IP:514;RSYSLOG_SyslogProtocol23Format
EOF
systemctl restart rsyslog
systemctl restart auditdVelociraptor: Live Threat Hunting and DFIR
Velociraptor is not a SIEM — it is a threat hunting and incident response tool that lets you run forensic queries on endpoints in real time. It complements the SIEM:
# Install the Velociraptor client
curl -L -o /usr/local/bin/velociraptor \
https://github.com/Velocidex/velociraptor/releases/latest/download/velociraptor-linux-amd64
chmod +x /usr/local/bin/velociraptor
# Generate the client config (from the server)
# velociraptor config client > client.config.yaml
# Install as a service
cat > /etc/systemd/system/velociraptor-client.service << 'EOF'
[Unit]
Description=Velociraptor Client
After=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/velociraptor client -config /etc/velociraptor/client.config.yaml
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
systemctl enable velociraptor-client
systemctl start velociraptor-clientWith Velociraptor you can run VQL queries live against the bastion:
-- Verify that KSPP kernel params are active
SELECT * FROM proc_cmdline() WHERE Key =~ "lockdown|pti|slab"
-- Look for processes without Seccomp
SELECT Pid, Name, Cmdline FROM pslist()
WHERE NOT SeccompMode
-- Verify the integrity of critical files
SELECT FullPath, hash(path=FullPath, hashselect="SHA256") as Hash
FROM glob(globs="/etc/ssh/**")
-- Detect loaded kernel modules (should be minimal)
SELECT * FROM modules()nftables considerations for telemetry
The outbound deny-all firewall requires additional rules to allow telemetry shipping:
# Add to the output chain to allow shipping to the SIEM
# Wazuh
tcp dport 1514 ip daddr WAZUH_MANAGER_IP accept
# Elastic
tcp dport 8220 ip daddr FLEET_SERVER_IP accept
# Splunk
tcp dport 9997 ip daddr SPLUNK_INDEXER_IP accept
# Velociraptor
tcp dport 8000 ip daddr VELOCIRAPTOR_SERVER_IP accept
# Syslog (QRadar)
tcp dport 514 ip daddr QRADAR_IP acceptComparison matrix
| Capability | Wazuh | Elastic SIEM | Splunk | QRadar | Velociraptor |
|---|---|---|---|---|---|
| Log collection | Yes | Yes | Yes | Yes | No (not a SIEM) |
| File integrity | Yes (syscheck) | With module | With addon | Not native | Yes (VQL) |
| Rootkit detection | Yes | No | No | No | Yes (hunting) |
| MITRE mapping | Yes | Yes | Yes (ES) | Yes | Yes |
| Threat hunting | Limited | KQL | SPL | AQL | VQL (advanced) |
| Live DFIR | No | No | No | No | Yes |
| Cost | Open source | License | License | License | Open source |
| Recommendation | Enterprise | Enterprise | Enterprise | Enterprise | Complement |
Recommendation for bastions
The ideal combination for a bastion is:
- Wazuh Agent as SIEM agent + FIM + rootkit detection (replaces AIDE)
- Velociraptor Client as an on-demand hunting/DFIR tool
- auditd with MITRE rules as the source of kernel events (immutable)
With this combination you have: real-time detection (Wazuh), immutable kernel auditing (auditd -e 2), integrity verification (Wazuh syscheck), and live forensic investigation capability (Velociraptor).
Phase 9: Kernel module blacklisting
A bastion does not need USB, Bluetooth, wireless or exotic network protocols. Every unnecessary module is attack surface:
# /etc/modprobe.d/bastion-blacklist.conf
# USB storage (exfiltration vector)
blacklist usb-storage
install usb-storage /bin/false
# Protocols with a history of vulnerabilities
blacklist dccp # CVE-2017-6074
blacklist sctp # Multiple CVEs
blacklist rds # CVE-2010-3904
blacklist tipc # CVE-2022-0435
# DMA attacks
blacklist firewire-core
blacklist thunderbolt
install thunderbolt /bin/false
# Wireless/Bluetooth (unnecessary on a server)
blacklist cfg80211
blacklist bluetoothPhase 10: SSH - Post-quantum algorithms
The SSH configuration uses the strongest algorithms available, including key exchange resistant to quantum computing:
# /etc/ssh/sshd_config.d/hardening.conf
# Restricted access
Port 22
AddressFamily inet
ListenAddress 192.168.1.200
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
MaxAuthTries 3
MaxSessions 2
LoginGraceTime 30
# Key exchange: sntrup761 is post-quantum hybrid + X25519
KexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256
# AEAD ciphers only
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com
# Encrypt-then-MAC MACs
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
# Host keys (keep RSA for compatibility with legacy clients)
HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256
# Zero forwarding
AllowTcpForwarding no
AllowAgentForwarding no
AllowStreamLocalForwarding no
X11Forwarding no
PermitTunnel no
GatewayPorts no
PermitUserEnvironment no
# Timeouts
ClientAliveInterval 300
ClientAliveCountMax 2
# Verbose logging for correlation with auditd
LogLevel VERBOSE
Banner /etc/issue.netNote on AppArmor and sshd: In Debian 13, OpenSSH 9.8+ separates the process into
sshd(listener) andsshd-session(user session). Creating a custom AppArmor profile for sshd is extremely fragile — the profile transition between the two processes requires exact paths that vary between versions, and enforce mode causes "Connection reset" without generating useful DENIED logs. The recommendation is to protect sshd via: (1) hardened config, (2) nftables rate-limiting, (3) fail2ban, (4) auditd monitoring. The real sandboxing is provided by OpenSSH's internal privilege separation.
Validation: Security Test Suite
The bastion includes an automated validation script that verifies the 12 hardening areas:
$ sudo /usr/local/bin/security-test-suite.sh
============================================================
BASTION SECURITY TEST SUITE
============================================================
[1/12] KSPP kernel parameters
✓ slab_nomerge
✓ init_on_alloc=1
✓ pti=on
✓ lockdown=confidentiality
...
[4/12] Seccomp-BPF on services
✓ ssh: SystemCallFilter active
✓ auditd: hardening drop-in present
✓ Seccomp profiles generated: 3
[5/12] nftables firewall
✓ nftables active
✓ Deny-all policy (input + forward)
============================================================
RESULTS
Passed: 42 / 45
Warnings: 3 / 45
Failed: 0 / 45
STATUS: BASTION OPERATIONAL
============================================================Emergency mode
For situations where hardening prevents diagnosing problems, there is an audit mode that swaps blocks for logs:
# Audit mode: AppArmor complain + Seccomp log (without blocking)
sudo /usr/local/bin/bastion-emergency.sh audit-mode
# Monitor what would have been blocked
journalctl -f | grep -i 'apparmor\|seccomp\|denied'
# Restore full hardening
sudo /usr/local/bin/bastion-emergency.sh restore-hardeningValidation with Lynis
Result of lynis audit system on the server with all hardening applied:
Hardening index : 79 [############### ]
Tests performed : 256
Warnings : 1 (AIDE database not yet initialized)
Suggestions : 26Score 79/100 is solid for a bastion with this configuration. The pending suggestions are minor:
GRUB password— not applicable on a VM with console restricted by the hypervisorTCPKeepAlive=NOin SSH — we prefer to keep it to detect dead connectionsumask 027— already applied via PAM but login.defs uses 022 as fallbackprocess accounting— covered by auditd with MITRE rulesmalware scanner— Wazuh FIM + AIDE cover this function
To raise the score to 85+ it is enough to: initialize the AIDE database (aideinit), install libpam-tmpdir, and configure a GRUB password.
Operational success metrics
A Lynis score does not mean you are secure. These metrics do indicate whether the hardening is working:
Prevention
| Metric | How to measure | Target | |
|---|---|---|---|
| Syscalls blocked by Seccomp | `journalctl -k | grep -c seccomp` | 0 in normal operation (blocks = attack or misconfiguration) |
| Connections blocked by nftables | nft list counters | Stable trend; spikes = external scanning | |
| Kernel modules rejected | auditd rule init_module | 0 unauthorized attempts |
Detection
| Metric | How to measure | Target |
|---|---|---|
| Mean time to detect (MTTD) | auditd timestamp → SIEM alert timestamp | < 5 min for critical events |
| False positives per day | SIEM alerts flagged as FP | < 5/day per bastion |
| MITRE ATT&CK coverage | auditd rules with T* tag / applicable techniques | > 80% |
Resilience
| Metric | How to measure | Target |
|---|---|---|
| Configuration MTTR | Simulate drift → time to restore baseline | < 15 min with Ansible |
| Performance impact | Pre/post hardening benchmark | < 10% degradation |
| Recovery success | Run the emergency runbook quarterly | 100% |
These metrics should feed an executive dashboard. Security that is not measured is not managed.
Conclusions
This bastion implements 10 security layers that operate independently, validated in a real environment (Debian 13 Trixie, kernel 6.12.88). An attacker would need to:
- Evade the nftables firewall (bidirectional deny-all, rate-limited)
- Authenticate via SSH with a public key (no password, post-quantum algorithms)
- Escape the Seccomp-BPF sandbox (critical syscalls = KILL on non-privileged services)
- Avoid detection by auditd (immutable rules -e 2, 20 MITRE ATT&CK rules)
- Not trigger SIEM alerts (telemetry exfiltrated in real time, locally irrevocable)
- Not trigger AIDE/Wazuh syscheck (verification every 15 min, SHA-512 hash)
- All of this on a kernel with full KSPP (lockdown=confidentiality, pti, 32-bit ASLR)
Lessons learned in production
During the real implementation we found that:
ProtectSystem=strictin global drop-ins breaks the system: It mounts/as read-only for all services including systemd-core ones. Only use per-service.SystemCallFilteris incompatible with sshd: OpenSSH needs@privileged(setuid, setgid, chroot) for its privilege separation. Forcing Seccomp here causes SIGSYS (signal 31) and immediate core-dump.- AppArmor enforce for sshd is fragile on Debian 13: The sshd/sshd-session separation (OpenSSH 9.8+) makes profiles require constant maintenance between versions.
audit=1is mandatory in GRUB: Without this parameter, the audit subsystem initializes as disabled even thoughCONFIG_AUDIT=yin the kernel.- Aggressive rate-limiting in nftables causes lockouts: 3/minute with burst 5 is too restrictive during testing. Use 10/minute burst 20 and reduce in production.
- tcp_wrappers (libwrap) is still active on Debian 13: sshd-session tries to read
/etc/hosts.allowand/etc/hosts.deny. If AppArmor or sandboxing prevents access, the connection is silently rejected.
The philosophy is clear: there is no implicit trust. Each layer assumes the previous ones have been compromised. But the implementation must be pragmatic — a system you cannot administer remotely is not secure, it is useless.
All the configuration is applicable in any environment — from a laptop running Debian to an enterprise VMware cluster. The configuration files are the same regardless of where it is deployed.
Mapping to regulatory frameworks
The auditd rules map to MITRE ATT&CK, but regulated environments need traceability to compliance frameworks:
| Technical control | CIS Benchmark | PCI DSS 4.0 | NIST 800-53 | ISO 27001 | SOC 2 |
|---|---|---|---|---|---|
kptr_restrict=2 | 1.5.2 | Req 2.2.1 | SC-39 | A.12.6.1 | CC6.1 |
ptrace_scope=3 | 1.5.4 | Req 2.2.1 | AC-3 | A.9.4.1 | CC6.1 |
| nftables deny-all | 3.5.x | Req 1.2-1.5 | SC-7 | A.13.1.1 | CC6.6 |
| SSH key-only | 5.3.x | Req 8.3.1 | IA-2 | A.9.2.1 | CC6.1 |
| auditd -e 2 | 4.1.x | Req 10.2-10.3 | AU-9 | A.12.4.2 | CC7.2 |
| Seccomp per-service | - | Req 2.2.1 | SC-39 | A.12.6.1 | CC6.1 |
| AIDE/FIM | 1.3.x | Req 11.5 | SI-7 | A.12.4.3 | CC7.1 |
| lockdown=confidentiality | - | Req 2.2.1 | SC-34 | A.12.5.1 | CC6.1 |
| Telemetry to SIEM | 4.1.x | Req 10.6 | AU-6 | A.12.4.1 | CC7.2 |
Limitations and residual attack vectors
No hardening is absolute. This section honestly documents what this guide does NOT solve and the vectors that require additional layers:
What lockdown + KSPP do not cover
| Vector | Why it persists | Additional mitigation |
|---|---|---|
| Use-after-free in valid kernel code | lockdown blocks direct memory access, not logic bugs | Intel TDX / AMD SEV-SNP with hardware-encrypted memory |
| Side-channels (new Spectre variants) | Mitigations cover known variants, not future ones | Microcode updates + isolation via CPU pinning |
| Compromised firmware/UEFI | lockdown operates at the kernel level, not firmware | Coreboot + Heads, measured boot with TPM 2.0 |
| Intel ME / AMD PSP | Coprocessors with full memory access | Hardware without ME (System76, Purism) or me_cleaner |
| Modules signed with a stolen key | module.sig_enforce validates the signature, not the CA's integrity | Signing keys in HSM (YubiHSM), periodic rotation |
What Seccomp-BPF cannot block
- Vulnerabilities within allowed syscalls: a buffer overflow in
read()requires no extra syscalls - Exfiltration via legitimate syscalls:
sendto()to a server allowed by nftables - New syscalls in future kernels: if the list is not updated, they remain allowed by default
Mitigation: combine Seccomp with Landlock (filesystem path restriction), eBPF LSM for dynamic policies, or micro-VMs (Firecracker/gVisor) for complete isolation.
Network: residual vectors with deny-all
- Established connections:
ct state established acceptallows tunneling traffic over legitimate connections - DNS tunneling: if outbound UDP/53 is allowed, tools like
iodine/dnscat2can exfiltrate data - Compromise of allowed destinations: if the apt or NTP server is compromised, the traffic is "legitimate"
Mitigation: local DNS proxy with a domain allowlist, mTLS for all outbound connections, egress filtering at the FQDN level (not just IP).
Authentication: the human link
- TOTP is phishable: an attacker can obtain the code in real time via a fake site
- Stolen SSH certificates: valid until expiration even if a compromise is detected
- Compromised administrator: if the admin's endpoint has a keylogger, all the hardening is irrelevant
Mitigation: FIDO2/WebAuthn (phishing-resistant), certificates with TTL < 1 hour, dedicated Privileged Access Workstations (PAW).
Auditing: what auditd does not see
- An attacker with root can saturate the audit buffer before executing malicious actions
- If the receiving SIEM is compromised, logs can be manipulated or ignored
- AIDE can be evaded if the attacker updates the database before the next check
Mitigation: WORM storage for logs (S3 Object Lock), multi-party logging to independent SIEMs, fs-verity for block-level integrity.
Supply chain: transitive trust
Every package installed via apt depends on a chain of trust:
- An uncompromised Debian mirror
- Unstolen GPG signing keys
- Transitive dependencies (libssl, libc6) without backdoors
Mitigation: reproducible builds, binary transparency logs, IMA/EVM for runtime binary verification, strict version pinning.
Conclusion on limitations
This guide solidly covers the software layers of hardening (kernel, userspace, network, authentication, auditing). For maximum-sensitivity environments (defense, critical infrastructure) the following are also required:
- Hardware trust: TPM 2.0, measured boot, remote attestation
- Physical isolation: air-gapped SIEMs, HSMs for keys
- Organizational processes: Zero Trust for administrators, just-in-time access, PAWs
Security is a continuous process, not a final state. This bastion is a solid starting point — not a destination.
References
- Kernel Self-Protection Project (KSPP)
- Systemd Security Sandboxing
- MITRE ATT&CK Linux Matrix
- CIS Debian Linux Benchmark
- Madaidan's Linux Hardening Guide
- Wazuh Documentation
- Velociraptor - Digital Forensics and Incident Response
- OpenSSH 9.8 Release Notes (sshd-session separation)
- NIST SP 800-53 Rev. 5 - Security Controls
- PCI DSS v4.0 - Requirement 2 (Secure Configurations)
Comments