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).
This commit is contained in:
2026-07-20 00:26:01 +02:00
parent 4644c983ff
commit a67895b4dd
2 changed files with 66 additions and 28 deletions
+31 -12
View File
@@ -61,26 +61,45 @@ Nothing here is urgent; all are low-risk, no-downtime changes.
`sshd -t`, applied via `systemctl reload sshd`, verified live with
`sshd -T` and a fresh key-only connection before considering it done.
- [ ] **Portainer's `:9443` UI is unreachable in HSTS-enforcing browsers**found 2026-07-20
`https://alpha.jayfield.org:9443` throws
- [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) still serves its stock
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.
Planned fix (not yet applied): rebind Portainer's published port to
`127.0.0.1:9443:9443`, add a `portainer.jayfield.org` Apache vhost
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` for the loopback hop),
issue a real cert via `certbot --apache -d portainer.jayfield.org`,
and add the standard HSTS header — same pattern already used for
`web.jayfield.org`/`cloud.jayfield.org` (`SETUP.md` §4/§5). DNS needs
a new `portainer` A record in `db.jayfield.org` first (SOA serial
bump). Leave the `8000` edge-agent tunnel port as-is — no evidence
it's in use.
`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`,