diff --git a/SECURITY.md b/SECURITY.md index d90eed1d..f267b74c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,11 +4,12 @@ GOWDK is an experimental 0.x compiler/runtime. Do not treat generated apps as production-ready security enforcement. First slices exist for generated action decoding, unexpected-field rejection, -direct literal request-shape validation, opt-in CSRF, configurable action/API -request body caps, generated `http.Server` read/header/write/idle timeout -defaults, `MaxHeaderBytes`, safe local redirects, guard execution, SSR panic -boundaries, log redaction, and no-store request-time responses. These are not a -complete production security model. +direct literal request-shape validation, default CSRF with staged multi-key +secret rotation, configurable action/API request body caps, generated +`http.Server` read/header/write/idle timeout defaults, `MaxHeaderBytes`, safe +local redirects, guard execution, SSR panic boundaries, log redaction, and +no-store request-time responses. These are not a complete production security +model. ## Reporting Vulnerabilities @@ -47,7 +48,6 @@ Known incomplete production areas include: - Authentication and session policy. - Full guard contract coverage. -- Multi-key CSRF secret rotation. - Full redirect policy. - Per-route request body/header limit policy beyond the current generated body caps and server header cap. diff --git a/addons/observability/observability.go b/addons/observability/observability.go index 4a60342a..f2d3c223 100644 --- a/addons/observability/observability.go +++ b/addons/observability/observability.go @@ -51,6 +51,16 @@ func WithSink(sink gowdktrace.Sink) gowdktrace.TracerOption { return gowdktrace.WithSink(sink) } +// WithExportQueueSize configures the bounded completed-span queue. +func WithExportQueueSize(size int) gowdktrace.TracerOption { + return gowdktrace.WithExportQueueSize(size) +} + +// WithExportTimeout configures the deadline for one sink export. +func WithExportTimeout(timeout time.Duration) gowdktrace.TracerOption { + return gowdktrace.WithExportTimeout(timeout) +} + // AlwaysOn samples every span. func AlwaysOn() gowdktrace.Sampler { return gowdktrace.AlwaysOn() diff --git a/csrf_test.go b/csrf_test.go new file mode 100644 index 00000000..d15b8a17 --- /dev/null +++ b/csrf_test.go @@ -0,0 +1,58 @@ +package gowdk + +import ( + "strings" + "testing" +) + +func TestCSRFConfigSecretEnvNames(t *testing.T) { + config := CSRFConfig{ + SecretEnv: " PRIMARY_CSRF_SECRET ", + VerificationSecretEnvs: []string{" NEXT_CSRF_SECRET ", "PREVIOUS_CSRF_SECRET"}, + } + + if got := config.SecretEnvName(); got != "PRIMARY_CSRF_SECRET" { + t.Fatalf("primary CSRF secret env = %q", got) + } + if got := strings.Join(config.SecretEnvNames(), ","); got != "PRIMARY_CSRF_SECRET,NEXT_CSRF_SECRET,PREVIOUS_CSRF_SECRET" { + t.Fatalf("CSRF secret env names = %q", got) + } +} + +func TestCSRFConfigValidateRejectsBlankAndDuplicateVerificationSecretEnvs(t *testing.T) { + tests := []struct { + name string + config CSRFConfig + want string + }{ + { + name: "blank", + config: CSRFConfig{VerificationSecretEnvs: []string{" "}}, + want: "VerificationSecretEnvs[0]", + }, + { + name: "duplicates primary", + config: CSRFConfig{ + SecretEnv: "PRIMARY_CSRF_SECRET", + VerificationSecretEnvs: []string{"PRIMARY_CSRF_SECRET"}, + }, + want: "declared more than once", + }, + { + name: "duplicates verification key", + config: CSRFConfig{ + VerificationSecretEnvs: []string{"OLD_CSRF_SECRET", " OLD_CSRF_SECRET "}, + }, + want: "declared more than once", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.config.Validate() + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("Validate() error = %v, want %q", err, test.want) + } + }) + } +} diff --git a/docs/compiler/generated-output.md b/docs/compiler/generated-output.md index cee54e45..953b9a85 100644 --- a/docs/compiler/generated-output.md +++ b/docs/compiler/generated-output.md @@ -115,6 +115,11 @@ Action, API, fragment, command, query, SSR, hybrid, realtime, guard, rate-limit, CSRF, CORS, and tracing behavior is included only when declared, enabled, and validated for the selected build. +Generated CSRF startup reads the primary signing secret named by +`Build.CSRF.SecretEnv` and every verification-only secret named by +`Build.CSRF.VerificationSecretEnvs`. Generated source contains environment +variable names, never secret values. + Worker and cron role outputs follow the same generated-app rule: they are normal Go modules downstream of contract metadata, and `--worker-bin` / `--cron-bin` compile their generated commands with `go build`. diff --git a/docs/engineering/README.md b/docs/engineering/README.md index 90aab49f..541cecf8 100644 --- a/docs/engineering/README.md +++ b/docs/engineering/README.md @@ -27,6 +27,10 @@ current product, reference, compiler, and engineering docs. Keep new plans short and scoped to active work. Delete or fold them into current contracts when they stop being useful. +Current secure-runtime slice: +[CSRF secret rotation spec](csrf-secret-rotation-spec.md) and +[implementation plan](csrf-secret-rotation-plan.md). + Use an ADR instead of a plan when the durable decision is more important than the execution checklist. diff --git a/docs/engineering/architecture.md b/docs/engineering/architecture.md index 92c67c8b..0370602f 100644 --- a/docs/engineering/architecture.md +++ b/docs/engineering/architecture.md @@ -1,8 +1,8 @@ # Architecture GOWDK is a compile-first Go web compiler and runtime. `.gwdk` files declare web -surface contracts; normal Go packages own application behavior; generated Go is -inspectable adapter glue. +surface contracts and markup; normal Go packages own application behavior and +data; generated Go is inspectable adapter glue. ## System Shape @@ -24,7 +24,7 @@ request-time lane selected with `server {}` or `go server {}`. | Layer | Owns | Does not own | | --- | --- | --- | -| GOWDK source | Page, component, layout, route, endpoint, asset, guard, cache, and bounded browser declarations | Business logic, storage, authorization policy, production operations | +| GOWDK source | Page, component, layout, and fragment markup plus route, endpoint, asset, guard, cache, and bounded browser declarations | Business logic, storage, authorization policy, production operations | | Compiler internals | Parsing, typed AST, analysis, IR, diagnostics, validation, build reports, manifests, and generated-output planning | Request serving and app-owned runtime state | | Generated app | Adapter glue, route registration, decoding, response writing, guard/rate-limit/CSRF ordering, embedded assets, lifecycle hooks | Domain behavior and external infrastructure | | Runtime packages | `net/http` helpers, request context, response envelopes, assets, guards, contracts, tracing, and addon helpers | Application schemas, migrations, secrets, auth policy, backups, incidents | diff --git a/docs/engineering/csrf-secret-rotation-plan.md b/docs/engineering/csrf-secret-rotation-plan.md new file mode 100644 index 00000000..e0b8c040 --- /dev/null +++ b/docs/engineering/csrf-secret-rotation-plan.md @@ -0,0 +1,84 @@ +# Implementation Plan: CSRF Secret Rotation + +## Context + +The feature contract is +[CSRF Secret Rotation](csrf-secret-rotation-spec.md). It closes the multi-key +rotation gap recorded in `SECURITY.md` and the deployment reference. + +## Assumptions + +- The existing HMAC token format remains secure and does not need a key ID. +- Secret environment names are compile-time config; values are runtime-only. +- A three-phase deployment is acceptable because it permits rolling deploys + and rollback without invalidating tokens. + +## Proposed Changes + +- Add verification-secret environment names to `gowdk.CSRFConfig`, its + structural parser, and config validation. +- Extend `runtime/actions.CSRF` with a primary signer and multiple verification + keys. +- Generate AST-backed startup code that loads all configured keys and passes + them to `actions.NewCSRF`. +- Seed configured verification secrets in generated audit tests. +- Update security, config, action, deployment, operations, generated-output, + and product-status documentation. + +## Files Expected To Change + +- `gowdk.go` +- `internal/project/config.go` and config validation/tests +- `runtime/actions/csrf.go` and tests +- `internal/appgen/source.go`, audit test generation, and generator tests +- `SECURITY.md` +- `docs/compiler/generated-output.md` +- `docs/reference/config.md` and `docs/reference/deployment.md` +- `docs/language/actions.md` +- `docs/engineering/security*.md` and `operations.md` +- `docs/product/requirements.md` + +## Data And API Impact + +- Additive public Go config field: + `CSRFConfig.VerificationSecretEnvs []string`. +- Additive public runtime option: + `CSRFOptions.VerificationSecrets [][]byte`. +- No `.gwdk` syntax, manifest schema, token format, or persisted-data change. +- Existing binaries/configs with only `SecretEnv` behave as before. + +## Tests + +- Unit: config validation; primary/verification signing, validation, refresh, + retirement, short-key rejection, and principal binding. +- Integration: generated source contains all environment reads and compiles. +- End-to-end: generated binary accepts a token minted by an overlapping key. +- Manual: follow the documented three-phase rollout against a generated app. + +## Verification Commands + +```sh +gofmt -w gowdk.go internal/project/config.go internal/project/config_validation.go internal/project/config_test.go internal/gowdkcmd/project_inputs.go runtime/actions/csrf.go runtime/actions/actions_test.go internal/appgen/source.go internal/appgen/audit_tests.go internal/appgen/appgen_test.go +go test ./runtime/actions ./internal/project ./internal/appgen +go build ./cmd/gowdk +scripts/check-docs-links.sh +scripts/check-docs-style.sh +scripts/test-go-modules.sh +``` + +## Rollback Plan + +- Remove verification keys from config and redeploy with the current primary. +- Because the token format is unchanged, reverting the code keeps primary-key + tokens valid. +- During phase two, rollback to phase one; phase-one instances already verify + the promoted key. + +## Risks + +- Operators can remove an old key before the overlap window ends, invalidating + open forms. The deployment guide makes the sequencing explicit. +- Large key lists add HMAC work per validation. The list is trusted config and + expected to contain only the staged/current/retiring keys. +- Generator changes overlap active app-generation work; edits must remain + confined to the CSRF AST builder and focused tests. diff --git a/docs/engineering/csrf-secret-rotation-spec.md b/docs/engineering/csrf-secret-rotation-spec.md new file mode 100644 index 00000000..b6513f6e --- /dev/null +++ b/docs/engineering/csrf-secret-rotation-spec.md @@ -0,0 +1,103 @@ +# Feature Spec: CSRF Secret Rotation + +## Problem + +Generated apps currently sign and validate CSRF tokens with one runtime secret. +Changing that secret invalidates open forms and makes a rolling deployment +unsafe: old instances reject tokens minted by new instances, and new instances +reject tokens minted by old instances. + +## Goals + +- Let generated apps sign with one primary secret and validate with additional + verification-only secrets. +- Keep the existing single-secret configuration fully compatible. +- Refresh a valid token signed by a verification key to the primary key when a + generated page next injects a CSRF token. +- Fail generated-app startup when any configured secret is absent or shorter + than 32 bytes. +- Document a rollback-safe staged rotation procedure. + +## Non-Goals + +- Owning deployment-platform secret storage or distribution. +- Replacing app-owned authentication, sessions, or resource authorization. +- Adding key identifiers or changing the existing token wire format. +- Adding token expiration or cross-instance server-side CSRF state. + +## Users And Permissions + +- Primary users: operators of generated GOWDK applications with actions, + commands, or state-changing APIs. +- Roles or permissions: deployment operators control secret environment + variables and generated app rollout order. +- Data visibility rules: config stores environment-variable names only; secret + values remain runtime-only and must not appear in generated source, reports, + diagnostics, or logs. + +## User Flow + +1. Deploy every instance with the current key as primary and the next key as a + verification key. +2. Deploy every instance with the next key as primary and the current key as a + verification key. +3. After the overlap window, deploy without the retired key. + +## Requirements + +### Functional + +- `gowdk.CSRFConfig` exposes `VerificationSecretEnvs []string`. +- `actions.CSRFOptions` exposes `VerificationSecrets [][]byte`. +- `CSRF.Token` signs new tokens only with `Secret`. +- `CSRF.Validate` accepts signatures from the primary or any verification key. +- `CSRF.Token` replaces a request cookie signed only by a verification key with + a newly generated primary-key token. +- Config validation rejects blank or duplicate verification environment names, + including duplication of the primary environment name. +- Generated frontend, backend-only, and split app outputs read every configured + CSRF secret and fail closed when a value is missing. + +### Non-Functional + +- Performance: verification is linear in the small operator-configured key + list; all configured keys are checked without an early return. +- Reliability: primary-only behavior and the existing token format remain + unchanged. +- Accessibility: no user-interface contract changes. +- Security/privacy: secret bytes are copied into runtime-owned memory and never + emitted into generated source or reports. +- Observability: startup errors identify the missing environment-variable name + without printing a value. + +## Acceptance Criteria + +- [x] A token minted with the old primary validates after that key moves to the + verification list. +- [x] A token minted with the new primary validates on an instance that + pre-staged the new key as verification-only. +- [x] Rendering a form with an old valid cookie emits a new primary-key cookie. +- [x] Removing a verification key makes its tokens fail validation. +- [x] Generated source reads the configured primary and verification + environment variables and compiles. +- [x] A generated binary accepts an overlapping-key token end to end. +- [x] Existing primary-only runtime and generated-app tests remain green. + +## Edge Cases + +- Missing, blank, short, or duplicate configured keys. +- Principal-bound CSRF tokens during rotation. +- Repeated verification keys supplied directly through the runtime API. +- A request containing a cookie and submitted token that match each other but + were signed by a retired key. + +## Dependencies + +- Internal: `gowdk.CSRFConfig`, structural/native config loading, + `runtime/actions`, and generated app source under `internal/appgen`. +- External: none. + +## Open Questions + +- None for this slice. Key identifiers can be reconsidered only if measured + verification cost justifies a token-format migration. diff --git a/docs/engineering/decisions/0006-gowdk-compiler-and-runtime-boundary.md b/docs/engineering/decisions/0006-gowdk-compiler-and-runtime-boundary.md index 90e2856a..06544780 100644 --- a/docs/engineering/decisions/0006-gowdk-compiler-and-runtime-boundary.md +++ b/docs/engineering/decisions/0006-gowdk-compiler-and-runtime-boundary.md @@ -171,4 +171,5 @@ domain logic, handlers, stores, auth, validation policy, or storage code. - Keep `docs/product/roadmap.md` as the active product direction. - Keep `docs/engineering/architecture.md` as the implementation boundary source of truth. -- Keep server fragments in runtime responses, not old action body syntax. +- Keep server-fragment response decisions in runtime responses, keep markup in + `.gwdk`, and do not restore old action body syntax. diff --git a/docs/engineering/operations.md b/docs/engineering/operations.md index 7d771144..51b59848 100644 --- a/docs/engineering/operations.md +++ b/docs/engineering/operations.md @@ -124,7 +124,9 @@ readiness claim: - Do not set `Build.CSRF.Disabled` for production generated action, command, or state-changing API handlers unless another cross-site request strategy is in place. Provide a stable `GOWDK_CSRF_SECRET` or configured - `Build.CSRF.SecretEnv` value in each runtime environment. + `Build.CSRF.SecretEnv` value in each runtime environment. Use + `Build.CSRF.VerificationSecretEnvs` for the staged + [CSRF rotation procedure](../reference/deployment.md#csrf-secret-rotation). - Return explicit method-not-allowed responses for unsupported methods. - Serve app assets with deterministic cache headers. - Avoid public debug endpoints by default. diff --git a/docs/engineering/security-threat-model.md b/docs/engineering/security-threat-model.md index 2dde12b0..77f8c280 100644 --- a/docs/engineering/security-threat-model.md +++ b/docs/engineering/security-threat-model.md @@ -33,7 +33,7 @@ current controls, and open follow-up areas for review. | --- | --- | --- | --- | | `.gwdk` source to compiler diagnostics | Parser, analyzer, `gowdk check`, LSP diagnostics | Stable diagnostic registry, source spans where available, redaction policy for secret-like values | Broader exact spans and diagnostic expansion remain tracked outside M5. | | Generated logs and panic sinks | `runtime/app` panic boundaries, contract worker logs | Panic responses avoid stack traces; recovered panic logs pass through secret redaction | Broader app-owned logging guidance and redaction coverage remain planned. | -| Browser/client to action endpoints | POST forms, enhanced partial forms, multipart action forms, command form adapters | Expected-field decoding, direct literal validation, configurable action body cap defaulting to 1 MiB, explicit generated file count/size/MIME upload policy, default CSRF, 405 on wrong methods, no-store error responses | Per-route limits, content scanning, storage policy, and full production CSRF rotation remain app-owned or planned. | +| Browser/client to action endpoints | POST forms, enhanced partial forms, multipart action forms, command form adapters | Expected-field decoding, direct literal validation, configurable action body cap defaulting to 1 MiB, explicit generated file count/size/MIME upload policy, default CSRF with primary and verification-only keys, old-key cookie refresh, 405 on wrong methods, no-store error responses | Per-route limits, content scanning, storage policy, and deployment-owned CSRF key retirement remain app-owned. | | Browser/client to API endpoints | Generated API routes, contract query routes | Method dispatch, configurable API body cap defaulting to 1 MiB, generated CSRF for state-changing API methods, rate-limit hook when addon is enabled | Public API hardening, typed helper expansion, and per-route policy are tracked in #24. | | Browser/client to fragments | Standalone fragments, action fragment responses | Fragment routing through generated handlers, escaped render core, no-store request-time responses; standalone fragments are GET-only and action fragments share the action cap | Broader auth/session policy remains planned. | | Browser/client to SSR `server {}` | Request-time SSR routes, route-local error pages | SSR feature gate, guard execution, safe local redirect helpers, panic boundaries, no-store failures | Full guard contract, route-local auth/session policy, and richer request-time error policy remain planned. | @@ -50,7 +50,7 @@ current controls, and open follow-up areas for review. | --- | --- | --- | --- | | Submit unexpected action fields to overwrite handler input. | Integrity of action input. | Generated decoders reject unexpected fields and skip runtime fields such as CSRF. | Medium until broader typed helper contracts stabilize. | | Send large request bodies to exhaust memory or handler time. | Availability of generated servers. | Generated action/API adapters cap bodies with configurable app-level limits; generated server entrypoints set HTTP timeouts and max-header defaults. | Medium because per-route limits remain planned. | -| Reuse or forge generated CSRF tokens. | Cross-site action, command, or state-changing API execution. | Generated CSRF is enabled by default for generated action POSTs, command POSTs, and state-changing APIs, and validates before decoding or user handlers run. | Medium while secret rotation and deployment guidance continue to harden. | +| Reuse or forge generated CSRF tokens. | Cross-site action, command, or state-changing API execution. | Generated CSRF is enabled by default for generated action POSTs, command POSTs, and state-changing APIs, validates before decoding or user handlers run, and supports staged primary/verification-key rotation. | Medium because secret storage, overlap duration, and final key retirement remain deployment-owned. | | Trigger handler panics and read stack traces or secret values. | Secret exposure and debugging data leakage. | Runtime panic boundaries avoid stack traces in responses and redact recovered-panic logs. | Medium because app-owned logs are outside generated control. | | Use unsafe redirects to move users off-site. | Phishing or token leakage through redirects. | First slices require safe local redirects for generated action/SSR redirect paths. | Medium until full redirect allowlists and diagnostics are complete. | | Embed local secrets into generated output. | Secret exposure in release artifacts. | Docs require generated output to avoid local env/private files. | High until exclusion tests cover `.env`, source maps, private files, and temp artifacts. | diff --git a/docs/engineering/security.md b/docs/engineering/security.md index dc527fef..9b2a06a2 100644 --- a/docs/engineering/security.md +++ b/docs/engineering/security.md @@ -21,7 +21,9 @@ Do not treat current `act`, `api`, `partial`, `guard`, or SSR scaffolding as com - Generated actions, command endpoints, and state-changing API endpoints enable CSRF by default. Production configs must not set `Build.CSRF.Disabled` unless another cross-site request strategy is enforced, and every runtime environment - must provide a stable CSRF secret. + must provide a stable primary CSRF secret. Rolling deployments use + verification-only secrets so old and new instances accept each other's + tokens while only the primary key signs new tokens. - Generated form decoders must validate expected fields and avoid mass assignment. - Generated action forms must reject direct file inputs unless the enclosing `g:post` form is multipart and every file control declares explicit count, @@ -42,7 +44,8 @@ Do not treat current `act`, `api`, `partial`, `guard`, or SSR scaffolding as com Before generated app output is considered production-ready: - Generated action, command, and state-changing API CSRF must be enabled and - configured with a runtime secret. + configured with a primary runtime secret. Multi-key verification and + old-cookie refresh must support rollback-safe staged rotation. - Redirects must reject unsafe external destinations unless explicitly allowed. - Generated decoders must define how unknown, missing, repeated, and file fields are handled. - Guards must have a documented execution contract, failure behavior, and test coverage. diff --git a/docs/language/actions.md b/docs/language/actions.md index dc7efccc..bea9c563 100644 --- a/docs/language/actions.md +++ b/docs/language/actions.md @@ -1,7 +1,9 @@ # Actions Actions are endpoint declarations. A page declares the exported same-package Go -symbol, HTTP method, and endpoint path in `.gwdk`; normal Go owns the behavior. +symbol, HTTP method, and endpoint path in `.gwdk`. Page and fragment markup also +stays in GOWDK source; normal Go owns action data, domain behavior, and response +decisions. The supported declaration shape is: @@ -20,8 +22,8 @@ Current behavior: errors still follow normal `runtime/response.Response` behavior. - Old `act submit { ... }` blocks are rejected with a migration diagnostic. - Actions currently require `POST`. -- Redirects, fragments, validation, and business rules come from the Go handler - response, not from generated `.gwdk` action body code. +- Validation, business rules, state changes, and response outcomes come from + the Go handler. Page and fragment markup stays in `.gwdk` source. - `