Fix staging HTTPS: force tls internal, require a hostname not a bare IP
Two real, verified findings from actually testing the "let Caddy issue a self-signed cert automatically" approach against a literal IP: 1. TLS SNI is not sent for literal IP connections (out of spec — SNI's HostName type explicitly excludes IPs). Confirmed via openssl s_client: the handshake fails with a TLS-layer internal_error, even though Caddy logs "certificate obtained successfully" — Caddy has no way to select a cert without SNI. No Caddyfile config fixes this; the address has to be a hostname. 2. Caddy's automatic-HTTPS heuristic only treats bare IPs and "localhost" as obviously-private. Any other name — including a made-up LAN hostname like "bookstore-staging.test" — it assumes might be real and tries Let's Encrypt, which fails the same way staging.example.com did. Fixed by forcing `tls internal` explicitly in Caddyfile.staging, removing the guesswork entirely. Verified end-to-end with curl --resolve (proper SNI, no real DNS/hosts change needed to test): direct HTTPS 200, HTTP->HTTPS redirect chain 200. README now documents the /etc/hosts requirement and how to trust Caddy's root cert to skip the one-time browser warning. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -46,10 +46,30 @@ drops into the app container.
|
|||||||
```
|
```
|
||||||
cp .env.example .env.staging
|
cp .env.example .env.staging
|
||||||
# fill in SITE_DOMAIN, SITE_URL, DB_*
|
# fill in SITE_DOMAIN, SITE_URL, DB_*
|
||||||
|
# SITE_DOMAIN must be a HOSTNAME, not a bare IP — see below
|
||||||
|
|
||||||
make deploy ENV=staging
|
make deploy ENV=staging
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**SITE_DOMAIN needs to be a hostname, not the VM's IP directly.** TLS SNI
|
||||||
|
(how Caddy picks which certificate to present) isn't sent for literal IP
|
||||||
|
connections per spec, so HTTPS to a bare IP fails at the handshake itself
|
||||||
|
no matter what Caddy does. Pick any hostname (e.g. `bookstore.lan`), add it
|
||||||
|
to `/etc/hosts` (or the Windows equivalent) on whatever machine you're
|
||||||
|
browsing from, pointing at the VM's LAN IP, and use that hostname as both
|
||||||
|
`SITE_DOMAIN` and in `SITE_URL`.
|
||||||
|
|
||||||
|
Caddyfile.staging forces `tls internal` — Caddy's own self-signed CA,
|
||||||
|
issued locally with no external network calls — rather than letting Caddy
|
||||||
|
guess whether the name looks "public" (its automatic heuristic only
|
||||||
|
recognizes bare IPs and `localhost` as obviously-private; anything else,
|
||||||
|
including a made-up LAN hostname, it assumes might be real and tries
|
||||||
|
Let's Encrypt, which then fails). Your browser will show an untrusted-cert
|
||||||
|
warning once; click through it, or trust Caddy's root cert to skip that:
|
||||||
|
```
|
||||||
|
docker compose -p bookstore-staging exec caddy cat /data/caddy/pki/authorities/local/root.crt
|
||||||
|
```
|
||||||
|
|
||||||
Staging is `noindex`'d (`blog_public=0` plus the `X-Robots-Tag` header in
|
Staging is `noindex`'d (`blog_public=0` plus the `X-Robots-Tag` header in
|
||||||
Caddyfile.staging) so search engines won't index it — there's no basic-auth
|
Caddyfile.staging) so search engines won't index it — there's no basic-auth
|
||||||
wall on top of that, since this box is LAN-only and not reachable from
|
wall on top of that, since this box is LAN-only and not reachable from
|
||||||
|
|||||||
@@ -1,6 +1,28 @@
|
|||||||
http://{$SITE_DOMAIN} {
|
{$SITE_DOMAIN} {
|
||||||
encode gzip
|
encode gzip
|
||||||
|
|
||||||
|
# `tls internal` forces Caddy's own self-signed CA unconditionally,
|
||||||
|
# instead of guessing from the domain name — its automatic-HTTPS
|
||||||
|
# heuristic only recognizes specific patterns (bare IPs, "localhost")
|
||||||
|
# as "obviously private"; anything else (including a private-network
|
||||||
|
# hostname like this one) it assumes might be a real public domain and
|
||||||
|
# tries Let's Encrypt, which then fails exactly like it did for
|
||||||
|
# staging.example.com. This directive skips that guesswork entirely.
|
||||||
|
#
|
||||||
|
# SITE_DOMAIN must be an actual HOSTNAME here, not a bare IP: TLS SNI
|
||||||
|
# (which Caddy needs to pick a certificate) isn't sent for literal IP
|
||||||
|
# connections per spec — confirmed this fails at the TLS handshake
|
||||||
|
# itself (curl: "TLS alert, internal error") even though Caddy reports
|
||||||
|
# the cert as obtained successfully. Point this hostname at the VM's
|
||||||
|
# IP via /etc/hosts (or your router/local DNS) on whatever machine
|
||||||
|
# you're browsing from.
|
||||||
|
tls internal
|
||||||
|
|
||||||
|
# Browser will show an untrusted-cert warning once (self-signed, not
|
||||||
|
# from a public CA) — click through it, or trust Caddy's root cert
|
||||||
|
# (docker compose exec caddy cat /data/caddy/pki/authorities/local/root.crt)
|
||||||
|
# in your OS/browser once to skip that entirely.
|
||||||
|
|
||||||
# Launch gate: "No staging URLs are publicly indexed." Belt-and-suspenders
|
# Launch gate: "No staging URLs are publicly indexed." Belt-and-suspenders
|
||||||
# with wp_option blog_public=0, which deploy.sh sets on staging. No
|
# with wp_option blog_public=0, which deploy.sh sets on staging. No
|
||||||
# basic-auth wall — this box is LAN-only, not reachable from outside.
|
# basic-auth wall — this box is LAN-only, not reachable from outside.
|
||||||
|
|||||||
Reference in New Issue
Block a user