Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 102 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -674,9 +674,13 @@ timedatectl status

### Kernel Hardening (sysctl)

Save as `/etc/sysctl.d/99-hardening.conf` (identical on every distro):
The `sysctl` step writes `/etc/sysctl.d/99-hardening.conf` and runs
`sudo sysctl --system`. The profile below is what the script actually emits, in
the same order. It is identical on every distro except for the two server-only
keys noted at the end.

```
# information disclosure
# Kernel security hardening
kernel.dmesg_restrict=1
kernel.kptr_restrict=2
kernel.unprivileged_bpf_disabled=1
Expand All @@ -690,27 +694,115 @@ fs.protected_symlinks=1
fs.protected_hardlinks=1
fs.protected_fifos=2
fs.protected_regular=2
kernel.panic=60
kernel.panic_on_oops=1
kernel.perf_event_paranoid=2
vm.mmap_rnd_bits=32
vm.mmap_rnd_compat_bits=16

# Network hardening - reverse path filtering
net.ipv4.conf.all.rp_filter=1
net.ipv4.conf.default.rp_filter=1

# SYN flood protection
net.ipv4.tcp_syncookies=1
net.ipv4.tcp_max_syn_backlog=2048
net.ipv4.tcp_synack_retries=2
net.ipv4.tcp_syn_retries=5

# network stack
net.ipv4.ip_forward=0
# Source route / redirects
net.ipv4.conf.all.accept_source_route=0
net.ipv4.conf.default.accept_source_route=0
net.ipv4.conf.all.accept_redirects=0
net.ipv4.conf.default.accept_redirects=0
net.ipv4.conf.all.secure_redirects=0
net.ipv4.conf.default.secure_redirects=0
net.ipv4.conf.all.send_redirects=0
net.ipv4.conf.all.accept_source_route=0
net.ipv4.conf.default.accept_source_route=0
net.ipv4.conf.all.rp_filter=1
net.ipv4.conf.default.rp_filter=1
net.ipv4.conf.default.send_redirects=0

# Martian logging & ICMP
net.ipv4.conf.all.log_martians=1
net.ipv4.conf.default.log_martians=1
net.ipv4.icmp_echo_ignore_broadcasts=1
net.ipv4.tcp_syncookies=1
net.ipv4.icmp_ignore_bogus_error_responses=1

# Time-wait assassination protection
net.ipv4.tcp_rfc1337=1

# TCP tuning
net.ipv4.tcp_fin_timeout=15
net.ipv4.tcp_tw_reuse=1
net.ipv4.tcp_keepalive_time=1200
net.ipv4.tcp_keepalive_intvl=30
net.ipv4.tcp_keepalive_probes=3
net.ipv4.tcp_no_metrics_save=1
net.ipv4.tcp_moderate_rcvbuf=1

# ARP hardening
net.ipv4.conf.all.arp_ignore=1
net.ipv4.conf.all.arp_announce=2
net.ipv4.conf.all.proxy_arp=0

# Forwarding (disable if not a router)
net.ipv4.conf.all.forwarding=0
net.ipv4.conf.default.forwarding=0

# IPv6 hardening
net.ipv6.conf.all.accept_redirects=0
net.ipv6.conf.default.accept_redirects=0
net.ipv6.conf.all.accept_ra=0
net.ipv6.conf.default.accept_ra=0
net.ipv6.conf.all.autoconf=0
net.ipv6.conf.default.autoconf=0

# Congestion control & buffers
net.core.default_qdisc=fq
net.ipv4.tcp_congestion_control=bbr
net.core.rmem_max=134217728
net.core.wmem_max=134217728
net.ipv4.tcp_rmem=4096 87380 134217728
net.ipv4.tcp_wmem=4096 65536 134217728
net.core.somaxconn=4096
```

Apply:
Server-only (added on servers, skipped when a container runtime is detected):

```
user.max_user_namespaces=0
```

Apply manually:
```bash
sudo sysctl --system
```

Notes:
- `net.ipv4.conf.*.forwarding=0` replaces the older `net.ipv4.ip_forward=0`;
per-interface forwarding is what modern network namespaces and containers use.
- `tcp_fin_timeout`, `tcp_tw_reuse`, keepalive and buffer sizes are capacity
tuning, not exploit mitigations. They are safe defaults, but on a busy host
tune them to your link rather than treating them as security settings.
- `net.ipv4.tcp_congestion_control=bbr` is ignored on kernels without BBR; the
rest of the file still applies.

