Files
Paul LütznerandClaude Opus 5.5 a8aea33874 Open a fresh install from the LAN, not only from the machine it runs on
The stock compose file served https://localhost and nothing else. Reached
by address or hostname, Caddy had no certificate to offer and ended the
handshake, which a browser reports as an SSL protocol error with no way
to proceed. A headless server could not be set up without first setting
SERVER_NAME.

The default now answers every name and address with the self-signed
localhost certificate (default_sni/fallback_sni, and `:443` in the default
SERVER_NAME), so the browser shows its usual warning.

Live updates follow: compose.yaml's https://localhost hub address is
treated as the placeholder it is and gives way to the public address
stored during setup. Any other MERCURE_PUBLIC_URL, and any SERVER_NAME an
operator sets, is left alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 15:58:54 +02:00

115 lines
5.4 KiB
Caddyfile

{
# Five seconds to finish what is in flight when told to stop, then the
# connections are closed. Caddy's default is to wait for every one, and
# this server proxies the live-update stream, which never ends by
# itself: with one browser tab open, "Restart now" in the admin panel
# stopped PHP and then sat there, neither serving nor exiting, until
# the tab was closed — and compose cannot restart a container that has
# not exited. `docker stop` only ever got out of it by killing the
# process after its own timeout. The hub has the same five seconds for
# the same reason (BackgroundProcesses); browsers reconnect.
grace_period 5s
# The certificate a browser gets when it asked for a name this server
# holds none for — `default_sni` for a handshake that names nothing,
# which is every one to an IP address, `fallback_sni` for a name
# nobody listed, such as the machine's own hostname on a LAN.
#
# Without them Caddy has nothing to offer and ends the handshake. The
# browser reports that as a protocol error, not a certificate error,
# and a protocol error has no "proceed anyway": an install on a
# headless machine could not be opened from any other machine, and the
# README's "open https://localhost" was the only address that worked.
# With them the answer is the self-signed `localhost` certificate and
# the warning every browser lets a person click through.
#
# Inert wherever there is no such certificate to hand out: an install
# serving a real domain never issues one, and SERVER_NAME ":80" does
# no TLS at all. compose.yaml's default SERVER_NAME is the other half.
default_sni localhost
fallback_sni localhost
{$CADDY_GLOBAL_OPTIONS}
frankenphp {
{$FRANKENPHP_CONFIG}
}
}
{$CADDY_EXTRA_CONFIG}
{$SERVER_NAME:localhost} {
log {
format filter {
wrap console
fields {
uri query {
replace authorization REDACTED
}
}
}
}
root public/
encode zstd br gzip
# The headers a plMail install gets whether or not anyone put a proxy in
# front of it.
#
# None of these existed anywhere in the repository — not here, not in
# config/, not in a listener — so the ones a running instance showed
# were its operator's reverse proxy, and the README's own install
# instructions are `docker compose up -d` and "open https://localhost".
# An instance set up the documented way ran with no clickjacking
# defence, no HSTS and a full Referer going out to every link a mail
# points at. The operator had nothing telling them to fix that.
#
# `defer` so an application response that sets one of these itself wins:
# the attachment and image-proxy routes send their own, stricter,
# per-response policies and must not have them overwritten from here.
header {
defer
# Two years, and includeSubDomains, is the usual answer. It is
# only sent over HTTPS — Caddy omits it on plaintext — so an
# instance reached over http on a LAN is unaffected rather than
# locked out of its own hostname.
Strict-Transport-Security "max-age=63072000; includeSubDomains"
# SAMEORIGIN rather than DENY: the reading frame is our own
# document embedding our own route, and DENY would break it.
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
# A mail client sends its users to other people's links all day.
# Without this the full URL of the page they were on goes with
# them; with it, the origin does, and only to https.
Referrer-Policy "strict-origin-when-cross-origin"
# Nothing here asks for any of them, and a compromised page
# should not be able to start.
Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=(), interest-cohort=()"
# The version banner. Not a vulnerability, just free
# reconnaissance.
-Server
}
# Mercure hub — proxied to the container that runs it. `mercure` is the
# name plMail's own compose files give it; MERCURE_UPSTREAM is for a
# stack that names its containers by rules of its own.
#
# The subscriber cookie loses its `__Secure-` prefix on the way. The
# browser holds the prefixed one on an https install and the bare one
# otherwise, and which it is follows the public address, which can
# change under a running hub. So the hub always reads the bare name
# and this is where the two are made the same; see BackgroundProcesses.
handle /.well-known/mercure* {
reverse_proxy {$MERCURE_UPSTREAM:mercure:80} {
header_up Cookie "__Secure-mercure_access_token=" "mercure_access_token="
}
}
php_server {
try_files {path} index.php
}
}