Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion apps/frontend/src/lib/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
Expand Down
10 changes: 10 additions & 0 deletions docker-compose.demo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
# ---------------------------------------------------------------------------
Expand Down
29 changes: 26 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
Loading