### Attack surface reduction (built-in)

The `attack_surface` step (`--step attack_surface`) applies, all backed up and
logged to the rollback log first:

- **Filesystem blacklist** — `cramfs`, `freevxfs`, `jffs2`, `hfs`, `hfsplus`,
`udf`, plus `squashfs` when no snap runtime is present. Written to
`/etc/modprobe.d/blacklist-filesystems.conf`.
- **SSH crypto** — restricts `Ciphers`, `MACs` and `KexAlgorithms` to modern
AEAD/curve255 suites. Validates with `sshd -t`, backs up and restores
`sshd_config.d` drop-ins, then reloads (never restarts, so the session
survives).
- **`/etc/login.defs`** — `PASS_MAX_DAYS 90`, `PASS_MIN_DAYS 7`,
`PASS_WARN_AGE 14`. An existing `ENCRYPT_METHOD` (yescrypt/bcrypt) is kept;
it is never downgraded to `SHA512`.
- **Core dumps** — `* hard core 0` and `* soft core 0` in
`/etc/security/limits.conf`.

### Mandatory Access Control: AppArmor vs SELinux

| Family | MAC system | Status | Script action |
Expand Down
62 changes: 62 additions & 0 deletions tests/test_linuxinstall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,68 @@ if [ -f "$SRC" ]; then
fail_t "harden step reachable via --step: $_step" "_valid_step rejected a wired-up step"
fi
done

# --- README <-> code consistency -----------------------------------------
# The README documents the sysctl profile as a copy-paste block. It drifted
# from the script (stale net.ipv4.ip_forward, dozens of undocumented keys).
# This compares the two directions so neither can silently diverge again:
# 1. every key the script emits must be documented
# 2. every key documented in the profile block must still be emitted
_README="$ROOT/README.md"
if [ -f "$_README" ]; then
# Isolate the documented profile block (the fenced block after the
# "Kernel Hardening (sysctl)" heading).
_README_BLOCK=$(sed -n '/^### Kernel Hardening (sysctl)/,/^Apply manually:/p' "$_README")

# Keys emitted by the script, excluding comments, blanks, and the
# server-only ${_extra} interpolation line (asserted separately).
_CODE_KEYS=$(printf '%s\n' "$SYSCTL_FN" \
| sed -n '/^# Kernel security hardening/,/^EOF$/p' \
| grep -E '^[a-z][a-z0-9_.]*=' \
| cut -d= -f1 \
| sort -u)
# Keys documented in the README profile block (both blocks).
_DOC_KEYS=$(printf '%s\n' "$_README_BLOCK" \
| grep -E '^[a-z][a-z0-9_.]*=' \
| cut -d= -f1 \
| sort -u)

if [ -n "$_CODE_KEYS" ] && [ -n "$_DOC_KEYS" ]; then
_missing=$(printf '%s\n' "$_CODE_KEYS" | while IFS= read -r k; do
printf '%s\n' "$_DOC_KEYS" | grep -qxF "$k" || printf '%s\n' "$k"
done)
if [ -z "$_missing" ]; then
ok_t "README: every sysctl key emitted by harden_sysctl is documented"
else
fail_t "README: documents every emitted sysctl key" \
"undocumented: $(printf '%s' "$_missing" | tr '\n' ' ')"
fi

_stale=$(printf '%s\n' "$_DOC_KEYS" | while IFS= read -r k; do
[ "$k" = "user.max_user_namespaces" ] && continue
printf '%s\n' "$_CODE_KEYS" | grep -qxF "$k" || printf '%s\n' "$k"
done)
if [ -z "$_stale" ]; then
ok_t "README: no stale sysctl keys left in the documented profile"
else
fail_t "README: no stale sysctl keys" \
"documented but not emitted: $(printf '%s' "$_stale" | tr '\n' ' ')"
fi

# The server-only key must be documented as conditional.
if printf '%s\n' "$_DOC_KEYS" | grep -qxF "user.max_user_namespaces"; then
ok_t "README: server-only user.max_user_namespaces is documented"
else
fail_t "README: server-only user.max_user_namespaces" \
"script emits it conditionally on servers but README omits it"
fi
else
fail_t "README: sysctl profile block parsed" \
"could not extract keys from code or README (README profile block may have moved)"
fi
else
fail_t "README: present for sysctl consistency check" "README.md not found at $_README"
fi
fi

echo
Expand Down
Loading