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
12 changes: 6 additions & 6 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions addons/observability/observability.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
58 changes: 58 additions & 0 deletions csrf_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
5 changes: 5 additions & 0 deletions docs/compiler/generated-output.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
4 changes: 4 additions & 0 deletions docs/engineering/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 3 additions & 3 deletions docs/engineering/architecture.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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 |
Expand Down
84 changes: 84 additions & 0 deletions docs/engineering/csrf-secret-rotation-plan.md
Original file line number Diff line number Diff line change
@@ -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.
103 changes: 103 additions & 0 deletions docs/engineering/csrf-secret-rotation-spec.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 3 additions & 1 deletion docs/engineering/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions docs/engineering/security-threat-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand All @@ -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. |
Expand Down
Loading