Files
docs-alpha.jayfield.org/TODO.md
T
jens a67895b4dd Fix Portainer :9443 HSTS/self-signed-cert conflict
Moved Portainer's HTTPS UI behind an Apache reverse proxy on its own
subdomain (portainer.jayfield.org) instead of publishing it directly on
:9443 with a self-signed cert, which HSTS's includeSubDomains policy made
unreachable in Firefox with no click-through exception. Same pattern as
the existing web.jayfield.org/cloud.jayfield.org proxied subdomains: DNS
A record, Apache vhost proxying to 127.0.0.1:9443 over HTTPS, real
Let's Encrypt cert via certbot, standard HSTS header. Portainer container
recreated to bind 9443 to loopback only; verified end-to-end (cert chain,
HSTS header, redirect, and that :9443 is no longer reachable externally).
2026-07-20 00:26:01 +02:00

117 lines
7.2 KiB
Markdown

# alpha.jayfield.org — TODO
Outstanding hardening items (see `README.md` for the full service overview).
Nothing here is urgent; all are low-risk, no-downtime changes.
- [x] **Lock down the dyndns TSIG key file permissions** — done 2026-07-19
`/etc/bind/dyndns.jayfield.org.key` was `644` (world-readable), holding
the shared secret that authenticates dynamic DNS updates for
`dyndns.jayfield.org` with no IP-based `allow-update` backing it up.
Fixed: `chmod 640` + `chown root:bind` (owner was already correct),
matching `rndc.key`'s permissions.
- [x] **Suppress BIND version disclosure** — done 2026-07-19
No `version` statement in `named.conf.options`, so
`dig CH TXT version.bind @ns1.jayfield.org` leaked the exact BIND
version (`9.18.39`) — a minor fingerprinting aid for attackers.
Fixed: added `version "unknown";` inside the `options {}` block,
validated with `named-checkconf`, and applied via `rndc reload`.
Verified `dig CH TXT version.bind` now returns `"unknown"`.
- [x] **Add response rate limiting (RRL)** — done 2026-07-19
No `rate-limit {}` block was configured. Since `allow-query { any; }`
is required for a public authoritative zone, RRL is the standard
mitigation against the server being used for spoofed-source DNS
reflection/amplification against third parties.
Fixed: added `rate-limit { responses-per-second 10; window 5; }` to
`named.conf.options`, validated with `named-checkconf`, and applied
via `rndc reload`. Started with conservative defaults (10/s, 5s
window) — revisit if legitimate high-volume resolvers get throttled.
- [x] **Add HSTS headers to all vhosts** — done 2026-07-19
No vhost sent a `Strict-Transport-Security` header, leaving a window
for on-path SSL-stripping on a user's very first plaintext request to
any domain.
Fixed: added `Header always set Strict-Transport-Security
"max-age=63072000; includeSubDomains"` to all 6 domain `:443` blocks
(`dyndns`, `jayfield.org`, `www`, `mail`, `web`, `cloud`) — skipped
`default-ssl.conf` since it's the self-signed catch-all with no real
trusted hostname for a browser to pin. Validated with
`apache2ctl configtest`, applied via `systemctl reload apache2`,
verified live with `curl -sI` against all 6 domains.
- [x] **`jayfield.org` had no `:80` vhost of its own** — done 2026-07-19
Bare `http://jayfield.org` fell through to the `dyndns` default vhost
and redirected to the wrong domain.
Fixed: added a `:80` block to `jayfield.org.conf` redirecting to
`https://jayfield.org/`, matching the `dyndns`/`mail` pattern.
Verified: `curl -sI http://jayfield.org/``302` with
`Location: https://jayfield.org/`.
- [x] **SSH: disable root login + password auth** — done 2026-07-19
`PermitRootLogin yes` and `PasswordAuthentication yes` were both still
enabled (the "known deviation" flagged in `SETUP.md` §1) — every
credential-stuffing bot on the internet got a real login prompt to
grind against.
Fixed: `PermitRootLogin no`, `PasswordAuthentication no`,
`PubkeyAuthentication yes` in `/etc/ssh/sshd_config` (backed up as
`sshd_config.bak.20260719225249`). The `Match Group sftp` block's own
`PasswordAuthentication yes` was left as-is — separate, already-chrooted,
no-shell/no-forwarding accounts, not part of this risk. Validated with
`sshd -t`, applied via `systemctl reload sshd`, verified live with
`sshd -T` and a fresh key-only connection before considering it done.
- [x] **Portainer's `:9443` UI is unreachable in HSTS-enforcing browsers** — done 2026-07-20
`https://alpha.jayfield.org:9443` threw
`MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT` in Firefox with no click-through
option ("keine Ausnahme kann hinzugefügt werden"). Root cause: the
`includeSubDomains` HSTS policy sent by the real `alpha.jayfield.org`
vhost (`Strict-Transport-Security` header, added in the item above)
pins the *hostname* to HTTPS-with-a-trusted-cert on every port, but
Portainer (`README.md` §Docker services) was still serving its stock
self-signed cert directly on `9443`. Not a MITM — self-inflicted by
combining HSTS with an unproxied self-signed service.
Fixed: added a `portainer` A record to `db.jayfield.org` (SOA serial
`2022032216``2022032217`); recreated the `portainer` container
binding the HTTPS UI to `127.0.0.1:9443:9443` instead of publishing it
(old container kept renamed until the new one was verified, then
removed, matching the update procedure documented in `README.md`);
added an Apache vhost for `portainer.jayfield.org`
(`ProxyPass`/`ProxyPassReverse` to `https://127.0.0.1:9443/`,
`SSLProxyEngine on`, `SSLProxyVerify none` + `SSLProxyCheckPeerCN off`/
`SSLProxyCheckPeerName off` for the loopback hop to Portainer's
self-signed cert) — same pattern as `web.jayfield.org`/
`cloud.jayfield.org` (`SETUP.md` §4/§5); issued a real cert via
`certbot --apache -d portainer.jayfield.org --redirect`; added the
standard HSTS header. Left the `8000` edge-agent tunnel port
published as-is — no evidence it's in use.
Verified: `openssl s_client`/`curl` confirm the Let's Encrypt cert
(not self-signed) and `Strict-Transport-Security` header on
`https://portainer.jayfield.org`, `http://` redirects 301 to
`https://`, and `ss -ltnp` shows `9443` bound to `127.0.0.1` only
(via `docker-proxy`), so it's no longer reachable on the public
interface at all — access is now exclusively via
`https://portainer.jayfield.org`.
One transient wrinkle during rollout: the very first few requests
through the new proxy vhost returned a stray `401` with dyndns's
Basic Auth realm ("Restricted Content") — looked alarming, but
`LogLevel trace8` on the vhost showed Apache correctly SNI-matching
and proxying to the new container with a clean backend `200`;
resolved itself within seconds (looked like a stale pooled proxy
connection surviving the container recreate) and was stable across
repeated checks before the debug logging was removed again.
- [x] **fail2ban: `sshd` jail was evadable via low-and-slow attempts** — done 2026-07-19
Default `findtime=30m`/`maxretry=3` never triggered for `47.76.192.176`,
which had been hitting `root`/`jayfield` logins roughly every 33 minutes
for most of a day — just outside the 30-minute window, so failures kept
aging out before reaching 3 strikes. Confirmed via `fail2ban.log`
(`Found`/`Ban` counts) that no other jail showed the same per-IP timing
pattern; a separate, *distributed* single-shot scan on `apache-noscript`
was noted but left open (see `README.md`).
Fixed: `/etc/fail2ban/jail.d/sshd-findtime.local` overrides `sshd` to
`findtime=1d`, `maxretry=4`, `bantime=1d`, kept in its own drop-in
rather than editing the Debian-packaged jail file. Applied via
`fail2ban-client reload sshd`; `47.76.192.176` was banned on its very
next attempt after the change.