Files
HiveGrid-Wireframes/hivegrid-cloud/docs/security/runbook.md
T

162 lines
10 KiB
Markdown

# Security Header Runbook
This runbook defines the expected security-header contract for the web server and Hono API runtime, plus deterministic checks for local and production triage. It intentionally avoids secrets, tenant identifiers, environment-specific values, and placeholder language.
## Expected Web Headers
The Node web server in `server/server.ts` applies these headers to React Router responses, redirects, and static asset responses:
| Header | Expected value | Purpose |
| --- | --- | --- |
| `x-content-type-options` | `nosniff` | Prevent MIME sniffing. |
| `referrer-policy` | `strict-origin-when-cross-origin` | Keep same-origin referrer detail while reducing cross-origin leakage. |
| `x-frame-options` | `DENY` | Prevent clickjacking through legacy frame controls. |
| `permissions-policy` | `camera=(), microphone=(), geolocation=()` | Disable high-risk browser capabilities by default. |
| `cross-origin-opener-policy` | `same-origin` | Isolate top-level browsing context where supported. |
| `cross-origin-resource-policy` | `same-origin` | Restrict cross-origin resource embedding by default. |
| `cross-origin-embedder-policy` | `unsafe-none` | Preserve compatibility for embedded/static assets while making the policy explicit. |
Web checks must also confirm that existing response behavior is preserved:
- `cache-control` remains unchanged for immutable static assets such as `/assets/**`.
- Redirect status and `location` behavior remain unchanged when React Router emits redirects.
- Header assignment uses single values rather than appended comma-joined duplicates.
## Expected API Headers
The API app in `apps/api/src/app.ts` applies these headers to health, OpenAPI, preflight, error, and `/v1` responses:
| Header | Expected value | Purpose |
| --- | --- | --- |
| `x-content-type-options` | `nosniff` | Prevent MIME sniffing. |
| `referrer-policy` | `no-referrer` | Avoid leaking API request paths through referrers. |
| `x-frame-options` | `DENY` | Prevent API documents or error pages from being framed. |
| `permissions-policy` | `camera=(), microphone=(), geolocation=()` | Disable browser feature access for API responses. |
| `cross-origin-opener-policy` | `same-origin` | Keep browser isolation policy explicit. |
| `cross-origin-resource-policy` | `same-origin` | Limit cross-origin resource reuse by default. |
| `cross-origin-embedder-policy` | `unsafe-none` | Avoid over-broad isolation that could break clients. |
API checks must also confirm that existing middleware contracts are preserved:
- `x-request-id` is still emitted and forwarded according to the request-id middleware contract.
- Allowed CORS requests still include the expected `access-control-allow-origin` value.
- Canonical app and admin browser origins are accepted from routing config, and explicit `WEB_ORIGIN` and `ADMIN_ORIGIN` values are treated as full origins rather than host-only aliases.
- Denied origins, including malformed variants and `https://attacker.example.invalid`, do not emit `access-control-allow-origin` while still returning `x-request-id` for incident correlation.
- Preflight responses keep CORS behavior while receiving the request-id and security headers.
- No response header includes secrets, personal data, tenant identifiers, or raw environment values.
## Local Verification Commands
Run focused checks from the repository root. These commands are scoped to the files and tests that define the security-header and CORS contracts:
```bash
pnpm --dir server exec eslint server.test.ts server.ts --max-warnings=0
pnpm --dir server exec vitest run server.test.ts
pnpm exec eslint apps/api/src/app.test.ts apps/api/src/cors.test.ts apps/api/src/app.ts apps/api/src/cors.ts --max-warnings=0
pnpm exec vitest run apps/api/src/app.test.ts apps/api/src/cors.test.ts packages/config/src/env-access-enforcement.test.ts packages/config/src/api.test.ts
test -s docs/security/runbook.md
python - <<'PY'
from pathlib import Path
text = Path('docs/security/runbook.md').read_text()
assert 'apps/api/src/cors.test.ts' in text
assert 'attacker.example.invalid' in text
assert 'T' + 'BD' not in text
assert 'TO' + 'DO' not in text
PY
```
Browser, Playwright, Cypress, or other E2E execution is manual unless a human explicitly approves the exact focused run in the current session.
## Production Curl Checklist
Use the deployed application origin and API origin appropriate for the environment under investigation. Do not paste secrets or authenticated tokens into shared logs.
### Web HTML response
```bash
curl -sSI https://example-web-origin/ | tr -d '\r' | grep -Ei '^(x-content-type-options|referrer-policy|x-frame-options|permissions-policy|cross-origin-(opener|resource|embedder)-policy):'
```
Confirm that every expected web security header is present with the values listed above.
### Web static asset response
```bash
curl -sSI https://example-web-origin/assets/app.js | tr -d '\r' | grep -Ei '^(cache-control|x-content-type-options|referrer-policy|x-frame-options|permissions-policy|cross-origin-(opener|resource|embedder)-policy):'
```
Confirm that static assets still expose the intended `cache-control` policy while also carrying the security headers.
### API health response
```bash
curl -sSI https://example-api-origin/healthz | tr -d '\r' | grep -Ei '^(x-request-id|x-content-type-options|referrer-policy|x-frame-options|permissions-policy|cross-origin-(opener|resource|embedder)-policy):'
```
Confirm that `x-request-id` and all expected API security headers are present.
### API CORS allowed app origin response
```bash
curl -sSI https://example-api-origin/v1/example \
-H 'Origin: https://example-web-origin' | \
tr -d '\r' | grep -Ei '^(access-control-allow-origin|x-request-id|x-content-type-options|referrer-policy|x-frame-options|permissions-policy|cross-origin-(opener|resource|embedder)-policy):'
```
Confirm that `access-control-allow-origin` exactly matches `https://example-web-origin`, that `x-request-id` is present, and that the API security headers match the values above.
### API CORS allowed admin origin response
```bash
curl -sSI https://example-api-origin/v1/example \
-H 'Origin: https://example-admin-origin' | \
tr -d '\r' | grep -Ei '^(access-control-allow-origin|x-request-id|x-content-type-options|referrer-policy|x-frame-options|permissions-policy|cross-origin-(opener|resource|embedder)-policy):'
```
Confirm that `access-control-allow-origin` exactly matches `https://example-admin-origin`. If the admin origin is configured through `ADMIN_ORIGIN`, compare against the full configured origin string only; do not rewrite it to a bare host in notes or logs.
### API CORS denied origin response
```bash
curl -sSI https://example-api-origin/v1/example \
-H 'Origin: https://attacker.example.invalid' | \
tr -d '\r' | grep -Ei '^(access-control-allow-origin|x-request-id|x-content-type-options|referrer-policy|x-frame-options|permissions-policy|cross-origin-(opener|resource|embedder)-policy):' || true
```
Confirm that no `access-control-allow-origin` header is emitted for `https://attacker.example.invalid`, while `x-request-id` and the API security headers remain present for safe incident correlation.
### API CORS preflight response
```bash
curl -sSI -X OPTIONS https://example-api-origin/v1/example \
-H 'Origin: https://example-web-origin' \
-H 'Access-Control-Request-Method: GET' | \
tr -d '\r' | grep -Ei '^(access-control-allow-origin|access-control-allow-methods|access-control-allow-credentials|x-request-id|x-content-type-options|referrer-policy|x-frame-options|permissions-policy|cross-origin-(opener|resource|embedder)-policy):'
```
Confirm that the allowed origin is preserved, CORS preflight headers remain present, `x-request-id` is present, and security headers are still attached to the preflight response.
## CORS Configuration Guidance
- Prefer canonical routing-derived app and admin origins for normal deployments, then add `WEB_ORIGIN` and `ADMIN_ORIGIN` only when an environment needs explicit browser origins.
- Treat `WEB_ORIGIN` and `ADMIN_ORIGIN` as complete origin strings including scheme and host, for example `https://example-web-origin` and `https://example-admin-origin`; do not document or compare them as host-only aliases.
- Do not paste raw environment dumps, secrets, tokens, tenant identifiers, or customer-specific domains into tickets or shared logs. Record only the generic origin label under test, the response header names observed, and the `x-request-id` value when needed.
- Test both positive and negative origins during triage. A positive-only check can miss an over-broad allowlist regression.
## Known Interactions
- Security headers are set rather than appended, so a response should contain one effective value per header.
- Web security headers must not overwrite static asset `cache-control` headers.
- API security headers run alongside request-id and CORS middleware; missing `x-request-id` on any API response is a regression in middleware ordering, and missing `access-control-allow-origin` is expected only for denied browser origins.
- `cross-origin-embedder-policy: unsafe-none` is intentional for both runtimes to avoid breaking existing embedding and asset-loading behavior.
- CORS remains the source of truth for allowed browser origins; the security-header layer should not relax or replace CORS decisions.
## Rollback and Triage Notes
1. Reproduce with the focused local Vitest command for the affected runtime before changing middleware order.
2. Compare curl output from a known-good response against the failing path, checking both missing security headers and preserved headers such as `cache-control`, `x-request-id`, and `access-control-allow-origin`.
3. If security headers are missing only on static assets, inspect the static response path in `server/server.ts` before changing React Router forwarding logic.
4. If API CORS headers disappear, inspect middleware ordering around request-id, security headers, and CORS in `apps/api/src/app.ts`.
5. If duplicate comma-joined policy values appear, verify that code uses `set` or `setHeader` semantics rather than `append`.
6. Roll back the smallest runtime-specific header change first; do not remove request-id, CORS, or cache-control behavior as part of header rollback.