Home Linux & Systems Cybersecurity Cloud & DevOps Networks & Infrastructure SIEM & Monitoring DFIR & Threat Intel Development & Other All categories Projects About Tools

Hardened Bastion on Debian 13: Extreme Hardening with KSPP, Seccomp-BPF and SIEM Telemetry

Leer en espanol
Hardened Bastion on Debian 13: Extreme Hardening with KSPP, Seccomp-BPF and SIEM Telemetry

Table of contents

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.

PrincipleImplementation
Deny-all syscallsSeccomp-BPF per-service via systemd (sshd excluded)
Deny-all networknftables with DROP policy on input, output and forward
Deny-all filesystemModule blacklist + noexec partitions
Deny-all modulesAggressive blacklist + module.sig_enforce
Deny-all capabilitiesMinimal CapabilityBoundingSet per service
Immutable kernellockdown=confidentiality + immutable auditd (-e 2)
Irrevocable telemetryReal-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

CODE
┌─────────────────────────────────────────────────────┐
│              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)

LevelExampleImplication
CriticalProduction bastion, payment systemsMaximum hardening, even with operational impact
HighApplication servers with sensitive dataAdapted hardening, prioritizing detection
MediumStaging environments, internal toolsBase hardening + monitoring
LowLabs, ephemeral VMsMinimal hardening, focus on network isolation

2. Who could attack us (Threat Modeling)

ActorMotivationPriority controls
Script kiddie / BotnetOpportunism, cryptominingStrict firewall, automatic updates
Competitor / EspionageIP theftAdvanced telemetry, FIM, immutable auditing
APT / Nation-statePersistence in critical infrastructureKernel hardening, remote attestation, Zero Trust
Malicious insiderSabotage, exfiltrationSegregation 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:

ControlBastionWeb AppDatabaseContainer Host
lockdown=confidentialityYesIntegrityIntegrityIntegrity
io_uring_disabled=2YesYesNo (*)Yes
user.max_user_namespaces=0YesYesYesNo (**)
Seccomp per-serviceAggressiveAdaptedMinimalContainer-native
nftables output deny-allYesFQDN allowlistDB peers allowlistRegistry allowlist
ptrace_scope=3YesScope=2Scope=2Scope=2
FIM every 15 minYesEvery 1hConfigs onlyBinaries only
Mandatory MFAYesYesYesAdmin 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

ControlBastionWeb ServerDatabaseContainer host
user.max_user_namespaces0001024 (Docker needs it)
lockdownconfidentialityintegrityintegrityintegrity
slub_debug=FZPYesNo (perf impact ~5-15%)NoNo
init_on_free=1YesYesNo (I/O impact)Yes
io_uring_disabled220 (PostgreSQL uses io_uring)2
module.sig_enforce110 (DKMS/drivers)0 (overlay fs)
nftables outputOnly DNS/APT/NTP/SIEM+HTTP/HTTPS to backends+DB ports+registry
Seccomp per-serviceYes (aggressive)Yes (adapted)No (performance)Yes (containerd)

Real performance impact: slub_debug=FZP + init_on_free=1 + page_poison=1 can 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

ToolConflicting controlSolution
Docker/Podmanuser.max_user_namespaces=0Raise to 1024+ on container hosts
DKMS / proprietary drivers (NVIDIA, etc.)module.sig_enforce=1Disable or sign modules with MOK
eBPF tools (Cilium, Falco, bpftrace)lockdown=confidentialityUse lockdown=integrity or exclude
Backup agents (Veeam, NetBackup)Seccomp @system-serviceSpecific drop-in without SystemCallFilter
Monitoring with perf/flamegraphkernel.yama.ptrace_scope=3Reduce to 2 in profiling environments
Enterprise antivirus (CrowdStrike, SentinelOne)lockdown=confidentiality + module.sigRequires 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:

HypervisorRecommended actions
VMware ESXi/WorkstationDisable: copy-paste, drag-drop, shared folders, 3D acceleration. Enable Secure Boot on the VM
Proxmox/KVMUse machine type q35, disable USB passthrough, tablet device and audio. Enable cpu host
Hyper-VDisable unnecessary Integration Services, enable Secure Boot, use Generation 2
XenUse HVM mode with IOMMU, disable unnecessary PV drivers
Bare-metalBIOS/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:

BASH
# 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 -y

Create the administration user

Access to the bastion is done exclusively via SSH with public key. No password login, no direct root access:

BASH
# 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 root

Password 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:

BASH
# /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
BASH
# /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
BASH
# 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=900

This 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:

BASH
# /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
BASH
# /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-status

Note 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:

