bugwarden

mcp
Guvenlik Denetimi
Basarisiz
Health Uyari
  • License — License: Apache-2.0
  • Description — Repository has a description
  • Active repo — Last push 0 days ago
  • Low visibility — Only 5 GitHub stars
Code Basarisiz
  • rm -rf — Recursive force deletion command in .github/workflows/ci.yml
Permissions Gecti
  • Permissions — No dangerous permissions requested

Bu listing icin henuz AI raporu yok.

SUMMARY

Security-guarded MCP server for Bugzilla, written in Rust

README.md

bugwarden logo

bugwarden

bugwarden is a Model Context Protocol (MCP) server, written in Rust, with
operator-controlled security guards. It exposes a Bugzilla instance to LLM
clients — querying bugs,
searching, reading comments and history, and (where permitted) updating bugs —
while a policy file that the model can neither see nor change decides, per bug,
what the model is allowed to do.

The Bugzilla REST API already enforces user permissions via the API key. What
it cannot do is enforce a narrower set of permissions for an AI agent acting
on that user's behalf. bugwarden sits in between: the operator writes a small
TOML policy ("embargoed security bugs are invisible", "on the Security product
the agent may only read summaries and leave comments", "nothing younger than a
week exists"), and every tool call is checked against it before Bugzilla is
touched or data is returned.

Features

  • Complete Bugzilla tool surface: bug details, history, comments,
    attachment metadata and content, quicksearch, comment/status/field/
    assignee/CC/dependency updates, duplicate marking, bug filing, attachment
    upload, server info, quicksearch syntax docs, and a bug-summarization
    prompt tool.
  • Guard policy engine: per-bug allow / deny / restrict decisions
    matched on product, component, group, keyword, status, severity, priority,
    whiteboard, summary, group-restrictedness, bug age, and authorship (whether
    the requesting account filed the bug) — with a fine-grained 13-capability
    vocabulary for restrict.
  • No existence oracle: a policy-denied bug is indistinguishable from a
    nonexistent one.
  • Silent search filtering: denied bugs simply never appear in search
    results; summary-only bugs appear redacted.
  • Minimum-age quarantine: min_bug_age_days makes recently filed bugs
    (the ones most likely to contain not-yet-triaged sensitive data) invisible.
  • Read-only mode and tool disabling remove write tools from the MCP tool
    listing entirely — clients never see them, rather than seeing them error.
  • Two transports: streamable HTTP (per-request API key header, or a
    server-held key via --api-key-file for fleet deployments) and stdio
    (subprocess launch by a desktop MCP client).
  • Single static binary, async throughout (tokio + rmcp).

Security model

The guard concept

The guard policy is loaded once, at startup, from a TOML file passed via
--policy (or BUGWARDEN_POLICY). It lives on the operator's filesystem. The
MCP client — i.e. the model — has no tool to read it, list its rules, or
modify it. mcp_server_info intentionally exposes only coarse facts: the rule
count, the default action, min_bug_age_days, whether the server is
read-only, and which tool names are disabled. Rule names and match criteria
are never revealed.

Every tool that takes a bug id first fetches the bug's classification metadata
(product, groups, keywords, creation time, …) and evaluates the policy
before any side effect happens or any data is returned. The only exception
is bug_url, which computes a URL string locally and contacts nothing.

Invariants

  • Uniform denial. A denied bug and a nonexistent bug produce the exact
    same response: Bug {id} is not accessible through this server. No wording
    or detail difference can be used as an existence oracle for embargoed bugs.
  • Silent search filtering. Search results are post-filtered through the
    policy; the client is never told how many results were dropped or that
    filtering happened at all. (Server-side debug logs do record it for the
    operator.)
  • Fail closed. If the classification fetch fails, if a bug is absent from
    the response, or if a rule consulted for the operation being decided cannot
    be decided because the bug object did not carry a field that rule asks
    about — or, for the identity criterion created_by_me, because the
    bug–caller relationship could not be established — the bug is treated as
    denied — never as allowed. (A rule scoped away from the operation via
    operations is not consulted at all — scoping changes which rules run,
    never how a consulted rule resolves.)
  • Private-comment gate. Private comments are returned only when the policy
    sets allow_private_comments = true and the individual call opts in
    with include_private = true. Either alone is not enough.
  • Custom fields cannot smuggle writes. update_bug_fields.custom_fields
    accepts only keys starting with cf_; anything else (e.g. groups, cc,
    assigned_to) is rejected before Bugzilla is contacted.
  • The API key never leaks. The Bugzilla API key is never written to logs,
    error messages, or tool results; HTTP errors are sanitized so that a key
    passed as a URL query parameter cannot appear in error text.
  • CLI can only tighten. --read-only ORs into the policy's read-only
    flag; there is no CLI switch that loosens the policy.

Deliberate omissions and strict defaults

  • No header-echo tool. Incoming request headers — including the API-key
    header — are never exposed to the model.
  • Private comments default to off. The default policy has
    allow_private_comments = false, so a policy file is required to enable
    them.
  • update_bug_fields custom fields are restricted to cf_* keys as
    described above.

Installation

openSUSE (zypper)

bugwarden is packaged in openSUSE Tumbleweed:

sudo zypper install bugwarden

For other openSUSE distributions (Leap 16.x, Slowroll), packages are built in
the devel:tools
project on the openSUSE Build Service.

The package installs worked-example configuration files —
/etc/bugwarden/policy.toml (guard policy) and /etc/bugwarden/audit.toml
(audit stream) — marked %config(noreplace), so local edits survive package
upgrades. Neither is loaded implicitly: the server reads a policy only when
one is named via --policy / BUGWARDEN_POLICY, and an audit configuration
only via --audit-config / BUGWARDEN_AUDIT_CONFIG, so installing the
package does not by itself activate anything.

crates.io (cargo)

cargo install bugwarden

This installs the bugwarden binary into ~/.cargo/bin. Unlike the openSUSE
package it ships no configuration files — copy
examples/policy.toml somewhere and name it via
--policy.

From source

git clone https://github.com/plusky/bugwarden
cd bugwarden
cargo build --release
# binary at target/release/bugwarden

The repository pins its Rust toolchain via rust-toolchain.toml; cargo
picks it up automatically (rustup-managed installs). Any recent stable Rust
works if you build without the pin.

Usage

Note: some Bugzilla deployments protect their interactive host with an
anti-bot challenge that rejects API clients regardless of credentials. If
tools fail with "response body is not valid JSON", check whether the
instance offers a dedicated API host (for example apibugzilla.suse.com
instead of bugzilla.suse.com) and point --bugzilla-server at that.

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:

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):

