Skip to content

Releases: TAIPANBOX/wardryx

v2.4.1

Choose a tag to compare

@github-actions github-actions released this 26 Oct 00:04
217fb8f

What's Changed

Full Changelog: v2.4.0...v2.4.1

wardryx 2.4.0

Choose a tag to compare

@TAIPANBOX TAIPANBOX released this 19 Oct 19:37
811da4a

Wardryx 2.4.0

2.4.0 closes three gaps in how wardryx checks a caller and records an approval. A bearer key is now compared in constant time. An agent's DPoP proof is good for one decision, so a proof captured within its window cannot be presented a second time. An approval can name the person who gave it, through a statement the console signs with a key set the operator configures.

It is a minor release. The new settings and refusals are additive, and two refusals are breaking only for a deployment that sets the setting each one belongs to, as the Breaking section names. A deployment that sets neither WARDRYX_APPROVER_JWKS nor WARDRYX_AGENT_TOKEN_ISSUER decides as 2.3.0 did. The one visible difference for it is the constant-time key comparison, which a caller cannot see.

Upgrade

Read this section first. It lists what an operator must check, by setting.

  • Bearer keys. No action. Keys are configured as before, and key_fp is the same 16 hex characters.
  • WARDRYX_AGENT_TOKEN_ISSUER set. Each /v1/decide call needs a DPoP proof made for that call. A proof sent a second time is refused with 403 agent_token_proof_invalid. Confirm the enforcement point sends a new proof with each call before you upgrade (Breaking, item 1).
  • WARDRYX_APPROVER_JWKS set. Every grant and every denial needs an approver_statement from the console. Without one it is refused with 403 approver_statement_required and the hold stays pending. Until the console sends statements, set WARDRYX_ALLOW_KEY_APPROVALS=1 (Migration, approver statements, step 4).
  • Startup. A WARDRYX_APPROVER_JWKS file that does not read or parse stops serve. With the file set, the start output names it. WARDRYX_ALLOW_KEY_APPROVALS=1 prints a warning at start, and when WARDRYX_APPROVER_JWKS is unset it also says the switch has no effect.
  • Neither setting. Every decision is made as 2.3.0 made it.

Rollback. A 2.3.0 build ignores both settings. A bare key decides again, and a repeated proof is accepted again. No stored state changes: internal/store and its schema are unchanged, and an approval decided with a statement keeps approver:<sub> in decided_by, which 2.3.0 shows as text. The proof record is in memory, so a restart clears it.

Breaking

Two refusals are new. Each applies only to a deployment that sets the setting it belongs to.

  1. A DPoP proof presented twice (with WARDRYX_AGENT_TOKEN_ISSUER set). A proof is good for one decision. A caller that sends the same agent_dpop_proof on a second /v1/decide call gets 403 agent_token_proof_invalid. A caller that sends a fresh proof with each call is not affected, because a DPoP proof is made per request (RFC 9449).
  2. A bare key decision (with WARDRYX_APPROVER_JWKS set). A grant or a denial with no verified approver_statement is refused with 403 approver_statement_required, and the hold stays pending. Nothing is decided and no hold moves.

Migration: single-use DPoP proofs

  1. Check the enforcement point's release. It must make a new DPoP proof for each /v1/decide call, bound to that call's token, and must not reuse one. One agent call can cause several decides, and each needs its own proof.
  2. In a test deployment, send two decides with the same proof. The second must be refused with 403 agent_token_proof_invalid. A decide with a fresh proof must be allowed.
  3. Upgrade wardryx. A refused proof is logged with the reason "already used within its window".
  4. If the enforcement point cannot send fresh proofs yet, fix that release before you upgrade a deployment that sets the issuer. Until it does, each decide that reuses a proof is refused.

