Security headers (CSP, HSTS, Permissions-Policy, …)
OrbitalReg ships strict HTTP security headers on every response from the API, the SPA static-asset container, and every operator-hosted nginx vhost (orbitalreg.com / orbitalreg.com / portal.orbitalreg.com / docs.orbitalreg.com). Pen-test reports and procurement reviews routinely ask for Content-Security-Policy, Strict-Transport-Security, X-Frame-Options, Referrer-Policy, and Permissions-Policy; this page documents what we emit, why, and how to customise it.
What we emit
| Header | Default value | Source |
|---|---|---|
Strict-Transport-Security | max-age=63072000; includeSubDomains | API + SPA + nginx snippet |
Content-Security-Policy | strict per-surface variant (see below) | API + SPA + nginx snippet |
X-Frame-Options | DENY | API + SPA + nginx snippet |
X-Content-Type-Options | nosniff | API + SPA + nginx snippet |
Referrer-Policy | strict-origin-when-cross-origin | API + SPA + nginx snippet |
Permissions-Policy | every surface OrbitalReg never asks for is disabled | API + SPA + nginx snippet |
Cross-Origin-Opener-Policy | same-origin | API + SPA + nginx snippet |
Why three CSP variants?
The Content-Security-Policy is tuned per surface:
- API responses (
/api/v1/*,/api/admin/*,/auth/*,/health/*) returndefault-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'. The API never serves a navigation document, so the most restrictive directive is the right default — every loader path blocked, no inline-script footgun. - SPA pages served by the frontend nginx allow
script-src 'self'(Vite emits hashed module scripts under/assets/),style-src 'self' 'unsafe-inline'(Tailwind injects per-element style attributes),img-src 'self' data: https:, andconnect-src 'self'. - Portal (Astro-SSR) relaxes
style-srcto allow hashed inline styles. The strict-defaultsecurity-headers.confis replaced withsecurity-headers-portal.conffor that vhost only.
Where the headers come from
1. The API server (appmw.SecurityHeaders)
The chi middleware at api/internal/middleware/security_headers.go stamps the headers on every response — including 503 maintenance gates, 403 block hits, and CORS preflight responses. Procurement scanners hit error paths far more often than the happy path; the headers ride on every status code.
Configurable via env vars:
ORBITALREG_HTTP_SECURITY_HEADERS_DISABLE=false # delegate to a sibling reverse proxy
ORBITALREG_HTTP_HSTS_MAX_AGE_SECONDS=63072000 # set 0 to omit HSTS (plaintext local dev)
ORBITALREG_HTTP_HSTS_PRELOAD=false # only flip true after submitting to hstspreload.org
ORBITALREG_HTTP_CONTENT_SECURITY_POLICY="" # override the strict default2. The SPA static-asset container (frontend/nginx.conf)
The bundled nginx config in frontend/Dockerfile ships the same headers with add_header ... always; so they survive 404 / 5xx pages too.
3. The operator-hosted nginx vhosts
Drop deploy/nginx/security-headers.conf (or security-headers-portal.conf for portal) into /etc/nginx/conf.d/ on the IONOS Frankfurt VM and add an include /etc/nginx/conf.d/security-headers.conf; directive to each server { … } stanza in:
/etc/nginx/sites-available/orbitalreg.com/etc/nginx/sites-available/orbitalreg.com/etc/nginx/sites-available/portal.orbitalreg.com← portal-relaxed variant/etc/nginx/sites-available/docs.orbitalreg.com/etc/nginx/sites-available/status.orbitalreg.com
Reload after editing:
sudo nginx -t && sudo systemctl reload nginxVerification
Quick smoke-check:
curl -sI https://orbitalreg.com | grep -Ei 'strict-transport-security|content-security-policy|x-frame-options|referrer-policy|permissions-policy|cross-origin-opener-policy'Online graders:
- https://securityheaders.com/?q=orbitalreg.com — target A+
- https://observatory.mozilla.org/analyze/orbitalreg.com — target B+
Both should return their top tier with the snippet in place.
Customisation knobs
Helm (charts/orbitalreg) — customer K8s installs
The chart's nginx-ingress annotations include a configuration-snippet hook that mirrors the strict header set by default. Customers who run a sibling reverse proxy that already adds these headers (e.g. an upstream WAF / Cloudflare / Akamai) can flip the toggle off:
ingress:
securityHeaders:
enabled: true # default
contentSecurityPolicy: "" # leave empty for the default
hstsMaxAgeSeconds: 63072000
hstsPreload: falseThe API itself emits the same headers via the chi middleware regardless of whether the ingress annotations are wired up — even with the toggle off, the end-user response carries them.
Environment variables (any deployment)
Bare-metal / docker-compose deployments that don't run the Helm chart can override via ORBITALREG_HTTP_* env vars; see the table above.
Out of scope for v1
- Subresource Integrity (SRI) for CDN-loaded assets — OrbitalReg hosts every asset itself, so SRI hashes have no externally-mutable target to pin against. Not added.
- CSP reporting endpoint (
report-uri/report-to) — added once we have an actual violation stream worth triaging. Today the strict default-src 'none' on the API + 'self'-bound SPA CSP rarely fires, so a reporting pipeline would be mostly empty. - Per-route CSP nonces for the SPA — Vite's emit pattern doesn't need them today; revisit if a future admin embed surface introduces inline scripts.
What changed (item 82)
api/internal/middleware/security_headers.go— chi middleware, config struct, defaults, six unit tests covering header set / HSTS variants / disable path / handler-side override / custom CSP.api/cmd/server/main.go— wires the middleware afterRecovererandMetricsso degraded responses still carry the headers.api/internal/config/config.go— four newHTTPfields driven byhttp.security_headers_disable/http.hsts_max_age_seconds/http.hsts_preload/http.content_security_policy.frontend/nginx.conf— strict CSP + HSTS + the rest, applied withalwaysso 4xx/5xx pages also carry them.deploy/nginx/security-headers.conf+security-headers-portal.conf— drop-in nginx snippets for the IONOS-hosted marketing / docs / status / portal vhosts.charts/orbitalreg— Helm values + ingress annotations expose theingress.securityHeadersknob.