Identity and scoped delegation

Authentication identifies a caller. Authorization decides what that caller may do. AgentGuard keeps those operations separate so an untrusted model cannot choose a stronger identity or security context.

Standalone PDP authentication

The server supports disabled authentication for local development and API-key bearer validation for decision routes. The persisted ApiKeyStore stores Argon2id hashes with expiry and revocation state. Protected HTTP and gRPC evaluation requires a key bound to one User or Agent subject and the authorize scope (or *). The submitted subject must match the key. A key’s optional tenant is injected as trusted request/audit metadata; it is not itself a Cedar policy attribute, so tenant isolation must still be encoded using trusted entities and policy inputs. A conflicting caller-supplied context.tenant_id is rejected. Unbound legacy keys remain readable for migration but cannot authorize standalone evaluations. Rotate or reissue them as bound keys before enabling this enforcement.

Trusted policy consoles that must simulate different subjects can use the explicit authorize:any scope. The key must still be bound to a service identity; the scope permits its holder to submit a different evaluation subject. Treat this as a privileged impersonation capability: store it only in the console’s server-side secret store, never expose it to browsers, restrict console access with OIDC/RBAC, and prefer tenant-bound service identities. Ordinary authorize keys remain identity-bound, and wildcard * does not implicitly grant authorize:any.

Authenticated PDP audit records retain the evaluated principal separately from the bound caller identity, credential id, tenant, and act-as flag. This makes service-key use attributable without adding caller-controlled values to Cedar context. JSONL, CEF, LEEF, and ECS exports retain these provenance fields.

Argon2id verification runs on Tokio’s blocking pool and is capped at two concurrent verifications per process to bound CPU and memory use. Requests with missing or structurally malformed bearer credentials are rejected before acquiring a verification slot. If all slots are busy, protected HTTP requests receive 503 Service Unavailable and gRPC requests receive UNAVAILABLE; the caller should retry with bounded backoff. Put network-level rate limiting at the ingress as an additional control against sustained credential guessing. The gRPC evaluation transport also caps decoded requests at 64 KiB, matching the HTTP request-body budget.

Create keys through the operator CLI, assigning only the narrow scope and identity required by that integration:

agentguard api-key create --key-store ./keys.json \
  --subject-type Agent --subject-id research --tenant-id tenant-a \
  --scope authorize --ttl-seconds 2592000
agentguard api-key list --key-store ./keys.json
agentguard api-key revoke --key-store ./keys.json KEY_ID

Creation prints the raw secret once, after the hashed record has been saved; listing omits secret hashes and raw secrets. Protect the store as a secret and mount it read-only into the PDP. Never let an untrusted caller choose the identity bound to its own credential. metrics:read gates the metrics endpoint. API-key scopes gate endpoint capabilities; Cedar policies still make the resource/action authorization decision.

The standalone server polls the key-store content and applies valid file updates (including Kubernetes projected Secret updates) without a restart. Once new bytes are visible at the mounted path, the server checks them every 250 ms. Kubernetes projection itself is eventually consistent and depends on the kubelet’s sync/cache configuration; do not promise an end-to-end one-second revocation bound. Mount the Secret as a directory, not with subPath, so projected updates can become visible. Invalid or partially projected JSON is logged and the last-known-good key set remains active; correct the file and let the next content update trigger another reload.

Do not expose a listener with disabled authentication. For a non-loopback listener, configure AGENTGUARD_AUTH=apikey:<path> or the equivalent CLI flags, then terminate TLS at a trusted ingress or service mesh.

Library identity validators

agentguard-auth provides library primitives for JWT/JWKS validation, OIDC discovery, DPoP proof validation, and feature-gated SPIFFE/SPIRE workload identity. These are not automatically enabled server authentication modes. Embedding applications must invoke the validator, map the validated claims to a User or Agent Cedar principal, and pass only trusted facts to evaluation.

DpopVerifier replay tracking is process-local. Its default tracker retains up to 262,144 live proof identifiers; callers may choose another positive cap with JtiTracker::with_capacity. At capacity, new proofs fail closed with DpopCapacityExceeded until old entries expire. The tracker does not share replay state across processes and loses it on restart, so deployments requiring cluster-wide replay protection can inject a shared implementation of DpopReplayStore or AsyncDpopReplayStore. Both must atomically retain each accepted JTI across the deployment scope and fail closed on storage errors. Synchronous implementations must not perform blocking remote I/O on an async runtime worker. For remote stores, implement AsyncDpopReplayStore, construct the verifier with DpopVerifier::new_async, and call verify_async; this awaits the storage operation without blocking the worker. The bundled JtiTracker remains process-local, and no shared backend is included in the standalone PDP contract.