BASH
# 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 yes

Each user configures their TOTP:

BASH
# 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)

nullok in 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 to auth required pam_google_authenticator.so (without nullok) to make it mandatory.

Resulting authentication flow:

  1. Client presents SSH public key → verified against authorized_keys
  2. Server requests TOTP code via keyboard-interactive
  3. User enters the 6-digit code from their app
  4. 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

BASH
# 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

BASH
# 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 restrictions

Generic LDAP (OpenLDAP, 389ds)

BASH
# /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-ssh

SSH Certificate Authority (no passwords, no static keys)

The most secure solution eliminates authorized_keys entirely. A CA signs short-lived SSH certificates:

BASH
# 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 when

Phase 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.

BASH
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

ParameterPurposeWhat's new
slub_debug=FZPFree/Zero/Poison checking in the allocatorDetects use-after-free
randomize_kstack_offset=onRandomizes the kernel stack offsetAnti-ROP in the kernel
oops=panicImmediate panic on oopsAvoids inconsistent state
lsm=landlock,lockdown,yama,integrity,apparmor,bpfFull LSM stackMultiple MAC layers
iommu=forceForce IOMMU for DMAAnti-DMA attacks
efi=disable_early_pci_dmaBlock pre-boot DMAEvil-maid protection
mmio_stale_data=full,nosmtMMIO mitigationNew CPU vulns
retbleed=auto,nosmtRetbleed mitigationAMD/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:

BASH
# 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:

BASH
#!/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 needs setuid/setgid/sys_chroot for its internal privilege separation. The aggressive SystemCallFilter causes SIGSYS (signal 31) and kills the process immediately. The same happens with ProtectSystem=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:

BASH
# /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=true

For sshd we use a more conservative drop-in — filesystem sandboxing without syscall filtering:

BASH
# /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=native

Critical note: Do not include SystemCallFilter, MemoryDenyWriteExecute=true or ProtectSystem=strict in the sshd drop-in. OpenSSH 9.8+ separates sshd (listener) from sshd-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:

  1. Breaks critical services: sshd, auditd, and systemd internals need @privileged
  2. Unrecoverable remotely: If sshd dies from SIGSYS, you can only recover via physical console
  3. 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:

  1. Individual drop-ins for services that do not need privileges (fail2ban, cron, timesyncd)
  2. Conservative sandboxing for sshd (filesystem + namespaces, no syscall filter)
  3. AppArmor as a reinforcement layer for paths and capabilities (see the note on sshd below)
  4. 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).

NFT
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=y but audit initializes as disabled by default. It is mandatory to add audit=1 audit_backlog_limit=8192 to 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:

BASH
# /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 2

The -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:

BASH
# Timer: every 15 min with 2 min jitter
[Timer]
OnBootSec=10min
OnUnitActiveSec=15min
RandomizedDelaySec=2min
Persistent=true

The monitored attributes include SHA-256 + SHA-512 for critical security files, making it extremely difficult to create collisions:

CODE
SecurityFiles = p+u+g+s+b+m+c+sha256+sha512
/etc/apparmor.d SecurityFiles
/etc/seccomp SecurityFiles
/etc/ssh/sshd_config SecurityFiles
/boot Binlib

Phase 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

CODE
┌─────────────────────────┐
│       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:

BASH
# 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-agent

Option 2: Elastic Agent (Elastic SIEM)

BASH
# 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 \
  --insecure

In Kibana, configure the Auditd Manager + System integrations to automatically collect auditd events with the MITRE ATT&CK tags.

Option 3: Splunk Universal Forwarder

BASH
# 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
EOF

Option 4: QRadar (shipping via syslog/LEEF)

BASH
# 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 auditd

Velociraptor: 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:

BASH
# 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-client

With Velociraptor you can run VQL queries live against the bastion:

SQL
-- 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:

NFT
# 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 accept

Comparison matrix

CapabilityWazuhElastic SIEMSplunkQRadarVelociraptor
Log collectionYesYesYesYesNo (not a SIEM)
File integrityYes (syscheck)With moduleWith addonNot nativeYes (VQL)
Rootkit detectionYesNoNoNoYes (hunting)
MITRE mappingYesYesYes (ES)YesYes
Threat huntingLimitedKQLSPLAQLVQL (advanced)
Live DFIRNoNoNoNoYes
CostOpen sourceLicenseLicenseLicenseOpen source
RecommendationEnterpriseEnterpriseEnterpriseEnterpriseComplement

Recommendation for bastions

