feat(a2a): OpenBao B3 token-provider issuance (lr-890fae) - #30
Conversation
Signs a short-lived assertion with gatekeeper's own key (sub=attested caller), exchanges it at OpenBao's JWT auth mount, then reads identity/oidc/token/<role> to obtain the peer-facing token. OpenBao alone signs the returned token; gatekeeper holds no OpenBao signing key. internal/a2amint orchestrates: attestation (DomainA2A, fail-closed per-spawn) -> a2apolicy entitlement check -> a2atoken.Issue, refusing before any broker read or OpenBao call on any gate failure. Every decision (permit or refuse) is reported to an injected AuditFunc. Includes both negative controls proven live in the openbao probe: untrusted assertion key rejected, bogus/unregistered subject rejected. TASK: lr-890fae
Config.A2AProvider holds the OpenBao B3 issuance surface: endpoint, gatekeeper's assertion signing key path, issuer, assertion TTL, JWT auth mount, and a per-A2A-role auth_role/oidc_role mapping. Enabled() reports whether the stanza is complete enough to drive a mint (endpoint, assertion_private_key_path, and auth_mount all required together) — partial config is treated as not-enabled, not guessed at. A separate stanza from a2a_mapping (lr-0ae541, the entitlement policy): this configures issuance mechanics, not the identity->role->audience decision. Additive and optional; absent stanza has zero effect on any existing config field. TASK: lr-890fae
Wires internal/a2amint into the CLI: gatekeeper mint-a2a --audience <audience> [--config path] [--json]. Resolves the attested caller under attestation.DomainA2A (per-spawn required, fail-closed), checks a2apolicy entitlement, and mints via the OpenBao B3 exchange. Refuses with a clear config error when a2a_provider is not configured. Audit events (permit or refuse) are written to stderr so they never contaminate the token/JSON output on stdout, and remain capturable by any log-collecting harness; OpenBao's own audit device stays the mint-of-record for the issuance leg itself. Additive and off by default: a deployment with no a2a_provider configured sees byte-identical behavior on the existing GitHub-domain 'gatekeeper mint' command, exercised directly by TestRunMintA2A_NoProviderConfigured plus the pre-existing main_test.go suite (untouched by this change). TASK: lr-890fae
…890fae) AC7 (docs ship in the same PR): config.example.yaml gets a fully annotated a2a_provider stanza; docs/SETUP.md gets a new 'A2A token flow' section covering sidecar attestation -> gatekeeper PDP -> OpenBao issuance -> wire JWT -> peer validation, including the corrected claim set (native sub + TTL only, per lr-890fae comment #5) and why that is the right end state rather than a degradation to route around; docs/DESIGN.md gets internal/a2atoken and internal/a2amint entries plus the updated dependency-direction line; README documents the mint-a2a command and points at the SETUP.md walkthrough. TASK: lr-890fae
|
BOBBIE security audit of PR #30 (A2A token provider, OpenBao B3 issuance, lr-890fae). Scope: base 4de101e..head 24e9956. 12 files changed: internal/a2atoken (issuance mechanics), internal/a2amint (orchestration), config.A2AProviderConfig, cmd/gatekeeper mint-a2a, tests, docs (README/DESIGN/SETUP), config.example.yaml. Verified against the B3 design (gatekeeper signs a short-lived assertion with its own key, exchanges at OpenBao auth/a2a-jwt/login, reads identity/oidc/token/ -- OpenBao alone signs the peer-facing token):
Scanners: gitleaks flagged cmd/gatekeeper/main_a2a_test.go:93,143 (generic-api-key rule) -- reviewed in context: this is the env-var NAME GATEKEEPER_TEST_A2A_KEY_LR890FAE used as a broker path placeholder in a test fixture (assertion_private_key_path: ), whose actual value is a freshly-generated RSA key set via t.Setenv at test time. Not a secret literal -- false positive, no finding raised. semgrep (p/golang, p/secrets, p/security-audit) against the changed packages: clean, zero hits. go.mod/go.sum are untouched by this diff (verified via git diff --stat) so osv-scanner findings against the repo's declared Go stdlib version are pre-existing and out of scope for this review. No findings. review.status: clean. scanners_run: gitleaks (detect, base..head range) status=ran findings=1 reviewed-false-positive; semgrep (p/golang+p/secrets+p/security-audit) status=ran findings=0; osv-scanner status=ran findings=0-in-scope (go.mod/go.sum unchanged by this diff). |
|
PEACHES — clean (0 blocking findings) Reviewed against task lr-890fae AC1–AC7 and structural requirements:
All B3 mechanism requirements satisfied. No signing-key confusion, no custom claims, fail-closed gates enforced, audit record complete. Build-ready. |
|
Merged via clagentic-loadout v0.2.0
|
|
PEACHES — blocking: 2 major, 2 nits. Codex found critical issues after manual verification. BLOCKING: (1) internal/a2atoken/a2atoken.go lines 219, 265 — OpenBao error response bodies leak bearer material (assertion JWT, client token) into error messages, then audited to stderr. Redact response bodies or limit errors to status codes only. amos.code-craft.10 (secrets in error paths). (2) cmd/gatekeeper/main.go line 316 — ResolveSidecars() prepends legacy attestation.sidecar to sidecars array. If config has both, mint-a2a treats session sidecar as per-spawn. Per-spawn MISS must fail closed (AC2); can resolve via session fallback instead, breaking DomainA2A semantics. Tests only mix legacy+new. Gate ordering broken. amos.path-choice.3 (real fix + test for fail-closed). NITS: (3) cmd/gatekeeper/main.go lines 263, 392 — a2aMintResult includes unverified subject in --json output, violating AC6 (vanilla OIDC, no side channels). Remove subject or document consumer must ignore it. amos.path-choice.4 (silent band-aid). (4) docs/SETUP.md line 322 — Still states No A2A mint command exists after PR adds gatekeeper mint-a2a. Update. project.docs-accuracy. |
Summary
Implements the A2A token provider for gatekeeper: OpenBao issues and signs a short-lived, peer-facing JWT via the B3 attest-and-route mechanism settled at lr-890fae comment #4, live-provisioned per openbao lr-fbbf32 (comment #12, CLOSED 2026-08-14 on LXC 107). Builds to lr-890fae comment #5's CORRECTED acceptance criteria, not the task description's original ACs.
Mechanism (B3)
Corrected claim set (per lr-890fae comment #5)
The peer-facing token carries a native sub claim (caller entity UUID) and a TTL bound - nothing else. The upstream provisioning dropped the brokered_by/caller_role/aud_peer/parent_session claims from the live role template (unresolved Vault/OpenBao parser bug, tracked as openbao lr-1e7c97, not gating). No side channel was built to carry the dropped claims - deliberate, per seq 5: this is a public repo with multiple consumers, and the wire format stays a vanilla OIDC token any standard validator handles. Gatekeeper's own audit record (not the wire token) carries caller identity, resolved role, requested audience, and parent session id for every mint decision, permitted or refused.
What changed
Roster-agnostic
No real crew/agent/org names anywhere in code, config examples, tests, or docs. Caller -> entity resolution is entirely config/attestation-driven (a2a_mapping plus the sidecar's attested identity) - no compiled-in roster table.
Additive / off by default
A deployment with no a2a_provider configured gets a clear config error from mint-a2a and byte-identical behavior on the existing gatekeeper mint (GitHub-domain) path - proven by TestRunMintA2A_NoProviderConfigured plus the untouched main_test.go suite.
Tests
Both negative controls proven live in the openbao probe are reproduced:
Also covers: fail-closed refusal before any broker read/OpenBao call (attestation MISS, entitlement denial, incomplete role mapping), the no-aud-claim invariant (present with no bound_audiences is a hard OpenBao failure), config wiring (a2a_provider parse and Enabled()), and full CLI wiring (mint-a2a happy path with a stub OpenBao server, json and default output shapes).
go build, go vet, and go test all pass, no new dependencies (go.mod/go.sum untouched).
TASK: lr-890fae
Generated with Claude Code
Co-Authored-By: Claude Opus 5 noreply@anthropic.com