V1.1 tier a#16
Merged
Merged
Conversation
…(K-AUTHN-2) A misbehaving IdP would otherwise become a per-request DoS amplifier: every retry during a blip re-dials the wounded IdP. The Guard circuit-breaker is per (tenant, upstream); this adds per-credential coalescing on top, keyed on sha256(token) with a short TTL (errorTtl, default 5s, set to 0 to disable). Three cache lines now run side-by-side, all keyed by the same digest: positive (claims, TTL = min(exp - now, maxCacheTtl)), negative (active:false sentinel, negativeTtl), and the new error line (ErrUpstream sentinel, errorTtl). Singleflight still coalesces concurrent first-misses. Tests: TestIntrospection_ErrorCacheCoalescesUpstreamErrors, TestIntrospection_ErrorCacheTTLExpires, TestIntrospection_ErrorCachePerCredential, TestIntrospection_ErrorCacheNotPoisonedBySuccess, TestIntrospection_ErrorCacheKeyHashed. Docs updated; DESIGN.md and v1.0-review.md mark K-AUTHN-2 shipped.
Optional plugin/v1.1 application-layer signature: plugins HMAC-SHA256 their IdentifyResponse / AuthorizePluginResponse / MutateResponse over a deterministic length-prefixed canonical encoding, and the host verifies before trusting the result. Carried as gRPC trailing metadata (lwauth-sig / lwauth-kid / lwauth-alg) so the proto messages don't need a security-only field and old plugins remain wire-compatible. Three modes: disabled (v1.0 default - existing configs untouched), verify (accept signed or unsigned, reject *bad* signatures - rolling-deployment escape valve), require (every response must be signed - safe-by-default). Alg/kid are bound into the protected payload, so a downgrade attempt invalidates the signature instead of silently degrading. v1.1 ships HMAC only; X.509/asymmetric is forward-compatible (alg name lives in the protected payload). Closes the same-host attacker scenarios TLS doesn't: Unix-socket path race between dial and call, plaintext loopback bind race, compromised mesh terminator that can rewrite plugin replies. Application-layer signing is defense-in-depth on top of TLS, not a replacement. New: pkg/plugin/sign (canonical encoder + Sign/Verify, 11 tests), pkg/plugin/grpc/sign.go (signing config + verifyTrailer policy, 14 integration tests). Wired into all three remote adapters (identifier, authorizer, mutator). Existing plugin tests untouched and still green; the 34-package go test ./... is clean. Docs: docs/modules/plugin-grpc.md gains a 'Application-layer signing' section with mode-vs-behaviour table and threat model. v1.0-review.md and DESIGN.md mark F-PLUGIN-2 shipped.
Both items previously sat as numbered entries 15 and 16 in the legacy Next-milestones list in DESIGN.md, alongside completed milestones. Neither is on a v1.0.x patch line nor a Tier-A hardening slice: - M13 (supply-chain hardening: dhi.io bases, cosign, SBOM, mirrored images) is operator-trust quality work that ships on its own cadence and complements but does not overlap K-CRYPTO-2 (FIPS, Tier A). Now B4. - M14 (opt-in revocation surface: token revocation list, decision-cache invalidation API, shared session store) is net-new feature surface; the v1.0 short-TTL + refresh-rotation default still covers most deployments at zero cost. Now C3. Also updates the section 4 introspection note cross-reference to point at M14-REVOCATION (Tier C) instead of bare M14, and mirrors the new rows in docs/security/v1.0-review.md tier tables.
Opt-in supervisor for grpc-plugin children: process exec, periodic grpc.health.v1.Health.Check, exponential-backoff restart with jitter. Operators on Kubernetes / systemd / launchd keep the v1.0 default "platform owns the sidecar" model unchanged (no lifecycle block = no supervisor). Operators running outside an orchestrator add a lifecycle: block to the grpc-plugin config and let lwauth itself own the plugin process. New package pkg/plugin/supervisor: - Config validates required fields and applies sane defaults (gracefulTimeout=5s, healthCheck.interval=5s, .timeout=1s, .failureThreshold=3, restart.initialBackoff=200ms, .maxBackoff=30s, .jitter=0.2, maxRestarts=0=unlimited). - Supervisor spawns the child via os/exec, runs a health-probe loop alongside it, terminates the process on failureThreshold consecutive failures (SIGTERM on Unix, Kill on Windows via build- tag-split signal_unix.go / signal_windows.go), waits up to gracefulTimeout, then SIGKILL. - Restart loop: exponential backoff (initial * 2^n capped at maxBackoff) with uniform ±jitter; MaxRestarts==0 = unlimited; exhaustion enters StateGaveUp. - Stdout/stderr captured line-by-line into the host's slog under "plugin stdout"/"plugin stderr" keys. - WaitReady blocks until the first successful probe (or give-up). Wiring (pkg/plugin/grpc/lifecycle.go): - New lifecycle: yaml block parsed by parseLifecycle (full schema: command/args/env/workDir/gracefulTimeout/startTimeout + healthCheck/restart sub-blocks). Absent => nil => v1.0 behaviour. - startSupervisorIfConfigured pools supervisors by poolKey(cfg) so multiple module kinds (identifier+authorizer+mutator) referencing one plugin share one child + one ClientConn + one supervisor. - Health probe uses the same dial() the data plane uses, so a probe failure means exactly what an Authorize failure would (TLS, mTLS, signing all behave identically). - Engine startup blocks up to startTimeout (default 30s) on the first successful probe and surfaces failure as ErrConfig. Tests (all green, race-clean): - pkg/plugin/supervisor: 11 tests via TestMain helper-process pattern (sleep / sleep_then_exit / exit_immediately / exit_nonzero) + pure computeBackoff / validate tests. Covers ready, health-failure restart, child-crash restart, give-up, idempotent stop, double- start error, probe context honours timeout, bad-command path. - pkg/plugin/grpc: 9 lifecycle wiring tests covering parse/absent/required/full-schema/defaults/bad-duration/bad-args- type plus startSupervisor nil-noop, bad-command-surfaces-ErrConfig, and pool-deduplication. Docs: - docs/modules/plugin-grpc.md Lifecycle section rewritten end-to-end with full schema and behaviour notes. - docs/DESIGN.md A4 marked shipped with implementation summary. - docs/security/v1.0-review.md A4 row marked shipped. Tier A progress: 3/5 shipped (F-PLUGIN-2, K-AUTHN-2, M10). Remaining: K-DOS-1, K-CRYPTO-2.
Optional Valkey-backed cluster-wide rate-limit aggregator. Per-replica
buckets remain the default and continue to act as a safety floor; the
new rateLimit.distributed: block adds a cross-replica cap so under N
pods a tenant cannot spend N x limit before any replica trips.
Schema (under AuthConfig.rateLimit):
distributed:
type: valkey
addr: valkey-master.cache.svc:6379
password: ${VALKEY_PASSWORD} # optional
keyPrefix: lwauth-rl/ # optional
tls: false
window: 1s # rolling window length
timeout: 50ms # per-call deadline
failOpen: false # allow on backend error?
Cluster cap = perTenant.burst per window (or perTenant.rps * window
when burst is zero). On backend success the local bucket is ALSO
charged so a single replica cannot exceed its configured rps even if
the cluster cap had headroom. On backend error / circuit-open / ctx
timeout the limiter falls back to the local bucket (or, with
failOpen: true, allows unconditionally) - the gateway stays
available during a Valkey blip with a transient N x rps worst case
instead of unbounded.
Backend abstraction (pkg/ratelimit/backend.go):
- DistributedSpec / DistributedBackend interface (Allow, Close).
- DistributedFactory + RegisterBackend / BuildBackend - same pattern
the cache layer uses, so pkg/ratelimit stays dependency-free.
Valkey backend (pkg/ratelimit/valkey/valkey.go):
- Registers under type "valkey".
- Atomic Lua script per Allow:
ZREMRANGEBYSCORE key -inf (now-window)
if ZCARD key >= limit then return 0 end
ZADD key now <unique-member>
PEXPIRE key window
return 1
- Per-key memory bounded by limit; TTL refreshed on every admission
so idle tenants don't accumulate state.
- Wraps every call in upstream.Guard so circuit-breaking integrates
with the rest of the upstream policy.
- Auto-imported via pkg/builtins so cmd/lwauth picks it up by default.
API change:
- ratelimit.New(spec) -> (*Limiter, error). MustNew added for tests
and tightly-scoped callers. internal/config/loader.go and
internal/pipeline/engine_test.go updated.
- Limiter.Close() releases the configured backend.
- Empty TenantID skips the aggregator (cluster-wide-cap-per-tenant is
meaningless without a tenant key); routes to the local Default
bucket only.
Tests (all green, race-clean):
- pkg/ratelimit/valkey: 9 miniredis-backed tests covering admit-
under-limit, deny-at-limit, sliding-window expiry, tenant
isolation, key-prefix isolation, TTL bounding, atomic concurrent
admission (50x10 goroutines x attempts must yield exactly limit
admissions), zero-limit, zero-window.
- pkg/ratelimit: 11 dispatch tests (factory dispatch, limit-from-RPS
fallback, unknown-backend, distributed-deny-authoritative, key-
prefix, fallback-on-error, failOpen, success-also-charges-local,
empty-tenant-skip, Close passthrough, duplicate-register panic).
Docs:
- docs/modules/ratelimit.md gains "Cluster-wide aggregation
(K-DOS-1, v1.1+)" section: full schema, semantics-under-outage
table, wire protocol, ACL note.
- docs/DESIGN.md A3 marked shipped with implementation summary.
- docs/security/v1.0-review.md A3 row marked shipped.
Tier A progress: 4/5 shipped. Remaining: K-CRYPTO-2 (FIPS).
Last item of v1.1 Tier A. Operators in regulated environments (FedRAMP High, DoD IL5, PCI-DSS, HIPAA-with-cryptographic-controls) can now consume a separately-tagged FIPS image alongside the stock artifact, with three independent verification paths. Build path uses Go 1.24+'s in-tree FIPS 140-3 module selected via GOFIPS140 — pure Go, no CGO, ~3 % overhead — rather than the older GOEXPERIMENT=boringcrypto path (~10–20 % regression). Switching the build mode flips the standard library's crypto backend; nothing in lwauth's runtime code path needs to change. New code: - pkg/buildinfo: tiny zero-dep package exposing Version/Commit/Date (-ldflags-stamped), GoVersion(), FIPSEnabled() (wraps crypto/fips140.Enabled), and a Summary() helper for log lines. - pkg/observability/metrics: registers two new collectors — lwauth_fips_enabled (always-present 0/1 gauge for alerting) and lwauth_build_info (constant labelled gauge with version / commit / go_version / fips). Test added for label set. - pkg/lwauthd: logs build identity at startup (version, commit, go_version, fips_enabled). New --print-build-info flag prints a deterministic single-line banner that CI / Dockerfile self-checks grep for `fips_enabled=true`. Build / packaging: - Makefile: new targets `fips`, `fips-test` (runs full race suite under GOFIPS140 so a primitive that only differs in FIPS mode — e.g. RSA<2048, MD5 fallback, non-approved cipher — fails here rather than at promotion), `fips-verify` (asserts the built artifact self-reports fips_enabled=true), `docker-fips`. Default `build` target now stamps Version/Commit/Date via -ldflags into pkg/buildinfo. - Dockerfile.fips: separate FIPS image with `GOFIPS140=v1.0.0` env, `org.lightweightauth.fips140=enabled` OCI label, and an in-build self-assertion that fails the image build if the produced binary doesn't carry the FIPS module. Tag suffix `-fips` plus the OCI label give image-policy admission webhooks two independent ways to refuse a stock image landing in a regulated namespace. CI: - .github/workflows/build.yaml: new fips-test job (vet + race tests under GOFIPS140 + make fips + make fips-verify) and a build-fips job that publishes <image>:<tag>-fips alongside the stock image with provenance and SBOM. Docs: - docs/operations/fips.md: full operator guide. Build modes table, three-pronged verification (admission webhook, Prometheus gauge, runtime self-report), primitive-by-primitive backend table, common pitfalls the FIPS test suite catches, performance notes, and validation lifecycle pointers. - docs/DESIGN.md A5 marked shipped with implementation summary. - docs/security/v1.0-review.md A5 row marked shipped. Tier A complete — 5/5 shipped: F-PLUGIN-2, K-AUTHN-2, M10-PLUGIN- LIFECYCLE, K-DOS-1, K-CRYPTO-2. Validation: full `go vet ./...` clean and `go test ./... -race -timeout=180s` green across all 35 test packages.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.