feat: scoped hot-reload of client + policy config on SIGHUP#34
Merged
Conversation
Reload the client/auth map and the policy rules without dropping connections or restarting (design §5). Downstream topology, redaction, and audit changes remain restart-required. - policy.Engine: Reload atomically swaps the default + rule set behind the engine's lock and clears the wildcard pins computed for the old rules (fail-closed until the next Sync, never an over-permit). Decide now reads the rule set under the read lock; Sync holds the write lock across its computation. compileRules is shared with New. - edge.Guard: Reload atomically swaps the allowed origins and client keys behind an RWMutex; origin checking and authentication read a snapshot, so a request in flight finishes against a consistent view. - app.Gateway.Reload: validates the new config as a whole (a bad edit is rejected and the running config stands), reloads the guard and engine, re-pins wildcards against the live catalog, and logs that a restart is needed when anything outside the hot-reload scope changed. - cmd/portcullis: SIGHUP re-reads the same config file and applies the reload; SIGINT/SIGTERM still drive graceful shutdown. Each swap is exercised under -race with concurrent reads. Repo coverage 95.2%, -race + lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The SIGHUP reload path logs a rejected reload's error, and that error could carry a client's api_key_env variable name (CodeQL flags any read of an *Key*-named field flowing to a log as clear-text logging of sensitive information). Identify the client by its configured id instead; the env var name is an internal detail and is not needed to diagnose a missing key. Applied at both sources that feed the reload error — the app's client key resolution and the config validator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
The last functional V1 item: reload the client/auth map and policy rules without dropping connections or restarting (design §5). Downstream topology, redaction, and audit changes remain restart-required.
Atomic-swap primitives
policy.Engine.Reloadswaps the default + rule set behind the engine's lock and clears the wildcard pins computed for the old rules — fail-closed (a wildcard admits nothing) until the nextSync, never an over-permit.Decidenow reads the rule set under the read lock;Syncholds the write lock across its computation.compileRulesis shared withNew.edge.Guard.Reloadswaps the allowed origins and client keys behind anRWMutex; origin checking and authentication read a consistent snapshot, so a request in flight finishes against one view.Wiring
app.Gateway.Reloadvalidates the new config as a whole (a bad edit is rejected and the running config stands), reloads the guard and engine, re-pins wildcards against the live catalog, and logs that a restart is needed when anything outside the hot-reload scope changed (topology/redaction/audit diffed viareflect.DeepEqualagainst the running snapshot).cmd/portcullishandlesSIGHUP(re-read the same config file → reload) alongsideSIGINT/SIGTERM(graceful shutdown).Verification
-racewith concurrent reads (TestEngineReloadIsConcurrencySafe,TestGuardReloadIsConcurrencySafe).go test -race ./...,go vet,gofmt,golangci-lint— all clean.This completes the functional V1 surface (§10): edge hardening, per-session downstream sessions, aggregation/pagination, deny-by-default policy + catalog filtering, connection-level secret injection + redaction, failure-class breaker + timeouts + supervision, async audit,
/metrics, declarative config, and now scoped hot-reload — proven end-to-end by #33.🤖 Generated with Claude Code