fix(security): ship real offline license + policy signing keys (SEC-H1)#701
Merged
Conversation
The embedded "current" trust anchors for license JWTs and admin-policy
envelopes were byte-for-byte the public halves of the committed test
private keys (internal/{license,policy}/testdata). Anyone with repo
access could forge licenses of any tier/feature or sign admin policies.
The verification code was correct; the trust root was the defect.
- Replace internal/license/keys/license-pubkey-current.pem and
internal/policy/keys/policy-pubkey-current.pem with the real
Ed25519 public keys generated offline (private keys live only in the
operator vault, never in the repo or CI).
- Add a regression guard in each package (keys_guard_test.go) that fails
the build if an embedded current public key equals the testdata key.
Verified to fail loudly on the test key and pass on the real key.
- Decouple the license validator tests from the embedded key: mustRing
now builds the ring from the testdata key (what signJWT signs with),
so verification-logic tests are independent of the shipped production
key. The embedded key is covered separately by the new guard.
- Spec it: system-license-validation C-09/AC-14 and system-policy
C-10/AC-13 (both bumped to 1.1.0); annotation coverage stays 100%.
Follow-ups (not in this change): cmd/owlicgen still defaults to the test
private key for dev minting (production must pass --key <vault>); a
production policy-signing tool does not exist yet.
Shipping the real signing keys broke every test that signs an artifact
with the testdata key and verifies it against the embedded trust anchor
(it used to be that same test key). These are DB-touching tests, so they
skipped locally without OPENWATCH_TEST_DSN and only failed in CI.
Add an internal/-scoped SetVerificationKeyForTesting(pub) to both the
license and policy packages (returns a restore func; never on a
production path) and install the testdata public key as the active
verifier from the helpers that sign with the testdata private key:
- policy: setupKeys (covers the 6 loader tests)
- server: mintTestLicenseJWT (4 license/premium tests),
mintSignedAlertThresholds (signoff)
So the binary embeds the real offline key while tests verify their own
test-signed artifacts. Verified with OPENWATCH_TEST_DSN: full
internal/{server,policy,license} suites pass.
remyluslosius
added a commit
that referenced
this pull request
Jun 27, 2026
Stage 1 docs freeze for rc.17. - packaging/version.env -> 0.2.0-rc.17 - CHANGELOG.md: roll [Unreleased] into a dated [0.2.0-rc.17] section; add the two operator-facing security entries from this cycle (real offline license/policy signing keys + build guard; OIDC/notification SSRF hardening). - refresh the rc version string in the SERVICE_DOWN / DISK_FULL runbooks. Bundles since rc.16: #701 (SEC-H1 real signing keys + regression guards), #702 (owlicgen removed from the product), #703 (SEC-H2 OIDC SSRF guard + non-gating transactionlog perf test). Verified: changelog format gate green, version injects, specter check + coverage 100%.
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.
SEC-H1 (P0 GA blocker)
The embedded "current" trust anchors for license JWTs and admin-policy envelopes were byte-for-byte the public halves of the committed test private keys in
internal/{license,policy}/testdata/. Anyone with repo access could forge a license of any tier/feature/quota (bypassing allinternal/licensegating) or sign admin-policy envelopes. The verification code was correct; the trust root was the defect.What changed
internal/license/keys/license-pubkey-current.pemandinternal/policy/keys/policy-pubkey-current.pemare now the real Ed25519 public keys generated offline. The private keys live only in the operator vault, never in the repo or CI. (Verified: each embedded public key corresponds to its vault private key; neither equals the testdata key.)keys_guard_test.goin each package fails the build if an embedded current public key equals the testdata key. Proven to fail loudly on the test key and pass on the real key, so this can never silently regress.mustRingnow builds the ring from the testdata key, making verification-logic tests independent of the shipped production key; the embedded key is covered separately by the guard.system-license-validationC-09/AC-14 andsystem-policyC-10/AC-13 (both -> v1.1.0). Annotation coverage stays 100%.Verification
go test ./internal/license/... ./internal/policy/...greenspecter check(114 specs),specter check --test(0 errors),specter coverage --strictness annotation(100%, T1 37/37)gofmt/go vet/go buildcleanFollow-ups (not in this PR)
cmd/owlicgenstill defaults to the test private key for dev minting; production must pass--key <vault path>. Could harden to refuse the testdata key.