Migration: approver statements

  1. Put the console's public keys in one file as a JWK set. Every key must be EC P-256. The loader computes each key's kid as its RFC 7638 thumbprint, and refuses a set with two keys of one thumbprint.
  2. Set WARDRYX_APPROVER_JWKS to that file's path. A server with no key set refuses every statement, so set the file before the console starts to send statements. Keys are read at start, so a rotation needs a restart.
  3. Make the console send approver_statement on POST /v1/approvals/{id}/decide, for each grant and each denial. The statement is a compact JWS signed ES256 by a console key, with typ approver-statement+jwt and a header that carries only alg, typ and kid. Its claims are v 1, sub (the approver), cred (the passkey credential), aid (the approval), dec (grant or deny), iat, exp (at most 300 seconds after iat) and jti. The statement's aid must be the approval in the path and its dec must be the decision in the request. Anything else is 403 approver_statement_invalid, and nothing is decided.
  4. While the console does not yet send statements, set WARDRYX_ALLOW_KEY_APPROVALS=1. It is read as exactly 1. Each decision it allows is logged with a WARNING line. Remove it once statements flow.
  5. Readers of decided_by must accept both forms. A statement decision reads approver:<sub> with approver_kind person. A key decision reads key:<fingerprint> with approver_kind key.

Added

  • WARDRYX_APPROVER_JWKS, the file of console keys, and WARDRYX_ALLOW_KEY_APPROVALS, read as exactly 1. Both are optional and unset by default. The README's environment table gives each one's default.
  • approver_statement, an optional request field on POST /v1/approvals/{id}/decide.
  • approver_kind (person or key) on each approval in GET /v1/approvals, and on the approval_granted and approval_denied events. A statement decision also writes approver_id, credential_id and statement_jti on those events.
  • Two refusals on that route: 403 approver_statement_invalid, which decides nothing and leaves the hold pending, and 403 approver_statement_required. The reason strings are the response, not wire fields.
  • Start output: a line naming the key file when it is set, and the warnings for WARDRYX_ALLOW_KEY_APPROVALS.

Changed

  • Bearer comparison. authenticate takes the digest of the presented bearer and compares it with every configured digest in constant time, with no early exit. It no longer looks the digest up. Callers see no difference. The time taken grows with the number of configured keys, which the operator sets.
  • Proof record. With the issuer set, the jti of each accepted proof is remembered until its window closes, which is 60 seconds by default and at most 300. A second presentation is refused. The record is in memory, per process, and bounded at 262,144 entries. When it is full a new proof is refused until an entry's window closes, so it fails closed.
  • Who approved. With WARDRYX_APPROVER_JWKS set, a verified statement sets decided_by to approver:<sub>. A decision with no statement is recorded as the key, as before. With a key set, a key decision is refused unless WARDRYX_ALLOW_KEY_APPROVALS=1 is set. The refusal is checked after the org check and before anything is written.
  • Invariants. CLAUDE.md invariant 13 names the proof record in its still-trusted paragraph, invariant 42 states the constant-time comparison, and invariant 43 is new.
  • Components and README. components.json declares the two new settings as optional. The README gains two environment rows, a paragraph on approval statements, a checklist line, and a note that the approver is the deciding key unless a statement names the person.

Changed since 2.3.0

No other commit lies between the 2.3.0 tag and this release.

  • #124, fix: constant-time bearer compare, single-use DPoP proofs, approvals that name the person. One squashed commit with the three changes described above.

Security fixes, by what they close

  • A bearer key matched by a map lookup. The lookup of a presented key's digest did work that depended on the digest. Every configured digest is now compared in constant time, with no early exit, so the time taken does not say which key matched. A gate refuses any index of the key store in internal/api.
  • An agent's DPoP proof presented again. A captured proof could be presented at any configured door within its window, which is at most 300 seconds. Each accepted proof is now single use within its window.
  • An approval that named a key, not a person. A grant or denial recorded the bearer key that sent it, so the record could not say which person decided. With WARDRYX_APPROVER_JWKS, the console signs a statement naming the approver, and wardryx verifies it against the console's keys. While a key set is configured, a bare key cannot decide an approval unless the operator allows it, and each such decision is logged.

