Skip to main content

managing-workflow-secrets

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.

Informações da origem

Repositório
bitwarden/ai-plugins
Última atividade na origem
24 de agosto de 2026 às 18:39
Idioma detectado do SKILL.md
inglês
Estrelas
154
Forks
21

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Explorador de arquivos
13 arquivos

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
name
managing-workflow-secrets
description
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.
allowed-tools
Read, Glob, Grep, Edit
## Boundaries 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`. ## Secret exposure is the overriding concern 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](#secret-hygiene-checklist) is a concrete pass/fail check against the job you touched. Run it every time. ## The AKV + OIDC lifecycle Every job that needs a Key Vault secret follows the same four-beat sequence: ``` azure-login → get-keyvault-secrets → azure-logout → consume the step outputs ``` ```yaml jobs: 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: bitwarden/gh-actions/azure-login@main with: subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} tenant_id: ${{ secrets.AZURE_TENANT_ID }} client_id: ${{ secrets.AZURE_CLIENT_ID }} - name: Get Azure Key Vault secrets id: secrets uses: bitwarden/gh-actions/get-keyvault-secrets@main with: keyvault: KEY-VAULT secrets: "SECRET-NAME-1,SECRET-NAME-2" - name: Log out from Azure uses: bitwarden/gh-actions/azure-logout@main - name: Do work env: MY_TOKEN: ${{ steps.secrets.outputs.SECRET-NAME-1 }} # step outputs survive logout run: ./do-work.sh ``` `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: ```yaml secrets: >- SECRET-NAME-1, SECRET-NAME-2, SECRET-NAME-3 ``` Output 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. ## Golden rules (invariants) Treat a deviation as a finding. 1. **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)`.) 2. **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. 3. **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. 4. **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. 5. **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. ## Getting a secret to a downstream job or reusable workflow 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: ```yaml - 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: bitwarden/gh-actions/azure-logout@main - name: Generate GH App token id: app-token uses: actions/create-github-app-token@<full-40-char-sha> # vX.Y.Z — replace both with the real values with: app-id: ${{ steps.secrets.outputs.GH-APP-ID }} private-key: ${{ steps.secrets.outputs.GH-APP-KEY }} owner: ${{ github.repository_owner }} repositories: self-host # narrow the token's scope when possible - uses: actions/checkout@<full-40-char-sha> # vX.Y.Z — replace both with the real values with: token: ${{ steps.app-token.outputs.token }} ``` `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. **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. ```yaml 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: contents: read id-token: write # OIDC token is minted against the caller job's permissions ``` 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. ```yaml 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. ## Authoring procedure When asked to add or correct secret retrieval in a job: 1. **Confirm the secret genuinely needs AKV.** Pure CI steps (`format`, `lint`, `test`, `build` with no external service) usually need no secrets. See "When AKV is needed" below. 2. **Use the vault and secret names you were given — never infer them.** The `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. 3. **Wire `azure-login → get-keyvault-secrets → azure-logout`** in the job, using `id: secrets` on the retrieval step. 4. **Ensure `id-token: write`** is on the job, and keep the surrounding `permissions:` minimal. 5. **Place `azure-logout` correctly** — right after retrieval, unless a later step needs the live session, and matching any `if:` on the login. 6. **Use `@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. 7. **If the secret must reach another job or a reusable workflow**, use the mechanism above — re-retrieve per job, mint an App token for GitHub access, or forward the OIDC triad to the reusable workflow. If a reusable workflow is involved, **edit both sides**: match the caller's `secrets:` keys to the callee's `on.workflow_call.secrets:` declarations, and confirm each logging-in job carries `id-token: write`. 8. **Run the secret-hygiene checklist** before finishing. ### Secret hygiene checklist Because an exposed token is a critical failure, verify each of these on any job you touch: - Every secret is consumed through a **step-scoped `env:`**, not inlined into a `run:` argument. - No step `echo`s, `cat`s, prints, or writes a secret to a log, artifact, or committed file. - The retrieval step uses `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. ## When AKV is needed | 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 - `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.
Ver no GitHub