Console OIDC

The optional Next.js console uses OIDC Authorization Code + PKCE. Every signed session is at least a viewer session. Admin access is granted only when a configured claim contains an explicitly configured admin value. Missing OIDC, session, or production shared-store configuration fails closed with 503. See console deployment for the complete environment contract.

Delegation is a separate enforcement boundary

The CLI and Rust library can mint and verify Ed25519 JWS grants containing an issuer, subject, audience, expiry, allowed actions, resource patterns, and optional actor/constraint claims:

agentguard delegate \
  --from 'Agent::"research"' \
  --to 'Agent::"summarizer"' \
  --actions ToolCall::repo_read \
  --resources 'Repository::demo' \
  --ttl 300 --key-file delegation.key --out delegation.jws

Signature verification alone is not authorization. A consuming adapter must:

  1. authorize the parent before it issues a grant;
  2. verify the signature, token type, audience, key identity, non-empty issuer, subject and token id, and consistent iat/nbf/exp claims;
  3. bind the trusted acting identity to VerifiedDelegation::allows and enforce action/resource/constraint scope plus any sender binding;
  4. evaluate the effective Cedar policy; and
  5. check revocation state on every authorization attempt.

The core crate exposes the async DelegationRevocationStore port. The agentguard-redis-store crate provides a Redis-compatible HTTPS REST adapter for multi-process deployments; it hashes token ids into namespaced keys, retains revocation through token expiry plus configured clock skew, and fails closed on transport or protocol errors. Configure it with shared storage and a unique prefix per environment. The adapter uses the Redis EVAL and EXISTS commands and expects the service’s JSON command-result envelope. Use a Redis configuration with maxmemory-policy noeviction, durable persistence, and replication/availability appropriate to the application’s threat model; alert on memory pressure and persistence/replication lag. Revocation entries are security state, not cache data. A Redis service that silently evicts keys can make a revoked, otherwise-valid token appear active.

use agentguard_redis_store::RedisDelegationRevocationStore;
use std::sync::Arc;

let store = Arc::new(RedisDelegationRevocationStore::new(
    &std::env::var("AGENTGUARD_DELEGATION_REDIS_URL")?,
    std::env::var("AGENTGUARD_DELEGATION_REDIS_TOKEN")?,
)?);
// Pass `store.as_ref()` to `DelegationVerifier::revoke` and
// `verify_with_revocation_store` at the protected tool boundary.
# Ok::<(), agentguard_core::Error>(())

The standalone PDP does not consume delegation tokens or expose a revocation endpoint; this adapter is for applications enforcing delegation at their own tool boundary. Keep credentials in a secret manager and test store outage and recovery behavior against the exact Redis-compatible service in the consuming service; the crate’s protocol tests use a local mock and do not certify any provider’s command compatibility or durability guarantees.

The Rust compact-JWS parser rejects delegation tokens larger than 64 KiB before base64 decoding or JSON parsing. Applications should retain their own transport/request-size limits as well; this library bound is not a substitute for ingress limits.

VerifiedDelegation::allows(subject, action, resource, request_facts) is a fail-closed scope check: the trusted subject, exact action, resource glob, and every declared constraint must match. It does not decide whether the issuer was authorized to delegate or evaluate Cedar policy. Never pass an untrusted request field as subject or rely on this scope check as a replacement for the PDP decision. Do not use plain verify where revocation is part of the deployment contract; use verify_with_revocation_store for each decision.

For delegation chains, DelegationSigner::mint_attenuated accepts only a verified parent grant and requires the same signing key that verified it. Child actions must be a subset, resources must be parent-matched literals or identical parent patterns, inherited constraints cannot be removed or changed (new conjunctive constraints may be added), and child lifetime cannot exceed the parent’s remaining lifetime. Pattern containment is intentionally conservative; the helper rejects broader child wildcard patterns instead of trying to infer arbitrary glob-language inclusion. The application must still authorize the parent’s original grant.

AgentGuard does not ship an OAuth token-exchange endpoint, built-in revocation backend, or standalone PDP delegation enforcement. RFC 8693-style actor claims are primitives for an adapter, not a claim that the standalone PDP enforces parent authority automatically.