feat(deploy): host hardening on the frontend server (FH-D.3)

server-setup.sh configured ufw and stopped there, which leaves SSH open
to unlimited password guessing and the kernel on defaults that are wrong
for an internet-facing host.

Adds three drop-in files, so a re-run replaces its own config and never
edits a distro file in place:

  /etc/ssh/sshd_config.d/10-marketplaces-hardening.conf
      password and keyboard-interactive auth off, root key-only,
      no agent/X11 forwarding, MaxAuthTries 3, 30s login grace
  /etc/fail2ban/jail.d/marketplaces.local
      sshd, nginx-http-auth, nginx-bad-request; 5 in 10m, 1h ban
  /etc/sysctl.d/99-marketplaces-hardening.conf
      no redirects or source routing, rp_filter, SYN cookies,
      forwarding off, restricted kernel pointers and dmesg

Both accounts on the host are key-only by construction - the deploy user
is created with no password at all - so disabling password auth cannot
lock anyone out. It only closes guessing against a credential nobody
intended to exist.

The sshd block runs `sshd -t` first and removes its own drop-in if the
test fails. A bad sshd config that takes effect on a remote box is how
people lock themselves out permanently.

DEPLOYMENT.md §3.2 documents all three plus the post-provision checks.

Not copied from the reference implementation: its hardcoded server IP.
Kept as-is because ours is already better: add-domain.sh pre-checks the
DNS A record and runs nginx -t before and after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-08-21 13:17:02 +04:00
parent cf17b0b6c6
commit a3808842c1
3 changed files with 111 additions and 5 deletions

View File

@@ -207,9 +207,9 @@ The **client half and the contract are done** (2026-08-21). What remains is back
- [ ] **FH-D.2 — Database unreachable from the internet, structurally** · S · Lane B/D
Data network `internal: true`; API bound to loopback only; `no-new-privileges` on every service. Makes it a property of the topology rather than a firewall promise.
- [ ] **FH-D.3 — Host hardening we lack** · M
fail2ban jail, sshd hardening drop-in, sysctl hardening, scoped sudoers per deploy role. Add to `scripts/deploy/server-setup.sh`.
*Keep ours where ours is better:* `add-domain.sh` already pre-checks the DNS A record and runs `nginx -t` before and after; `server-setup.sh` already configures ufw. Do **not** copy their hardcoded server IP.
- [x] **FH-D.3 — Host hardening we lack** · M · **done 2026-08-21**
Three drop-in files in `scripts/deploy/server-setup.sh`, documented in [DEPLOYMENT.md](DEPLOYMENT.md) §3.2: sshd hardening (password and keyboard-interactive auth off, root key-only, `MaxAuthTries 3`, 30 s grace, no forwarding), fail2ban (`sshd`, `nginx-http-auth`, `nginx-bad-request`; 5 failures in 10 min, 1 h ban), and sysctl (no redirects or source routing, rp_filter, SYN cookies, forwarding off, restricted kernel pointers and dmesg). The script runs `sshd -t` before reloading and removes its own drop-in if the test fails — a bad sshd config taking effect remotely is how people lock themselves out permanently.
Scoped sudoers was already in place. *Kept ours where ours is better:* `add-domain.sh` pre-checks the DNS A record and runs `nginx -t` before and after; ufw was already configured. Their hardcoded server IP deliberately not copied.
---
@@ -248,9 +248,9 @@ The **client half and the contract are done** (2026-08-21). What remains is back
| 2 — Contracts | 14 | — | 0 | 1 rejected (FH-2.12) |
| 3 — Proof | 2 | — | 3 | see note below |
| 4 — Identity | 2 | 3 | 3 | OAuth apps, which FH-0.1 gates |
| Ops | 0 | — | 3 | — |
| Ops | 1 | — | 2 | — |
| Process | 4 | — | 2 | — |
| **Total** | **24** | **3** | **15** | 1 rejected |
| **Total** | **25** | **3** | **14** | 1 rejected |
**Landed 2026-08-21**