Sfoglia il codice sorgente

fix: make HSTS header operator-tuneable for preload opt-in (SEC_REVIEW F60)

The HSTS header was hardcoded to
`max-age=31536000; includeSubDomains` in both Caddyfiles. Operators
who wanted to submit to https://hstspreload.org/ had to patch the
Caddyfile in place — high-friction, easy to forget on a Caddyfile
upgrade.

Read the value from a new `HSTS_HEADER` env var with the previous
value as the default. Operators who want preload set:

    HSTS_HEADER="max-age=31536000; includeSubDomains; preload"

We deliberately don't enable `preload` by default. Preload-listing
is a one-way commitment — browser preload removals take months —
and the M01 default deployment is "operator runs the bundled
compose stack on a hostname they may want to retire". Defaulting
to the conservative no-preload value means a fresh deploy doesn't
silently sign up for the browser preload list.

`.env.example` documents the override with the SEC_REVIEW F60
reference and the exact preload-eligible syntax.

Caddyfile syntax validated with `frankenphp validate --adapter
caddyfile -e APP_ENV=production` on both files — both report
"Valid configuration".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chiappa 4 giorni fa
parent
commit
68121febe2
3 ha cambiato i file con 25 aggiunte e 2 eliminazioni
  1. 8 0
      .env.example
  2. 8 1
      api/docker/Caddyfile
  3. 9 1
      ui/docker/Caddyfile

+ 8 - 0
.env.example

@@ -56,6 +56,14 @@ INTERNAL_CIDR_ALLOWLIST=
 # SEC_REVIEW F25: never include broad RFC1918 ranges in deployments
 # where untrusted neighbours can reach the api on the same docker bridge.
 TRUSTED_PROXIES=
+# HSTS header value — applied prod-only by both Caddyfiles. Default is
+# 1 year + subdomains, NO preload (preload-listing is a one-way
+# commitment; browser preload removals take months). Operators who
+# want to apply for the HSTS preload list at https://hstspreload.org/
+# set:
+#     HSTS_HEADER="max-age=31536000; includeSubDomains; preload"
+# SEC_REVIEW F60.
+HSTS_HEADER=
 JOB_RECOMPUTE_MAX_RUNTIME_SECONDS=240
 JOB_RECOMPUTE_MAX_ROWS_PER_TICK=5000
 JOB_AUDIT_RETENTION_DAYS=180

+ 8 - 1
api/docker/Caddyfile

@@ -56,8 +56,15 @@
 
     # HSTS: prod-only. Setting it in dev would lock you out of plain-HTTP
     # localhost on the same hostname (sticky for 1 year). Gate strictly.
+    # Value is operator-tuneable via the `HSTS_HEADER` env var so a
+    # deployment that wants to apply for the browser preload list can
+    # opt in by setting:
+    #     HSTS_HEADER="max-age=31536000; includeSubDomains; preload"
+    # Default keeps the conservative no-preload value — preload-listing
+    # is a one-way commitment (browser preload removals take months) so
+    # we don't enable it by default (SEC_REVIEW F60).
     @prod expression `{env.APP_ENV} == "production"`
-    header @prod Strict-Transport-Security "max-age=31536000; includeSubDomains"
+    header @prod Strict-Transport-Security "{$HSTS_HEADER:max-age=31536000; includeSubDomains}"
 
     # CSP: docs viewer needs RapiDoc from jsDelivr + inline styles + the
     # try-it-now feature posting to /api/v1/*. Everything else is JSON.

+ 9 - 1
ui/docker/Caddyfile

@@ -45,8 +45,16 @@
         X-Permitted-Cross-Domain-Policies "none"
     }
 
+    # HSTS: prod-only (setting it in dev would lock you out of plain-HTTP
+    # localhost on the same hostname for a year). The value is operator-
+    # tuneable via the `HSTS_HEADER` env var so a deployment that wants
+    # to apply for the browser preload list can opt in by setting:
+    #     HSTS_HEADER="max-age=31536000; includeSubDomains; preload"
+    # Default keeps the conservative no-preload value — preload-listing
+    # is a one-way commitment (browser preload removals take months) so
+    # we don't enable it by default (SEC_REVIEW F60).
     @prod expression `{env.APP_ENV} == "production"`
-    header @prod Strict-Transport-Security "max-age=31536000; includeSubDomains"
+    header @prod Strict-Transport-Security "{$HSTS_HEADER:max-age=31536000; includeSubDomains}"
 
     php_server
 }