Skip to content

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

  1. System Configuration and Immutable Administrative Policies (ADR-058): Deploy a machine-level configuration file (/etc/bb/config.yaml on Linux/macOS, %ProgramData%\bb\config.yaml on 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
    
  2. JSON Schema Validation: All configuration files are validated against config.schema.json. Supplying the $schema directive enables live linting and autocompletion in VS Code and IntelliJ. On the host, bb doctor reports every key the schema rejects in the deployed file, and the source each policy setting comes from.
  3. require_keyring: true: Enforces OS keyring storage machine-wide; refuses fallback to plaintext files even if BB_REQUIRE_KEYRING is unset or set to 0. If a user sets BB_REQUIRE_KEYRING=0, bb outputs an explicit warning to stderr and continues enforcing keyring policy.
  4. ca_file: <path>: Mandates corporate Root CA bundle. Attempts to pass a conflicting CA file abort with an authorization error.
  5. allowed_hosts: [...]: Whitelists permitted Bitbucket Server / Data Center instances. Connection attempts to unlisted hosts abort with an authorization error.
  6. allow_insecure_skip_verify: false: Hard-refuses --insecure-skip-verify and BB_INSECURE_SKIP_VERIFY=true.
  7. allow_http_update: false: Hard-refuses plain-HTTP update URLs, bb update --allow-http and BB_ALLOW_HTTP_UPDATE=1.

  8. Enterprise Update Controls and Release Mirrors (ADR-059):

  9. 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 update by setting disable_update: true in system configuration or export BB_DISABLE_UPDATE=1. Alternatively, deploy the _noupdate builds described below, which cannot self-update whatever the configuration says.
  10. Internal Release Mirrors: In firewalled or air-gapped enterprise enclaves, configure bb update to query internal mirrors (e.g. JFrog Artifactory, Sonatype Nexus) instead of api.github.com via --base-url <url>, BB_UPDATE_BASE_URL, or update_base_url in 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 be https: a plain-HTTP mirror needs bb update --allow-http or BB_ALLOW_HTTP_UPDATE=1, and allow_http_update: false in system configuration refuses it for every user (true permits it fleet-wide).
  11. Offline Signature Verification (ADR-063): By default, bb update fetches Sigstore trust material from https://tuf-repo-cdn.sigstore.dev on every run. Deploy a trusted_root.json alongside the corporate CA bundle and point at it to verify releases with no internet access at all:
    update_trusted_root: /etc/bb/trusted_root.json
    
    Produce the file once, on a host that does have access:
    cosign trusted-root create > trusted_root.json
    
    This 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 set update_tuf_url: <url> instead; the two are mutually exclusive, it must be an absolute https URL, and the TUF fetch uses the configured ca_file and client certificates.
  12. Re-Signed Artifacts: Organisations that rebuild or re-sign bb against their own Fulcio instance replace the pinned signer with update_signature_identity (certificate SAN) and update_signature_issuer (OIDC issuer).
  13. Unverified Updates (Last Resort): allow_unverified_update: true skips signature verification entirely. SHA256 checksum verification remains mandatory, so this still catches corruption but not tampering; every run prints a warning to stderr and reports signature_skipped: true in --json output. Prefer an offline trust root.
  14. Policy Only: update_trusted_root, update_tuf_url, update_signature_identity, update_signature_issuer and allow_unverified_update are read from system configuration and Windows registry policy only — never from an environment variable or a flag. Each decides who may vouch for a binary bb is about to execute, and that decision does not belong to whoever can set a variable in a user's shell. update_base_url keeps its flag and environment forms, because signature verification still gates whatever the mirror serves.
  15. 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/latest or /latest, which bb tries in that order:

    {
      "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" }
      ]
    }
    
    Mirror bb_<version>_<os>_<arch>.tar.gz (.zip on Windows), sha256sums.txt, and sha256sums.txt.sigstore.json at the base URL. Asset URLs may be relative, as above, or absolute mirror URLs; a manifest copied verbatim from GitHub also works, since bb fetches off-mirror asset URLs from {base_url}/{asset_name} first rather than stalling on a firewalled github.com address. Verify a mirror without replacing any binary:
    bb update --dry-run --base-url https://artifactory.corp.internal/artifactory/bb-releases
    
    The dry run reports which trust material was used and whether the manifest signature and checksum entry were found.

  16. Mandate Keyring Storage (Advisory / User Tier):

    export BB_REQUIRE_KEYRING=1
    
    When set in user environments where system policy is not yet deployed, bb refuses to read credentials from or write credentials to the plaintext configuration fallback (~/.config/bb/config.yaml on Linux, ~/Library/Application Support/bb/config.yaml on macOS, or %AppData%\bb\config.yaml on Windows). Any command that would otherwise rely on plaintext fallback aborts with an error (ADR-047).

  17. Configure Host-Scoped Git Credential Helper:

    bb auth setup-git
    
    Writes a host-scoped credential helper into the user's global ~/.gitconfig:
    [credential "https://bitbucket.example.com"]
     helper = !"/usr/local/bin/bb" auth git-credential
    
    Note: bb writes the absolute executable path into the git configuration. Git queries bb dynamically on demand for that specific host, ensuring zero credentials are ever written into local repository .git/config files and credentials are never offered to external remotes (ADR-044).

  18. Disable Stored Config for Headless CI:

    export BB_DISABLE_STORED_CONFIG=1
    
    Ensures that ephemeral CI/CD runners read authentication strictly from BITBUCKET_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

  1. 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 in ps 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
    

  2. Clean Up Legacy Clones: Existing clones made before bb auth setup-git may 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
Ensure provisioning scripts place the CA certificate on disk before exporting 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_requests with 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_status and set_build_status address a commit SHA, which Bitbucket does not scope to a project. They disappear from tools/list while a scope is set. search_repositories is withheld under --repo for 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.

{
  "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
3. Because git queries 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