Documentation and tests

  • 645 test functions, the README badge's count, up from 614 in 2.3.0: 31 new.
  • features/approver-statement.feature is new with 16 scenarios. features/bearer-key-store.feature has 5 scenarios, up from 4, and features/agent-token-verified-by-wardryx.feature has 32, up from 30. scripts/scenarios-bind-to-tests.sh reports 267 scenarios and 344 bindings, each naming a test that exists.
  • Invariant 42 is held by scripts/no-raw-secret-lookup.sh, with five cases in scripts/gates-have-teeth.sh. Invariant 43 has tests and no gate script.
  • The mutants recorded under invariants 42 and 43 were each caught by a named test.

Not proven: where it says nothing

  • No run against a live console or a live enforcement point. The tests make statements and proofs with generated keys.
  • The proof record is per process. Behind several wardryx instances, a proof presented once to each is accepted by each. A restart clears the record, so a proof seen before a restart can be presented again within its window.
  • A full proof record refuses new proofs until an entry's window closes. The bound is a refusal, not a queue.
  • A statement captured before it is used can decide its approval within its validity, at most 300 seconds. A second dec...
Read more

v2.3.0

Choose a tag to compare

@github-actions github-actions released this 19 Oct 12:04
f5db8a9

What's Changed

  • chore(deps): bump agent-stack-go to v2.0.1 by @TAIPANBOX in #119
  • docs(release): v2.2.1 upgrade section, the event stream file rule by @TAIPANBOX in #120
  • fix(auth): bearer keys stored and looked up by their SHA-256 digest (F-023) by @TAIPANBOX in #121
  • feat(decide): verify the agent's token and DPoP proof instead of trusting the enforcement point by @TAIPANBOX in #122
  • docs(release): wardryx v2.3.0 release notes by @TAIPANBOX in #123

Full Changelog: v2.2.1...v2.3.0

v2.2.1

Choose a tag to compare

@github-actions github-actions released this 19 Oct 03:42
8ded8a5

Wardryx 2.2.1

The wardryx image, ghcr.io/taipanbox/wardryx, is signed in the legacy .sig format that the cluster admission controller (policy-controller v0.15.1) reads. Its reader takes only that tag, so the image signature was not verifiable there before this release. cosign stays at v3.1.3, and the release workflow's sign-blob steps are unchanged. Nothing else changes in behaviour: no decision, approval, token or record format moves, and no name compat/1.0.json freezes is removed or renamed.

Changed since 2.2.0

  • #116, chore(deps): move agent-stack-go to the v2 module, v2.0.0. The shared wire-type import moves from github.com/TAIPANBOX/agent-stack-go to its /v2 module path. Nothing an operator sets or sees moves with it.
  • #117, ci(release): sign the image in the legacy format policy-controller reads. The image sign step adds --new-bundle-format=false --use-signing-config=false.

No other commit lies between the 2.2.0 tag and this release.

Upgrade: the event stream file

This is an operator-visible change that arrived in a patch release, and these notes say so rather than leave it out.

Since agent-stack-go v2, a stream file this service creates is mode 0640, not 0644: the owning account and its group can read it, and no other account can. A new stream file is created only in a directory this service's account owns. An existing file is opened as it is.

A bus directory that is shared between services and not owned by this service's account gets no new stream, unless each file is created beforehand by the uid of the service that writes it. Before you start this release, check who owns the bus directory and the mode of each stream file in it.

v2.2.0

Choose a tag to compare

@github-actions github-actions released this 18 Oct 19:17
6cc0bdc

What's Changed

  • feat(approvals): approval tokens v3, ES256 signed through Keyryx and verified from a cached key set by @TAIPANBOX in #113
  • docs(release): wardryx 2.2.0 release notes by @TAIPANBOX in #114

Full Changelog: v2.1.0...v2.2.0

v2.1.0

Choose a tag to compare

@github-actions github-actions released this 18 Oct 15:38
25636f8

What's Changed

  • feat(decide): identity_source from the enforcement point, and a rule that requires one by @TAIPANBOX in #110
  • feat(record): a decision the record cannot hold is refused (WARDRYX_RECORD_FAILMODE) by @TAIPANBOX in #111
  • docs(release): wardryx 2.1.0 release notes by @TAIPANBOX in #112

Full Changelog: v2.0.0...v2.1.0

v2.0.0

Choose a tag to compare