{
  "mcpServers": {
    "bugzilla": {
      "url": "http://127.0.0.1:8000/mcp",
      "headers": {
        "ApiKey": "YOUR_BUGZILLA_API_KEY"
      }
    }
  }
}

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
this affects only server-to-Bugzilla authentication, not the client-facing
header.

Server-held key mode (fleet deployments)

With --api-key-file the Bugzilla API key belongs to the server: every
request is served with the key read from that file, clients present no
credential at all, and the per-request header is not consulted — a request
that does carry one is served with the server's key, and the header value is
never read. There is no fallback between the two modes in either
direction
(handing clients the real key would let them bypass the guard by
talking to Bugzilla directly). This fits deployments where the key is
provisioned as a container secret or a systemd credential
(LoadCredential=bugzilla-key:/etc/bugwarden/bugzilla-key plus
--api-key-file ${CREDENTIALS_DIRECTORY}/bugzilla-key):

bugwarden \
  --bugzilla-server https://bugzilla.opensuse.org \
  --policy /etc/bugwarden/policy.toml \
  --api-key-file /run/secrets/bugzilla-key \
  --host 127.0.0.1 --port 8000

The file's content is trimmed, so a trailing newline is fine; an empty or
unreadable file is a startup error naming the path (never its contents). The
file is read exactly once, at startup — rotating the key requires a restart.
Keep it mode 0600: bugwarden warns when group or others can access it.

One policy consequence to know: in this mode every client authenticates to
Bugzilla — and resolves identity — as the service account that owns the key,
so a policy rule matching on created_by_me describes that one account's
bug reports for all clients, not each caller's own. bugwarden warns at
startup when server-held mode meets such a policy.

