Webhook Secrets¶
A Bitbucket webhook can carry two credentials, and bb treats both as things it
holds rather than things it prints.
- The shared secret signs every delivery. Bitbucket sends it as
X-Hub-Signature: sha256=…so the receiving endpoint can prove the request came from Bitbucket and not from someone who learned the URL. - The endpoint credentials are a username and password
bbnever sees the second half of. Bitbucket sends them as anAuthorization: Basic …header on every delivery, so the endpoint can authenticate the caller.
What bb publishes, and what it does not¶
Bitbucket returns the shared secret in plaintext on every read of a webhook.
bb does not pass it on. What the model publishes instead is whether one is
configured:
$ bb webhook get 42 --json
{
"data": {
"webhook": {
"id": 42,
"name": "ci",
"url": "https://ci.example.com/hooks/bitbucket",
"active": true,
"events": ["repo:refs_changed"],
"sslVerificationRequired": true,
"scopeType": "repository",
"secretConfigured": true,
"credentialsUsername": "bitbucket"
}
}
}
Three details are worth knowing:
secretConfiguredis always present.secretis not — it appears only with--reveal-secret.sslVerificationRequiredis absent when the server did not report it, rather thanfalse. Absent and "TLS verification is off" are different answers, and this is the field an audit reads.- There is no field for the endpoint password. Bitbucket never returns it, so
bbhas nothing to publish.credentialsUsernameis the only half that comes back.
bb webhook test publishes the delivery record Bitbucket produced, and that
record contains the request headers — including Authorization. bb replaces
the value with <redacted>. Base64 is not encryption: the header carries the
endpoint password, and under --json that record is the machine contract.
Recovering a secret on purpose¶
--reveal-secret prints what would otherwise be redacted. It exists so that
losing a secret does not mean going to the database, and it has to be typed:
bb webhook get 42 --json --reveal-secret # the shared secret
bb webhook test 42 --json --reveal-secret # the Authorization header
Both write a warning to stderr saying a credential went through stdout, so a log that contains one also contains the note that it does. The warning never repeats the value.
Setting a secret¶
No flag takes a secret as its value (ADR-047): a flag value lands in the process argument list, which is world-readable on Linux, and in shell history. Two routes are open instead.
From the environment — the automation path¶
| Variable | Sets |
|---|---|
BB_WEBHOOK_SECRET |
the shared secret |
BB_WEBHOOK_PASSWORD |
the endpoint password |
export BB_WEBHOOK_SECRET="$(vault read -field=value secret/bitbucket/webhook)"
bb webhook create ci https://ci.example.com/hooks/bitbucket \
--event repo:refs_changed --ssl-verification=true
This is the form to reach for in a pipeline: the secret comes from wherever the runner keeps secrets and never appears in a command line.
An empty variable counts as unset, so a typo in a variable name two files away cannot quietly configure an empty credential.
From stdin — one secret at a time¶
printf '%s' "$SECRET" | bb webhook create ci https://ci.example.com/hooks/bitbucket --secret-stdin
printf '%s' "$PASSWORD" | bb webhook update 42 --credentials-username bitbucket --credentials-password-stdin
--secret-stdin and --credentials-password-stdin cannot both be given: there
is one stdin, and bb refuses rather than guessing which secret arrived. Put
the other one in its environment variable.
A secret piped on stdin may not contain spaces, tabs or newlines. A value that arrived with a stray space is far more likely to be a piping mistake than a real credential, and the failure it causes shows up much later, at the receiving endpoint. If your secret genuinely contains whitespace, set it through the environment instead.
Precedence¶
stdin wins over the environment. Piping is the explicit act; a variable can come from a shell profile the caller has forgotten.
A flag typed on this invocation outranks the environment entirely, which is what
makes --no-secret usable on a host that exports BB_WEBHOOK_SECRET for
everything:
bb webhook update 42 --no-secret # removes it, variable or no variable
bb webhook update 42 --no-credentials # removes the endpoint credentials
Combining --no-secret with --secret-stdin is refused. One of the two has to
win and neither should.
What an update leaves alone¶
Bitbucket's update endpoint replaces the webhook rather than patching it: a
field that does not arrive is cleared. bb reads the webhook first and sends
back what you did not mention, so --name changes the name and nothing else.
This matters most for the two credentials:
- The shared secret survives because Bitbucket returns it and
bbsends it straight back. Sending an update without a configuration object clears it. - The endpoint password survives for a different reason.
bbcannot send it back — Bitbucket never returned it — so it sends the credentials object with the username alone, and Bitbucket keeps the password it already had. This is verified against a live instance by observing theAuthorizationheader on a real delivery before and after an update, because no API response can show it.
Dry runs name the variable, not the value¶
A dry run is written to stdout, is what an operator reads before applying, and is what gets pasted into a ticket when it looks wrong. It says where the secret will come from and never what it is:
$ export BB_WEBHOOK_SECRET=…
$ bb webhook create ci https://ci.example.com/hook --dry-run --json
{
"preview": {
"tier": "preconditions-checked",
"effects": [
{
"action": "create",
"target": {
"repository": "PROJ/repo",
"name": "ci",
"url": "https://ci.example.com/hook",
"secret": "will be set from $BB_WEBHOOK_SECRET"
},
"outcome": "would-apply",
"reasons": ["webhook will be created"]
}
]
},
"meta": { "command": "webhook create", "bbVersion": "…" }
}
Naming the variable is also the more useful answer: the mistake a preview is
there to catch is reading the wrong one. Removing a credential shows as
"will be removed".
Where the fields can be set¶
The same flags are registered wherever a webhook is configured, so the answer to "can I set the shared secret here" does not depend on which command you reached for:
| Command | create | update |
|---|---|---|
bb webhook |
yes | yes |
bb project webhook |
yes | yes |
bb repo settings workflow webhooks |
yes | no update subcommand |