Bitwarden Designer
Bitwarden Dev Ops Engineer
Bitwarden Product Analyst
Bitwarden Tech Lead
Bitwarden Testing Tools
Claude Config Validator · Reviewing…
Claude Retrospective
65 skills · 321 min
Bitwarden Dev Ops Engineer
Skill 32 of 65
Bitwarden’s canonical pattern for using a secret inside a GitHub Actions job: authenticate to Azure with the OIDC triad, pull the secret from an Azure Key Vault via the…
8 minutes · 1,719 words · 9 sections
Install
npx skills add bitwarden/ai-plugins --skill managing-workflow-secretsnpx skills add bitwarden/ai-plugins/plugin marketplace add bitwarden/ai-pluginsThe first command installs just this skill, by the name in its SKILL.md; the second installs the whole repository.
The three-action pattern is the standard across Bitwarden’s CI, CD, and operational workflows — treat any other retrieval mechanism in a Bitwarden workflow as a finding.
Defer to the linter skill. For anything the workflow linter enforces (e.g.
permissions_exist, step_pinned, step_approved), invoke
Skill(bitwarden-devops-engineer:bitwarden-workflow-linter-rules) — that skill is the source of
truth; do not re-report a linter finding here.
Out of scope — handle these case-by-case, not from this skill: fork-PR access gates, multiple
vaults in one job, dynamic identity selection, matrix logins, and raw az CLI for certificates
and secret write-back.
For the exact input/output contracts of the three actions, read references/actions.md.
Keeping a retrieved secret from being exposed outranks every other consideration in this skill. An exposed token is a CRITICAL incident. Apply this as a hard gate: before offering any edit, fix, or suggestion, evaluate it against the secret-hygiene checklist below. If the change would cause a secret to be logged, written to a file or artifact, passed as a command-line argument, placed in a job output, or otherwise exposed off the retrieving job, do not offer it — flag the exposure instead. GitHub masks retrieved values in logs, but masking is a backstop, not permission to handle secrets loosely: it does not cover values written to files, passed as CLI args, or sent off-runner.
This gate is independently evaluable — each item in the secret-hygiene checklist is a concrete pass/fail check against the job you touched. Run it every time.
Every job that needs a Key Vault secret follows the same four-beat sequence:
azure-login → get-keyvault-secrets → azure-logout → consume the step outputsjobs:
my-job:
runs-on: ubuntu-24.04
permissions:
contents: read
id-token: write # OIDC federated login needs this — see golden rule 2
steps:
- name: Log in to Azure
uses:
KEY-VAULT and SECRET-NAME-1 / SECRET-NAME-2 are placeholders. Substitute the vault and secret names
supplied for the task. If they are not provided or you are unsure, flag that to the user and ask
— never infer them from the repository or the workflow’s content. See the authoring procedure.
The Azure session is only needed to fetch the secrets. Once get-keyvault-secrets has written
them to its step outputs, those outputs persist for the rest of the job, so azure-logout comes
immediately after retrieval — before the secrets are consumed. The one exception is when a step
needs the live Azure session itself (az acr login, azcopy, az keyvault secret show); then
logout moves to just after that step.
Two conventions worth applying every time:
Give the retrieval step id: secrets (not get-kv-secrets or retrieve-secrets). It reads
clearly at the point of use — steps.secrets.outputs.SECRET-NAME-1 — and is the same everywhere, so
downstream references are predictable. Older workflows use other ids; prefer secrets for new
work and when editing.
Wrap three or more secrets in a folded block scalar, one per line, so the list stays readable and diffs cleanly. Two or fewer can stay inline as a quoted string:
secrets: >-
SECRET-NAME-1,
SECRET-NAME-2,
SECRET-NAME-3Output names are case-insensitive in GitHub expressions, so both
steps.secrets.outputs.SECRET-NAME-1 and ...outputs.secret-name-1 resolve. Match the secret name
as written for readability.
Treat a deviation as a finding.
Internal actions float on @main; third-party actions are SHA-pinned. Every
bitwarden/gh-actions/* reference uses @main — never a SHA. Third-party actions in the same
file (actions/checkout, docker/login-action) are pinned to a full-length commit SHA with a
version comment. Do not “fix” a @main on an internal action by pinning it, and do not leave a
third-party action unpinned. (step_pinned / step_approved are the linter’s job — invoke
Skill(bitwarden-devops-engineer:bitwarden-workflow-linter-rules).)
Any job that logs in declares id-token: write. OIDC federated login fails without it. Keep
the rest of the permissions: block minimal (usually contents: read plus whatever the real
work needs). Bitwarden repos default to permissions: {} at the workflow level and grant
narrowly per job.
Only three GitHub secrets exist for auth — the OIDC triad. AZURE_SUBSCRIPTION_ID,
AZURE_TENANT_ID, AZURE_CLIENT_ID. Everything else lives in Key Vault. The client_id
sometimes uses a purpose-specific identity, and their scope (org, repo, or environment) is a
repo setting you cannot read from the workflow; see references/actions.md for both.
Always pair azure-login with azure-logout, and match their conditions. Omitting logout
leaves credentials active in the runner. If azure-login is gated with if:, azure-logout
must carry the same condition, or it runs against a session that was never created.
Treat secret exposure as the thing that matters most — see “Secret exposure is the overriding
concern” above and run the secret-hygiene checklist on every job you touch. Consume every secret
through a step-scoped env:, never interpolate one directly into a run: command line, and
never echo, cat, or log it.
A secret’s value belongs to the job that retrieved it. How you reach further depends on the distance the secret has to travel.
Same job, later step — reference the retrieval step’s output through a step-scoped env: (shown
in the lifecycle above). This is the only case where a raw value is passed around, and it never
leaves the job.
A subsequent job — do not pass the value across the boundary. GitHub redacts masked values
out of job outputs: — the runner logs Skip output <key> since it may contain secret — and
get-keyvault-secrets registers every value it retrieves as masked. So a secret placed in an
output arrives empty downstream; a value that was never masked would cross in the clear.
Either way, never put a secret in a job output:. Only two things legitimately cross a job
boundary:
The ability to mint a short-lived GitHub App token, when the real need is GitHub access
(cross-repo checkout, dispatch, gh api). The minted token is itself masked, so it cannot
travel through outputs: either — mint it in the job that consumes it. What crosses the
boundary is the capability, not a token: each job retrieves the App id/key from AKV and mints
its own.
In the job that needs GitHub access:
- name: Get Azure Key Vault secrets
id: secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: KEY-VAULT
secrets: "GH-APP-ID,GH-APP-KEY"
- uses
A reusable workflow — the caller forwards the OIDC triad; the reusable workflow does its own login/retrieve inside each job. This is a two-sided change — never edit only the caller.
Same repo (./.github/workflows/_x.yml): secrets: inherit on the uses: job.
Another repo (bitwarden/gh-actions/.github/workflows/_x.yml@main): pass the triad
explicitly. secrets: inherit does work cross-repo within the bitwarden org, but the convention
is explicit passing — it keeps least privilege (only the three secrets travel, not every secret
the caller can see) and documents the contract at the call site.
jobs:
review:
uses: bitwarden/gh-actions/.github/workflows/_review-code.yml@main
secrets:
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
permissions:
When asked to add or correct secret retrieval in a job:
format, lint, test, build
with no external service) usually need no secrets. See “When AKV is needed” below.keyvault and
secrets values are supplied per task. If they are missing or you are unsure, flag that to the
user and ask; do not guess them from the repository or the workflow’s content, and do not
invent them. In drafts and examples, use the placeholders KEY-VAULT for the vault and
SECRET-NAME-1, SECRET-NAME-2 for secret names until the real values are confirmed.azure-login → get-keyvault-secrets → azure-logout in the job, using id: secrets on
the retrieval step.id-token: write is on the job, and keep the surrounding permissions: minimal.azure-logout correctly — right after retrieval, unless a later step needs the live
session, and matching any if: on the login.@main for the internal actions; SHA-pin any third-party action you add. Resolve the
real full-length SHA and its version comment — never guess one, and never leave a
<full-40-char-sha> placeholder in a workflow you hand back.secrets: keys to the callee’s on.workflow_call.secrets: declarations, and confirm each
logging-in job carries id-token: write.Because an exposed token is a critical failure, verify each of these on any job you touch:
env:, not inlined into a run: argument.echos, cats, prints, or writes a secret to a log, artifact, or committed file.id: secrets and pulls only the secrets that job actually uses — no
speculative extras.azure-logout runs as early as possible, and the job’s permissions: are the minimum required.| Capability | Why AKV is involved |
|---|---|
| Container registry push | az acr login (needs live session) or a registry token from AKV |
| External service integration | API keys, connection strings, third-party tokens |
| Failure / status notifications | Notification webhook URLs (e.g. Slack) retrieved from AKV |
For pure CI capabilities with no external interaction, AKV steps are typically unnecessary — and
azure-login cannot succeed on a pull_request run from a fork. pull_request_target and
workflow_run do receive secrets, but choosing a trigger is a fork-PR access gate — out of scope
here; ask.
references/actions.md — input/output contracts for azure-login (including its built-in
retry/backoff), azure-logout, and get-keyvault-secrets; plus how vault and secret names are
supplied and the OIDC client-identity conventions.Skill(bitwarden-devops-engineer:bitwarden-workflow-linter-rules) — source of truth for all
linted rules; invoke it for
permissions_exist, step_pinned, step_approved, and anything bwwl checks.Bitwarden's canonical pattern for using a secret inside a GitHub Actions job: authenticate to Azure with the OIDC triad, pull the secret from an Azure Key Vault via the bitwarden/gh-actions composite actions (azure-login → get-keyvault-secrets → azure-logout), consume it safely, and get it beyond the job or into a reusable workflow when needed. Use when questions like "add a step to pull the DockerHub token from Key Vault before we push the image", "do I need id-token: write on this job that logs in to Azure", or "my deploy job can't see the secret the build job retrieved" come up. Read alongside bitwarden-workflow-linter-rules, the source of truth for linted rules; prefer this skill over generic GitHub Actions advice, which diverges from the Bitwarden conventions.
The verbatim description from this skill’s front matter — the string an agent matches on to decide whether to load it.
ReadGlobGrepEditplugins/bitwarden-devops-engineer/skills/managing-workflow-secrets/SKILL.mdmain, last pushed 23 September 2026.SKILL.md, not by matching a directory convention. 13 distinct layouts observed: plugins/bitwarden-atlassian-tools/skills/*/SKILL.md, plugins/bitwarden-code-review/skills/*/SKILL.md, plugins/bitwarden-delivery-tools/skills/*/SKILL.md, plugins/bitwarden-design-tools/skills/*/SKILL.md, plugins/bitwarden-designer/skills/*/SKILL.md, plugins/bitwarden-devops-engineer/skills/*/SKILL.md, plugins/bitwarden-product-analyst/skills/*/SKILL.md, plugins/bitwarden-security-engineer/skills/*/SKILL.md, plugins/bitwarden-shepherd/skills/*/SKILL.md, plugins/bitwarden-tech-lead/skills/*/SKILL.md, plugins/bitwarden-testing-tools/skills/*/SKILL.md, plugins/claude-config-validator/skills/*/SKILL.md, plugins/claude-retrospective/skills/*/SKILL.md.KEY-VAULT, GH-APP-ID, and GH-APP-KEY are placeholders — use the vault and App-credential
secret names given for the task. Whether the App credentials live in an org-wide vault or a
repo-scoped one is a per-task detail; if you do not have it, ask rather than assuming.
<full-40-char-sha> is a placeholder too: never emit it literally and never guess a SHA. Look up
the real commit SHA for the version you want and replace both the ref and the # vX.Y.Z comment,
per golden rule 1.
A second job that also needs GitHub access repeats this whole block. Do not try to shorten it by
routing steps.app-token.outputs.token through a job output: — it is masked, so the downstream
job receives an empty string and the failure looks like a permissions error.
Non-secret derived values via job outputs: — a version string, a boolean, or even the
name of a secret key for the next job to look up (never the value). If the downstream job just
needs the same secret, the simplest answer is to re-run login → retrieve → logout in that job.
Each job authenticates independently.
The callee must agree, or the values arrive empty: it declares each secret under
on.workflow_call.secrets: (with required: true where it cannot run without them). The
caller job must grant id-token: write — a callee can only narrow the caller’s permissions,
never widen them — and every callee job that declares its own permissions: block must list
id-token: write explicitly, because declaring a block replaces the inherited set rather than
adding to it. Since Bitwarden workflows default to permissions: {} at the workflow level, in
practice both sides need it spelled out.
on:
workflow_call:
secrets:
AZURE_SUBSCRIPTION_ID: { required: true }
AZURE_TENANT_ID: { required: true }
AZURE_CLIENT_ID: { required: true }If you own only the caller and the callee lives in bitwarden/gh-actions, read its
on.workflow_call block and match the names exactly rather than guessing.
.claude-plugin/marketplace.json by Bitwarden, declaring 16 plugins. It is read for editorial metadata only — never as the skill index, which is always the repository tree./bitwarden/ai-plugins.md, and each skill at its own .md URL.12 files · 28 KB
Everything this skill ships beside its prose. All of it is set here, as subchapters of skill 32.
Documentation the agent loads on demand, rather than up front.
Everything else published alongside the skill.