Repository navigation
feat(security): Spec 101 US1 signed TPA bundle format and downgrade-proof loader - #1701
Merged
Merged
Conversation
…tatus and config fields) Related #938
…in BBolt Adds the tpa_bundle_state bucket (lazy create, never by a read), atomic state + activation-history commit, and *storage.Manager implementing scanner.TPABundleStateStore. Server removal leaves the bucket intact. Related #938
Schema v15: four typed bundle_* keys inside tpa_scanner (source, signature_verified, schema-line version, sequence), the TPABundleSnapshot provider seam merged into the heartbeat even with zero scan counters, and a whole-tuple rule in the anonymity backstop (T050, T051). Related #938
…open Adds readCandidateFile for the TPA bundle loader: no-follow open, fstat on the descriptor, size ceiling checked on the descriptor and re-bounded while reading, goroutine read bounded by ctx, and a one-parked-reader-per-path cap. Covers T020, T021 (FR-011, SC-011). Related #938
… CLI Add internal/security/bundlesig (strict sidecar grammar rows 1-10, trust set, Ed25519 verify, key-id agreement, canonical SignSidecar) and cmd/tpa-db (keygen, sign, verify). Byte-level fixtures are generated from a fixed test seed and checked in. Related #938
… compare and redaction Validate tpa_active_root and tpa_operator_signers on the write path only (boot stays up), compare the security block by its JSON form on hot-reload so a nil-vs-empty signer slice is not a change, keep tpa_operator_signers readable in GET /api/v1/config, and document the five keys. Related #938
…de state logic Adds the deterministic layer of the signed TPA bundle loader: publisher trust anchors and trust-set construction, the config-derived bundle policy with its idempotence fingerprint, the manifest grammar (sequence string, trust epoch, key id, 2000-rule ceiling, compile counter seam), evaluateCandidate with the FR-008 stage order, and the pure state functions (normalize, downgrade check, activation, rollback, coverage). The runtime that drives them lands next. Related #938
…llback and coverage gating Adds the runtime layer over the pure candidate pipeline: SetBundleRuntime, ConfigureBundleFromConfig, ReloadBundle and an idempotent, mutex-serialized run (read once, evaluate, choose what serves, write the content-addressed last-known-good files, commit state plus history row in one transaction, publish). Rollback by full digest, unpin, activation history, StalenessDays, state_commit_failed (a candidate that cannot be made durable never activates), and coverageOK now follows the published coverage verdict taken from the same snapshot as the check. Related #938
…banner Show sequence, authority, trust epoch, signature verified, age, the short fingerprint with the full digest on demand, a role=alert banner per degraded_reason (auto-approval suspended, changes go to review), and the last rejection. No banner when coverage is not degraded. Related #938
Add GET /security/bundle, GET /security/bundle/history and POST
/security/bundle/{rollback,unpin,reload}. The reads are admin-read; the
mutations sit behind a new agent-denied server op (bundle_manage). A short
fingerprint is refused with 400, a refused rollback returns 409 carrying the
rejection reason.
Add the mcpproxy security bundle command group (status, history, rollback,
unpin, reload) and extend signatureBundleLines with the full digest, sequence,
authority, epoch, signature verdict, DEGRADED coverage line, pin flags and the
last rejection. Rollback validates its flags locally before any request.
Related #938
…age order Fixes from the Wave 2 independent review: - a binding rollback pin now governs unsigned candidates, and the pin refuses the watermark-setting release too, so an operator rollback is not reverted by the next reload - a transient last-known-good read error keeps the active record and its files instead of reconciling them away - while the anti-downgrade state cannot be loaded a new external activation is refused (state_commit_failed) - a malformed sidecar is reported ahead of an oversized bundle (FR-008 stage order) - the embedded default without a sequence uses the publisher baseline, so a non-zero fallback baseline does not degrade a fresh install Related #938
…pec 101 US1 wave 3)
…ec 101 US1 wave 3)
…admission gate, telemetry provider Related #938
Bound the candidate read through a seam that wraps the raw descriptor, and test the limiter and its +1 byte; cover the policy fingerprint's per-signer key ids, UnpinBundle's forced re-run and the ratchet armed by a rollback to a never-activated signed target; content-address the LKG sidecar by bundle and sidecar digest so a reformatted sidecar cannot clobber the recorded pair; prove state and history share one bbolt transaction; scope the tpa_operator_signers redaction exemption to the security config path; resolve security bundle rollback --path against the CLI's cwd; keep ExternalEverActivated across a trust-epoch re-scope so an operator epoch bump with no new bundle degrades (operator_bundle_missing); restore a binding rollback pin's bundle after an active-root round trip. Related #938
…ar atomically Cross-model review (Sol 6.1, round 1): verify read the bundle and sidecar with unbounded os.ReadFile (a planted FIFO blocks, a huge file exhausts memory) and sign wrote <bundle>.sig through a planted symlink. Reads now refuse non-regular files, check the descriptor against the pre-open lstat and cap at the loader's ceilings (sidecar first); writes go to a temp file renamed over the destination entry. Related #938
…sync, signer redaction door) Sol 6.1 round 1 findings: - lkgWrite compared an existing LKG file with os.ReadFile; the data dir is attacker-writable, so compare only through the descriptor-first bounded reader and replace anything else. - creating tpa-lkg/ now fsyncs its parent so the directory entry is durable before the state commit (FR-011a). - the tpa_operator_signers public-key exemption moved out of the generic redaction walker (which also redacts audit payloads, where the field names are attacker-chosen) into RedactedConfig, restoring only canonical 32-byte public keys. Rejected as by-design (recorded as tasks.md A11): activation at exactly the baseline sequence. Related #938
Deploying mcpproxy-docs with
|
| Latest commit: |
74074b2
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://2730cf3b.mcpproxy-docs.pages.dev |
| Branch Preview URL: | https://claude-101-tpa-db-format-8c7.mcpproxy-docs.pages.dev |
…ar mode on the descriptor Sol 6.1 round 2: a FIFO swapped in between the lstat and a plain open blocked forever before any check ran, and writeReplace chmod-ed the temp file by name, which an attacker who can write the bundle directory could swap for a symlink to the private key. Unix opens now use O_NOFOLLOW|O_NONBLOCK and check the descriptor; the mode is set via the open temp file. Related #938
…w and Home /security redirects to /review (Spec 109), so the bundle status panel and degraded-coverage banner in Security.vue were unreachable. Extract them into SignatureBundleStatus, mount it (folded unless degraded) on Review, and show a role=alert on Home when scan-mode auto-approval is suspended, with the reason and a link to /review. The signature_bundle rides the shared useSecurityScannerStatus overview fetch; nothing renders when it is unavailable. Related #938
…tory Every directory-level operation on <dataDir>/tpa-lkg (create, temp file, write, rename, enumerate, delete, fsync) now goes through an os.Root opened on the real directory. A tpa-lkg that is a symlink, reparse point or regular file is refused (no write, no reconcile), and the lstat'd identity is compared with the opened root to close the lstat/open race. The data directory is also fsynced on every activation, not only the one that created tpa-lkg, so a failed parent fsync cannot be skipped by the retry (FR-011a). Related #938
…ed sidecar Sol 6.1 round 3: the bounded read refused the oversized sidecar before ParseSidecar, so stdout carried no contract code. The size refusal is now routed through the same code path as every other rejection. Related #938
…-neutral epoch_ahead label Sol 6.1 Web UI review: Review read the shared cached overview once, so a bundle that turned degraded later stayed hidden; it now refreshes on entry and every 30 s while mounted (never for tenant sessions). epoch_ahead no longer tells an operator-root user to update the binary. Related #938
…from the descriptor CodeQL go/path-injection (2 high) traced the admin rollback request path to the candidate open, and a by-path os.Stat for the unsigned-drop mtime fallback. A rollback --path must now be an absolute file directly in the directory of security.tpa_bundle_path (usage error otherwise, refused before anything is opened), so the rollback door cannot probe arbitrary files; the unsigned freshness fallback takes the modification time from the descriptor the bytes were read through instead of re-stating the path. Related #938
… filesystem root Sol 6.1 final round (P2): with tpa_bundle_path directly under / (or a drive root) the prefix check required a doubled separator and refused every sibling target. Related #938
Windows reports a path under a regular file as ERROR_PATH_NOT_FOUND. Related #938
…mat-8c7227 # Conflicts: # CLAUDE.md # oas/docs.go
Contributor
📦 Build ArtifactsWorkflow Run: View Run Available Artifacts
How to DownloadOption 1: GitHub Web UI (easiest)
Option 2: GitHub CLI gh run download 39478785002 --repo smart-mcp-proxy/mcpproxy-go
|
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.
Related #938
Spec 101 (tpa-db), User Story 1: an external TPA signature bundle becomes a signed, sequence-versioned artifact, and the loader verifies it before parsing it and refuses downgrades. US2 (seed corpus,
signatures[],scan-eval --bundle) is in a sibling session. US3 (publication channel, Spec 087 fetch) comes later.What changes for an operator
A bundle can ship with a detached
<bundle>.sig: a one-line JSON object with exactly four keys and an Ed25519 signature over the exact bundle bytes (specs/101-tpa-db/contracts/sidecar-format.md).New
securitykeys:require_signed_bundle(default off, maintainer decision D2)tpa_active_root(publisher|operator)tpa_operator_signers(base64 Ed25519 public keys)tpa_operator_baselinetpa_operator_epochmcpproxy security bundle [history|rollback|unpin|reload]andGET/POST /api/v1/security/bundle{,/history,/rollback,/unpin,/reload}. Mutations are refused to agent tokens.cmd/tpa-db keygen|sign|verifyfor operators who sign their own corpus.Bundle status adds:
full_digest,sequence,authority,trust_epochsignature_verified,staleness_dayscoverage_degradedanddegraded_reasonpinned,rollback, andlast_rejection{reason,detail,at}These show on the CLI, REST and the Web UI Security page, which has a degraded banner.
Docs:
docs/features/tpa-bundle-signing.mdcovers the full operator lifecycle, including an air-gapped walkthrough and the key-compromise procedure. Also updated: security-quarantine, tool-scanner, security-commands and configuration.How it works
Verify before parse (FR-006/FR-008). This is a source-neutral
Candidate{bytes, sidecar, source}pipeline inscanner/tpa_candidate.go, so Spec 087's fetch adapter reuses the verifier rather than forking it. Stages run in order:Candidate open (
candidate_open_{unix,windows}.go):O_NOFOLLOW|O_NONBLOCK, thenfstats the descriptor.CreateFilewithFILE_FLAG_OPEN_REPARSE_POINTand refuses reparse points.SetReadDeadline.Anti-downgrade (FR-004a/004b/005a/009/009b):
Crash consistency (FR-011a):
<datadir>/tpa-lkg/<sha256>.json(plus.<sidecar-sha256>.sig): temp file, fsync, rename, directory fsync.Updatethen writes the state and the history row to the newtpa_bundle_statebucket.Rollback (FR-010):
6cd669dbf408.Coverage (FR-007/009a/009b):
coverageOKand the async scan-mode admission path both stop auto-approving under exactly four conditions:The must-not-degrade cases are tested too.
Telemetry (FR-021, D7): the schema goes to 15. There are four typed
tpa_scannerkeys, and the anonymity backstop validates them as a whole tuple. The provenance gate means only publisher-authority bundles report the version line and sequence.Assumptions and decisions (no maintainer input requested)
Recorded in full in
specs/101-tpa-db/tasks.md, which also has the 2026-10-20 delta against plan.md.key_not_trusted. The operator root is the only signed path until a release adds a key.tpa_operator_signersandtpa_active_root.GET /api/v1/configmasks any key containingKEYorAUTH. Public keys are restored only in the typed config read door, and only when each value is a canonical 32-byte key.ExternalEverActivatedis kept, so an operator epoch bump still reportsoperator_bundle_missing.security bundle reload.Review record
Round 1 raised 6 findings. 5 were fixed. One (activation at exactly the baseline) was rejected as by design and recorded as A11.
Round 2 raised 4 findings, all fixed:
tpa-db: chmod on the descriptor, and a no-follow non-blocking open.os.Root, and the parent fsync is now unconditional.Round 3 raised 1 finding (the
sidecar_too_largecode intpa-db verify), fixed.Round 4: all Go chunks are CLEAN: sig, open, eval, state, runtime, rollback, wiring, surface.
Web UI chunk: 3 small findings fixed:
One P2 was declined: a
trust_epochabove 2^53 would render rounded in JS. Epochs advance by one per key-compromise event, and the backend compares them exactly asuint64./securityredirects to/review, so the bundle panel was unreachable. It is now on Review, and Home shows a degraded banner.Verified on a live instance
This ran on head
3c25ac1:go build, a scratchdata_dir, port 18711, and a throwaway Ed25519 key fromtpa-db keygen. The config used the operator root,tpa_operator_baseline: 1andrequire_signed_bundle: true. Bundles were the embedded corpus plussequence,trust_epoch: 0,key_idandgenerated_at, signed withtpa-db sign. This followsspecs/101-tpa-db/quickstart.md. The output below is trimmed withjq.CI notes
go/path-injection(2 alerts) is fixed in5d99de4:--pathmust now be a file directly in the directory oftpa_bundle_path. Anything else is a usage error, refused before anything is opened, so the rollback door cannot probe arbitrary files.os.Statof the path.bundlesigfixtures are byte-exact, so.gitattributesnow pins them-text.Out of scope / follow-ups
signatures[], the seed corpus, stamping the embeddedsequence/trust_epoch,scan-eval --bundle, and the contributor docs. A hand-over note is in the shared session notes.