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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

125 changes: 111 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ is `bug_url`, which computes a URL string locally and contacts nothing.
them.
- **`update_bug_fields` and `create_bug` custom fields are restricted** to
`cf_*` keys as described above.
- **HTTP is deny-by-default.** The http transport refuses to start without a
bearer token unless `--insecure-no-auth` says so explicitly; see
[Authentication and scopes](#authentication-and-scopes).

## Installation

Expand Down Expand Up @@ -224,32 +227,117 @@ no host.

### HTTP transport (default)

The server listens on `http://<host>:<port>/mcp`. Each client request carries
the Bugzilla API key in an HTTP header (default header name: `ApiKey`), so one
server can serve multiple users with their own keys:
The server listens on `http://<host>:<port>/mcp`.

#### Authentication and scopes

Unlike stdio — where the client already owns the process it launched — HTTP
serves anyone who can reach the port, and in [server-held key
mode](#server-held-key-mode-fleet-deployments) that means serving them with the
deployment's own Bugzilla credential. Authentication is therefore **mandatory
and deny-by-default**. Two tokens define two scopes:

| Variable | Scope | Tools |
| --- | --- | --- |
| `BUGWARDEN_HTTP_TOKEN` | write | every tool this deployment's policy serves |
| `BUGWARDEN_HTTP_READ_TOKEN` | read | the read tools only |

Either may be set alone. A caller presents its token as `Authorization: Bearer
<token>`, which is compared in constant time; anything else — no header, the
wrong token, another scheme, an unrouted path — gets the same empty `401` with
`WWW-Authenticate: Bearer`, so probing tells a stranger nothing.

A read-scope caller is offered only the read tools in `tools/list`, and calling
a write tool anyway is answered exactly as calling a tool that does not exist,
with no Bugzilla request made. The split is the same set `--read-only` removes
from the tool router, so the two cannot drift. Because the advertised tool set
depends on the credential, a client that caches `tools/list` across tokens will
show a stale list.

Tokens are never accepted as command-line flags — argv is world-readable via
`ps` — so they come from the environment only. bugwarden reads no `.env` file:
supply them through the container runtime, or through a systemd unit's
`EnvironmentFile=` written with a restrictive umask.

Mint one token and keep it: the clients have to present the same value, so a
token generated inline on the command line (`BUGWARDEN_HTTP_TOKEN=$(openssl
rand -hex 32) bugwarden …`) starts a server nobody can talk to.

```bash
umask 077
openssl rand -hex 32 > /etc/bugwarden/http-token
```

```bash
export BUGWARDEN_HTTP_TOKEN="$(cat /etc/bugwarden/http-token)"

bugwarden \
--bugzilla-server https://bugzilla.opensuse.org \
--policy /etc/bugwarden/policy.toml \
--host 127.0.0.1 --port 8000
```

MCP client configuration (exact format varies by client):
Under systemd, name the file in an `EnvironmentFile=` instead — one
`BUGWARDEN_HTTP_TOKEN=<value>` line, mode 0600 — so the value never reaches a
shell history or a `ps` listing.

The server refuses to start — before the port is bound, exit status `1` — when:

- the http transport is in use (it is the default) with no token and no
`--insecure-no-auth`;
- `--insecure-no-auth` is combined with either token;
- a token is shorter than 32 characters, or contains anything but printable
non-space ASCII;
- the read token equals the write token.

Tokens set while running over stdio are ignored, and so is `--insecure-no-auth`
— there is no network principal to authenticate there.

`--insecure-no-auth` serves the endpoint with no authentication at all: every
caller reaching the port gets the full write scope. It is a command-line flag
with no environment variable, so no ambient value can turn authentication off,
and the server says so loudly at startup.

> **The transport is plaintext HTTP.** A bearer token sent over it is readable
> by anything on the path, so do not expose the port beyond a trusted network
> without terminating TLS in front of it (reverse proxy, service mesh, or an
> SSH tunnel).
>
> **A static bearer token is not MCP's OAuth 2.1 authorization flow.** A client
> that only implements the spec's `401` → resource-metadata → OAuth dance will
> not authenticate; use one that lets you set a header. And the token
> authenticates a *deployment*, not a caller: audit records carry no
> `principal`, which stays [issue #32](https://github.com/plusky/bugwarden/issues/32)'s
> job.

#### Bugzilla credentials

Each client request carries the Bugzilla API key in an HTTP header (default
header name: `ApiKey`), so one server can serve multiple users with their own
keys. This is independent of the bearer gate above: bearer authenticates the
caller to bugwarden, the API key authenticates bugwarden's request to Bugzilla.

MCP client configuration (exact format varies by client) — both headers are
required over http, one for each hop:

```json
{
"mcpServers": {
"bugzilla": {
"url": "http://127.0.0.1:8000/mcp",
"headers": {
"Authorization": "Bearer YOUR_BUGWARDEN_HTTP_TOKEN",
"ApiKey": "YOUR_BUGZILLA_API_KEY"
}
}
}
}
```

In [server-held key mode](#server-held-key-mode-fleet-deployments) drop the
`ApiKey` line: the server supplies the Bugzilla key, so `Authorization` is the
only credential a client holds — which is the point of that mode.

The header name is configurable with `--api-key-header`. For Bugzilla
instances that reject the `api_key` query parameter and require
`Authorization: Bearer` (e.g. Red Hat Bugzilla), add `--use-auth-header` —
Expand All @@ -270,6 +358,8 @@ provisioned as a container secret or a systemd credential
`--api-key-file ${CREDENTIALS_DIRECTORY}/bugzilla-key`):

```bash
export BUGWARDEN_HTTP_TOKEN="$(cat /etc/bugwarden/http-token)"

bugwarden \
--bugzilla-server https://bugzilla.opensuse.org \
--policy /etc/bugwarden/policy.toml \
Expand Down Expand Up @@ -358,21 +448,28 @@ Command-line arguments take precedence over environment variables.
| `--api-key <KEY>` | `BUGZILLA_API_KEY` | — | Bugzilla API key. **Required** for `--transport stdio` unless `--api-key-file` provides it; with `http` it is ignored with a warning (clients send the key per request — use `--api-key-file` for a server-held key) |
| `--api-key-file <PATH>` | `BUGZILLA_API_KEY_FILE` | — | Path to a file holding the Bugzilla API key (container secret, systemd `LoadCredential` path). Mutually exclusive with `--api-key`; an empty value counts as unset. Over `http` this selects server-held key mode: every request is served with this key and the per-request header is not consulted |
| `--use-auth-header` | `BUGZILLA_USE_AUTH_HEADER` | `false` | Authenticate to Bugzilla with `Authorization: Bearer <key>` instead of the `api_key` query parameter. As an environment variable it takes the literal `true` or `false`, exactly like `MCP_READ_ONLY` |
| `--insecure-no-auth` | — | `false` | Serve the http endpoint with no bearer authentication: every caller reaching the port gets the full write scope. Command line only — no environment variable can turn authentication off. Refuses to start if a token is also set |
| — | `BUGWARDEN_HTTP_TOKEN` | — | Bearer token granting the **write** scope over http: every tool the policy serves. Environment only (argv is world-readable); at least 32 printable non-space ASCII characters |
| — | `BUGWARDEN_HTTP_READ_TOKEN` | — | Bearer token granting the **read** scope over http: the read tools only. Same rules, and it must differ from the write token. Either token may be set alone |
| `--read-only` | `MCP_READ_ONLY` | `false` | Disable all write tools. Tighten-only: ORed with the policy's `global.read_only`; cannot re-enable writes a policy forbids. As an environment variable it takes the literal `true` or `false` — `1`, `yes` and an empty value are a usage error, not a synonym |
| `--policy <PATH>` | `BUGWARDEN_POLICY` | — | Path to the guard policy TOML. Without it, an allow-all policy applies (with private comments off and the 2 MiB attachment cap still in force) |
| `--audit-config <PATH>` | `BUGWARDEN_AUDIT_CONFIG` | — | Path to the audit stream configuration TOML (worked example in [`examples/audit.toml`](examples/audit.toml)). Without it, no audit stream is written. Records carry W3C trace ids when the client sends a `traceparent` in the request's `_meta`, enabling correlation with client-side traces |
| — | `RUST_LOG` | `info` | Tracing filter for the diagnostic log, which always goes to **stderr** — stdout belongs to the stdio transport. An unparsable value falls back to `info` |

An empty value counts as unset for `--api-key`, `--api-key-file` and
`--allowed-hosts`, so `BUGZILLA_API_KEY_FILE=` in a unit file leaves the two
key modes unaffected rather than erroring (under stdio it then leaves no key
source at all, which is a startup error of its own), and `MCP_ALLOWED_HOSTS=`
names no host, leaving `Host` validation off as if it were never set. An empty
`BUGWARDEN_POLICY` or `BUGWARDEN_AUDIT_CONFIG` is a usage error.

Exit status: `0` on clean shutdown, `1` on a startup or runtime failure (an
unreadable policy or audit configuration, a key misconfiguration, a Bugzilla
client or transport error), `2` on a command-line usage error.
An empty value counts as unset for `--api-key`, `--api-key-file`,
`--allowed-hosts`, `BUGWARDEN_HTTP_TOKEN` and `BUGWARDEN_HTTP_READ_TOKEN`, so
`BUGZILLA_API_KEY_FILE=` in a unit file leaves the two key modes unaffected
rather than erroring (under stdio it then leaves no key source at all, which
is a startup error of its own), `MCP_ALLOWED_HOSTS=` names no host, leaving
`Host` validation off as if it were never set, and an emptied token variable
is an unset one — which over http means the deny-by-default refusal, not an
open port. An empty `BUGWARDEN_POLICY` or `BUGWARDEN_AUDIT_CONFIG` is a usage
error.

Exit status: `0` on clean shutdown, `1` on a startup or runtime failure (a
missing or malformed http bearer token, an unreadable policy or audit
configuration, a key misconfiguration, a Bugzilla client or transport error),
`2` on a command-line usage error.

## Policy file reference

Expand Down
3 changes: 3 additions & 0 deletions crates/bugwarden/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ serde_json = "1"
schemars = { version = "1", features = ["chrono04"] }
chrono = { version = "0.4", features = ["serde"] }
sha2 = "0.11"
# Constant-time byte comparison for the HTTP bearer tokens. Already in the
# lock file through rustls; this makes the dependency the one it is used as.
subtle = "2.6"
thiserror = "2"
toml = "1.1"

Expand Down
1 change: 1 addition & 0 deletions crates/bugwarden/completions/_bugwarden
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ stdio\:"Stdio transport. The API key comes from \`--api-key\` / \`BUGZILLA_API_K
'--audit-config=[Path to the audit configuration TOML file (see examples/audit.toml). Environment variable BUGWARDEN_AUDIT_CONFIG can also be used. Without it no audit stream is written]:AUDIT_CONFIG:_files' \
'--use-auth-header[Use '\''Authorization\: Bearer'\'' header instead of the api_key query parameter (required for some Bugzilla instances). Environment variable BUGZILLA_USE_AUTH_HEADER=true can also be used]' \
'--read-only[Disables all tools which modify the state of a bug. Environment variable MCP_READ_ONLY=true can also be used. Can only tighten the guard policy, never loosen it]' \
'--insecure-no-auth[Serve the http transport without bearer authentication. Only for a trusted, isolated network\: every caller that reaches the port gets the full write scope. Command line only, with no environment variable, so no ambient value can turn authentication off. Tokens are never taken from the command line either (argv is world-readable)\: set BUGWARDEN_HTTP_TOKEN / BUGWARDEN_HTTP_READ_TOKEN in the environment]' \
'-h[Print help (see more with '\''--help'\'')]' \
'--help[Print help (see more with '\''--help'\'')]' \
'-V[Print version]' \
Expand Down
2 changes: 1 addition & 1 deletion crates/bugwarden/completions/bugwarden.bash
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ _bugwarden() {

case "${cmd}" in
bugwarden)
opts="-h -V --bugzilla-server --transport --host --port --allowed-hosts --api-key-header --api-key --api-key-file --use-auth-header --read-only --policy --audit-config --help --version"
opts="-h -V --bugzilla-server --transport --host --port --allowed-hosts --api-key-header --api-key --api-key-file --use-auth-header --read-only --policy --audit-config --insecure-no-auth --help --version"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
Expand Down
1 change: 1 addition & 0 deletions crates/bugwarden/completions/bugwarden.fish
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ complete -c bugwarden -l policy -d 'Path to the guard policy TOML file. Environm
complete -c bugwarden -l audit-config -d 'Path to the audit configuration TOML file (see examples/audit.toml). Environment variable BUGWARDEN_AUDIT_CONFIG can also be used. Without it no audit stream is written' -r -F
complete -c bugwarden -l use-auth-header -d 'Use \'Authorization: Bearer\' header instead of the api_key query parameter (required for some Bugzilla instances). Environment variable BUGZILLA_USE_AUTH_HEADER=true can also be used'
complete -c bugwarden -l read-only -d 'Disables all tools which modify the state of a bug. Environment variable MCP_READ_ONLY=true can also be used. Can only tighten the guard policy, never loosen it'
complete -c bugwarden -l insecure-no-auth -d 'Serve the http transport without bearer authentication. Only for a trusted, isolated network: every caller that reaches the port gets the full write scope. Command line only, with no environment variable, so no ambient value can turn authentication off. Tokens are never taken from the command line either (argv is world-readable): set BUGWARDEN_HTTP_TOKEN / BUGWARDEN_HTTP_READ_TOKEN in the environment'
complete -c bugwarden -s h -l help -d 'Print help (see more with \'--help\')'
complete -c bugwarden -s V -l version -d 'Print version'
39 changes: 33 additions & 6 deletions crates/bugwarden/man/bugwarden.1
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ bugwarden \- MCP server for Bugzilla with operator\-controlled security guards
[\fB\-\-read\-only\fR]
[\fB\-\-policy\fR <POLICY>]
[\fB\-\-audit\-config\fR <AUDIT_CONFIG>]
[\fB\-\-insecure\-no\-auth\fR]
[\fB\-h\fR|\fB\-\-help\fR]
[\fB\-V\fR|\fB\-\-version\fR]
.ie \n(.g .ds Aq \(aq
Expand Down Expand Up @@ -73,6 +74,9 @@ Path to the guard policy TOML file. Environment variable BUGWARDEN_POLICY can al
\fB\-\-audit\-config\fR \fI<AUDIT_CONFIG>\fR
Path to the audit configuration TOML file (see examples/audit.toml). Environment variable BUGWARDEN_AUDIT_CONFIG can also be used. Without it no audit stream is written
.TP
\fB\-\-insecure\-no\-auth\fR
Serve the http transport without bearer authentication. Only for a trusted, isolated network: every caller that reaches the port gets the full write scope. Command line only, with no environment variable, so no ambient value can turn authentication off. Tokens are never taken from the command line either (argv is world\-readable): set BUGWARDEN_HTTP_TOKEN / BUGWARDEN_HTTP_READ_TOKEN in the environment
.TP
\fB\-h\fR, \fB\-\-help\fR
Print help (see a summary with \*(Aq\-h\*(Aq)
.TP
Expand All @@ -84,15 +88,18 @@ Print version
Clean shutdown.
.TP
.B 1
Startup or runtime failure: an unreadable policy or audit configuration, a
key misconfiguration, a Bugzilla client or transport error.
Startup or runtime failure: a missing or malformed http bearer token, an
unreadable policy or audit configuration, a key misconfiguration, a Bugzilla
client or transport error.
.TP
.B 2
Command\-line usage error.
.SH ENVIRONMENT
Each variable is the fallback for one option; a command\-line argument
Most variables are the fallback for one option; a command\-line argument
always wins over the environment, and the built\-in default applies when
neither is given.
neither is given. The two bearer tokens are the exception: they have no
option at all and are read from the environment only, because argv is
world\-readable.
.TP
.B BUGZILLA_SERVER
Fallback for \fB\-\-bugzilla\-server\fR.
Expand Down Expand Up @@ -129,6 +136,21 @@ Fallback for \fB\-\-policy\fR.
.TP
.B BUGWARDEN_AUDIT_CONFIG
Fallback for \fB\-\-audit\-config\fR.
.TP
.B BUGWARDEN_HTTP_TOKEN
Bearer token granting the write scope over the http transport: every tool the
guard policy serves. Environment only \- there is no command\-line flag,
because argv is world\-readable. At least 32 printable non\-space ASCII
characters.
.TP
.B BUGWARDEN_HTTP_READ_TOKEN
Bearer token granting the read scope over the http transport: the read tools
only. Same rules, and it must differ from
.BR BUGWARDEN_HTTP_TOKEN .
Either token may be set alone; over http, setting neither is a startup error
unless
.B \-\-insecure\-no\-auth
is given.
.SH FILES
.TP
.I /etc/bugwarden/policy.toml
Expand Down Expand Up @@ -163,21 +185,26 @@ bugwarden \-\-transport stdio \e
.RE
.PP
Listen on HTTP (the default transport, 127.0.0.1:8000), each client
presenting its own key in the API key header:
presenting a bearer token to this server and its own key in the API key
header. The token is minted once and kept \- a value generated inline would
start a server no client could present a credential to:
.PP
.RS 4
.nf
export BUGWARDEN_HTTP_TOKEN="$(cat /etc/bugwarden/http\-token)"
bugwarden \-\-bugzilla\-server https://bugzilla.example.com \e
\-\-policy /etc/bugwarden/policy.toml
.fi
.RE
.PP
Listen on HTTP with a server\-held key (container secret, systemd
LoadCredential): every request is served with this key and the per\-request
key header is not consulted:
key header is not consulted, so the bearer token is the only thing a client
presents:
.PP
.RS 4
.nf
export BUGWARDEN_HTTP_TOKEN="$(cat /etc/bugwarden/http\-token)"
bugwarden \-\-bugzilla\-server https://bugzilla.example.com \e
\-\-api\-key\-file /run/secrets/bugzilla\-api\-key \e
\-\-policy /etc/bugwarden/policy.toml
Expand Down
Loading