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.

Quellinformationen

Repository
bitwarden/ai-plugins
Letzte Quellaktivität
24. August 2026 um 18:39
Erkannte Sprache von SKILL.md
Englisch
Sterne
154
Forks
21

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

Datei-Explorer
13 Dateien

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
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.
Auf GitHub ansehen