Fleet Hardening Runbook¶
A practical, recipe-driven deployment and operational runbook for platform engineers, systems administrators, and DevOps teams deploying and governing bb across corporate fleets.
For the formal threat model, trust boundaries, and compliance evaluations, see the Security Architecture and Threat Model.
1. Release Verification (Pre-Deployment)¶
Before packaging or mirroring bb into internal registries (e.g. Artifactory, Nexus, internal apt/yum/winget repos), verify the authenticity and build provenance of the downloaded release artifacts. Set the target version (e.g. 4.1.0) in your verification environment:
A. Sigstore Keyless Signature Verification¶
Every release publishes keyless OIDC signatures bound to the official GitHub Actions release workflow on refs/heads/main:
VERSION="4.1.0"
cosign verify-blob \
--bundle "bb_${VERSION}_linux_amd64.tar.gz.sigstore.json" \
--certificate-identity 'https://github.com/vriesdemichael/bitbucket-data-center-cli/.github/workflows/release.yml@refs/heads/main' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
"bb_${VERSION}_linux_amd64.tar.gz"
B. GitHub Build Provenance Attestation¶
Verify that the binary was built on official GitHub-hosted runners directly from the source repository:
VERSION="4.1.0"
gh attestation verify "bb_${VERSION}_linux_amd64.tar.gz" \
--repo vriesdemichael/bitbucket-data-center-cli
C. Software Bill of Materials (SPDX 2.3 SBOM)¶
Each archive has its own SBOM, generated from the binary inside it and named after the archive (bb_${VERSION}_linux_amd64.spdx.json). It lists the Go modules that binary links, the Go standard library it was linked with, and each module's licence. Dependencies differ by platform, so an SBOM describes one platform only. The .deb and .rpm install the _noupdate binary and are attested with its SBOM (bb_${VERSION}_linux_amd64_noupdate.spdx.json). Before a release is published, each SBOM is checked against the build information the Go linker wrote into its binary. Verify that the released archive is attested with its SBOM:
VERSION="4.1.0"
gh attestation verify "bb_${VERSION}_linux_amd64.tar.gz" \
--repo vriesdemichael/bitbucket-data-center-cli \
--predicate-type https://spdx.dev/Document/v2.3
To inspect an SBOM itself, download it and verify its signature like any other artifact:
VERSION="4.1.0"
curl -LO "https://github.com/vriesdemichael/bitbucket-data-center-cli/releases/download/v${VERSION}/bb_${VERSION}_linux_amd64.spdx.json"
curl -LO "https://github.com/vriesdemichael/bitbucket-data-center-cli/releases/download/v${VERSION}/bb_${VERSION}_linux_amd64.spdx.json.sigstore.json"
cosign verify-blob \
--bundle "bb_${VERSION}_linux_amd64.spdx.json.sigstore.json" \
--certificate-identity 'https://github.com/vriesdemichael/bitbucket-data-center-cli/.github/workflows/release.yml@refs/heads/main' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
"bb_${VERSION}_linux_amd64.spdx.json"
2. Fleet Security Controls¶
Distinguish between enforceable technical controls (which systems engineers deploy via configuration) and socialized practices (which developers and CI authors follow).
Deployable Fleet Controls¶
- System Configuration and Immutable Administrative Policies (ADR-058):
Deploy a machine-level configuration file (
/etc/bb/config.yamlon Linux/macOS,%ProgramData%\bb\config.yamlon Windows) or native Windows Registry policy keys (HKLM\Software\Policies\bb). Policies defined at this tier are immutable and cannot be overridden by user shell environment variables, user config files, or repository workspace configs — including the path the policy file is read from:# yaml-language-server: $schema=https://raw.githubusercontent.com/vriesdemichael/bitbucket-data-center-cli/main/docs/reference/schemas/config.schema.json $schema: https://raw.githubusercontent.com/vriesdemichael/bitbucket-data-center-cli/main/docs/reference/schemas/config.schema.json require_keyring: true ca_file: /etc/ssl/certs/corp-root-ca.pem allowed_hosts: - https://bitbucket.corp.internal allow_insecure_skip_verify: false allow_http_update: false disable_update: true update_base_url: https://artifactory.corp.internal/artifactory/bb-releases - JSON Schema Validation: All configuration files are validated against
config.schema.json. Supplying the$schemadirective enables live linting and autocompletion in VS Code and IntelliJ. On the host,bb doctorreports every key the schema rejects in the deployed file, and the source each policy setting comes from. require_keyring: true: Enforces OS keyring storage machine-wide; refuses fallback to plaintext files even ifBB_REQUIRE_KEYRINGis unset or set to0. If a user setsBB_REQUIRE_KEYRING=0,bboutputs an explicit warning tostderrand continues enforcing keyring policy.ca_file: <path>: Mandates corporate Root CA bundle. Attempts to pass a conflicting CA file abort with an authorization error.allowed_hosts: [...]: Whitelists permitted Bitbucket Server / Data Center instances. Connection attempts to unlisted hosts abort with an authorization error.allow_insecure_skip_verify: false: Hard-refuses--insecure-skip-verifyandBB_INSECURE_SKIP_VERIFY=true.-
allow_http_update: false: Hard-refuses plain-HTTP update URLs,bb update --allow-httpandBB_ALLOW_HTTP_UPDATE=1. -
Enterprise Update Controls and Release Mirrors (ADR-059):
- Disabling In-Place Self-Updates: On managed corporate machines where software must be installed exclusively through IT package managers (e.g. Jamf, Ansible, Intune, SCCM), disable
bb updateby settingdisable_update: truein system configuration orexport BB_DISABLE_UPDATE=1. Alternatively, deploy the_noupdatebuilds described below, which cannot self-update whatever the configuration says. - Internal Release Mirrors: In firewalled or air-gapped enterprise enclaves, configure
bb updateto query internal mirrors (e.g. JFrog Artifactory, Sonatype Nexus) instead ofapi.github.comvia--base-url <url>,BB_UPDATE_BASE_URL, orupdate_base_urlin system/user config. A mirror alone is not sufficient on a host with no internet access: pair it with an offline trust root, below. Mirror URLs must behttps: a plain-HTTP mirror needsbb update --allow-httporBB_ALLOW_HTTP_UPDATE=1, andallow_http_update: falsein system configuration refuses it for every user (truepermits it fleet-wide). - Offline Signature Verification (ADR-063): By default,
bb updatefetches Sigstore trust material fromhttps://tuf-repo-cdn.sigstore.devon every run. Deploy atrusted_root.jsonalongside the corporate CA bundle and point at it to verify releases with no internet access at all:Produce the file once, on a host that does have access:update_trusted_root: /etc/bb/trusted_root.jsonThis verifies the public releases as published — signed certificate timestamps, the Rekor inclusion promise, and observer timestamps are all checked against keys inside the file — so mirroring artifacts byte-for-byte needs no re-signing. Refresh the file when Sigstore rotates its keys, which is a multi-year event and another file push. Organisations that mirror the Sigstore TUF repository itself can setcosign trusted-root create > trusted_root.jsonupdate_tuf_url: <url>instead; the two are mutually exclusive, it must be an absolutehttpsURL, and the TUF fetch uses the configuredca_fileand client certificates. - Re-Signed Artifacts: Organisations that rebuild or re-sign
bbagainst their own Fulcio instance replace the pinned signer withupdate_signature_identity(certificate SAN) andupdate_signature_issuer(OIDC issuer). - Unverified Updates (Last Resort):
allow_unverified_update: trueskips signature verification entirely. SHA256 checksum verification remains mandatory, so this still catches corruption but not tampering; every run prints a warning to stderr and reportssignature_skipped: truein--jsonoutput. Prefer an offline trust root. - Policy Only:
update_trusted_root,update_tuf_url,update_signature_identity,update_signature_issuerandallow_unverified_updateare read from system configuration and Windows registry policy only — never from an environment variable or a flag. Each decides who may vouch for a binarybbis about to execute, and that decision does not belong to whoever can set a variable in a user's shell.update_base_urlkeeps its flag and environment forms, because signature verification still gates whatever the mirror serves. -
Mirror Layout: The mirror serves a GitHub-release-shaped manifest at
/repos/vriesdemichael/bitbucket-data-center-cli/releases/latest, or — for generic Artifactory/Nexus repositories — at/releases/latestor/latest, whichbbtries in that order:Mirror{ "tag_name": "v4.1.0", "html_url": "https://artifactory.corp.internal/artifactory/bb-releases", "assets": [ { "name": "bb_4.1.0_linux_amd64.tar.gz", "browser_download_url": "bb_4.1.0_linux_amd64.tar.gz" }, { "name": "sha256sums.txt", "browser_download_url": "sha256sums.txt" }, { "name": "sha256sums.txt.sigstore.json", "browser_download_url": "sha256sums.txt.sigstore.json" } ] }bb_<version>_<os>_<arch>.tar.gz(.zipon Windows),sha256sums.txt, andsha256sums.txt.sigstore.jsonat the base URL. Asset URLs may be relative, as above, or absolute mirror URLs; a manifest copied verbatim from GitHub also works, sincebbfetches off-mirror asset URLs from{base_url}/{asset_name}first rather than stalling on a firewalledgithub.comaddress. Verify a mirror without replacing any binary:The dry run reports which trust material was used and whether the manifest signature and checksum entry were found.bb update --dry-run --base-url https://artifactory.corp.internal/artifactory/bb-releases -
Mandate Keyring Storage (Advisory / User Tier):
When set in user environments where system policy is not yet deployed,export BB_REQUIRE_KEYRING=1bbrefuses to read credentials from or write credentials to the plaintext configuration fallback (~/.config/bb/config.yamlon Linux,~/Library/Application Support/bb/config.yamlon macOS, or%AppData%\bb\config.yamlon Windows). Any command that would otherwise rely on plaintext fallback aborts with an error (ADR-047). -
Configure Host-Scoped Git Credential Helper:
Writes a host-scoped credential helper into the user's globalbb auth setup-git~/.gitconfig:Note:[credential "https://bitbucket.example.com"] helper = !"/usr/local/bin/bb" auth git-credentialbbwrites the absolute executable path into the git configuration. Git queriesbbdynamically on demand for that specific host, ensuring zero credentials are ever written into local repository.git/configfiles and credentials are never offered to external remotes (ADR-044). -
Disable Stored Config for Headless CI:
Ensures that ephemeral CI/CD runners read authentication strictly fromexport BB_DISABLE_STORED_CONFIG=1BITBUCKET_TOKEN, guaranteeing that no stored credential profile is read and no desktop keyring daemon is contacted.
Builds With Self-Update Compiled Out¶
Every release publishes each platform twice. The _noupdate archives are built
with -tags no_self_update, so bb update in one of them exits 3
(authorization) with:
self-update is disabled in this build; update bb using your system package manager
Everything else behaves identically.
This matters for a fleet in two ways.
It is what your users already have. The WinGet, Scoop and Homebrew manifests
all reference _noupdate archives, so a developer who installed through a
package manager cannot self-update regardless of policy. A binary that replaces
itself behind a package manager leaves that manager's records describing a
version no longer on disk.
It survives a machine you do not control. disable_update and
BB_DISABLE_UPDATE are configuration, and configuration can be missing —
an imaging step that has not run yet, a container built from a bare archive, a
developer who installed the binary by hand. A _noupdate build refuses in all
of those cases because the capability is not compiled in. Prefer it wherever the
requirement is that a host never fetches its own binary; use disable_update
where you need the same result on hosts that already have the ordinary build.
Both spellings are published, versioned and version-less:
bb_4.1.0_linux_amd64_noupdate.tar.gz
bb_linux_amd64_noupdate.tar.gz
Socialized Developer Practices¶
-
Pipe Tokens via Stdin (Never in CLI Flags): No flag takes a credential value; the secret goes over stdin or
BITBUCKET_TOKEN. Flag values are visible to local processes inps aux,/proc/<pid>/cmdline, Windows Task Manager, EDR sensors, and shell history files.# Provide the token via pipe printf "%s" "$BITBUCKET_TOKEN" | bb auth login https://bitbucket.example.com --token-stdin # Or stream directly from a secure file cat /run/secrets/bitbucket_token | bb auth login https://bitbucket.example.com --token-stdin -
Clean Up Legacy Clones: Existing clones made before
bb auth setup-gitmay contain plaintext tokens in their local.git/config:git config --local --unset-all http.extraHeader
3. Multi-OS Fleet Deployment Recipes¶
A. macOS (Jamf Pro / Kandji / Intune)¶
macOS developer workstations authenticate through the Apple Keychain (Security framework). Deploy system configuration to /etc/bb/config.yaml so that all terminal sessions and GUI applications (like VS Code or Cursor invoking MCP servers) automatically inherit policy without relying on shell environment inheritance:
# 1. Distribute via Homebrew or universal binary
brew install vriesdemichael/tap/bb
# 2. Deploy Enterprise Root CA to System Keychain
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain \
/Library/Application\ Support/Corporate/Certs/corp-root-ca.pem
# 3. Deploy Immutable System Configuration (/etc/bb/config.yaml)
# Note: Unlike /etc/zshenv, /etc/bb/config.yaml is read directly by bb in GUI IDEs as well.
sudo mkdir -p /etc/bb
sudo tee /etc/bb/config.yaml >/dev/null <<'EOF'
require_keyring: true
ca_file: /Library/Application Support/Corporate/Certs/corp-root-ca.pem
allowed_hosts:
- https://bitbucket.corp.internal
allow_insecure_skip_verify: false
allow_http_update: false
disable_update: true
EOF
sudo chmod 644 /etc/bb/config.yaml
B. Linux Workstations (Ansible)¶
Linux workstations authenticate through the Secret Service API over D-Bus (GNOME Keyring / KWallet). Deploy /etc/bb/config.yaml to enforce security postures across all local users:
- name: Deploy and harden bb across Linux workstations
hosts: workstations
become: true
vars:
bb_version: "4.1.0"
tasks:
- name: Deploy Corporate Root CA bundle
copy:
src: files/corp-root-ca.pem
dest: /etc/ssl/certs/corp-root-ca.pem
owner: root
group: root
mode: '0644'
- name: Download verified bb Debian package
get_url:
url: "https://artifactory.corp.internal/binaries/bb_{{ bb_version }}_linux_amd64.deb"
dest: "/tmp/bb_{{ bb_version }}_linux_amd64.deb"
mode: '0644'
when: ansible_os_family == "Debian"
- name: Install bb package (Debian/Ubuntu)
apt:
deb: "/tmp/bb_{{ bb_version }}_linux_amd64.deb"
when: ansible_os_family == "Debian"
- name: Deploy system-wide policy configuration
copy:
dest: /etc/bb/config.yaml
owner: root
group: root
mode: '0644'
content: |
require_keyring: true
ca_file: /etc/ssl/certs/corp-root-ca.pem
allowed_hosts:
- https://bitbucket.corp.internal
allow_insecure_skip_verify: false
allow_http_update: false
disable_update: true
update_base_url: https://artifactory.corp.internal/artifactory/bb-releases
C. Windows Workstations (Microsoft Intune / PowerShell / GPO)¶
Windows workstations authenticate through Windows Credential Manager (DPAPI). Administrators can deploy system configuration via %ProgramData%\bb\config.yaml or through native Windows Group Policy / Intune CSP targeting the Windows Registry (HKLM\Software\Policies\bb):
# Run as Administrator via Intune or administrative PowerShell
$Version = "4.1.0"
# 1. Install via WinGet
winget install --id vriesdemichael.bb --exact --version $Version --accept-source-agreements --accept-package-agreements
# 2. Deploy Corporate Root CA
$CertDir = "C:\ProgramData\Corporate\Certs"
New-Item -ItemType Directory -Force -Path $CertDir | Out-Null
Copy-Item ".\corp-root-ca.pem" -Destination "$CertDir\corp-root-ca.pem"
# 3. Option A: Deploy System Configuration File (%ProgramData%\bb\config.yaml)
# Create this directory as an administrator before any developer runs bb. C:\ProgramData
# lets an unprivileged account create a subdirectory and become its owner, and bb never
# creates the directory itself (ADR-058, point 5). The ACL below removes the inherited
# Users write entry so the policy file cannot be replaced by the accounts it governs.
$ConfigDir = "C:\ProgramData\bb"
New-Item -ItemType Directory -Force -Path $ConfigDir | Out-Null
$Acl = Get-Acl $ConfigDir
$Acl.SetAccessRuleProtection($true, $false)
foreach ($Identity in "SYSTEM", "Administrators") {
$Acl.AddAccessRule((New-Object Security.AccessControl.FileSystemAccessRule(
$Identity, "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow")))
}
$Acl.AddAccessRule((New-Object Security.AccessControl.FileSystemAccessRule(
"Users", "ReadAndExecute", "ContainerInherit,ObjectInherit", "None", "Allow")))
Set-Acl -Path $ConfigDir -AclObject $Acl
@"
require_keyring: true
ca_file: $CertDir\corp-root-ca.pem
allowed_hosts:
- https://bitbucket.corp.internal
allow_insecure_skip_verify: false
allow_http_update: false
disable_update: true
update_base_url: https://artifactory.corp.internal/artifactory/bb-releases
"@ | Set-Content -Path "$ConfigDir\config.yaml" -Encoding UTF8
# 4. Option B: Native Windows Registry GPO Policies (HKLM\Software\Policies\bb)
$RegPath = "HKLM:\Software\Policies\bb"
if (!(Test-Path $RegPath)) { New-Item -Path $RegPath -Force | Out-Null }
Set-ItemProperty -Path $RegPath -Name "RequireKeyring" -Value 1 -Type DWord
Set-ItemProperty -Path $RegPath -Name "CAFile" -Value "$CertDir\corp-root-ca.pem" -Type String
Set-ItemProperty -Path $RegPath -Name "AllowedHosts" -Value "https://bitbucket.corp.internal" -Type String
Set-ItemProperty -Path $RegPath -Name "AllowInsecureSkipVerify" -Value 0 -Type DWord
Set-ItemProperty -Path $RegPath -Name "AllowHTTPUpdate" -Value 0 -Type DWord
Set-ItemProperty -Path $RegPath -Name "DisableUpdate" -Value 1 -Type DWord
D. CI/CD & Headless Containers (Docker / Kubernetes)¶
Headless runners do not have interactive desktop sessions or D-Bus daemons. Configure runners to read secrets entirely from process memory and bypass disk storage.
# Hardened CI Container Pattern
FROM alpine:3.21
ARG BB_VERSION=4.1.0
# Install runtime dependencies (ca-certificates and git)
RUN apk add --no-cache ca-certificates git curl
# Download and install official release binary
RUN curl -fsSL "https://github.com/vriesdemichael/bitbucket-data-center-cli/releases/download/v${BB_VERSION}/bb_${BB_VERSION}_linux_amd64.tar.gz" \
| tar -xz -C /usr/local/bin bb \
&& chmod +x /usr/local/bin/bb
# Install corporate CA
COPY corp-root-ca.pem /etc/ssl/certs/corp-root-ca.pem
# Environment flags for headless isolation
ENV BB_CA_FILE=/etc/ssl/certs/corp-root-ca.pem
ENV BB_DISABLE_STORED_CONFIG=1
# Execution in CI: pass token via environment, zero disk persistence
# docker run --rm -e BITBUCKET_TOKEN=$SECRET -e BITBUCKET_URL=https://bitbucket.corp.internal my-image bb repo list
4. Internal PKI, Proxies & Multi-Server Estates¶
Critical Imaging Order: Deploy CA Before Env Var¶
bb initializes its TLS transport during client construction. If BB_CA_FILE points to a non-existent path, bb immediately aborts with:
read CA bundle: open /etc/ssl/certs/corp-root-ca.pem: no such file or directory
BB_CA_FILE.
Additive Trust Pool¶
bb appends your corporate CA bundle to the system's root certificate pool (x509.SystemCertPool()). It does not replace public roots, allowing connections to public services (e.g. GitHub release verification) to succeed alongside internal Bitbucket calls.
Mutual TLS (mTLS) Client Authentication¶
In zero-trust or defense networks requiring hardware- or PKI-backed mutual TLS at ingress gateways (Envoy, NGINX, F5, Cloudflare Access), configure client certificates and private keys (ADR-060):
bb --client-cert /etc/ssl/certs/client.pem --client-key /etc/ssl/private/client.key repo list
In CI/CD runners or shell environments:
export BB_CLIENT_CERT=/etc/ssl/certs/client.pem
export BB_CLIENT_KEY=/etc/ssl/private/client.key
Or persist client certificate paths per host in stored profiles:
printf '%s' "$abc" | bb auth login https://bitbucket.corp.example --token-stdin --client-cert /etc/ssl/certs/client.pem --client-key /etc/ssl/private/client.key
Private keys are loaded directly in-memory via Go's standard crypto/tls package and are never logged, serialized into machine JSON envelopes, or written to configuration files.
Corporate Forward Proxies¶
Configure standard proxy environment variables:
export HTTPS_PROXY=http://proxy.corp.example:3128
export NO_PROXY=.corp.internal,localhost,127.0.0.1
Multi-Server Estates (Avoid Blanket BITBUCKET_URL)¶
Do Not Pin BITBUCKET_URL Fleet-Wide in Multi-Server Estates
If your organization operates multiple Bitbucket instances (e.g. post-acquisition environments, production vs. staging), do not export a static BITBUCKET_URL in /etc/profile.d/ or /etc/zshenv. Setting BITBUCKET_URL globally overrides local repository context discovery. Instead, let developers configure server profiles (bb auth login <host>) or rely on automatic clone URL discovery (Repository Discovery and Server Switching).
5. AI & IDE MCP Server Governance (bb ai mcp serve)¶
bb includes a built-in Model Context Protocol (MCP) server for integration with AI developer tools (VS Code Agent, Cursor, Claude Desktop).
Principle 1: Default and Gated Tools¶
bb withholds the tools that decide whether code merges (ADR-039), and draws that line by consequence rather than by whether a tool writes:
- Exposed by default: every read tool, and the writes that change no branch and cause no merge: create_pull_request, update_pull_request, add_pr_comment, create_tag and disable_auto_merge.
- Withheld unless --yolo (or --allow-writes): merge_pull_request and enable_auto_merge, which merge now or later, and submit_pr_review and set_build_status, which feed the checks that decide whether a merge is allowed. Gating submit_pr_review ensures an agent cannot approve its own pull requests.
The MCP tool reference lists every tool with what it can change and when it is exposed.
Inspect exposed tools and their gating status:
bb ai mcp tools
Principle 2: Dedicated Read-Only Token Scoping¶
Never run IDE MCP servers under personal developer credentials. Generate a dedicated read-only PAT and bind the MCP server to it:
# Give the agent its own PAT through the MCP client's env block:
# "env": { "BITBUCKET_TOKEN": "${BB_MCP_TOKEN}" }
# The ${VAR} form keeps it out of the config file, and lets the agent run on a
# read-only token while your own shell keeps a wider one.
bb ai mcp serve --host https://bitbucket.example.com
Principle 3: Workspace Scoping¶
The PAT the server runs under bounds what an agent may do; --project and --repo bound where (ADR-062). On a multi-tenant instance a read-only PAT still reaches every repository its owner can read, which for most developers is most of the organisation.
bb ai mcp serve --host https://bitbucket.example.com --project PAYMENTS
bb ai mcp serve --host https://bitbucket.example.com --repo PAYMENTS/ledger
Scoping is enforced at a single choke point over every tool call, not per tool. Three behaviours are worth knowing before you configure it:
- Omitted arguments are bound, not rejected.
list_pull_requestswith no project reaches every repository the token can see. Under a scope the arguments are filled in, so the unbounded mode becomes the bounded one and the agent never needs to know. - Conflicting arguments are refused. A call naming another project fails with an error the agent can read and correct.
- Tools that cannot be bounded are withheld entirely.
get_build_statusandset_build_statusaddress a commit SHA, which Bitbucket does not scope to a project. They disappear fromtools/listwhile a scope is set.search_repositoriesis withheld under--repofor the same reason: pinning its project filter would still list sibling repositories, and a filter is not a boundary.
Principle 4: Agent Audit Trail¶
--audit-file appends one JSON Lines record per tool call, for SIEM collection:
bb ai mcp serve --host https://bitbucket.example.com --project PAYMENTS --audit-file /var/log/bb/mcp-audit.jsonl
{"timestamp":"2026-08-29T09:30:00Z","event":"mcp_tool_invocation","tool":"get_pull_request","project":"PAYMENTS","repo":"ledger","status":"success","duration_ms":45,"user_identity":"alice","host":"https://bitbucket.example.com","scope":"PAYMENTS"}
status is success, error, or denied. Argument values and error messages are recorded, with tokens, passwords and URL credentials redacted. When the client sends W3C trace context, trace_id carries it so a record correlates with the agent's own trace.
Auditing is off by default — a developer who never turns it on should not accumulate a log file they will not find. Turn it on by fleet policy, not by asking developers to.
Why audit here when Bitbucket already has an audit log. The two answer different questions, and the CLI one is not a duplicate:
- Attribution. Every MCP call arrives at Bitbucket as the same user with the same PAT. Bitbucket cannot distinguish a developer reviewing a PR in a browser from an agent acting autonomously in their IDE. That distinction exists only here.
- Denied attempts. A call refused by the scope boundary or the safety gate never reaches Bitbucket, so its audit log has no record of it. Attempted-and-blocked is precisely the prompt-injection signal worth alerting on: one successful read is noise, forty denied cross-project reads in ten seconds is an incident.
- Reads in practice. Bitbucket's repository read events sit at Full coverage, which most operators do not run in production because of volume. "Bitbucket already logs everything" holds far better for writes than for reads — and an exfiltrating agent is doing reads.
Bitbucket's audit log remains authoritative for what actually changed. Correlate the two on (timestamp, user_identity).
Two limitations to state plainly.
This log is not tamper-evident. It is written on the developer's machine, as the developer, to a path they can edit. Against a determined insider it proves nothing. Against a prompt-injected agent confined to MCP tools — the ADV-3 threat it is designed for — it holds, because that agent has no shell.
An agent with shell access can bypass all of this. Nothing stops it running bb pr merge directly, or any other command in the CLI, none of which are scoped, gated, or audited. That is not a gap this feature can close: an agent that can run shell commands can also edit the audit file. The control that survives it is the token the server runs under, because a read-only PAT binds at the Bitbucket server and does not care which local process made the call. Treat MCP scoping and auditing as defence in depth over a correctly scoped token, never as a substitute for one.
Principle 5: Mandating Audit by Policy¶
An audit destination a developer can change by editing their IDE config records only what they permit. Mandate it machine-wide instead (ADR-058):
# /etc/bb/config.yaml (or %ProgramData%\bb\config.yaml)
policy:
mcp_audit_file: /var/log/bb/mcp-audit.jsonl
The server then audits whether or not --audit-file is passed, and refuses a --audit-file pointing anywhere else with an authorization error.
The mandate is worth what the policy file's permissions are worth. bb reads the system configuration file and never creates the directory holding it, which is deliberate — see ADR-058, point 5. On Linux and macOS creating /etc/bb/ already requires root. On Windows it does not: C:\ProgramData lets any account add a subdirectory and hands its creator full control of it, so C:\ProgramData\bb must be created by an administrator, before any developer runs bb, with unprivileged accounts left read access only. Until that is done, "the developer cannot redirect this" is not a claim you can make on a Windows workstation.
mcp_audit_file is also the one policy setting with no HKLM\Software\Policies\bb value, so on Windows it is set through the file and not by GPO. The registry is the stronger channel for everything it does carry.
When a record cannot be written the call is refused. An audit trail that silently stops recording is worse than none, because the absence of a record then carries no information. --audit-failure=warn relaxes this for an operator who would rather lose records than lose the server.
Collection. The audit log is a file because every SIEM already tails files — Splunk Universal Forwarder, Datadog Agent, Fluent Bit, Vector, Filebeat. bb deliberately ships no direct SIEM integration: it would put a network call, an auth secret and retry buffering inside the tool-call path of a process that is spawned per IDE session and killed without warning. For a containerised or wrapper-managed deployment, pass --audit-file stderr and let the cluster log collector read the process streams. Rotation is the collector's job; bb appends and never truncates.
Recommended IDE Configuration (.vscode/settings.json)¶
{
"mcp": {
"servers": {
"bitbucket": {
"type": "stdio",
"command": "bb",
"args": [
"ai",
"mcp",
"serve",
"--host",
"https://bitbucket.example.com",
"--tools",
"get_pull_request,list_pull_requests,get_pr_diff,list_pr_comments,add_pr_comment"
],
"env": {
"BITBUCKET_TOKEN": "${env:BITBUCKET_RO_TOKEN}",
"BB_CA_FILE": "/Library/Application Support/Corporate/Certs/corp-root-ca.pem"
}
}
}
}
}
6. Verification and Troubleshooting¶
Verification Audit¶
Run the machine-readable auth status check to verify configuration:
bb auth status --json
Confirm:
- .data.credentialStorage: Must report keyring (on workstations) or environment (in CI). If it reports config-file-plaintext, BB_REQUIRE_KEYRING=1 is missing.
- Check active git helper for your Bitbucket host:
git config --global --get "credential.https://bitbucket.example.com.helper"
Check the deployed configuration itself. bb doctor needs no host and no login, so it runs as soon as the file is in place:
bb doctor --json
Confirm:
- It exits 0. Any issue exits 1, and the output is then the failure envelope rather than the report, with each issue in .error.details under its own key: violation/system/policies/require_keyrng for a key the schema rejects, ignored/stored/require_keyring for a policy key in a user's own file, which mandates nothing.
- Each policy setting you deployed has a source.kind of system or registry in .data.settings, not default.
Helpdesk Troubleshooting Guide¶
| Symptom / Error Message | Root Cause | Remediation |
|---|---|---|
read CA bundle: open ...: no such file or directory |
Imaging race condition: BB_CA_FILE was set before the CA certificate was written to disk. |
Ensure the provisioning script copies the .pem file before setting the environment variable. |
OS keyring is unavailable and keyring-backed storage is required |
Running on a headless Linux host or remote SSH session without an active D-Bus session bus. | Launch a temporary D-Bus session: eval $(dbus-launch --sh-syntax) or supply credentials via BITBUCKET_TOKEN. |
Git prompts for password on git push/git pull |
Git credential helper is not scoped to the exact URL or scheme used by the remote. | Run git remote -v and configure: bb auth setup-git --host <remote-url>. |
certificate signed by unknown authority |
BB_CA_FILE is not set, or a GUI IDE failed to inherit shell environment variables. |
Set BB_CA_FILE in the IDE's "env" block or export it in /etc/zshenv / /etc/profile.d/bb.sh. |
host "..." is not permitted by administrative policy |
Target Bitbucket instance is not listed in allowed_hosts in system configuration or registry policy. |
Connect only to approved corporate hosts, or request security to add the instance to allowed_hosts. |
insecure TLS verification is disabled by administrative policy |
Attempted --insecure-skip-verify when prohibited by allow_insecure_skip_verify: false in system policy. |
Configure the corporate CA certificate rather than disabling TLS verification. |
overriding CA bundle is disabled by administrative policy |
Attempted to override mandated corporate CA bundle with a conflicting custom certificate. | Remove user-level BB_CA_FILE override and use the mandated corporate CA. |
self-update is disabled by administrative policy |
Self-update is disabled machine-wide (disable_update: true or BB_DISABLE_UPDATE=1). |
Update bb through your IT system package manager (apt, dnf, brew, winget). |
could not load the Sigstore trust material needed to verify the release manifest |
The host cannot reach https://tuf-repo-cdn.sigstore.dev, and no offline trust root is configured. The release itself is not implicated. |
Deploy a trusted_root.json and set update_trusted_root in system configuration (or update_tuf_url for a mirrored TUF repository). |
update_trusted_root is invalid |
The configured trusted root path does not exist on this host — typically an imaging race, the same one that bites ca_file. |
Ensure the provisioning script writes trusted_root.json before the configuration file that references it. |
update_trusted_root and update_tuf_url are mutually exclusive |
Both Sigstore trust sources are configured. | Keep the trusted root file for air-gapped hosts, or the TUF mirror URL — not both. |
the system configuration at ... could not be read |
The system configuration file is malformed, typically from a provisioning template or a partial write. bb fails closed rather than run without the policy. | Run bb doctor on the host: it lists every problem in the file with its line (see Checking the configuration). Redeploy the corrected file. Users cannot work around it, by design. |
| A policy setting is not enforced | The key is in a user or workspace configuration file, which bb reads no policy from. | Run bb doctor: it lists the key as ignored in that file, and names the source each policy setting comes from. Move the key to the system configuration. |
update_tuf_url must be an absolute https URL |
The configured mirror is a bare hostname, a relative path, or plain http. |
Give the full origin, for example https://artifactory.corp.internal/tuf. |
uses plain HTTP; pass --allow-http or set BB_ALLOW_HTTP_UPDATE=1 to permit it |
The update base URL, an asset URL in the mirror's manifest, or a redirect uses http://. |
Serve the mirror over https, or opt in explicitly for a mirror that has no TLS. |
plain-HTTP update URLs are disabled by administrative policy |
--allow-http or BB_ALLOW_HTTP_UPDATE was set on a host whose policy sets allow_http_update: false. |
Serve the mirror over https; the policy exists so the fleet cannot fall back to plain HTTP. |
uses plain HTTP, which administrative policy forbids |
An update URL uses http:// and policy sets allow_http_update: false. |
Serve the mirror over https. |
7. Day-2 Operations¶
PAT Expiration & Rotation¶
Personal Access Tokens expire based on enterprise TTL policies (e.g. 90 days). When rotating a token:
1. Generate a replacement token in Bitbucket Server (bb auth token create "Dev Token" --expiry-days 90 or via web UI).
2. Update the stored credential in the OS keyring without downtime:
printf "%s" "$NEW_TOKEN" | bb auth login https://bitbucket.example.com --token-stdin
bb dynamically, all local repositories immediately begin using the new token without needing .git/config updates.
Fleet Upgrades & Rollback¶
- Upgrades: Deploy new packages via system package managers (
apt,dnf,brew,winget). Stored credentials and git helpers persist across version upgrades. - De-provisioning & Rollback:
# 1. Log out and remove secrets from the OS Keyring bb auth logout --host https://bitbucket.example.com # 2. Remove git credential helper configuration git config --global --unset-all "credential.https://bitbucket.example.com.helper" # 3. Remove package apt remove bb # or: brew uninstall bb / winget uninstall vriesdemichael.bb
Related Security Documents¶
- Security Architecture and Threat Model: Detailed STRIDE methodology, trust boundaries, multi-OS policy analysis, and compliance matrix.
- Git Authentication Guide: Deep dive into host-scoped credential helper mechanics.
- Networks, Proxies and TLS: Network diagnosis and connection testing.
- Security Policy: Vulnerability reporting and disclosure policy.