The ideal combination for a bastion is:

  1. Wazuh Agent as SIEM agent + FIM + rootkit detection (replaces AIDE)
  2. Velociraptor Client as an on-demand hunting/DFIR tool
  3. 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:

BASH
# /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 bluetooth

Phase 10: SSH - Post-quantum algorithms

The SSH configuration uses the strongest algorithms available, including key exchange resistant to quantum computing:

CODE
# /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.net

Note on AppArmor and sshd: In Debian 13, OpenSSH 9.8+ separates the process into sshd (listener) and sshd-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:

BASH
$ 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:

BASH
# 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-hardening

Validation with Lynis

Result of lynis audit system on the server with all hardening applied:

CODE
Hardening index : 79 [###############     ]
Tests performed : 256
Warnings        : 1 (AIDE database not yet initialized)
Suggestions     : 26

Score 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 hypervisor
  • TCPKeepAlive=NO in SSH — we prefer to keep it to detect dead connections
  • umask 027 — already applied via PAM but login.defs uses 022 as fallback
  • process accounting — covered by auditd with MITRE rules
  • malware 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

MetricHow to measureTarget
Syscalls blocked by Seccomp`journalctl -kgrep -c seccomp`0 in normal operation (blocks = attack or misconfiguration)
Connections blocked by nftablesnft list countersStable trend; spikes = external scanning
Kernel modules rejectedauditd rule init_module0 unauthorized attempts

Detection

MetricHow to measureTarget
Mean time to detect (MTTD)auditd timestamp → SIEM alert timestamp< 5 min for critical events
False positives per daySIEM alerts flagged as FP< 5/day per bastion
MITRE ATT&CK coverageauditd rules with T* tag / applicable techniques> 80%

Resilience

MetricHow to measureTarget
Configuration MTTRSimulate drift → time to restore baseline< 15 min with Ansible
Performance impactPre/post hardening benchmark< 10% degradation
Recovery successRun the emergency runbook quarterly100%

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:

  1. Evade the nftables firewall (bidirectional deny-all, rate-limited)
  2. Authenticate via SSH with a public key (no password, post-quantum algorithms)
  3. Escape the Seccomp-BPF sandbox (critical syscalls = KILL on non-privileged services)
  4. Avoid detection by auditd (immutable rules -e 2, 20 MITRE ATT&CK rules)
  5. Not trigger SIEM alerts (telemetry exfiltrated in real time, locally irrevocable)
  6. Not trigger AIDE/Wazuh syscheck (verification every 15 min, SHA-512 hash)
  7. 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=strict in global drop-ins breaks the system: It mounts / as read-only for all services including systemd-core ones. Only use per-service.
  • SystemCallFilter is 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=1 is mandatory in GRUB: Without this parameter, the audit subsystem initializes as disabled even though CONFIG_AUDIT=y in 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.allow and /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 controlCIS BenchmarkPCI DSS 4.0NIST 800-53ISO 27001SOC 2
kptr_restrict=21.5.2Req 2.2.1SC-39A.12.6.1CC6.1
ptrace_scope=31.5.4Req 2.2.1AC-3A.9.4.1CC6.1
nftables deny-all3.5.xReq 1.2-1.5SC-7A.13.1.1CC6.6
SSH key-only5.3.xReq 8.3.1IA-2A.9.2.1CC6.1
auditd -e 24.1.xReq 10.2-10.3AU-9A.12.4.2CC7.2
Seccomp per-service-Req 2.2.1SC-39A.12.6.1CC6.1
AIDE/FIM1.3.xReq 11.5SI-7A.12.4.3CC7.1
lockdown=confidentiality-Req 2.2.1SC-34A.12.5.1CC6.1
Telemetry to SIEM4.1.xReq 10.6AU-6A.12.4.1CC7.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

VectorWhy it persistsAdditional mitigation
Use-after-free in valid kernel codelockdown blocks direct memory access, not logic bugsIntel TDX / AMD SEV-SNP with hardware-encrypted memory
Side-channels (new Spectre variants)Mitigations cover known variants, not future onesMicrocode updates + isolation via CPU pinning
Compromised firmware/UEFIlockdown operates at the kernel level, not firmwareCoreboot + Heads, measured boot with TPM 2.0
Intel ME / AMD PSPCoprocessors with full memory accessHardware without ME (System76, Purism) or me_cleaner
Modules signed with a stolen keymodule.sig_enforce validates the signature, not the CA's integritySigning 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 accept allows tunneling traffic over legitimate connections
  • DNS tunneling: if outbound UDP/53 is allowed, tools like iodine/dnscat2 can 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:

  1. An uncompromised Debian mirror
  2. Unstolen GPG signing keys
  3. 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

Comments