Repository navigation
Releases: stephnangue/warden
Release list
Warden v0.21.0
Warden v0.21.0
v0.21.0 is the first-experience release. Trying Warden used to mean standing up an identity provider, finding an upstream key to hand it, and wiring a Compose stack before the first request — the better part of an hour before anything was governed. Now it is one command. warden server -dev-playground starts Warden with its own identity provider and a bank to protect, and a nine-scenario tour walks your own agent — Claude Code, Codex, Cursor, Gemini CLI, opencode, VS Code, or any MCP client — through what Warden does: the agent holds no secret, policy picks its tools and then its arguments, it acts for a person, a prompt-injected memo cannot move the money, every call is audited, it finds its own roles, and the tour ends on your own GitHub. Start at Getting started.
Discovery grows up alongside it. A role now says what it is in structured fields — the skill to read and the provider_path it fronts — and list_roles hands the agent the URL to call and a skill:// URI, read with read_skill or through the MCP Skills extension. What an agent can read is bound to who it is.
Keyless keeps widening: Anthropic and OpenAI federate, Cloudflare gets a keyless source, Azure Key Vault joins the chaining producers, and the default assertion becomes an RFC 8693 delegation token that names the user on top and the agent in act. A new keyless_enforcement_level warns — or refuses — when a write would leave a secret stored in Warden. And when Warden itself refuses a request, it now answers in the upstream's own error shape, so an AWS SDK, an OpenAI client or an Anthropic client reads the failure as it reads any other.
Nine breaking changes — read Upgrading before you bump.
Breaking Changes
- The
defaultassertion profile is an RFC 8693 delegation token. With a user disclosed, the top level is the user and the agent moves intoact.warden_sub,warden_auth_mount,warden_userand the agent-levelwarden_namespaceare gone; the agent's compositesubis unchanged, so agent-only trusts keep matching. get_skillis removed forread_skill(uri), and the(skill: …, url: …)description convention gives way to the role fieldsskillandprovider_path. Backfillprovider_path, or a role is listed with no URL.- Skill reads on the discovery server are identity-bound.
- Skill names drop the underscore: every stored skill with one is renamed at unseal —
mcp_awsbecomesmcp-aws,ansible_towerbecomesansible-tower. - MCP refusals are JSON-RPC errors (
-32090); the formererror/error_descriptionmove undererror.data. - Azure
key_vault_secretspecs fail every mint — move them tosecret_read. - GitHub's singular
repositoryis refused, and a storedpermissionsvalue is now enforced. - New AWS federated specs default to the
awsprofile (session tags, needssts:TagSession); new Azure federated specs default tominimal. - AWS and Alibaba Cloud config writes merge — a key left out keeps its value.
Security
- Azure Key Vault URL injection closed, and rotation no longer deletes live secrets.
- MCP policy is enforced on every POST to an MCP mount, whatever its
Content-Type.
New Features
The playground
warden server -dev-playground, its identity provider, a bank with MCP and REST faces, and GitHub's MCP server waiting for your PAT.warden dev jwt | scenarios | audit, and the tour written out for seven agents — on the page, or withwarden dev scenarios -client.
Discovery and skills
- Structured role fields,
list_roleswith a resolved provider, skill URI and URL, the MCP Skills extension, andread_skillreturning the skill's markdown in its structured output.
Keyless credentials
- Anthropic and OpenAI workload identity federation, a keyless Cloudflare source, and Azure Key Vault as a chaining producer.
assertion_profile—default,minimaloraws— and a per-specassertion_ttl.keyless_enforcement_level,stored_secretson every source and spec, andwarden cred source|spec keyless-planto describe the keyless replacement for one.- Public
token_exchangeclients, and GitHub App tokens scoped byrepositoriesandpermissions.
The gateway
- Native error shapes for AWS, OpenAI and Anthropic, and a
504— not an empty200— when a mount's timeout beats the upstream. - Policy reads request bodies on
restmounts, up to the mount'smax_body_size. - Anthropic workspaces, version and beta headers governed from mount config, and user-profile attribution.
- The audit log records the user's namespace, role and act chain.
Fixed
- A mint the upstream refuses answers
403, not500. - A config write takes effect only once it is saved; a refused write changes nothing.
- Rotation activations no longer revert operator edits or orphan keys, and source drivers are rebuilt after a node steps down.
- Each credential client owns its connection pool, so deleting one source no longer drops another's connections.
Upgrading
Nine changes need action. Work through Upgrading from v0.20.0 before rolling out the binary.
- Find every verifier bound to the old
defaultclaims — a Vault JWT role, a GCP attribute condition, a CEL rule readingwarden_sub,warden_userorwarden_auth_mount— and rewrite it. - Backfill
provider_pathon roles that relied on the description convention, and move agents fromget_skilltoread_skill. - Move Azure
key_vault_secretspecs tosecret_read, and GitHub specs fromrepositorytorepositories. - Behind a load balancer, list it in
trusted_proxies; and add internal hosts toNO_PROXY, which sources with a custom CA now honour.
A local-source github_token spec stored with repositories or permissions before this release keeps minting the unscoped token it holds; a static token cannot be narrowed. New writes are refused.
Warden v0.20.0
Warden v0.20.0
v0.20.0 is the agent-and-user authorization release. Warden has carried two principals per request since v0.19.0, but only one of them could be reasoned about: the agent authorized the request and the user was along for attribution. That changes here. The user principal is now visible to the policy layer, so a condition can require a human behind the agent — and, more importantly, require that this agent is the one the human authorized. The CEL token namespace is renamed agent to say plainly which principal a rule is talking about, and a new user namespace sits beside it.
Two consequences follow. A denial that a user could remedy no longer looks like a wall: Warden answers 401 with a WWW-Authenticate challenge and publishes RFC 9728 protected resource metadata, so a client discovers where to authenticate the person and retries. And MCP authorization becomes its own policy type — capability policies govern paths, MCP policies govern calls, and access is the intersection of both. MCP traffic with no MCP policy in scope is now denied rather than passing unrestricted.
On the credential side, keyless keeps widening: Alibaba Cloud and Kubernetes federate, AWS Secrets Manager and GCP Secret Manager join OpenBao/Vault as chaining producers, and a client assertion can now be signed inside a KMS that Warden never sees the key for.
Eight breaking changes — read Upgrading before you bump. Two of them can take a working deployment down if you roll the binary out first and migrate afterwards.
Breaking Changes
- The CEL
tokennamespace is nowagent. No alias. A stored policy referencingtokenfails to compile at load, and because policies are re-parsed on read, it cannot be read back afterwards — export your policy texts first. Auditsalt_fieldsselectors move with it, and a stale one does not error: it stops matching, and the value it protected begins logging in clear. - Dual-token extraction. On a
user_auth_pathmount the legs swap:Authorizationcarries the user, the agent moves toX-Warden-Agent-Tokenor a client certificate.user_token_headeris retired anduser_auth_pathis mount-only. - MCP rules move to
sys/policies/mcp/<name>. A nestedmcp { }block is rejected at parse, the grammar becomes family blocks, andallowed_params/denied_paramsgive way to a CEL condition overcall.args. - MCP traffic with no MCP policy in scope is denied (
no_mcp_policy). Mounts meant to stay open need an explicit wildcard policy. mcp/mcp_awstimeoutdefaults to 60 seconds and now caps a single call; long-lived streams answer to the newlisten_timeout.- IBM
iam_with_cos, OVHdynamic_s3andoauth2_token_and_s3are removed, replaced byaccess_keyssourced throughsecret_spec. Drain OVH leased pairs before upgrading. apikeysources renameoptional_metadatatocredential_fields— and the mechanism now works, where before the declared fields silently never reached the provider.
New Features
Agent and user in policy
- The user principal reaches CEL. A
usernamespace besideagent, carrying the same fields minuspolicies— the user never authorizes, so it cannot widen what an agent may do, only be made required. - Binding an agent to the user it acts for. It is not enough that both are present; the user must be paired with this agent. The pairing is attested on the user's token by an RFC 8693 claim —
may_actwhere the IdP pre-authorizes an agent,actwhere the credential is itself a delegation token — and either maps through a JSON Pointer. 401instead of403when a user would fix it, with aresource_metadataparameter, plus RFC 9728 protected resource metadata and awarden protected-resourcecommand to configure it. A retry that still fails gets a terminal403; the exchange cannot loop.
MCP
- A first-class MCP policy type with its own grammar, storage and
-typeflag, composing with capability policies as an intersection. - Protocol hardening: per-URI gating for
subscriptions/listen, transport headers validated against the body, modern-era batches refused, per-principal responses marked uncacheable.
Credentials
- Keyless federation for Alibaba Cloud and Kubernetes — the latter presents its assertion directly as the bearer token, with no exchange hop.
- AWS and GCP secret stores can produce chained secrets (
mint_method=secret_read), and can be federated themselves, so a chain is keyless at both hops. - KMS-signed client assertions (
client_auth=kms_private_key_jwt) fed bytransit_signer, which mints a scoped signing capability rather than key material — the private key is read by nobody, including Warden. - Nine more drivers can chain their standing secret, and claim templating now reaches AWS
secret_idand GCPsecret_namealongside Vault'ssecret_path, so one spec can resolve a different secret per caller. - The
honeycombcredential driver is removed — its keys could be neither revoked nor expired. Hold the key in a vault and chain it. The honeycomb provider is unaffected.
Fixed
cas_requiredis enforced on the policy write path, which never applied it — a policy that set it was unguarded. Refusals answer4xx, not500.- Responses longer than 10 seconds survive. A hardcoded listener write timeout severed every response over ~10s, so no LLM mount could reach its 120-second budget; raising the mount
timeoutdid nothing. The listener's HTTP timeouts are configurable now, and streaming sheds them. - Denied requests are audited once, not twice.
Upgrading
Eight changes need action. Work through Upgrading from v0.19.0 before rolling out the binary — the first two steps are irreversible once it is running.
- Export every policy that uses a
token.*condition — after the upgrade it cannot be read back:for name in $(warden policy list -o text); do warden policy read -o table "$name" > "policy-backup-$name.hcl" done
- Snapshot your audit configuration and rewrite
salt_fieldsselectors to theagentnamespace, or the values they protect begin logging in clear. - Drain OVH leased credential pairs, which are not cleaned up after the upgrade.
- Then: rewrite
token.*conditions, re-expressmcp { }blocks as-type=mcppolicies and attach them to the roles that need them, add wildcard MCP policies to mounts meant to stay open, settimeoutexplicitly on MCP mounts with long calls, move agents toX-Warden-Agent-Token, and renameoptional_metadatatocredential_fields.
Warden v0.19.0
Warden v0.19.0
v0.19.0 is the keyless federation release. Warden can now act as its own OIDC issuer — minting short-lived, signed identity assertions that describe the caller and publishing the JWKS / OIDC discovery to verify them — so an upstream trusts who the caller is instead of a secret Warden holds. On top of that, keyless credential sources (auth_method=oidc_federation) across AWS, Azure, GCP, and OpenBao/Vault store no cloud secret at all — each request federates an assertion for a short-lived credential — and credential chaining (secret_spec) sources any remaining standing secret from an external keyless-federated vault per request, so there is no key-trove to breach. The issuer's signing key can live in an external KMS (the private key never enters Warden), and a public publisher exposes the verification keys to internet-facing clouds without exposing Warden itself. Finally, a second user principal lets an agent act on behalf of a verified human — natively (RFC 8693 / RFC 7523 / ID-JAG) or, for upstreams with no on-behalf-of flow, via a per-user OAuth token from OpenBao/Vault at fleet scale. Three breaking changes — read Upgrading before bumping.
Breaking Changes
-
RFC 8693 token exchange reshaped around the user. The
headertoken source and its subject-trust keys (subject_issuer/subject_audience/subject_jwks_url/subject_oidc_discovery_url) are removed; thesubject_token_source/actor_token_sourcevalueauth_tokenis renamed toagent_identity; and delegation now forwards the end user's own validated credential viasubject_token_source=user_identity(withactor_token_source=warden_identityfor the on-behalf-of-user shape). A create/update with a retired value is rejected, and a persisted spec carrying one fails closed at mint. -
GitHub credential specs select the credential with
mint_method, notauth_method. Thegithubdriver now reads a required spec-levelmint_method(app|pat); the oldauth_methodkey is rejected. GitLab is unaffected. -
X-Warden-On-Behalf-Ofretired; audit/policy actors are verified-only. The self-reported header is gone — the actor chain now has a single source, the cryptographically-verified RFC 8693actclaim. Theverifiedfield is dropped everywhere, so a CELconditionreferencinga.verified(e.g.token.actors.all(a, a.verified)) must becomesize(token.actors) > 0.
New Features
Federation & keyless identity
- Warden is now an OIDC issuer. It mints short-lived, signed JWT identity assertions (
sub = wid:<namespaceID>:<mountAccessor>:<principalID>, pluswarden_*claims) and publishes the matching JWKS + OIDC discovery, so any upstream that trusts an OIDC issuer — a cloud STS, an IdP, a secrets vault — can verify a caller's identity directly. The issuer fails closed until a signing key is installed, and key rotation retires prior keys into the JWKS so in-flight assertions keep verifying. - The signing key can live in an external KMS. A
signer { }stanza binds the assertion-signing key to an external KMS (type = "transit"today) so the private key never enters Warden; the interface is built for more backends. - Publishers expose the keys to internet-facing upstreams. A publisher pushes the discovery + JWKS to a bucket/CDN (
s3,gcs,azure_blob,http_put,local_file) that becomes theissuer_urlupstreams fetch from — so Warden itself need not be internet-facing — and self-rotates its own write credential. - Keyless credential sources —
auth_method=oidc_federation. A source can hold no stored cloud secret: on each request Warden federates an identity assertion for a short-lived credential. Ships for AWS (AssumeRoleWithWebIdentity, keyless Secrets Manager), Azure (Entraclient_assertion), GCP (Workload Identity Federation), and OpenBao/Vault (per-request JWT-auth login). - Assertion shaping.
warden_identityassertions can pin a single downstream resource (warden_resource) and carry allowlisted login metadata (assertion_metadata_claims→ nestedwarden_metadata) for verifier-side least privilege.
Credential chaining
- Chain a spec's secret from another cred spec (
secret_spec). A secret-backed provider sources its standing secret from another spec at request time instead of storing it — keyless end-to-end, single-hop, nothing persisted. Includes a generickv2_readmint method +key_valuecredential type, an opt-in source-scoped cache (secret_cache_ttl), and support for sourcing thetoken_exchangeclient secret this way.
User principal & acting on behalf of a human
- Per-user credential chaining via secondary transparent authentication. A request can carry a second, per-request user credential (default header
X-Warden-User-Token) that Warden resolves into a first-class user principal — identity-only today. Awarden_identityspec can disclose the user under a nestedwarden_userclaim, and a per-usersecret_path/credential_nametemplate ({{user.<claim>}}) selects a secret or OAuth token scoped to the verified user — so an agent acts as the human, even against upstreams with no native on-behalf-of flow.
Upgrading
Three changes need action; each is a small, mechanical migration. See the Upgrading from v0.18.0 guide for the full detail.
- CEL actor conditions: rewrite
token.actors.all(a, a.verified)→size(token.actors) > 0(note the empty-chain semantics flip from allow to deny — drop the condition entirely if you did not mean to require an actor). - GitHub specs: rename
-config=auth_method=<app|pat>→-config=mint_method=<app|pat>. - Token-exchange specs: rename
subject_token_source=auth_token→agent_identity; re-express a header-sourced delegation assubject_token_source=user_identity(+actor_token_source=warden_identity) with auser_auth_pathconfigured; drop the removedheadersource andsubject_*trust keys.
Warden v0.18.0
Warden v0.18.0
v0.18.0 is the on-behalf-of release. A new token_exchange credential source lets a Warden role broker an upstream call as the agent acting for the end user rather than as a static service identity: it exchanges a caller-presented token at an STS/IdP endpoint following RFC 8693, with support for the id_jag cross-app-access flow, RFC 8707 resource indicators, and private_key_jwt client authentication. The exchange is verified-origin — the subject and actor tokens are drawn from the agent's own verified inbound Warden JWT (or explicitly-supplied headers), and the caller-supplied exchange headers are stripped before the request reaches the upstream. Alongside it, an always-on MCP discovery server at sys/mcp lets an agent discover its roles and skills over MCP itself, and MCP tools/list responses are pruned to the callable set so discovery matches enforcement. One breaking change — MCP mcp { } authorization is now deny-by-default; read Upgrading before bumping.
Breaking Changes
- MCP
mcp { }authorization is now deny-by-default. Anmcp { }block previously allowed anything it did not explicitly deny; it now denies anything it does not explicitly allow — an empty or absentallowed_methods/allowed_tools/allowed_resources/allowed_promptsdenies every method/tool/resource/prompt in that family. This silently tightens deployed policies:mcp { denied_tools = ["delete_*"] }(or a block carrying only acondition) goes from "all tools exceptdelete_*" to "deny every tool". The session-lifecycle methodsinitialize,ping, andnotifications/*are exempt — they pass without being allow-listed (adenied_methodsentry can still block them). Audit everymcp { }block before upgrading; see Upgrading.
New Features
-
New
token_exchangecredential source — RFC 8693 on-behalf-of token exchange. A source of typetoken_exchangemints an upstream access token by exchanging a caller-presented token at atoken_url.subject_token_source/actor_token_sourcechoose where the subject and actor tokens come from —auth_tokenreuses the agent's verified inbound Warden JWT (verifiedorigin),headerreadsX-Warden-Subject-Token/X-Warden-Actor-Token(unverified),noneomits it (the two sources can't both beauth_token). Selectable grants (grant):rfc8693,jwt_bearer(RFC 7523), andid_jag(two-leg ID-JAG, needsresource_token_url). Client auth isclient_secret_basic,client_secret_post, orprivate_key_jwt. Requestedaudience,scope, and RFC 8707resourceslive on the spec; a header-sourced subject token is verified against a configured issuer/JWKS. The source issues anoauth_bearer_tokencredential, so any bearer-token provider (including the genericmcpprovider) can carry it, and the caller-supplied exchange headers are stripped before forwarding upstream. -
MCP discovery server at
sys/mcp. An always-on MCP endpoint exposes two tools:list_rolesreturns the roles the caller can assume (each naming the skill that drives it), andget_skill(argumentskill) returns that skill as markdown. Namespace is selected with theX-Warden-Namespaceheader. This replaces the retired CLI-loop discovery skills. -
MCP list responses are filtered to the callable set. When a
tools/list,resources/list, orprompts/listrequest is allowed, Warden prunes the response to only the items the caller could actually use — an item survives iff the correspondingtools/call/resources/read/prompts/getwould pass policy (per-callconditions are deferred to call time). A batched request containing a list method is denied, and a list response that can't be buffered withinmax_body_sizefails closed rather than streaming unfiltered.
Upgrading
The MCP deny-by-default flip changes how existing mcp { } blocks behave. Audit every block and restore intended openness explicitly:
- Open everything, then subtract:
mcp { allowed_methods = ["*"], allowed_tools = ["*"], allowed_resources = ["*"], allowed_prompts = ["*"], denied_tools = ["delete_*"] } - A block that carried only a
condition(or onlydenied_*lists) now denies its whole family — add the matchingallowed_* = ["*"]to keep it open. - You no longer need to allow-list the MCP handshake:
initialize,ping, andnotifications/*pass without being listed (block them explicitly viadenied_methodsif you must).
The change is enforced at request time, not policy-write time, so review blocks before upgrading rather than relying on a parse error to catch them.
Warden v0.17.0
Warden v0.17.0
v0.17.0 is the CEL release. Capability-Based Policy gains one expressive predicate layer: a path rule can carry a condition (evaluated once per request) and an mcp { } block a per-call condition (evaluated once per tool call) — a CEL expression over a fixed request.* / token.* / now / call.* namespace, with helpers for CIDR matching (cidrContains), optional arguments (call.args.?x.orValue(d)), and timezone-aware time (now.getHours(tz), now.getDayOfWeek(tz)). Evaluation is fail-closed and cost-bounded, conditions are compiled once and identity-independent, and every decision is explainable in the audit log (the expression and the exact input values it read). CEL replaces — and this release removes — the three structured mechanisms it subsumes: the conditions {} block, MCP allowed_params/denied_params, and CBP request-body *_parameters. Alongside it, login-derived token metadata lands across all four auth methods (feeding token.metadata.* in conditions), a new generic rest provider proxies arbitrary JSON/REST upstreams, and the documentation is substantially restructured and expanded (including a 20-example CEL cookbook). Three breaking changes — read Upgrading before bumping.
Breaking Changes
-
The structured
conditions {}block is removed — express it as a CELcondition. Its four predicates map directly:source_ip→cidrContains("10.0.0.0/8", request.client_ip),time_window→now.getHours(tz)/now.getMinutes(tz),day_of_week→now.getDayOfWeek("UTC")(0 = Sunday),token_metadata→token.metadata.<key>. The parser rejects the block with a directed error. -
MCP
allowed_params/denied_paramsare removed — express them as a per-callconditionovercall.args. e.g.condition = "call.args.amount <= 1500 && call.args.currency in ['USD','EUR']". Semantics change deliberately: a condition is fail-closed (absent argument → deny), whereas the old lists let an absent argument pass — usecall.args.?x.orValue(d)where "absent is OK". -
CBP request-body
required_parameters/allowed_parameters/denied_parametersare removed — express them as aconditionoverrequest.data.has(request.data.owner)requires a field,request.data.tier in ['gold','silver']constrains a value,!has(request.data.internal)forbids one. Pagination (pagination_limit) and list-response filtering are unchanged. The parser rejects the removed keys.
New Features
-
CEL policy conditions. Path-level and per-call
mcp { }conditions overrequest.*(incl. the newrequest.namespace),token.*,now, andcall.*; helperscidrContains, optional types, and the timezone-awarenow.get*built-ins. Fail-closed and cost-bounded (type-checked and cost-limited at write time; a runtime cost limit backstops size-dependent expressions). A path-level condition that referencescall.*is a compile-time error. See the CEL condition cookbook. -
Login-derived token metadata across
jwt,cert,kubernetes, andspiffe. Per-rolemetadata_claims(claims, incl. nested via JSON Pointer) ormetadata_mappings(certificate fields, TokenReview attributes, SPIFFE-ID components) map verified identity attributes onto the token;token.metadata.<key>is then usable in conditions and attributed in audit. -
Explainable audit for CEL decisions.
auth.policy_results.conditionrecords the expression, decision, a sanitized error category, and the referencedinputs— logged in clear by default and HMAC-salt-able per key viasalt_fields. -
Generic
restprovider. A REST/HTTP reverse-proxy backend for JSON/REST APIs without a dedicated provider, with a Warden-injected credential.
Upgrading
Rewrite any policy that used the removed structured mechanisms as a CEL condition:
conditions { source_ip = ["10.0.0.0/8"] }→condition = "cidrContains('10.0.0.0/8', request.client_ip)"conditions { token_metadata = { env = ["prod"] } }→condition = "token.metadata.env == 'prod'"- MCP
allowed_params { amount = ["<=1500"] }→mcp { condition = "call.args.amount <= 1500" }(addcall.args.?amount.orValue(0) <= 1500if an absent argument should still pass) - CBP
required_parameters = ["owner"]→condition = "has(request.data.owner)"
The parser rejects each removed construct at policy-write time with an error naming its CEL equivalent, so a stale policy fails fast on warden policy write rather than changing behaviour silently.
Warden v0.16.0
Warden v0.16.0
v0.16.0 is the SPIFFE release. A new first-class spiffe auth method accepts a SPIFFE X.509-SVID (over mTLS or a trusted forwarding header) and a SPIFFE JWT-SVID (bearer) on the same mount, verifying each against the trust domain's bundle — which carries both the X.509 and JWT key sets — and issuing one token type. Roles bind on trust_domain, allowed_spiffe_ids, and bound_audiences; bundles are managed on the mount and can be federated from upstream endpoints with periodic refresh. The method leans on short-lived SVIDs and bundle rotation rather than CRL/OCSP. The control plane itself can now run on SPIFFE: the API listener can serve its TLS certificate straight from the SPIFFE Workload API, rotating automatically as the SVID renews. This release also consolidates the MCP providers — a single generic mcp provider replaces the typed mcp_github and mcp_gcp — and threads non-secret subject metadata into the audit log across the AWS, GCP, Azure, Kubernetes, and Vault sources, so a brokered call records which upstream identity acted. Two breaking changes — read the Upgrading section before bumping.
Breaking Changes
-
The
mcp_githubandmcp_gcpprovider types are removed in favour of a single genericmcpprovider. The newmcpprovider fronts any bearer-authenticated MCP server (GitHub, Google Cloud, Slack, Cloudflare, …) and injectsAuthorization: Bearerforoauth_bearer_token,api_key,github_token,gcp_access_token, andazure_bearer_token— a superset of the typed providers it replaces. Re-mount as typemcpand setmcp_url(https://api.githubcopilot.com/mcpfor GitHub; the operator's URL for Google Cloud).mcp_awsis unaffected — it SigV4-signs requests rather than injecting a bearer token. -
jwtauth method: thebound_uri_patternsanduri_claimrole fields are removed. They provided segment-aware string matching against a JWT claim as a stand-in for real SPIFFE JWT-SVID validation. SPIFFE identities now belong to the newspiffeauth method, which verifies a JWT-SVID against a trust-domain bundle and enforces its audience. Stored roles carrying these keys decode cleanly and ignore them — a role that relied on URI-pattern matching becomes more permissive, so migrate it to thespiffemethod.
New Features
-
New
spiffeauth method: first-class SPIFFE support for both SVID types on one mount. Accepts a SPIFFE X.509-SVID (presented over mTLS or a trusted forwarding header) and a SPIFFE JWT-SVID (bearer) on the same mount, verifying each against the trust domain's bundle and issuing a singlespiffe_roletoken type. When a request presents both, the explicitly-presented JWT-SVID wins over an ambient forwarded certificate. Roles bind ontrust_domain,allowed_spiffe_ids, andbound_audiences; an audience is required for JWT-SVID logins, as SPIFFE mandates. Trust-domain bundles are managed on the mount and can be federated from upstream endpoints with periodic refresh. There is no X.509-SVID revocation surface — the method relies on short-lived SVIDs and bundle rotation, and an issued token's TTL is capped by the SVID's expiry. -
The API listener can serve its TLS certificate from the SPIFFE Workload API. Instead of a static certificate and key on disk, the listener can obtain an X.509-SVID from a SPIFFE Workload API endpoint and rotate it automatically as the SVID is renewed, keeping the control-plane TLS identity short-lived and self-renewing.
-
certauth method consolidated to pure PKI. The never-shipped experimental SPIFFE mode (mode=spiffe, trust-domain config, bundle federation, and thespiffe_idprincipal claim) is removed in favour of the dedicatedspiffemethod. Thecertmethod keeps its X.509 chain-of-trust validation and URI-SAN matching (uri_sanprincipal claim plusallowed_uri_sans), so a SPIFFE X.509-SVID can still be accepted as an ordinary client certificate bound on its URI SAN. -
Audit-log subject metadata across the cloud and secrets sources. The AWS, GCP, Azure, Kubernetes, and Vault drivers now populate the non-secret
Credential.Metadataintroduced in v0.15.0 when they mint — the assume-role identity on an AWS STS mint, and the token/access-token subject on the others — so an audit record shows which upstream identity a brokered call acted as, not merely that a call occurred.
Bug Fixes
httpproxy: the gateway suffix is forwarded verbatim. Gateway-suffix path extraction now preserves the upstream path exactly — including trailing slashes — so the SigV4-signedmcp_awsgateway's canonical request stays valid.
Documentation
- Operator README for the new
spiffeauth method, with SPIFFE content retargeted across thecert,jwt, and Vault-provider docs to point SPIFFE workloads at the dedicated method. mcp_awsREADME: a Claude Code CLI registration step (claude mcp add) for pointing Claude Code at anmcp_awsmount.- Root README: a dedicated
spifferow in the authentication-methods table.
Upgrading
helm upgrade warden oci://ghcr.io/stephnangue/charts/warden \
--version 0.3.2 \
-n warden --reset-then-reuse-valuesChart 0.3.1 → 0.3.2 is an appVersion bump to track the v0.16.0 binary — there are no chart template or values changes, so existing values files apply as-is.
Two operator-facing migrations before bumping:
- Re-mount any
mcp_githubormcp_gcpprovider as typemcpand setmcp_url(https://api.githubcopilot.com/mcpfor GitHub; the operator's URL for Google Cloud). The generic provider reuses the same credential types, so existing credential specs apply unchanged.mcp_awsis unaffected. - Migrate any
jwtrole that usedbound_uri_patternsoruri_claimto thespiffemethod. Those fields are gone; a role that relied on them now ignores them and becomes more permissive until migrated.
Resources
- New installs and detailed upgrade procedures: docs/deployment/kubernetes.md.
- SPIFFE, MCP, and auth-method setup: README and the per-provider and per-method READMEs.
License
Warden v0.15.0
Warden v0.15.0
v0.15.0 is the MCP release. A new mcp { } policy block brings body-authoritative governance to MCP (JSON-RPC) traffic — gate by method, tool, resource, prompt, and tool-argument values, with the gateway strict-parsing the request body rather than trusting client-supplied headers — and three new providers carry it: mcp_github (transparent proxy to GitHub's hosted MCP server), mcp_aws (SigV4-signed gateway to AWS's hosted MCP servers and Bedrock AgentCore), and mcp_gcp (bearer-token gateway to a Google Cloud MCP server). The OAuth2 credential source gains an authorization-code flow, so a Warden role can broker calls as a consenting user — consent captured once through warden cred spec connect, refresh tokens rotated and written back automatically. github and gitlab now proxy Git smart-HTTP (clone / fetch / push) on the same mount as their REST API, and a new kubernetes auth method validates workload identities via TokenReview. Three breaking changes — read the Upgrading section before bumping.
Breaking Changes
-
Transparent-auth role resolution: the provider role-extractor now outranks
default_role. The chain isX-Warden-Roleheader > role embedded in the path > the provider's role-extractor hook > the mount'sdefault_role. Mounts that combine an extractor withdefault_rolechange behaviour —githubGit smart-HTTP, AWS-with-SigV4, and Alicloud-with-ACS3/SigV4 now resolve to the extractor-derived role instead ofdefault_role. Mounts without an extractor (or AWS/Alicloud requests that aren't SigV4-signed) are unchanged. -
githubprovider: thegit_urlconfig field is removed. It was declared but never read at runtime. Operators who had it set now get a clear "unknown field" error on config write instead of a silent no-op — drop it; the Git host is derived fromgithub_urlas before. -
SDK signature changes for out-of-tree backends.
StreamBodyParser.ShouldParseStreamBodyandTransparentModeProvider.IsUnauthenticatedPathnow take the*http.Request, andTransparentAuthRoleExtractorreturnsstring(was(string, bool)). In-tree backends are unaffected; only code implementing these interfaces out of tree needs updating.
New Features
-
OAuth2 authorization-code flow — a Warden role can broker calls as a consenting user. The
oauth2source gains anauthorization_codeauth method alongsideclient_credentials, so a credential spec can hold a user's consent rather than a service identity.warden cred spec connect <name>runs the one-time browser consent over a loopback callback with PKCE andstate, and the token exchange happens server-side — the client secret never reaches the operator's machine. Refresh tokens (and their expiry) are rotated and written back automatically under a per-spec lock that serializes concurrent mints. The audit log records which upstream identity acted, per call.mcp_githubaccepts these credentials, so one role can act as a GitHub App, a PAT, or a consenting GitHub user. -
mcp { }policy block: body-authoritative governance of MCP (JSON-RPC) traffic. A newmcp { }block inside a control-by-policypathrule gates requests by JSON-RPCmethod, by thenameintools/call/resources/read/prompts/get, and by tool-call argument values — each with allow/deny symmetry and case-insensitive trailing-*globs. The gateway strict-parses the JSON-RPC body and fails closed on any malformed 2.0 envelope, then evaluates against the parsed body rather than client headers. Opt-in per provider and method-aware (only JSON-RPC POSTs are evaluated, so Streamable HTTP's GET/DELETE pass through). Denials return an OAuth-shaped HTTP 403, and the audit log carries a structuredmcp_decision. -
Three new MCP providers.
mcp_githubtransparently proxies GitHub's hosted MCP server (reusing the GitHub credential, so one spec grants both REST and MCP reach).mcp_awsSigV4-signs traffic to AWS's hosted MCP servers and Bedrock AgentCore, inferring service and region from the endpoint.mcp_gcpmints a short-lived GCP access token per request for a Google Cloud MCP server. All three opt intomcp { }enforcement. -
githubandgitlabGit smart-HTTP. Both providers now proxygit clone/fetch/pushon the same mount that proxies their REST API. Clone URLs carry the Warden role as the Basic Auth username and the Warden JWT as the password, so Git's credential helpers cache cleanly per role. A newgit_max_body_sizeconfig field caps Git request bodies separately from the RESTmax_body_size. -
kubernetesauth method: validate workload tokens via TokenReview. The first cloud-native member of the transparent-auth family validates a workload service-account token by callingTokenReviewon the issuing kube-apiserver — no JWKS endpoint required on the spoke, awkward on hardened distros — matching theauth/kubernetesshape Vault and OpenBao operators know. The SA UID, namespace, and name land in the audit log for attribution. -
Provider
tuneendpoint.warden provider tune <path> -description=...updates a mount's description in place; previously this required an unmount and remount. -
X-Warden-Providerrequests may omit the/v1/prefix. Header-routed calls (the documentedgit cloneform viahttp.extraheader) no longer require the/v1/prefix on the path; the middleware prepends it when the header is present. The canonical/v1/-prefixed form is unchanged, andsys/-targeted requests with the header are rejected with 400.
Bug Fixes
- SigV4: client cancellation logs at debug, not error. A client that disconnects mid-request on a SigV4 provider no longer produces an error-level log line.
Documentation
- Operator READMEs for the
cert,jwt, andkubernetesauth methods. mcp_githubprovider docs: authorization-code flow setup around a GitHub App (with a Claude Codeclaude mcp addworked example) and a body-authoritativemcp { }policy section.mcp_awsskill and operator README;httpproxySDK hook reference; root README coverage of the MCP-server providers, on-behalf-of attribution, Kubernetes auth, and credential rotation.
Upgrading
helm upgrade warden oci://ghcr.io/stephnangue/charts/warden \
--version 0.3.1 \
-n warden --reset-then-reuse-valuesChart 0.3.0 → 0.3.1 is an appVersion bump to track the v0.15.0 binary — there are no chart template or values changes, so existing values files apply as-is.
The binary breaking changes are mostly transparent to a Helm install, but be aware of three things before bumping:
- Mounts that set
default_roletogether with a role-extractor (githubGit smart-HTTP, AWS/Alicloud with SigV4) now resolve to the extractor-derived role instead ofdefault_role. Review those mounts if you relied ondefault_rolewinning. - The
githubgit_urlconfig field is gone — drop it from any provider config (the Git host is derived fromgithub_url). - Out-of-tree providers that implement
StreamBodyParser.ShouldParseStreamBody,TransparentModeProvider.IsUnauthenticatedPath, orTransparentAuthRoleExtractormust update those signatures. In-tree providers and a stock Helm install need no changes.
Resources
- New installs and detailed upgrade procedures: docs/deployment/kubernetes.md.
- MCP, OAuth2, and auth-method setup: README and the per-provider READMEs under provider/.
License
Warden v0.14.0
Warden v0.14.0
Warden is the secure gateway connecting AI agents to the enterprise systems they need to do real work. Agents discover what they're allowed to access, Warden brokers every connection, and operators get one control plane for identity, policy, and audit — across every cloud, code-host, observability stack, database, and SaaS the agent reaches. No upstream credentials ever reach the agent: Warden authenticates the caller (JWT or TLS certificate), evaluates fine-grained policy at request time, and injects short-lived credentials before forwarding.
v0.14.0 lands per-call agent attribution in the audit log — both unverified (X-Warden-On-Behalf-Of header) and verified (RFC 8693 act claim) — so an MCP-style concentrator that authenticates to Warden once and forwards calls for many upstream agents is no longer a black box at audit time. The audit-config story moves from "auto-magic on first unseal" to "declared in HCL, registered before the listener accepts traffic." A new X-Warden-Provider header lets non-agent clients (legacy SDKs, hand-rolled curl) reach a provider mount without rewriting URLs. The CLI gains Vault-style single-dash long flags, a -path flag on every path-taking command, a top-level warden status, and a :debug Docker image variant published alongside :nonroot. Seven small breaking changes — read the Upgrading section before bumping.
Breaking Changes
-
auth enable,provider enable,audit enabletake TYPE as a positional, like Vault. The-typeflag is gone. Migration:-type=jwt jwt-prod→-path=jwt-prod jwt;-type=aws→aws. The mount path still defaults to TYPE; override with-path=<mount>. In-jsonmode the payload'stypefield, if present, must match the TYPE positional or the CLI rejects with a usage error. -
JWT auth:
modefield removed fromauth/{mount}/config.mode=jwt|oidcwas redundant with the three mutually-exclusive key-source fields (oidc_discovery_url,jwks_url,jwt_validation_pubkeys) — the backend can derive everything it needs from which one is set. Exactly one of those three must now be provided. Existing scripts passingmode=...emit a framework warning but continue to succeed. -
X-Warden-Roleheader now overrides any role embedded in the URL path. Previously a role in the URL took priority over the header; now the header wins unconditionally in both routing modes, matching the precedence of the newX-Warden-Provider. The in-tree skills, the Helm chart, and the CLI never produce the URL-role + header-role combination, so the affected surface is likely zero — but any caller relying on URL-role winning over a strayX-Warden-Rolemust drop the header or align it with the URL. -
/v1/sys/healthand/v1/sys/leader:is_selfis renamed tois_leader. Symmetric across both endpoints. Update any external script that grepped for"is_self":against these endpoints. -
/v1/sys/health:server_time_utc(int64 Unix seconds) is replaced withserver_time(RFC3339 string). Human-readable and sortable as text. The old integer field is gone, not aliased. -
Listener HCL:
tls_enabledis replaced bytls_disable(TLS on by default). Droptls_enabled = true; replacetls_enabled = falsewithtls_disable = true. Validation now rejects a listener that neither disables TLS nor supplies bothtls_cert_fileandtls_key_file. Dev mode setstls_disable = trueautomatically; the shipped sample HCL, the e2e configs, and the Helm chart's listener template were all updated. -
Audit auto-default device on first unseal is removed. The Helm chart ships a working
audit "file"block by default (see Upgrading); dev mode intentionally ships no audit; bare-binary operators add a one-line HCL block, or bootstrap via API (the broker now fail-opens at zero registered devices so the API path is reachable from a freshly-installed cluster).
New Features
-
Audit log: per-call actor attribution for delegated chains. A new
actorsarray onaudit.Authcarries{subject, verified}entries from two sources: unverified subjects via theX-Warden-On-Behalf-Ofrequest header (regex-validated, gated on an authenticated principal so an unauthenticated caller cannot plant an actor), and verified subjects via the standard RFC 8693 §4.1actclaim on JWTs from IdPs that mint act-tokens for delegation. The header is added to every provider strip-list so it never leaks upstream. MCP-style concentrators that fan out per-agent calls under one Warden identity now produce an audit log a security team can actually correlate. -
X-Warden-Providerheader for mount routing. Non-agent clients (legacy SDKs that take a single-stringbase_url, hand-rolled curl, scripted tools) can now reach a provider mount without rewriting the URL: send the request to/v1/<literal-upstream-api-path>withX-Warden-Providernaming the mount andX-Warden-Rolenaming the role, and the server synthesises the canonical<mount>/role/<role>/gateway/<api>shape before mount lookup. Multi-segment mount paths (gitlab/prod) are accepted; empty path segments and bare..are rejected as 400; unknown mounts fall through to the existing 404. Combined with a SigV4 Authorization header the call is refused with 400 (rather than silently invalidating the client signature and surfacing as 401). Newapi.Client.SetProvider/WithProviderhelpers mirror the existing role/namespace pattern. -
warden statussubcommand, plus cluster-aware/v1/sys/health. A new top-level CLI command that wraps/v1/sys/healthand reports init/seal/standby/leader/active-since/version in one round-trip — exit code0on a healthy node (active or standby),7on a transport error,10when sealed-or-uninit, scriptable for operator playbooks. The endpoint grewha_enabled,is_leader,leader_address,active_time(RFC3339, only set on the active leader), andversion(server build) so external tooling no longer needs three round-trips to assemble a status view. Leader lookup is gated on!sealed && ha_enabled, so a sealed HA node no longer logs an error on every k8s probe. -
JWT static public keys (
jwt_validation_pubkeys) now work. The field has lived in the config schema for several releases but the backend returned "not yet implemented" at runtime. PEM-encoded RSA and ECDSA keys now validate JWT signatures against a static keyset — no JWKS endpoint required, suitable for air-gapped clusters, CI/CD pipelines, and fixed-issuer workloads where neither OIDC discovery nor a reachable JWKS endpoint is available.jwks_urlandjwt_validation_pubkeysare mutually exclusive within a single mount. -
Audit devices declared in the HCL server config. A new
audit "TYPE" "PATH" { description = "..." options = { ... } }block registers devices before the API listener accepts traffic — a misconfigured sink (unwritable path, missing parent directory, permission denied) is a hard startup error, not a half-initialized cluster that takes requests and drops audit. Declarative (HCL) and imperative (API-enabled) devices coexist at different paths; an HCL block colliding with an API-enabled path refuses to start, and declarative devices cannot be modified or deleted viasys/audit/{path}(operators edit HCL and restart instead). The reconciler on every unseal preserves each entry's accessor and HMAC salt across restarts so audit-log HMACs stay stable.GET /v1/sys/auditresponses now carry adeclarativeboolean so operators can tell the two origins apart. -
:debugDocker image variant published alongside:nonroot. Same multi-arch binary ongcr.io/distroless/static-debian12:debug-nonrootinstead of the production distroless base — so operators cankubectl exec -- shinto a pod and inspect/config,/app, and the filesystem when something goes wrong. Swapimage.tag=v0.14.0-debugfor the day and back when done; UID, GID, entrypoint, exposed port, and the/configmount contract are identical to the production image. -
CLI accepts Vault-style single-dash long flags. Cobra/pflag is POSIX-strict, so
-format=jsonwould otherwise parse as-ftaking valueormat=jsonand fail. A new normalizer rewrites single-dash long-flag tokens to double-dash before flag parsing; short flags,--, and genuine typos pass through unchanged so error paths still fire. All operator-facing docs (READMEs, CHANGELOG, tutorial scripts, provider docs, cobra help text) switched to the single-dash convention to match Vault muscle memory. -
-pathlong flag on every path-taking CLI command.audit/auth/provider enable|disable|readand the basicread|write|delete|listcommands all accept the mount path as-path=<mount>as an alternative to the trailing positional, sovault secrets enable -path=foo awsports verbatim. Existing positional usage is unchanged. -
Server config: warn-and-ignore unknown HCL keys. A typo or stale field in
warden.hcl(thinkcluster_name,ui,default_lease_ttl) used to crash startup with an opaquegohclerror. The loader now walks the body against the schemas derived fromConfig{}and its nested blocks, deletes any unrecognized attribute or block, and emits one[WARN] config: ignoring unknown attribute "X" at <file:line:col>line on stderr per removal so the typo stays visible. Known fields still parse and validate exactly as before — this is a one-way relaxation, not a schema loosening.
Bug Fixes
- Audit broker is fail-open at zero registered devices. Previously
auditManager.LogRequest/LogResponsereturned(continue=false, nil)when no devices were registered, causing the request handler to block every non-streaming request with HTTP 500 — includingsys/audit/{path}, so a freshly-installed cluster with no audit had no way to bootstrap one via the API. The broker now short-circuits to(true, nil)when zero devices are registered. Once any device...
Warden v0.13.2
Changelog
- 8ebc17b ci(deps): bump azure/setup-helm from 4 to 5 (#212)
- 644194a deps(deps): bump github.com/aws/aws-sdk-go-v2/service/redshift (#213)
- 02820c9 docs(k8s): add Cleanup section to the Kubernetes deployment guide (#211)
- 8907574 docs(skills): vault/bao CLI choice, Slack canvas worked example, discovery error recovery (#217)
- 16ff274 docs(tutorial): rewrite vault-policy-hygiene around discover-and-connect (#218)
- 62c1ec1 feat(ansible_tower): add agent-facing skill (#219)
- 299d0d9 feat(atlassian): add agent-facing skill (#220)
- 47daf8f feat(helm): opt-in cert-manager integration for the TLS listener (#214)
- 9f32fe0 feat(slack): add agent-facing skill (#215)
- 749cac6 fix(ci): run all checks on release-tag pushes (#210)
- 79a7502 fix(cli): send JWT only via Authorization: Bearer, not X-Warden-Token (#216)