Aller au contenu

Security Hardening

Overview

Five hardening layers, all configured in security/ in the aether repo and deployed via make security-deploy.

Layer Repo path Installed at
SSH config security/sshd-00-hardening.conf /etc/ssh/sshd_config.d/00-hardening.conf
Firewall rules security/iptables-setup-docker.sh Run by systemd service
Firewall unit security/iptables-firewall.service /etc/systemd/system/
fail2ban jails security/fail2ban-aletheia.conf /etc/fail2ban/jail.d/aletheia.conf
fail2ban paths security/fail2ban-paths-overrides.local /etc/fail2ban/paths-overrides.local
Kernel tuning security/sysctl-hardening.conf /etc/sysctl.d/99-hardening.conf

The rationale for each setting lives in comments inside the config files — that's the authoritative source. The tables below are auto-extracted from those files.

SSH

Setting Value Rationale
Port 57361 Non-standard port — not real security against targeted attacks, but massively reduces bot scan noise and gives fail2ban fewer events to process.
PasswordAuthentication no Keys only. Combined with KbdInteractiveAuthentication, this blocks all interactive fallbacks (PAM, challenge-response).
KbdInteractiveAuthentication no Keys only. Combined with KbdInteractiveAuthentication, this blocks all interactive fallbacks (PAM, challenge-response).
PermitRootLogin no No direct root login. Use sudo after logging in as debian.
MaxAuthTries 3 Max attempts per connection before sshd drops it. Low value + rate-limit in iptables gives a tight brute-force envelope.
X11Forwarding no X11 not needed on a headless server. Disabling reduces attack surface.
AllowTcpForwarding yes TCP forwarding allowed for operational tunneling (e.g. ssh -L 5432:... for local db access). StreamLocal (Unix socket forwarding) not needed.
AllowStreamLocalForwarding no TCP forwarding allowed for operational tunneling (e.g. ssh -L 5432:... for local db access). StreamLocal (Unix socket forwarding) not needed.
AllowAgentForwarding no Agent forwarding disabled — if the bastion is compromised, attacker could use your keys to hop to other hosts. Use ProxyJump instead (doesn't forward the agent).
GatewayPorts no GatewayPorts would let remote forwards bind to external interfaces — not needed.
AllowUsers debian Only the debian user can log in. New operator? Add them here explicitly.
LoginGraceTime 30 Short login grace — slow or broken auth attempts should fail fast, not tie up slots for 2 minutes (the default).
ClientAliveInterval 120 Keep-alive probe every 2 minutes. Idle sessions eventually drop instead of hanging forever behind NAT/load balancer timeouts.

Changing the SSH port

Three files reference the port and all must be updated:

  1. security/sshd-00-hardening.confPort directive
  2. security/iptables-setup-docker.sh — two --dport rules
  3. security/fail2ban-aletheia.conf[sshd] section

Then:

make security-deploy
sudo systemctl daemon-reload && sudo systemctl restart sshd fail2ban iptables-firewall

Verify before disconnecting

Test the new port from a second terminal before closing your current session. Locking yourself out requires console access to recover.

Firewall (iptables)

Default policies:

  • INPUT: DROP — reject inbound unless explicitly allowed
  • OUTPUT: ACCEPT — all outbound allowed
  • FORWARD: ACCEPT — required by Docker

Open inbound ports:

Port Protocol Notes
57361 TCP SSH (rate-limited: 4 new conns per 60s)
80 TCP HTTP (redirects to HTTPS)
443 TCP HTTPS

Trusted IP: 82.67.73.170 has unrestricted access (including Docker-published ports via the DOCKER-USER chain).

How the script stays Docker-safe

The script only flushes INPUT and OUTPUT — it deliberately does NOT touch FORWARD, NAT, or the DOCKER chain, because Docker manages those. The systemd service runs After=docker.service so Docker's chains exist before we add rules to DOCKER-USER. See the rule-level comments in iptables-setup-docker.sh for the full logic.

Reloading firewall rules

# After editing iptables-setup-docker.sh:
sudo systemctl restart iptables-firewall

# Persist current rules across reboots (setup.sh does this automatically):
sudo iptables-save > /etc/iptables/rules.v4

Adding a new rule

Only edit security/iptables-setup-docker.sh in the repo, never the server copy directly. Then make security-deploy and restart the service. Common changes:

  • Open a new port: add iptables -A INPUT -p tcp --dport <PORT> -j ACCEPT
  • Block a Docker-exposed port externally: add iptables -I DOCKER-USER -p tcp --dport <PORT> -j DROP
  • Whitelist a new trusted IP: add iptables -A INPUT -s <IP> -j ACCEPT near the top

IPv6

Only IPv4 is managed. If the server has an IPv6 address, either block all IPv6 or mirror the rules — see initialization post-setup checklist for commands.

fail2ban

Jail Enabled maxretry bantime findtime Rationale
sshd true 3 1h 10m SSH brute-force protection. Explicit port needed because our sshd runs on 57361, not 22 — fail2ban wouldn't know otherwise.
nginx-http-auth true 3 1h 10m Failed basic auth on staging/dev htpasswd-protected vhosts.
nginx-botsearch true 2 1h 10m Scans for known exploits and common /wp-admin, /phpmyadmin, etc. paths. Lower maxretry (2) because these are almost always malicious — legitimate users don't probe for WordPress admin on a Django site.
nginx-bad-request true 3 1h 10m Malformed HTTP requests (often exploit attempts or port scanners).
nginx-limit-req true 5 1h 10m Triggers when nginx's limit_req module rejects a client. Higher maxretry (5) because rate limits can trip on legitimate bursts (AJAX polling, async loaders).

Defaults (applied when jail doesn't override): bantime=1h, findtime=10m, maxretry=3, banaction=iptables-multiport

Log paths

Nginx logs live in a Docker named volume, not /var/log/nginx/. The file security/fail2ban-paths-overrides.local points fail2ban at the actual location (/var/lib/docker/volumes/nginx_nginx_logs/_data/).

Common operations

# List active jails
sudo fail2ban-client status

# Status of a specific jail (banned IPs, totals)
sudo fail2ban-client status sshd

# Manually ban/unban an IP
sudo fail2ban-client set sshd banip 1.2.3.4
sudo fail2ban-client set sshd unbanip 1.2.3.4

Tuning thresholds

To change a jail's maxretry, bantime, or findtime:

  1. Edit security/fail2ban-aletheia.conf — update the rationale comment if you change a value
  2. Deploy: make security-deploy
  3. Apply: sudo systemctl restart fail2ban
  4. Verify: sudo fail2ban-client status <jail>

Kernel (sysctl)

Parameter Value Purpose
net.ipv4.conf.all.rp_filter 1 Reverse path filtering — drop packets with spoofed source IPs
net.ipv4.conf.default.rp_filter 1 Reverse path filtering — drop packets with spoofed source IPs
net.ipv4.conf.all.log_martians 1 Log packets with impossible source addresses
net.ipv4.conf.default.log_martians 1 Log packets with impossible source addresses

Martians show up in /var/log/kern.log prefixed with martian source. Useful for detecting misconfigured routers and occasional attack probes, but not actionable on their own.

Applying sysctl changes

make security-deploy
sudo sysctl -p /etc/sysctl.d/99-hardening.conf   # apply without reboot

Other layers

These aren't in security/ but contribute to the overall posture:

  • Unattended upgrades: setup.sh enables automatic Debian security patches (/etc/apt/apt.conf.d/20auto-upgrades)
  • Nginx basic auth: staging and dev environments require htpasswd credentials (.htpasswd is encrypted via SOPS + age)
  • SSL/TLS: Let's Encrypt certs via certbot, auto-renewed every 12h
  • Container isolation: apps only reachable via nginx; PostgreSQL bound to 127.0.0.1:5432 (not externally accessible)

Verification

After any security change:

# SSH config syntax
sudo sshd -t

# Active iptables rules
sudo iptables -L INPUT -v -n --line-numbers

# fail2ban jails
sudo fail2ban-client status

# Applied sysctl values
sudo sysctl net.ipv4.conf.all.rp_filter net.ipv4.conf.all.log_martians

# Open listening ports (should show only 57361, 80, 443)
sudo ss -tlnp | grep LISTEN