Repository navigation
Persist the self-signed certificate with proper SANs, and serve HTTPS in front of an HTTP backend - #1
Merged
Conversation
--bind (env BIND_ADDRESS, default 0.0.0.0) sets the address both listeners bind to, so the server can be restricted to a single interface instead of always accepting connections on every one of them. A value that is not a valid IP address is rejected at startup. --only-https (env ONLY_HTTPS) disables the HTTP listener even when a port is set. Without it there is no way to get an HTTPS-only container, because the image sets PORT=8080 in its environment.
The generated certificate carried only a CommonName, so it failed hostname verification in every client that has required SubjectAltName for years, even when the certificate was explicitly trusted. It was also recreated on every start, so a browser exception or a pinned copy never survived a restart. It now carries SubjectAltName entries for localhost, 127.0.0.1, ::1, the machine hostname and the address it listens on -- the bind address when one is given, every routable address of the machine when binding to 0.0.0.0 -- and is saved to the certificate directory as selfsigned-cert.pem / selfsigned-key.pem. The saved pair is reused until it expires (with a 30 day margin) or stops covering the requested hosts. When the directory cannot be written the certificate is kept in memory, as it was before. --tls-selfsigned-hosts adds names the server cannot discover by itself, such as a public DNS name. --tls-cert-file and --tls-key-file point at a certificate whose file names do not follow the cert.pem/key.pem convention, such as the fullchain.pem and privkey.pem written by Let's Encrypt. The README documented --tls-cert-dir for exactly that directory, where it silently fell back to a self-signed certificate because key.pem does not exist there. Unlike the directory, a file given explicitly is fatal when it cannot be read. --tls-cert-dir now defaults to /certs when running as root and to ~/.static-httpserver/certs otherwise, so an unprivileged process has somewhere to write. The image pins TLS_CERT_DIR=/certs and ships that directory owned by the runtime user, which a named volume inherits; without it the process cannot create /certs at all.
go build without -o leaves static-httpserver next to the sources; bin/ was already ignored, this covers the plain build too.
A backend that speaks only HTTP could not be put behind the HTTPS listener: "/" was rejected as a proxy prefix, so the whole site could never be handed to it. "/" is now the catch-all prefix, matching every path and stripping nothing, and routes are matched most specific first, so a catch-all never shadows a specific route regardless of the order they were given in. /health is still answered locally, so it keeps working as a probe. Proxied requests now also carry X-Forwarded-Proto and X-Forwarded-Host. TLS is terminated here, so without them a backend cannot tell that the client spoke HTTPS: Express reports req.protocol as http, drops secure cookies and builds http:// redirects. Values coming from an upstream proxy are left untouched.
/health sits on a path applications commonly serve themselves, and the catch-all proxy route turned that into a real collision: the server answered /health locally, so the backend behind it could never be reached there. The built-in endpoints are now /_health and /_headers, leaving an application's own /health to be proxied through untouched, and --health-path / --headers-path (env HEALTH_PATH / HEADERS_PATH) move them when even those names are taken. The parking page fetches whatever the headers path is set to, and the chart points its probes at parameters.healthPath. The health endpoint changes with this: probes, uptime monitors and load balancer checks defined outside the chart have to follow it to /_health, or restore the previous path with --health-path /health.
Putting HTTPS in front of a local app took a hand-written wrapper every time, and the obvious one -- backgrounding the app and exec'ing the server -- leaves the app orphaned when the server stops. examples/serve-https.sh starts the app, runs static-httpserver in front of it as a TLS terminator and forwards the stop signal to both. The command and the ports are environment variables, so the same script fits any stack, and it is meant to be copied into a project and called from package.json as "serve-https".
The workflow only built the Docker image, which compiles the binary but never runs a test, so nothing guarded the behaviour the tests cover. A Test job now runs make lint and make test, and the image build waits for it. Go is pinned from the go directive in go.mod. The module has no dependencies and therefore no go.sum, so the setup-go cache is turned off: with it enabled the step fails for the missing lock file.
The chart templates changed with the new health endpoint: the probes follow parameters.healthPath and default to /_health. Chart repositories key on the version, so publishing the change needs a new one.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
HTTPS worked, but not in a way anything could actually trust or keep: the generated certificate
carried only a CommonName, so it failed hostname verification in every modern client even when it
was explicitly trusted, and it was regenerated on every start. A backend that speaks only HTTP also
could not be put behind it, because
/was rejected as a proxy prefix.Certificates
SubjectAltNameentries —localhost,127.0.0.1,::1, the hostname and the address the server listens on — socurl --cacertand a trustedbrowser exception work instead of only
-k.covering the requested hosts. When the directory cannot be written it stays in memory, as before.
--tls-cert-dirdefaults to/certsfor root and~/.static-httpserver/certsotherwise, so anunprivileged process has somewhere to write. The image pins
TLS_CERT_DIR=/certsand ships thedirectory owned by the runtime user.
--tls-cert-file/--tls-key-filepoint at files that do not follow thecert.pem/key.pemconvention. The README documented
--tls-cert-dir /etc/letsencrypt/live/...for exactly thatcase, where it silently fell back to a self-signed certificate because certbot writes
privkey.pem, notkey.pem.--tls-selfsigned-hostsadds names the server cannot discover, such as a public DNS name.Listeners and proxying
--bindrestricts both listeners to one address;--only-httpsdisables HTTP even when a port isset, which is the only way to get an HTTPS-only container since the image sets
PORT=8080./is now a valid proxy prefix — it matches every path and strips nothing — so static-httpservercan terminate TLS in front of a plain-HTTP backend. Routes are matched most specific first, so a
catch-all never shadows a specific route.
X-Forwarded-ProtoandX-Forwarded-Host. Without them a backend behindthe TLS terminator reports
http, dropssecurecookies and buildshttp://redirects. Valuesfrom an upstream proxy are preserved.
examples/serve-https.shwraps the local-development case (npm run serve-https).Breaking change: the built-in endpoints moved from
/healthand/headersto/_healthand/_headers, so an application's own/healthis proxied through untouched instead of beingshadowed by ours.
--health-path/--headers-pathmove them. The chart's probes followparameters.healthPath; probes, uptime monitors and load balancer checks defined elsewhere have tobe pointed at
/_health, or the old path restored with--health-path /health.Checklist
component see Contributing to PHP components
README.mdordocs/) ifbehaviour changed
proxy/endpoint work. Happy to split it into two PRs if you prefer.