Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion crates/agentkeys-broker-server/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ pub enum Group {
Core,
/// OIDC issuer keypair + JWT TTL (used by AWS STS AssumeRoleWithWebIdentity).
Oidc,
/// Session JWT keypair + TTL (broker-internal, used by /v1/mint-aws-creds).
/// Session JWT keypair + TTL (broker-internal; minted by the
/// email-link / OAuth2 auth flows, consumed by /v1/mint-oidc-jwt).
SessionJwt,
/// Audit storage policy (anchor selection, multi-anchor strategy).
Audit,
Expand Down
4 changes: 2 additions & 2 deletions crates/agentkeys-broker-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ async fn main() -> anyhow::Result<()> {
"Tier-1 boot complete; Tier-2 reachability checks deferred until after listener bind"
);

// Legacy mint-log table opened alongside the plugin-trait audit anchors;
// mint_v2 mirrors success/failure rows here for monitoring continuity.
// Mint-log table opened alongside the plugin-trait audit anchors;
// /v1/mint-oidc-jwt writes success/failure rows here via record_mint.
let audit = AuditLog::open(&config.audit_db_path)?;

// Issue #71 OIDC-only migration: the broker mint flow uses
Expand Down
16 changes: 9 additions & 7 deletions crates/agentkeys-broker-server/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,21 @@ pub struct AppState {
pub audit_policy: AuditPolicy,
pub wallet_store: Arc<WalletStore>,
pub nonce_store: Arc<AuthNonceStore>,
/// Capability grants (Phase B, US-025/026/027). Always compiled in;
/// the mint endpoint consults this even if no grant has yet been
/// issued (Phase 0 grant-less mints continue to work via the
/// implicit-grant fallback documented in mint.rs).
/// Capability grants (Phase B, US-025/026/027). Backs the
/// `/v1/grant/{create,list,revoke}` CRUD endpoints. The mint-time
/// `try_consume` enforcement point disappeared with mint_v2 in PR #96
/// (issue #72); grants are kept in-tree for master-managed audit and
/// potential future re-introduction at the JWT-mint site.
pub grant_store: Arc<GrantStore>,
/// Identity links (Phase B, US-028). Maps verified identities
/// (email, oauth2 sub, secondary EVM wallet) to their owning master
/// OmniAccount. Recovery flow consults this to find which master
/// should sign the recovery grant.
pub identity_link_store: Arc<IdentityLinkStore>,
/// Idempotency-Key dedup (Phase D-rest, US-037). Mint endpoint
/// consults this on every request that carries an Idempotency-Key
/// header.
/// Idempotency-Key dedup (Phase D-rest, US-037). Originally consumed
/// by mint_v2; after PR #96 (issue #72) the only consumer is gone,
/// so this field is currently unread by any live handler. Slated for
/// removal — see follow-up task "Remove dead IdempotencyStore code".
pub idempotency_store: Arc<IdempotencyStore>,
/// Atomic counters surfaced via /metrics (Phase D-rest, US-036).
pub metrics: Arc<Metrics>,
Expand Down
6 changes: 3 additions & 3 deletions crates/agentkeys-broker-server/tests/grant_flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
//! - `POST /v1/grant/create` (master JWT) → 200, returns grant_id +
//! audit_proof (compact JWS).
//! - `GET /v1/grant/list` → 200, returns the just-created grant.
//! - `POST /v1/grant/revoke` → 200, instant revoke. Mint after revoke
//! would 403 (covered in `mint_v2_flow` separately when grant store is
//! wired into the mint endpoint — Phase B US-027).
//! - `POST /v1/grant/revoke` → 200, instant revoke. Mint-time enforcement
//! of revoked grants was retired with mint_v2 in PR #96 (issue #72);
//! today /v1/grant/* is CRUD-only (no consume point).
//! - Re-revoke is idempotent at storage level (caller sees 400 because
//! revoke() returns false).
//! - Cross-master revoke (different OmniAccount tries to revoke a grant
Expand Down
2 changes: 1 addition & 1 deletion crates/agentkeys-broker-server/tests/oidc_flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ async fn mint_oidc_jwt_signs_claims_for_session_wallet() {
// same path the SIWE wallet/email/oauth2 verify handlers take. Replaces
// the legacy `mint_session_against_backend` flow now that
// /v1/mint-oidc-jwt verifies session JWTs locally instead of round-
// tripping to /session/validate (parity with /v1/mint-aws-creds).
// tripping to /session/validate.
let wallet = "0xabcdef0123456789abcdef0123456789abcdef01".to_string();
let omni = derive_omni_account("evm", &wallet);
let session_token = mint_session_jwt(
Expand Down
20 changes: 11 additions & 9 deletions crates/agentkeys-provisioner/src/aws_creds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ pub struct OidcJwtResponse {
}

/// Final temp-cred shape passed to the scraper subprocess. The struct fields
/// match the broker's pre-issue-#71 `/v1/mint-aws-creds` response so callers
/// who already consume `AwsTempCreds.to_env(...)` need no changes.
/// match the response shape of the legacy `/v1/mint-aws-creds` route (deleted
/// in PR #96 / issue #72) so callers that already consume
/// `AwsTempCreds.to_env(...)` need no changes during the migration to the
/// daemon-side mint path.
#[derive(Debug, Clone)]
pub struct AwsTempCreds {
pub access_key_id: String,
Expand Down Expand Up @@ -212,12 +214,11 @@ async fn assume_role_with_jwt(
}

/// Wallet → STS session name (max 64 chars; alphanumeric + `=,.@-_`).
/// **Mirrors `crates/agentkeys-broker-server/src/handlers/mint.rs::build_session_name`
/// byte-for-byte** so audit rows + CloudTrail events line up across broker
/// mints (`/v1/mint-aws-creds` -> `mint_v2`) and daemon-side mints (this
/// function). The trailing micro-second timestamp gives every call a unique
/// session name even when the same wallet mints in rapid succession; without
/// it AWS returns the same temp creds for repeated calls within the
/// Daemon-side STS calls only — the server-side mint path was deleted in
/// PR #96 (issue #72), so this is the sole producer of STS session names
/// in the system. The trailing micro-second timestamp gives every call a
/// unique session name even when the same wallet mints in rapid succession;
/// without it AWS returns the same temp creds for repeated calls within the
/// `DurationSeconds` window (subtle caching footgun called out in critic M1).
fn build_session_name(wallet: &str) -> String {
let now = SystemTime::now()
Expand Down Expand Up @@ -294,7 +295,8 @@ mod tests {

#[test]
fn build_session_name_matches_broker_format() {
// Mirrors broker handlers/mint.rs build_session_name (critic M1).
// STS session-name format invariant — daemon-side only since PR #96
// deleted the broker's handlers/mint.rs (issue #72) (critic M1).
let name = build_session_name("0xAbCdEf0123456789ABCDEF0123456789AbCdEf0123456789");
assert!(name.starts_with("agentkeys-"));
assert!(name.len() <= 64, "STS rejects session names >64 chars");
Expand Down
2 changes: 1 addition & 1 deletion docs/dev-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ BIN=$(pwd)/target/release/agentkeys-daemon
$BIN --broker-url "$AGENTKEYS_BROKER_URL" --session "$AGENTKEYS_BEARER_TOKEN" --stdio
```

When the daemon needs to access the operator's S3 vault (to read or store a credential), it calls the broker's `POST /v1/mint-aws-creds` with the bearer token. The broker exchanges it for a 1-hour scoped AWS session and hands it back — you never touch the long-lived daemon AWS key.
When the daemon needs to access the operator's S3 vault (to read or store a credential), it calls the broker's `POST /v1/mint-oidc-jwt` with the bearer token, then exchanges the JWT for a 1-hour scoped AWS session via client-side `sts:AssumeRoleWithWebIdentity` (issue #71 / PR #96). The broker no longer holds any AWS principal — you never touch the long-lived daemon AWS key, and the broker can't either.

### 4.3 Provision a new service

Expand Down
92 changes: 41 additions & 51 deletions docs/operator-runbook-stage7.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ A concrete request flow makes the split obvious:
│ (PUBLIC — AWS reaches this)
┌──────────────────┐ legacy bearer ┌───────────▼───────────┐
│ agentkeys-cli ├──────────────────────▶│ agentkeys-broker- │
│ / agentkeys- │ /v1/mint-aws-creds │ server │
│ / agentkeys- │ /v1/mint-oidc-jwt │ server │
│ daemon │ │ │
└──────────────────┘ │ ┌───────────────────┐ │
│ │ POST /session/ │ │
Expand Down Expand Up @@ -367,49 +367,41 @@ by `aud=sts.amazonaws.com` and a `sub` prefix.

The broker's `BROKER_DATA_ROLE_ARN` must point at this role.

### Mint-time STS paths (issue #71)
### Mint-time STS path (issue #71 / issue #72)

There are two endpoints that result in AWS credentials, with **different
trust models** and **identical end-state security** (both go through
`AssumeRoleWithWebIdentity`, both emit creds tagged with the user's
`agentkeys_user_wallet` PrincipalTag):
One endpoint produces AWS credentials, via `AssumeRoleWithWebIdentity`,
with creds carrying the per-actor `agentkeys_actor_omni` PrincipalTag
that drives bucket-policy isolation per [`arch.md §17.2`](../docs/arch.md#172-pri-actor-isolation).
The legacy `agentkeys_user_wallet` tag is still emitted alongside it for
backward compatibility with v0.1 bucket policies; new policies should
key off `agentkeys_actor_omni`.

#### `POST /v1/mint-oidc-jwt` — daemon-side STS (recommended)
#### `POST /v1/mint-oidc-jwt` — daemon-side STS

The broker signs a short-lived OIDC JWT with the user's wallet claim
and returns it. The daemon exchanges that JWT for AWS creds **on its
own machine** by calling `sts:AssumeRoleWithWebIdentity` directly. This
is the path the provisioner / MCP / `agentkeys-daemon` use after the
issue #71 Option A migration.
issue #71 Option A + issue #72 retirement of the server-side mint.

- **Broker work**: validate bearer → sign JWT → return.
- **Daemon work**: receive JWT → `AssumeRoleWithWebIdentity` → inject
`AWS_*` env vars into scraper subprocess.
- **AWS principal on broker**: none required.
- **AWS principal on daemon**: none required (the JWT authenticates).

#### `POST /v1/mint-aws-creds` — server-side gated (kept for callers needing audit/grants/idempotency)

Broker handles the full mint pipeline:

1. Verifies the session JWT against the broker's session keypair.
2. Verifies a per-call EIP-191 signature on the request body.
3. Resolves any Phase B grant (consume → 403 if revoked/expired/exhausted).
4. Mints an internal user-scoped OIDC JWT (same claim shape as
`/v1/mint-oidc-jwt`).
5. Calls `sts:AssumeRoleWithWebIdentity` with that JWT (broker-side).
6. Writes the audit anchor row(s) per `BROKER_AUDIT_POLICY` (single
`sqlite` or `dual_strict` for multi-anchor durability).
7. Returns the temporary credentials.

Use this endpoint when:
- You want the broker to be the policy point (mandatory audit log,
Phase B grants, Idempotency-Key dedup, multi-anchor coordination).
- You can't trust callers to self-audit.
> **Retired in PR #96 (issue #72).** The previous server-side
> aggregator `POST /v1/mint-aws-creds` no longer exists — the route
> returns 404. Its in-process gates (Phase B grant `try_consume`,
> Idempotency-Key dedup, multi-anchor audit coordination) were dropped
> with the route; isolation now relies on `/v1/mint-oidc-jwt`'s audit
> row + AWS CloudTrail's `AssumeRoleWithWebIdentity` events + AWS
> PrincipalTag/bucket policy per `arch.md §17.2`. Daemons must not
> retry against the old route.

### Broker creds-free posture (post-migration)

Both paths above use `AssumeRoleWithWebIdentity`, which is JWT-authenticated. The broker **does not need** an IAM principal at
The path above uses `AssumeRoleWithWebIdentity`, which is JWT-authenticated. The broker **does not need** an IAM principal at
runtime for credential minting. After cutover you can:

- Drop `AWS_PROFILE` from `agentkeys-broker.service`.
Expand Down Expand Up @@ -549,24 +541,20 @@ curl -X POST https://broker.litentry.org/v1/grant/revoke \
-d '{"grant_id":"grn-..."}'
```

### Migration window — implicit-grant fallback

The mint endpoint currently allows mints WITHOUT an explicit grant for
backward-compatibility with Phase 0 daemons (legacy `NoGrant` path
documented inline in `src/handlers/mint.rs::mint_v2`). The audit log
records these mints with an empty `grant_id` column.
### Grant enforcement — retired with `/v1/mint-aws-creds` in PR #96 (issue #72)

**This is an intentional Phase 0→Phase B migration window.** Phase E
US-039 will flip the default to fail-closed (`NoGrant` → 403). Operators
should:
The grant `try_consume` enforcement point lived inside the deleted
`src/handlers/mint.rs::mint_v2`. With that route gone (issue #72), the
broker no longer consults `grant_store` at mint time at all — the
`NoGrant` fallback, the planned Phase E fail-closed flip
(`BROKER_REQUIRE_EXPLICIT_GRANT=true`), and the empty-`grant_id` audit
rows are all moot. The grant CRUD endpoints (`/v1/grant/create`,
`/v1/grant/list`, `/v1/grant/revoke`) still exist so master devices can
manage grants for audit / future re-introduction, but no broker path
consumes them today.

1. Roll out the broker with grants enabled (this build).
2. Call `/v1/grant/create` for every existing daemon address.
3. Verify mints continue to succeed (now with non-empty `grant_id` in
audit rows).
4. Set `BROKER_REQUIRE_EXPLICIT_GRANT=true` (Phase E env var) to flip
the default to fail-closed.
5. Audit any 403s for daemons that didn't get a grant.
Per-actor isolation now rides on `/v1/mint-oidc-jwt`'s audit row + AWS
CloudTrail + AWS PrincipalTag/bucket policy (see `arch.md §17.2`).

### Recovery flow

Expand Down Expand Up @@ -707,16 +695,18 @@ disabled to avoid leaking counter shapes to unauthenticated probers.
Histograms (mint_latency, audit_write_latency) + per-handler counter
bumps land in V0.1-FOLLOWUPS Phase E hardening.

### Idempotency-Key
### Idempotency-Key — retired with `/v1/mint-aws-creds` in PR #96 (issue #72)

The mint endpoint accepts an `Idempotency-Key: <ulid>` header. Bodies
that hash to the same fingerprint within the 5-minute window return
the cached response (no re-mint, no STS quota burn). Same key + a
different body returns 422.
The `Idempotency-Key` header was consumed by the deleted
`mint_v2` handler. No surviving broker route honors the header today —
`/v1/mint-oidc-jwt` always re-signs (the OIDC JWT TTL of 5 min, default
`BROKER_OIDC_JWT_TTL_SECONDS=300`, is the only knob bounding re-mint
cost). Callers that need rate-limiting / dedup must implement it
client-side.

`BROKER_REQUEST_BODY_LIMIT_BYTES` enforces the request body size limit
(default 1 MiB) at router level (DefaultBodyLimit middleware) — closes
Codex R2-F18 (declared-but-unenforced).
`BROKER_REQUEST_BODY_LIMIT_BYTES` (default 1 MiB) still enforces the
request body size limit at router level via the `DefaultBodyLimit`
middleware for every endpoint.

---

Expand Down
18 changes: 18 additions & 0 deletions docs/spec/plans/issue-64/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@

---

> **RETIREMENT NOTICE (2026-05-24, issue #72 / PR #96).** Substantial portions of this
> plan describe the `POST /v1/mint-aws-creds` server-side aggregator, its
> session-JWT + per-call EIP-191 signature wire format, Phase B grant `try_consume`
> enforcement, and Idempotency-Key dedup. **All of those surfaces were deleted**
> in PR #96 (closes issue #72) — the route, [`crates/agentkeys-broker-server/src/handlers/mint.rs`](../../../crates/agentkeys-broker-server/src/handlers/mint.rs)
> (which no longer exists), and [`crates/agentkeys-broker-server/tests/mint_v2_flow.rs`](../../../crates/agentkeys-broker-server/tests/mint_v2_flow.rs)
> (also gone). The current mint flow is `/v1/mint-oidc-jwt` (JWT signer only) +
> client-side `sts:AssumeRoleWithWebIdentity`; isolation rides on AWS
> CloudTrail + PrincipalTag/bucket policy per [`docs/arch.md`](../../arch.md) §17.2.
>
> Read the rest of this doc as **historical record of the pre-#96 design**, not as
> a description of the current system. The `prd.json` in this directory has
> matching stale acceptance criteria — same caveat applies. For the current
> mint + isolation contract see [`docs/arch.md`](../../arch.md) and the surviving
> tests under [`crates/agentkeys-broker-server/tests/`](../../../crates/agentkeys-broker-server/tests/).

---

## 0. Context — why this plan exists

PR #61 (broker phase 2 — OIDC issuer + AWS-cred wiring) merged to main. The broker today exposes 6 routes: `/healthz`, `/readyz`, `/v1/mint-aws-creds`, `/.well-known/openid-configuration`, `/.well-known/jwks.json`, `/v1/mint-oidc-jwt`. Auth is a bearer token validated by an HTTP call to `BROKER_BACKEND_URL/session/validate`. Audit is local SQLite. Wallet provisioning, user-identity verification, and chain anchoring are all implicit / external today.
Expand Down
6 changes: 4 additions & 2 deletions docs/spec/plans/issue-74-dev-key-service-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,11 @@ Move the daemon off the legacy `agentkeys init --mock-token` → backend `/sessi
└────┬───────────────────┬─────┘ └────────────────────┘
│ ① email/OAuth2 │ ③ /v1/wallet/link
│ auth flows │ ④ /v1/auth/wallet/{start,verify}
│ ④ /v1/mint-oidc-jwt ④ /v1/mint-aws-creds
│ ④ /v1/mint-oidc-jwt
Broker (stateless minter, no key material from this flow)
(/v1/mint-aws-creds retired in PR #96 / issue #72 —
daemons now do client-side AssumeRoleWithWebIdentity)
```

The backend → broker path doesn't change. The dev_key_service is a **new** edge: daemon → backend (signer), parallel to the existing daemon → backend (credential vault). When TEE lands, this edge re-routes to the TEE worker; daemon code doesn't change.
Expand Down
Loading
Loading