stdio transport

For MCP clients that launch the server as a subprocess and speak over
stdin/stdout. There are no per-request HTTP headers here, so the API key must
be provided up front via --api-key / BUGZILLA_API_KEY or --api-key-file
(starting without one is an error):

BUGZILLA_API_KEY=your_api_key \
  bugwarden \
  --bugzilla-server https://bugzilla.opensuse.org \
  --transport stdio \
  --policy /etc/bugwarden/policy.toml

MCP client configuration:

{
  "mcpServers": {
    "bugzilla": {
      "command": "/usr/local/bin/bugwarden",
      "args": [
        "--bugzilla-server", "https://bugzilla.opensuse.org",
        "--transport", "stdio",
        "--policy", "/etc/bugwarden/policy.toml"
      ],
      "env": {
        "BUGZILLA_API_KEY": "YOUR_BUGZILLA_API_KEY"
      }
    }
  }
}

CLI reference

Command-line arguments take precedence over environment variables.

Flag Environment variable Default Description
--bugzilla-server <URL> BUGZILLA_SERVER required Base URL of the Bugzilla server (e.g. https://bugzilla.opensuse.org)
--transport <http|stdio> MCP_TRANSPORT http MCP transport. stdio is for subprocess launches by an MCP client; http exposes a network endpoint at /mcp
--host <ADDRESS> MCP_HOST 127.0.0.1 Listen address (http transport only)
--port <PORT> MCP_PORT 8000 Listen port (http transport only)
--api-key-header <NAME> MCP_API_KEY_HEADER ApiKey HTTP header name in which clients send the Bugzilla API key (http transport only)
--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 false Authenticate to Bugzilla with Authorization: Bearer <key> instead of the api_key query parameter
--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
--policy <PATH> BUGWARDEN_POLICY Path to the guard policy TOML. Without it, an allow-all policy applies (with private comments off)
--audit-config <PATH> BUGWARDEN_AUDIT_CONFIG Path to the audit stream configuration TOML (worked example in 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

Policy file reference

The policy is strict TOML: unknown keys anywhere are a startup error, as
is a restrict rule without capabilities, an allow/deny rule with
capabilities, or default_action = "restrict". On Unix, bugwarden logs a
warning at startup if the policy file is group- or other-writable.

A complete, commented example ships in
examples/policy.toml.

Top level

Key Type Default Description
default_action "allow" | "deny" "allow" Applied when no rule matches a bug. Must not be "restrict" (a catch-all restrict rule expresses that instead)

[global]

Key Type Default Description
min_bug_age_days integer 0 (disabled) Bugs created less than N days ago are invisible — treated exactly like nonexistent bugs, evaluated before any rule. A bug whose creation_time is missing or unparsable is denied (fail closed)
allow_private_comments boolean false Master switch for all private content: comments, attachment metadata, and attachment downloads. Even when true, each call must also pass include_private = true. On an attachment download a missing privacy flag counts as private
read_only boolean false Strip write capabilities from every grant and remove write tools from the tool listing. The --read-only flag ORs into this
disabled_tools array of strings [] Tool names to remove from the tool listing entirely
max_attachment_bytes integer 2097152 (2 MiB) Largest attachment download_attachment may return (decoded size). 0 removes the cap. Attachment content is embedded base64 in the tool result and lands in the model's context — raise deliberately

[[rule]]

Rules are evaluated top to bottom; the first rule whose matcher matches the
bug wins
and later rules are ignored. If no rule matches, default_action
applies. Put your most specific (usually most restrictive) rules first.

Key Type Default Description
name string required Rule identifier (server-side logs only; never shown to clients)
description string "" Free-form operator documentation
match table {} (matches every bug) Match criteria, see below
action "allow" | "deny" | "restrict" required allow grants all capabilities, deny grants none, restrict grants exactly capabilities
capabilities array of capability strings [] Only for action = "restrict", where at least one is required. Must be empty/absent for allow and deny
operations array of "create" | "access" absent (rule applies to every operation) Scopes the rule to the named operations: create is the create gate judging a prospective create_bug request, access is every classification of an existing bug (retrieval, search filtering, comments, history, attachments, updates). The scope is checked before the matcher, so a scoped rule is completely invisible to the operations it does not cover — a create-scoped rule can never hide an existing bug. An explicitly empty list is a startup error, as is a restrict rule whose scope and capabilities disagree about create: a rule scoped to only create must grant exactly the create capability (the create gate consults nothing else), and a rule scoped away from create must not grant it (nothing else consults it). Older bugwarden versions reject a policy using this key at startup (strict parsing — the file fails closed rather than being misread)

Note that a restrict rule's capabilities list is the complete grant
for every operation the rule covers, not an addition to what other rules or
default_action would have granted — that is why a rule granting only
create should carry operations = ["create"], so it decides filing without
becoming the first-match rule for reads of the bugs it matches.

match criteria

All criteria present in a matcher must hold (AND). Within a single list,
any element may match (OR). An empty matcher matches every bug — a rule
with no match is a catch-all. To express "criterion A or criterion B",
write two consecutive rules.

Key Type Matched against
products array of globs the bug's product
components array of globs any of the bug's components
groups array of globs any of the bug's group names
keywords array of globs any of the bug's keywords
statuses array of globs the bug's status
severities array of globs the bug's severity
priorities array of globs the bug's priority
whiteboard_contains array of strings case-insensitive substring search in the whiteboard
summary_contains array of strings case-insensitive substring search in the bug's one-line summary
group_restricted boolean true matches bugs readable only through at least one Bugzilla group, false matches world-readable bugs
younger_than_days integer matches bugs created within the last N days
created_by_me boolean whether the API key's account authored the bug: the caller's login is resolved per request via Bugzilla's whoami endpoint (at most one lookup per tool call, and none at all under a policy without an access-covering created_by_me rule — a rule scoped to operations = ["create"] alone never triggers a lookup) and compared case-insensitively to the bug's creator. true matches the caller's own reports, false everyone else's. An unresolvable identity (whoami failure) makes the criterion unknown, which denies (see Unreadable metadata). In the create gate the prospective bug always counts as created by the caller — no lookup happens there. Older bugwarden versions reject a policy using this key at startup (strict parsing fails closed)

Unreadable metadata

Every criterion needs a field the bug object may not carry — absent, null,
of an unexpected type, or a list with an element the parser cannot read. Such
a field is unknown, and a rule that consults one is undecidable: it neither
holds nor fails. One criterion needs more than the bug object:
created_by_me also needs the caller's identity, and if either half is
missing — an unreadable creator, or a whoami lookup that failed — it is
just as undecidable and resolves the same way. A policy consulting identity
therefore denies everything its identity rules are consulted for while
whoami is failing; that is deliberate (treating unknown identity as "does
not match" would let a created_by_me deny rule be defeated by breaking
whoami). The criterion cannot widen exposure beyond the credential:
Bugzilla enforces its own permissions on every fetch, so an authorship rule
only surfaces bugs the API key could already read.

bugwarden resolves an undecidable rule by denying the bug, whatever the
rule's action. A deny rule denies because the bug may well be what it was
written to catch. An allow or restrict rule denies too — it may not grant
access on data nobody could check, and it may not simply be skipped either,
because skipping would hand the bug to a later rule or to default_action. So
unreadable metadata never buys a bug more access than readable metadata would.

Two things this deliberately does not do. A criterion that already failed
definitively wins over an unknown one, so a rule ruled out by another criterion
stays ruled out. And only the fields a rule actually consults matter — a
missing whiteboard is irrelevant to a rule that never mentions the whiteboard.
Likewise, only the rules actually consulted matter: a rule scoped away from
the operation being decided (operations) is skipped before its matcher runs,
so its criteria cannot make anything undecidable for that operation.
A field that is present but empty ("", []) is knowledge, not ignorance, and
is matched normally.

Glob syntax

Globs match the whole value, case-insensitively. * matches any (possibly
empty) substring; every other character is literal. There are no other
metacharacters. Examples: embargo*, *security*, SUSE *.

Capabilities

Thirteen capabilities exist. read implies summary; nothing else is
implied.

Upgrading from a version without create/attach: the capability set
grew from eleven to thirteen, and allow (rules and
default_action = "allow" alike) always grants the full set. A policy
written before these capabilities existed therefore starts permitting bug
filing and attachment upload the moment the server is upgraded, with no
change to the policy file. To keep the old behaviour, either add
disabled_tools = ["create_bug", "add_attachment"] under [global], or
replace allow grants with restrict rules listing exactly the
capabilities you mean. Read-only deployments are unaffected (both new
capabilities are writes).

Capability Kind Grants
read read full bug details (implies summary)
summary read redacted summary-only view (id, summary, status, resolution, product, component, severity, priority, creation/last-change time)
comments read reading comments (also needed by summarize_bug)
history read reading the bug's change history
attachments read listing attachment metadata and downloading attachment content
comment write adding a comment
status write changing status/resolution, marking duplicates
fields write changing priority, severity, resolution, summary, URL, whiteboard, version, target milestone, keywords, see-also links, cf_* custom fields
assign write changing the assignee
cc write modifying the CC list
deps write changing blocks/depends_on
create write filing a new bug — judged against the bug as requested, so a rule that hides a product by name also refuses filing into it. The request's groups claim is never trusted (Bugzilla adds mandatory groups server-side), so a rule consulting groups or group_restricted refuses every create request that reaches it — to accept new bugs under such a policy, grant create in a rule scoped with operations = ["create"] placed before the group-consulting rules; being create-scoped, the grant leaves reads of existing bugs untouched, and without such a grant the policy refuses all bug filing
attach write uploading an attachment to a bug

When the server is read-only (policy or CLI), the eight write capabilities
are stripped from every grant, including from allow rules and the default
action.

Tool reference

Tool What it does Required capability
bug_info Details for a set of bug ids. Per id: full details with read, redacted summary with summary, otherwise a uniform "not accessible" entry read / summary
bug_history Change history of a bug, optionally only entries newer than a timestamp history
bug_comments Comments on a bug; private comments only per the private-comment gate comments
bugs_quicksearch Bugzilla quicksearch — the status filter (default ALL) is prefixed to the query, and under any non-empty status a number in the query is content-matched, so it also matches bugs that merely mention it; with an empty status the query goes to Bugzilla bare, where a query of nothing but numbers is an exact id lookup (use bug_info for an exact set of known ids; an all-ids query gets an advisory note saying so). Results are silently policy-filtered (denied dropped, summary-only redacted) per result: read / summary
summarize_bug Returns a summarization prompt built from the bug's public comments comments
list_attachments Attachment metadata (never attachment content) attachments
download_attachment Content of one attachment (raster images as image content, everything else as a base64 blob resource), capped by max_attachment_bytes; private attachments need the private-content double opt-in and, on download, a missing privacy flag counts as private attachments on the owning bug
add_comment Add a comment to a bug comment
update_bug_status Change status/resolution (CLOSED requires a resolution) status
assign_bug Set the assignee assign
update_bug_fields Update priority/severity/resolution, summary, URL, whiteboard, version, target milestone, keywords and see-also links (both add/remove, never replace-all), and cf_* custom fields fields on the bug and at least summary on every see-also target on this instance
update_bug_dependencies Add/remove blocks and depends_on entries deps
add_cc_to_bug Add an email to the CC list cc
mark_as_duplicate Close a bug as DUPLICATE of another status on the bug and at least summary on the duplicate target
create_bug File a new bug; the request is policy-checked as described before anything is created. A policy refusal and a Bugzilla-side failure return the same refusal text at the same cost, so a failed create never says which of the two refused, or why create on the bug as requested
add_attachment Upload a base64-encoded attachment to a bug, capped by max_attachment_bytes (decoded size) attach on the target bug
bug_url Compute {server}/show_bug.cgi?id={id} locally none (contacts nothing)
bugzilla_server_info Bugzilla version, extensions, timezone, time, parameters none
quicksearch_syntax Bugzilla's quicksearch syntax documentation (HTML) none
mcp_server_info bugwarden version, Bugzilla URL, transport, coarse policy summary none

License

Apache License 2.0. See LICENSE for details.

Yorumlar (0)

Sonuc bulunamadi