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:
@@ -64,6 +64,28 @@ password, writes the catch-all nginx config, opens 80/443/OpenSSH in ufw, and
|
||||
installs a root-owned, argument-validating API-domain helper. The deploy user may
|
||||
run that helper and reload nginx, but cannot replace the helper.
|
||||
|
||||
It also applies host hardening (added 2026-08-21, FH-D.3) — three drop-in files,
|
||||
so a re-run replaces its own config and never edits a distro file in place:
|
||||
|
||||
| File | Effect |
|
||||
|---|---|
|
||||
| `/etc/ssh/sshd_config.d/10-marketplaces-hardening.conf` | Password and keyboard-interactive auth off, root key-only, no agent/X11 forwarding, `MaxAuthTries 3`, 30 s login grace |
|
||||
| `/etc/fail2ban/jail.d/marketplaces.local` | `sshd`, `nginx-http-auth`, `nginx-bad-request` jails — 5 failures in 10 min, 1 h ban |
|
||||
| `/etc/sysctl.d/99-marketplaces-hardening.conf` | No redirects or source routing, reverse-path filtering, SYN cookies, forwarding off, restricted kernel pointers and dmesg |
|
||||
|
||||
Both accounts on this host are key-only by construction, so disabling password
|
||||
auth cannot lock anyone out — it only closes unlimited guessing against a
|
||||
credential nobody intended to exist. The script runs `sshd -t` before reloading
|
||||
and removes its own drop-in if the test fails, because a bad sshd config taking
|
||||
effect on a remote box is how people lock themselves out permanently.
|
||||
|
||||
Confirm after provisioning:
|
||||
|
||||
```bash
|
||||
sudo fail2ban-client status sshd
|
||||
sudo sshd -T | grep -E 'passwordauthentication|permitrootlogin|maxauthtries'
|
||||
```
|
||||
|
||||
Verify before continuing:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -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**
|
||||
|
||||
|
||||
Reference in New Issue
Block a user