From 390f8c78af043efbd39182c951136b69173f4dab Mon Sep 17 00:00:00 2001
From: Oier Bravo Urtasun
Date: Sun, 24 May 2026 05:32:31 +0200
Subject: [PATCH] feat(caddy): serve Caddy root CA at /ca.crt for guest trust
Air-gap mirror uses tls internal; guests must trust Caddy's local CA. Serve it
at http:///ca.crt from a world-readable mount (the live pki dir is
root-only 0600). build-stack --up mirror|full exports the CA after bring-up;
landing page links it. Pre-creates the mount target to avoid a dir bind.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.gitignore | 3 +++
caddy/conf.d/10-static.caddy | 7 +++++++
docker-compose.static.yml | 1 +
landing/src/data/site.ts | 3 +++
landing/src/pages/index.astro | 5 +++++
tooling/build-stack.sh | 25 +++++++++++++++++++++++++
6 files changed, 44 insertions(+)
diff --git a/.gitignore b/.gitignore
index e6e06c5..6c870ab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,6 @@ www/
landing/node_modules/
landing/.astro/
landing/dist/
+
+# exported Caddy CA (per-deploy artifact)
+caddy/caddy-root-ca.crt
diff --git a/caddy/conf.d/10-static.caddy b/caddy/conf.d/10-static.caddy
index 29c1712..2f87d44 100644
--- a/caddy/conf.d/10-static.caddy
+++ b/caddy/conf.d/10-static.caddy
@@ -1,6 +1,13 @@
# Static toggle — apex landing page + launcher downloads.
# Mounted only when the stack runs with static files (build-stack.sh up static|full).
http://{$BASE_DOMAIN} {
+ # Caddy's local CA root, so guests can trust the air-gap HTTPS mirror.
+ # Mounted from ./caddy/caddy-root-ca.crt (exported by build-stack prep;
+ # the live pki dir is root-only 0600 and can't be served directly).
+ handle /ca.crt {
+ root * /srv/ca-pub
+ file_server
+ }
# Launcher downloads are a sibling mount (/srv/launcher), not nested under
# the read-only /srv/www. handle_path strips the /launcher prefix.
handle_path /launcher/* {
diff --git a/docker-compose.static.yml b/docker-compose.static.yml
index c8fbcfa..97f503d 100644
--- a/docker-compose.static.yml
+++ b/docker-compose.static.yml
@@ -8,3 +8,4 @@ services:
- ./caddy/conf.d/10-static.caddy:/etc/caddy/conf.d/10-static.caddy:ro
- ./www:/srv/www:ro
- ./mirror/launcher:/srv/launcher:ro
+ - ./caddy/caddy-root-ca.crt:/srv/ca-pub/ca.crt:ro
diff --git a/landing/src/data/site.ts b/landing/src/data/site.ts
index 19f4a3e..f6ebe61 100644
--- a/landing/src/data/site.ts
+++ b/landing/src/data/site.ts
@@ -18,6 +18,9 @@ export const site = {
// packwiz modpack entrypoint.
packwizUrl: `http://packwiz.${BASE_DOMAIN}/pack.toml`,
+ // Caddy local CA root — guests import this to trust the offline asset mirror.
+ caCertUrl: `http://${BASE_DOMAIN}/ca.crt`,
+
// Launcher downloads. Stable filenames are symlinks created by
// tooling/fetch-launcher.sh — keep these in sync with that script.
launcher: {
diff --git a/landing/src/pages/index.astro b/landing/src/pages/index.astro
index ade2e04..56d7105 100644
--- a/landing/src/pages/index.astro
+++ b/landing/src/pages/index.astro
@@ -50,6 +50,11 @@ const { launcher } = site;
Need credentials? Register at {site.authUrl}, then log in
with that username and password.
+
+ Offline party? Download and trust the
+ local CA certificate so the game-file
+ mirror works over HTTPS.
+
diff --git a/tooling/build-stack.sh b/tooling/build-stack.sh
index 7fce11b..5512b3b 100755
--- a/tooling/build-stack.sh
+++ b/tooling/build-stack.sh
@@ -109,7 +109,32 @@ up() {
log "PHASE up (mode=$mode)"
have_script render-config.sh
tooling/render-config.sh # cheap; ensures configs match current .env
+
+ # Static mode mounts ./caddy/caddy-root-ca.crt as a file at /srv/ca-pub/ca.crt.
+ # Pre-create it (empty) so Docker doesn't make a directory at that path; the
+ # real cert is exported below once Caddy's local CA exists.
+ case "$mode" in
+ static|full) [ -e caddy/caddy-root-ca.crt ] || : > caddy/caddy-root-ca.crt ;;
+ esac
+
"${COMPOSE[@]}" "${files[@]}" up -d
+
+ # Mirror/full provision Caddy's local CA — export it so guests can trust the
+ # HTTPS asset mirror (served at http://${BASE_DOMAIN}/ca.crt in static/full).
+ case "$mode" in
+ mirror|full)
+ local i
+ for i in 1 2 3 4 5; do
+ docker exec caddy cat /data/caddy/pki/authorities/local/root.crt \
+ > caddy/caddy-root-ca.crt 2>/dev/null && break
+ sleep 1
+ done
+ [ -s caddy/caddy-root-ca.crt ] \
+ && echo " exported Caddy CA → caddy/caddy-root-ca.crt (served at /ca.crt)" \
+ || warn "Caddy CA not ready; export later: docker exec caddy cat /data/caddy/pki/authorities/local/root.crt > caddy/caddy-root-ca.crt"
+ ;;
+ esac
+
log "stack up (mode=$mode). apex: http://${BASE_DOMAIN} auth: http://auth.${BASE_DOMAIN}"
}