diff --git a/CHANGELOG.md b/CHANGELOG.md index 19cfc91d..93b4953c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,32 @@ and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0. operator runbook. Producer-Reviewer: security-reviewer sign-off after two rounds (SSRF, sandbox confinement, resource bounds). +### Changed +- **First-time visitors default to English.** The language detector no longer + falls back to the browser locale for a fresh visitor — with no stored choice + it uses the app's English default (English is the product's primary language + and the public demo serves a global audience). A visitor's explicit language + pick is still remembered via `localStorage` and restored on return. + +### Fixed +- **Demo read-only lock is now actually enforced in the deployed stack.** + `DEMO_READ_ONLY` / `DEMO_ALLOW_SANDBOX_SCANS` were set in `.env` but never + passed into the backend container, so the backend read them as unset and the + public read-only boundary silently stayed OFF (writes were accepted). The + demo overlay now plumbs both flags into backend/worker/beat; verified via + `/health` (`demo_read_only:true`) and a write probe (`POST` → 403). +- **Traefik works on Docker Engine 29+.** Docker 29 raised its minimum + negotiable API to 1.44 and rejects Traefik v3.2.1's legacy 1.24 client, so + the docker provider discovered no routers and served its self-signed default + cert (no Let's Encrypt, no routing). Bumped Traefik to v3.6.1, which + negotiates the API version with the daemon. +- **`security-headers` middleware is now defined.** The backend and frontend + routers referenced `security-headers@file`, but no file provider or dynamic + config ever defined it — so once the docker provider worked (Traefik ≥ v3.6.1 + on Docker 29) both routers became invalid. It is now defined via + docker-provider labels and referenced as `@docker` (HSTS, nosniff, + frame-deny, referrer-policy). + ## [0.18.0] — 2026-07-25 ### Added diff --git a/apps/frontend/src/lib/i18n.ts b/apps/frontend/src/lib/i18n.ts index 930974d3..d669f878 100644 --- a/apps/frontend/src/lib/i18n.ts +++ b/apps/frontend/src/lib/i18n.ts @@ -88,7 +88,14 @@ void i18n ], interpolation: { escapeValue: false }, detection: { - order: ["localStorage", "navigator"], + // First-time visitors default to ENGLISH, not their browser language: + // the product's primary language is English (CLAUDE.md), and the public + // demo is shown to a global audience. `navigator` is deliberately omitted + // so a fresh visitor (no stored choice) falls through to `fallbackLng` + // ("en") instead of auto-switching to e.g. a Korean browser locale. A + // visitor who explicitly picks a language still has it remembered via + // localStorage and restored on return. + order: ["localStorage"], caches: ["localStorage"], lookupLocalStorage: "trustedoss.lang", }, diff --git a/docker-compose.demo.yml b/docker-compose.demo.yml index e9d90fad..1f66d82e 100644 --- a/docker-compose.demo.yml +++ b/docker-compose.demo.yml @@ -65,6 +65,16 @@ x-demo-scan-env: &demo-scan-env SCAN_TRIGGER_RATE_LIMIT: "5/minute" SBOM_INGEST_MAX_BYTES: "10485760" SBOM_INGEST_MAX_COMPONENTS: "3000" + # Demo carve-out toggles — the operator choices from .env (runbook §6 / §6.5). + # These MUST be plumbed into the container: the backend reads DEMO_READ_ONLY / + # DEMO_ALLOW_SANDBOX_SCANS via os.getenv() at RUNTIME (core/config.py), so a + # value that lives only in `.env` sits in compose-interpolation scope and never + # reaches the process — the read-only lock silently stayed OFF without this. + # Unlike the STATIC caps above these are `${VAR:-false}` so `.env` drives them; + # the `false` default keeps a misconfigured overlay CLOSED (no writes allowed, + # no sandbox carve-out) rather than silently exposing a writable public demo. + DEMO_READ_ONLY: ${DEMO_READ_ONLY:-false} + DEMO_ALLOW_SANDBOX_SCANS: ${DEMO_ALLOW_SANDBOX_SCANS:-false} services: # --------------------------------------------------------------------------- diff --git a/docker-compose.yml b/docker-compose.yml index 745b5da0..b59625f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -189,7 +189,15 @@ services: # Edge — Traefik v3.2 with HTTP→HTTPS redirect and Let's Encrypt HTTP-01. # --------------------------------------------------------------------------- traefik: - image: traefik:v3.2.1 + # Docker Engine 29+ raised its MINIMUM negotiable API to 1.44 and rejects the + # Docker client's legacy 1.24 default. Traefik ≤ v3.5 hard-used 1.24 for the + # docker provider (it ignores DOCKER_API_VERSION when negotiation is on), so + # on Docker 29 it logged "client version 1.24 is too old" on a loop, found no + # routers, and served its self-signed default cert (no Let's Encrypt, no + # routing). v3.6.1 added real Docker API version negotiation (queries the + # daemon's supported range and picks the highest mutual version) — the fix + # for this exact incident. Do not drop below v3.6.1 while hosts run Docker 29+. + image: traefik:v3.6.1 restart: unless-stopped command: - "--providers.docker=true" @@ -362,7 +370,22 @@ services: - "traefik.http.routers.backend.entrypoints=websecure" - "traefik.http.routers.backend.tls=true" - "traefik.http.routers.backend.tls.certresolver=le" - - "traefik.http.routers.backend.middlewares=security-headers@file" + - "traefik.http.routers.backend.middlewares=security-headers@docker" + # security-headers middleware — DEFINED here via docker-provider labels + # (backend carries traefik.enable=true, so the provider parses them) and + # referenced as `@docker` by BOTH the backend and frontend routers. It was + # previously referenced as `security-headers@file`, but no file provider was + # ever enabled and no dynamic config ever defined it — so the moment the + # docker provider actually worked (Traefik ≥ v3.6.1 on Docker 29) both + # routers became INVALID ("middleware security-headers@file does not + # exist"). Defining it inline keeps everything in the docker provider — no + # extra file mount or --providers.file flag needed. + - "traefik.http.middlewares.security-headers.headers.stsSeconds=63072000" + - "traefik.http.middlewares.security-headers.headers.stsIncludeSubdomains=true" + - "traefik.http.middlewares.security-headers.headers.stsPreload=true" + - "traefik.http.middlewares.security-headers.headers.contentTypeNosniff=true" + - "traefik.http.middlewares.security-headers.headers.frameDeny=true" + - "traefik.http.middlewares.security-headers.headers.referrerPolicy=strict-origin-when-cross-origin" deploy: resources: limits: @@ -540,7 +563,7 @@ services: - "traefik.http.routers.frontend.entrypoints=websecure" - "traefik.http.routers.frontend.tls=true" - "traefik.http.routers.frontend.tls.certresolver=le" - - "traefik.http.routers.frontend.middlewares=security-headers@file" + - "traefik.http.routers.frontend.middlewares=security-headers@docker" deploy: resources: limits: