Managing Workflow Secrets logo

Managing Workflow Secrets

Organization
bitwarden
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.

Overview

Publisherbitwarden
Repositoryai-plugins
Skill namemanaging-workflow-secrets
Stars
149
Forks
19
Bundled files
11
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 11 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by bitwarden on GitHub. Read the source before you install it.

Installation

Install the Managing Workflow Secrets AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/bitwarden/ai-plugins.git /tmp/ai-plugins
mkdir -p .claude/skills
cp -r /tmp/ai-plugins/plugins/bitwarden-devops-engineer/skills/managing-workflow-secrets .claude/skills/managing-workflow-secrets
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Managing Workflow Secrets in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Managing Workflow Secrets on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Managing Workflow Secrets is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

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 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 echos, cats, 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

CapabilityWhy AKV is involved
Container registry pushaz acr login (needs live session) or a registry token from AKV
External service integrationAPI keys, connection strings, third-party tokens
Failure / status notificationsNotification 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.

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

Frequently asked questions

What does the Managing Workflow Secrets AI skill do?

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 alo...

Why use Managing Workflow Secrets on TypingMind?

Because you install it once and use it with any model. Managing Workflow Secrets is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Managing Workflow Secrets in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/bitwarden/ai-plugins/tree/main/plugins/bitwarden-devops-engineer/skills/managing-workflow-secrets. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use Managing Workflow Secrets?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Managing Workflow Secrets?

As many as you like. As long as a model supports skills, you can use Managing Workflow Secrets with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Managing Workflow Secrets AI skill free?

It is published on GitHub by bitwarden. Check the repository for licensing terms. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