@github-actions github-actions released this 18 Oct 10:58
b66f8df

What's Changed

  • chore(deps): bump agent-stack-go to v1.2.0 by @TAIPANBOX in #104
  • feat(api): policy_updated names the key that made the change by @TAIPANBOX in #105
  • chore(deps): bump agent-stack-go to v1.3.0 by @TAIPANBOX in #106
  • test: name no add-on in the policy plane's tests and release notes by @TAIPANBOX in #107
  • fix(api): bind the asserted agent and the approver to the key, scope policies to their org by @TAIPANBOX in #108
  • release: wardryx 2.0.0 by @TAIPANBOX in #109

Full Changelog: v1.5.0...v2.0.0

v1.5.0

Choose a tag to compare

@github-actions github-actions released this 14 Oct 14:45
7a0f290

What's Changed

  • feat(policy): allow_models, a policy may name the models its agents may use by @TAIPANBOX in #102
  • docs(release): wardryx v1.5.0 release notes are final by @TAIPANBOX in #103

Full Changelog: v1.4.0...v1.5.0

v1.4.0

Choose a tag to compare

@github-actions github-actions released this 14 Oct 14:19
4b178eb

What's Changed

  • docs(readme): an approval token is a body field; the header is TokenFuse's by @TAIPANBOX in #95
  • docs: the diagrams and comments say what the code does about decisions and single-use by @TAIPANBOX in #96
  • fix(diagram): the policy pill has 9px or more to spare by @TAIPANBOX in #97
  • docs(readme): cacheable lists the chain rules, oversized arguments are refused by @TAIPANBOX in #98
  • docs: paraphrase the owner-attributed lines in the feature header and release.yml by @TAIPANBOX in #99
  • ci: bump the actions group with 3 updates by @dependabot[bot] in #90
  • fix(pdp): hold a tool call whose risk signal never arrived (when_absent) by @TAIPANBOX in #100
  • docs(release): wardryx v1.4.0 release notes are final by @TAIPANBOX in #101

Full Changelog: v1.3.2...v1.4.0

v1.3.2

Choose a tag to compare

@github-actions github-actions released this 13 Oct 12:36
b3c23f2

Wardryx 1.3.2

A policy write now refuses a field the policy does not declare. Before this
release, a PUT /v1/policies/{id} with a misspelled rule, for example
deny_tolls for deny_tool, was stored without that rule, answered 200, and the
call the rule was meant to deny was allowed. The write now answers 400 and stores
nothing.

Fixed

  • A PUT /v1/policies/{id} body with a key the policy does not declare is
    refused with a 400 (invariant 31).
    The 400 names the key and does not echo its
    value. Nothing is stored, and the live rules stay as they were. Before, the key
    was dropped and the write answered 200.
  • A policy read by GET can be written back unchanged. The two fields a GET
    returns beyond the policy itself, id and updated_at, are accepted on a PUT
    and ignored. The URL names the policy, and the server stamps the time of the
    write.
  • A key inside hold_if_signal that the rule does not have is refused on a PUT,
    as it already was in a policy file.

Upgrade

A client that sends a key the policy does not declare now gets a 400 on PUT.
Before, that key was dropped and the policy was stored without it, so such a
client has been writing policies that lack rules it meant to set. Check any
tooling that writes policies with PUT, and correct the spelling of the key or
remove it.

Rolling back to 1.3.1 brings back the silent drop for a misspelled key in a PUT
body.

The other JSON request bodies (decide, filter-tools, approval decide) are
unchanged, and they still ignore a key they do not declare.

Not proven

  • No live deployment's PUT traffic was read, so how many clients send an
    undeclared key is not known.
  • The key match is not case-sensitive. A PUT with DENY_TOOL is accepted and
    stored as deny_tool, so the rule is enforced. The decoder reads the key as the
    declared one, as the policy file loader does, and this release does not change
    that.
  • The other JSON request bodies still ignore an undeclared key. Refusing one there
    is a separate decision.
  • The Postgres integration suite was not run, since it needs a live database. The
    other gates were run locally, govulncheck and gosec included. No image was
    built.