Repository navigation
feat(accounts): narrow authority through the policy-log witness, verified against a pinned key - #1067
Closed
antoinetoussaint-byte wants to merge 6 commits into
Closed
antoinetoussaint-byte wants to merge 6 commits into
antoinetoussaint-byte wants to merge 6 commits into
Conversation
…and its generated bindings accounts is the witness's one client and cannot link the policy-log Go module, so it carries an identical copy of saas/policylog/v1/witness.proto, held equal to the source by TestSharedProtoCopiesMatchTheirSource. Everything else is the proto companion's output for the service's own template (codefly generate proto --template accounts/proto/buf.gen.yaml, proto:0.0.16) and go generate: the Go, gRPC and Connect bindings, the frontend's TypeScript, and a WitnessService tag in the OpenAPI documents with no path under it, the same shape the generator already gives every accounts service that has no REST route. Refs #1062 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fied against a pinned key accounts gets infra.WitnessPolicyLog, the host's PolicyLog over the witness's private gRPC API. Append offers an opaque entry (operation id, subject kind and decision, subject id, and the SHA-256 of the host's canonical encoding of the whole operation) and returns the receipt only once it verifies: signed by the key pinned in the trust anchor (policy_log_public_key.json), a valid chain link, for the bytes offered, above every sequence already verified and extending the verified chain. Only Unavailable is retried, with the identical entry. Entries, which refreshes the serving gate's reached_at, requires a signed cursor fresh within the staleness window, at or above the reconciled sequence, and verifies every record's signature, entry hash and chain link. The witness's published key is compared with the pin and never adopted. work.go wires it only when composition delivers both the witness endpoint and the pin; a delivered pin that cannot be used refuses the boot. Otherwise the host is configured without a witness and keeps today's behaviour: it serves and refuses every narrowing, and the startup warning says why as configuration. The reconciliation loop now runs when a witness is wired. The service dependency on policy-log/grpc is deliberately NOT declared here: it would pull the witness into every local run, where the witness refuses to boot by its own contract. That is an owner decision; see the pull request. Tests: fake-witness unit tests for each refusal (mutation-checked), and the real witness (its server package over its memory store, as a helper process in the policy-log module) verified by the client and driven through a real uninstall and scope-grant revocation against PostgreSQL, refused with the witness down. Refs #1062 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # module/services/accounts/code/work.go
…onment, dialled only under the mesh, and its cursor judged within seconds (#1062) The local-run decision #1067 left open is option (b), a deployed-only edge, and it is expressible with today's Codefly. accounts declares no service dependency on policy-log/grpc. It reads the witness address from a new `policy-log-witness` workspace group, which this module ships empty. A composition root that provisions the witness binds `endpoint=${endpoint:saas/policy-log/grpc}` in that environment, and the render both resolves the address and admits accounts as the endpoint's consumer there. A local run never starts a witness that refuses to boot, and its host stays fail-closed. (a) would add a storage path that is not the witness's, and (c) breaks the local loop. Review of 5f35c93: - F1 (major): the dial was plaintext to whatever resolved. It is now admitted only under internal-transport/mesh-protected=true to an in-cluster Service address, and refused by name otherwise, through pkg/meshtransport as the Vault binding does. - F3: cursor freshness drops from the 60 s staleness window to 10 s of clock skew. A cursor is asked for in the same pass that judges it. - F5: the pin is read through the trust anchor's one resolver, solutionHostTrustAnchorDir. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…thout a bound witness (#1062) Review of df82d9b (F7, major): the endpoint is now a value the composition root sets per environment. Before this fix, a host with a pinned key and an unbound endpoint only warned, wired no log, and served with the serving gate off. A restored database could then undo a witnessed narrowing. The pin comes from the platform-owned trust anchor, so the pin decides that this environment has a witness: pinned but unbound now refuses the boot, by name. F10: the witness's manifest comment no longer says the host declares a dependency on it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
antoinetoussaint-byte
force-pushed
the
issue-1062-policy-log-witness
branch
from
October 18, 2026 17:29
070fb6e to
3f52664
Compare
antoinetoussaint-byte
marked this pull request as ready for review
October 18, 2026 17:32
antoinetoussaint-byte
enabled auto-merge
October 18, 2026 17:32
Contributor
Author
auto-merge was automatically disabled
October 27, 2026 13:39
Pull request was closed
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.
Closes #1062
What we are trying to do
Every narrowing of authority — solution uninstall, solution-target close, scope-grant revocation, team-membership removal — is refused today, because accounts wires no policy log. The module already ships the log:
policy-logis the independent signed witness. This PR gives accounts a verified client for it and wires it when composition delivers it.Stated plainly: this PR alone makes no narrowing succeed in any environment. The client activates only where a composition root binds the witness endpoint AND the platform pins its key in the trust anchor (see "The local-run decision" below). Until both are delivered, every host keeps today's behaviour: it serves, and refuses every narrowing.
What is wired
infra.WitnessPolicyLog(pkg/infra/policy_log_witness.go) implementsbusiness.PolicyLogover the witness's gRPC API.Appendoffers an opaque entry: the operation id, a kind naming the subject kind and decision (installation.narrowed), the subject id, and the SHA-256 of the host's canonical encoding of the whole operation (decision, subject, supersedes, envelope revision, actor, and the policy as sorted-key JSON). The witness never sees the policy or the actor; the receipt still binds them.Unavailableis retried (the witness's "no sequence assigned"), with the identical entry and the samerequested_at; every other code is final.Entriesrefreshes the serving gate'sreached_at. It requires a cursor that is signed by the pin, issued within the staleness window of the host's clock (a signature alone cannot tell a replayed cursor from a current one), and at or above the reconciled sequence. It verifies every record's signature, entry hash and chain link, and refuses a page the witness withheld.policy_log_public_key.jsonin the existing trust anchor (infra.SolutionHostTrustAnchorPath, a constant, as the witness's README specifies). The witness'sKeysanswer is compared with the pin, so a mismatch is reported as a mismatch. It is never adopted. A delivered pin that cannot be used refuses the boot.work.gobuilds the client only when both the endpoint and the pin are delivered, and then startsReconcilePolicyLogEvery, which nothing started before. Otherwise it callsSetPolicyLog(nil, nil)and warns, naming which half is missing as configuration. The comment and warning that said "no receipt-issuing log exists" are rewritten.pkg/infra/warehouse_policy_log.go, whose whole content was that argument, is deleted.accounts/proto/saas/policylog/v1/(TestSharedProtoCopiesMatchTheirSourceholds it equal). Its bindings are the proto companion's output for the service's own template. The companion also adds aWitnessServicetag, with no path, to both OpenAPI documents. That is the shape the generator already gives every accounts service without a REST route:IdentityService,ModuleCapabilitiesServiceand five others.DATABASE_AUTHORITY.md,DEPLOYMENT_TOPOLOGY.mdand the policy-log README now state this truth. The witness's posture and its local configuration file are unchanged.The local-run decision: option (b), a deployed-only edge (2026-10-18)
The owner asked for the strongest-security option among (a), (b) and (c). That is (b), and today's Codefly can already express it:
codefly run). Rejected.How: accounts declares no service dependency on
policy-log/grpc. It reads the address from a newpolicy-log-witnessworkspace group (endpoint). This module ships that group empty, so locally there is no witness. A composition root that provisions the witness bindsendpoint=${endpoint:saas/policy-log/grpc}in that environment. The render then resolves it (host:porton a cell) and admits accounts as the endpoint's consumer. The CLI's cell producer derives consumers from configured endpoint references ("declared by the configuration, not by a service dependency"). No Codefly change was needed.Boot rules:
internal-transport/mesh-protected=trueto an in-cluster Service address, otherwise refused by name (review F1, handbook security posture rule 1)A bound value that cannot be read refuses the boot.
Rebased and reviewed (2026-10-18)
work.goimport block.policyLogWitnessTransport, tested);MayServewith no log, and a boot-timeKeysprobe are in accounts' witness client: persist the verified chain, refresh only at head, and re-offer an ambiguous append (#1067 review follow-ups) #1157;Test evidence
pkg/infra/policy_log_witness_test.go, run in the pure suite):Unavailableis retried with the identical entry, and every other code is not retried.business.Service: a verified receipt applies the narrowing and commits it. A bad signature, the wrong key, or a sequence regression refuses it: nothing applies and no receipt is recorded.policy_log_witness.go, all killed. Each mutant disabled or bypassed one check: the receipt signature, the receipt chain link, the cursor signature, the entry-hash comparison, the sequence floor, the fork link, cursor freshness, the published-key comparison, the record chain link, the record signature, the record entry hash, withheld records, the head below the cursor, the head below what was verified, the key-id digest, retry-all, no-retry, and dropping the actor from the narrowing hash.TestWitnessHelperProcessin policy-log'sserverpackage, over its memory store with a provisioned genesis), not in-process. accounts cannot link the policy-log module, and the protobuf registry refuses the second registration ofsaas.policylog.v1. This is the same pattern asqualification/module-authority.TestTheClientVerifiesWhatTheRealWitnessSigns(pkg/infra): receipts and records that the real witness signed verify under this host's restated encodings and a pin parsed from the witness's own key document. A host pinning another key believes none of them, and with the witness down nothing appends or reads.TestTheRealWitnessWitnessesUninstallAndScopeRevocation(pkg/business, real PostgreSQL): a realUninstallSolutionand a realRevokeScopeeach succeed and commit the receipt that the witness signed. A second replica reads the log back and finds both operations, with the same receipts and sequences. With the witness stopped, both are refused withErrPolicyLogUnreachable: the installation stays live, the grant stays, and no receipt and no gap is recorded.go build,go vet,gofmt -landgo test -race ./...all pass.go build ./...andgo vet ./...(with and without-tags=pure) pass, as doesgo test -tags=pure ./...../pkg/businessand./pkg/infrapass against PostgreSQL.module/tools:go test ./...passes, as do all node gates (naming-gate,generated-pins-gate,test-targets-gate,authz-coverage-gate,interface-docs-gate,migration-pairing-gate,migration-reference-gate,rls-migration-gate, and thestory-trace-gatetests) and thenode --test module/tools/*.test.mjstests (239 pass).codefly ci run --phase sync-driftand--phase lint,compilepass locally on CLI 0.1.181. CI pins 0.1.179.golangci-lint run ./...in accounts reports nothing in this change. It reports 31 findings in files this PR does not touch, all already on main: 15 errcheck, 12 staticcheck (3 of them SA1019 deprecations, which are design decisions), and 5 unused. They are not fixed here.Not verified
policy_log_cursoris a single row that the package shares with the recording log the other tests use, and pointing it at a second log is, correctly, refused as a rollback. The read path is covered at the client level against the real witness.witness_pb.tswas produced by the companion but not type-checked locally (nonode_moduleshere).Fix or hack, per change
_test.gofile the served binary never compiles.🤖 Generated with Claude Code