Skip to content

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

HeaderDefault valueSource
Strict-Transport-Securitymax-age=63072000; includeSubDomainsAPI + SPA + nginx snippet
Content-Security-Policystrict per-surface variant (see below)API + SPA + nginx snippet
X-Frame-OptionsDENYAPI + SPA + nginx snippet
X-Content-Type-OptionsnosniffAPI + SPA + nginx snippet
Referrer-Policystrict-origin-when-cross-originAPI + SPA + nginx snippet
Permissions-Policyevery surface OrbitalReg never asks for is disabledAPI + SPA + nginx snippet
Cross-Origin-Opener-Policysame-originAPI + SPA + nginx snippet

Why three CSP variants?

The Content-Security-Policy is tuned per surface:

  • API responses (/api/v1/*, /api/admin/*, /auth/*, /health/*) return default-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:, and connect-src 'self'.
  • Portal (Astro-SSR) relaxes style-src to allow hashed inline styles. The strict-default security-headers.conf is replaced with security-headers-portal.conf for 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:

bash
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 default

2. 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:

bash
sudo nginx -t && sudo systemctl reload nginx

Verification

Quick smoke-check:

bash
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:

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:

yaml
ingress:
  securityHeaders:
    enabled: true                          # default
    contentSecurityPolicy: ""              # leave empty for the default
    hstsMaxAgeSeconds: 63072000
    hstsPreload: false

The 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 after Recoverer and Metrics so degraded responses still carry the headers.
  • api/internal/config/config.go — four new HTTP fields driven by http.security_headers_disable / http.hsts_max_age_seconds / http.hsts_preload / http.content_security_policy.
  • frontend/nginx.conf — strict CSP + HSTS + the rest, applied with always so 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 the ingress.securityHeaders knob.

Released under the Apache-2.0 License.