mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 01:54:10 +00:00
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>
115 lines
5.4 KiB
Caddyfile
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
|
|
}
|
|
}
|