You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Depends on #27. Direction settled 2026-08-04: bearer tokens, minted per
(caller, RRID) pair.
Today an HTTP client authenticates to Bugzilla, not to bugwarden: it presents a
Bugzilla API key per request and the server has no notion of caller identity of
its own. Once the server holds the key instead (#27), that incidental identity
disappears — clients present nothing, and any client that can reach the port is
served. The network boundary becomes the only access control, and audit records
can attribute a call to a session and a source address but not to a caller.
The audit schema reserves the slot for the answer: ClientInfo.principal is
defined, always None today, and must never be filled from anything the client
declares about itself.
Which bearer scheme, and why not the other two
A static shared secret was declined here as the end state, because it
authenticates a deployment rather than a caller, so principal stays empty
and the acceptance criteria below stay unmet. Superseded in part
2026-08-18: a ruoqa-mcp-compatible scoped bearer pair
(BUGWARDEN_HTTP_TOKEN write / BUGWARDEN_HTTP_READ_TOKEN read, env-only,
deny-by-default at startup) lands NOW as the transport-level gate, for fleet
alignment with ruoqa-mcp. It is the degenerate case this issue already
anticipated: same Authorization: Bearer wire shape, and only the validator
changes when (caller, RRID) tokens arrive. The declined part stands where it
matters — a deployment credential must never fill principal, and this issue
stays open until a verified caller identity does.
A token bound to an RRID alone — the identifier of one maintenance update —
authenticates a work context, not a caller: every agent container working that
update presents the same secret. It is worth having (verified, revocable when the
update closes, and the join key with mtui's per-RRID state and the testreport),
but there is still no caller to name. The failure to avoid is filling principal
with the RRID anyway: that conflates "who called" with "what work this was for",
and the audit schema would freeze the conflation.
A token bound to (caller, RRID) carries both facts, separately and verified: principal is the agent identity, and the RRID rides alongside as the work
context. That is the decision.
Why the finer grain, given that today the Bug Reviewer is the only agent that
talks to bugwarden and the caller half is nearly constant:
It survives the move to OAuth. In OAuth 2.1 terms the caller is the subject
and the resource is a scope/audience claim; the per-RRID-only form has no
subject and would have to be re-modelled. All three schemes are Authorization: Bearer <token> on the wire, so what changes later is the
validator — not the request shape, the refusal path, the audit fields, or the
rule that principal is only ever server-established. That rule is the actual
security surface, and this is how it gets built and tested.
It is the hook per-caller policy needs. Varying the guard's capability set
by caller is impossible today (one operator policy per process, I1) and stays
unreachable forever without a verified caller identity to attach a scope to.
The plumbing exists. The fleet Dispatcher already hands each agent
container its own role credentials; minting per container instead of per update
is the same mechanism one loop deeper.
Blast radius. A leaked per-update token exposes that update's whole scope to
whoever holds it; a per-caller one is confined to a role.
The choice is recoverable in the cheap direction only: a per-RRID token is the
degenerate case where each update has one caller, so starting fine and collapsing
later is configuration, while starting coarse and splitting later means
re-issuing credentials and touching the schema twice.
Binding constraints
The token is not the RRID, nor derived from it. RRIDs are enumerable and
effectively public; a guessable bearer token is not authentication. The
credential is an unguessable secret bound server-side to the pair, or a signed
token carrying both as claims.
Scope is tightening-only. A token must never widen what the server-wide
guard policy allows (I1, I9). An RRID scope may narrow what a caller sees, and
fails closed. Note bugwarden has no RRID→bug mapping and no route to
SMELT/IBS: a design needing one is a much larger change than one where the
issuer asserts the scope.
An agent bound to no RRID must still authenticate. Fleet-level agents do
not belong to an update, so the work context is optional and independent of the
principal: absent means "not scoped to an update", which must be
distinguishable from "scoped, but the scope is unknown" — the latter must be
impossible. An unscoped token gets exactly the server-wide policy, which is
today's behaviour and widens nothing.
Token material is secret material under I12 — never in logs, errors, audit
records or tool results, with something structural enforcing it rather than
reviewers remembering.
Refusals must be recorded, and must not become an oracle (I2). Verification
at the transport boundary means an unauthenticated request never reaches call_tool, where records are written — so recording it is a mechanism to
design, not a property that comes for free.
Delivery in containers is by environment. Settled 2026-08-18: container
deployments configure bugwarden entirely through environment variables, e.g.
Every existing flag already has a clap env binding, so this adds one
requirement here: the verifier's secret must be readable from a BUGWARDEN_-prefixed env var with hide_env_values, like BUGZILLA_API_KEY. Custody is then the runtime's — anyone who can docker inspect the container or read /proc/<pid>/environ holds the
token — which is the same exposure already accepted for the server-held
Bugzilla key delivered the same way.
Delivered 2026-08-18 (PR #105) — and what it does not close
The transport gate landed: BUGWARDEN_HTTP_TOKEN (write) / BUGWARDEN_HTTP_READ_TOKEN (read), env-only, deny-by-default before the
port binds, constant-time both-token compare, one byte-identical empty 401 for every refusal cause (I2), read scope derived from I13's WRITE_TOOLS, --insecure-no-auth as the CLI-only escape hatch. Three
things that were open above are now settled by it:
server/discover does not stay open once auth exists — the layer
wraps the whole router. Recorded in DESIGN.md: the revision is unserved
today, and a pre-request identity surface is not worth opening ahead of
it.
Constraint 6's hide_env_values is moot, in the stronger direction:
the tokens never enter Cli at all (env-only, no flag — argv is
world-readable), so there is nothing for a Debug to redact.
Constraint 5 is partially met: transport-layer refusals are counted
and logged at powers of two (no token/path/peer material); a full audit
record needs a new event kind, which belongs to Adopt the 2026-07-28 MCP revision — audit schema v1 assumes three things it removes #34's v2 schema freeze.
A read-scope caller's refused write call IS fully audited.
CLOSED 2026-08-18 by decision: the deployment-level bearer gate is
accepted as bugwarden's end state for HTTP authentication. Per-caller
identity and the RRID mapping will be solved outside bugwarden; if that
ever changes, the wire shape shipped here is the one a finer-grained
validator would reuse, and this issue is the design record to reopen. ClientInfo.principal stays None — still never filled from anything
self-declared — and the acceptance criteria below are deliberately left
unmet rather than reworded.
Open, and not blocking anything
Bound-secret vs signed-claims; who issues and holds the signing key; storage and
constant-time comparison; whether verification sits in the axum layer or the
handler; whether server/discover — a mandatory unauthenticated pre-request
surface under the 2026-07-28 revision (#34 §3) — stays open once auth exists; and
whether the openSUSE package defaults to auth on.
What this unblocks
Filling principal with a verified identity, so the log answers "which caller"
and not only "which session".
Per-caller policy, if the guard's capability set should ever vary by caller.
Acceptance criteria
Identity is verified by the server; nothing self-declared is ever promoted
into principal.
An unauthenticated request is refused uniformly, with no signal about
whether a bug, tool, or policy exists (I2).
Refusals are recorded.
An agent with no RRID authenticates and is recorded with a principal and no
work context.
See also #34 — the audit schema must reserve room for both facts before it
is frozen at v2, or landing this needs a v3.
Depends on #27. Direction settled 2026-08-04: bearer tokens, minted per
(caller, RRID) pair.
Today an HTTP client authenticates to Bugzilla, not to bugwarden: it presents a
Bugzilla API key per request and the server has no notion of caller identity of
its own. Once the server holds the key instead (#27), that incidental identity
disappears — clients present nothing, and any client that can reach the port is
served. The network boundary becomes the only access control, and audit records
can attribute a call to a session and a source address but not to a caller.
The audit schema reserves the slot for the answer:
ClientInfo.principalisdefined, always
Nonetoday, and must never be filled from anything the clientdeclares about itself.
Which bearer scheme, and why not the other two
A static shared secret was declined here as the end state, because it
authenticates a deployment rather than a caller, so
principalstays emptyand the acceptance criteria below stay unmet. Superseded in part
2026-08-18: a ruoqa-mcp-compatible scoped bearer pair
(
BUGWARDEN_HTTP_TOKENwrite /BUGWARDEN_HTTP_READ_TOKENread, env-only,deny-by-default at startup) lands NOW as the transport-level gate, for fleet
alignment with ruoqa-mcp. It is the degenerate case this issue already
anticipated: same
Authorization: Bearerwire shape, and only the validatorchanges when (caller, RRID) tokens arrive. The declined part stands where it
matters — a deployment credential must never fill
principal, and this issuestays open until a verified caller identity does.
A token bound to an RRID alone — the identifier of one maintenance update —
authenticates a work context, not a caller: every agent container working that
update presents the same secret. It is worth having (verified, revocable when the
update closes, and the join key with mtui's per-RRID state and the testreport),
but there is still no caller to name. The failure to avoid is filling
principalwith the RRID anyway: that conflates "who called" with "what work this was for",
and the audit schema would freeze the conflation.
A token bound to (caller, RRID) carries both facts, separately and verified:
principalis the agent identity, and the RRID rides alongside as the workcontext. That is the decision.
Why the finer grain, given that today the Bug Reviewer is the only agent that
talks to bugwarden and the caller half is nearly constant:
and the resource is a scope/audience claim; the per-RRID-only form has no
subject and would have to be re-modelled. All three schemes are
Authorization: Bearer <token>on the wire, so what changes later is thevalidator — not the request shape, the refusal path, the audit fields, or the
rule that
principalis only ever server-established. That rule is the actualsecurity surface, and this is how it gets built and tested.
by caller is impossible today (one operator policy per process, I1) and stays
unreachable forever without a verified caller identity to attach a scope to.
container its own role credentials; minting per container instead of per update
is the same mechanism one loop deeper.
whoever holds it; a per-caller one is confined to a role.
The choice is recoverable in the cheap direction only: a per-RRID token is the
degenerate case where each update has one caller, so starting fine and collapsing
later is configuration, while starting coarse and splitting later means
re-issuing credentials and touching the schema twice.
Binding constraints
The token is not the RRID, nor derived from it. RRIDs are enumerable and
effectively public; a guessable bearer token is not authentication. The
credential is an unguessable secret bound server-side to the pair, or a signed
token carrying both as claims.
Scope is tightening-only. A token must never widen what the server-wide
guard policy allows (I1, I9). An RRID scope may narrow what a caller sees, and
fails closed. Note bugwarden has no RRID→bug mapping and no route to
SMELT/IBS: a design needing one is a much larger change than one where the
issuer asserts the scope.
An agent bound to no RRID must still authenticate. Fleet-level agents do
not belong to an update, so the work context is optional and independent of the
principal: absent means "not scoped to an update", which must be
distinguishable from "scoped, but the scope is unknown" — the latter must be
impossible. An unscoped token gets exactly the server-wide policy, which is
today's behaviour and widens nothing.
Token material is secret material under I12 — never in logs, errors, audit
records or tool results, with something structural enforcing it rather than
reviewers remembering.
Refusals must be recorded, and must not become an oracle (I2). Verification
at the transport boundary means an unauthenticated request never reaches
call_tool, where records are written — so recording it is a mechanism todesign, not a property that comes for free.
Delivery in containers is by environment. Settled 2026-08-18: container
deployments configure bugwarden entirely through environment variables, e.g.
docker run -e BUGZILLA_SERVER=https://bugzilla.suse.com \ -e BUGZILLA_API_KEY_FILE=/run/secrets/bz-key \ -e BUGWARDEN_HTTP_TOKEN=$(openssl rand -hex 32) \ -p 8000:8000 <image>Every existing flag already has a clap
envbinding, so this adds onerequirement here: the verifier's secret must be readable from a
BUGWARDEN_-prefixed env var withhide_env_values, likeBUGZILLA_API_KEY. Custody is then the runtime's — anyone who candocker inspectthe container or read/proc/<pid>/environholds thetoken — which is the same exposure already accepted for the server-held
Bugzilla key delivered the same way.
Delivered 2026-08-18 (PR #105) — and what it does not close
The transport gate landed:
BUGWARDEN_HTTP_TOKEN(write) /BUGWARDEN_HTTP_READ_TOKEN(read), env-only, deny-by-default before theport binds, constant-time both-token compare, one byte-identical empty
401for every refusal cause (I2), read scope derived from I13'sWRITE_TOOLS,--insecure-no-authas the CLI-only escape hatch. Threethings that were open above are now settled by it:
server/discoverdoes not stay open once auth exists — the layerwraps the whole router. Recorded in DESIGN.md: the revision is unserved
today, and a pre-request identity surface is not worth opening ahead of
it.
hide_env_valuesis moot, in the stronger direction:the tokens never enter
Cliat all (env-only, no flag — argv isworld-readable), so there is nothing for a
Debugto redact.and logged at powers of two (no token/path/peer material); a full audit
record needs a new event kind, which belongs to Adopt the 2026-07-28 MCP revision — audit schema v1 assumes three things it removes #34's v2 schema freeze.
A read-scope caller's refused write call IS fully audited.
CLOSED 2026-08-18 by decision: the deployment-level bearer gate is
accepted as bugwarden's end state for HTTP authentication. Per-caller
identity and the RRID mapping will be solved outside bugwarden; if that
ever changes, the wire shape shipped here is the one a finer-grained
validator would reuse, and this issue is the design record to reopen.
ClientInfo.principalstaysNone— still never filled from anythingself-declared — and the acceptance criteria below are deliberately left
unmet rather than reworded.
Open, and not blocking anything
Bound-secret vs signed-claims; who issues and holds the signing key; storage and
constant-time comparison; whether verification sits in the axum layer or the
handler; whether
server/discover— a mandatory unauthenticated pre-requestsurface under the 2026-07-28 revision (#34 §3) — stays open once auth exists; and
whether the openSUSE package defaults to auth on.
What this unblocks
principalwith a verified identity, so the log answers "which caller"and not only "which session".
Acceptance criteria
into
principal.whether a bug, tool, or policy exists (I2).
work context.
See also #34 — the audit schema must reserve room for both facts before it
is frozen at v2, or landing this needs a v3.