CI Secrets Management - Woodpecker Secrets Done Right

Status: Active Last Updated: 2026-08-26 Category: CI/CD - Woodpecker Security Prerequisites: woodpecker-installation, vault-secrets Time: 1-2 hours Tags: woodpecker, secrets, security, vault, masking, registry

Summary

Woodpecker stores pipeline credentials as encrypted secrets with four scoping levels (repository, organization, global) and optional per-image restrictions. This article covers managing them via CLI and UI, masking in logs, and how to combine Woodpecker-native secrets with the homelab's existing Vault setup.

๐ŸŽฏ What You'll Learn

By the end of this article, you'll be able to:


Context / Why This Matters

CI is where your most dangerous credentials concentrate: registry push tokens, deploy SSH keys, cloud API keys. A leak here means an attacker can push poisoned images or reach production. The design goal: each secret readable by the fewest pipelines possible, never printed, rotated on a schedule. This article extends the patterns from vault-secrets into the pipeline layer.


Implementation / Core Content

Scoping Model

Scope Visible to Use for
Repository One repo's pipelines Deploy keys, per-project API tokens
Organization All repos in one org Shared registry creds, org-wide Sonar token
Global Admin-managed; all repos Base images pull creds, infra-wide tokens

Rule of thumb: start at repository scope; promote to org only when โ‰ฅ3 repos need it; global only for truly instance-wide values. Org and global secrets can additionally be limited by event (push, tag, โ€ฆ) so, e.g., production deploy keys are only available on tag events.

Managing Secrets via CLI

Authenticate first:

export WOODPECKER_SERVER="https://ci.example.com"
export WOODPECKER_TOKEN="<personal token: avatar โ†’ Settings โ†’ API clients>"

woodpecker-cli secret ls --org acme-corp

Create examples:

# Repository-scoped registry password, push-only events
woodpecker-cli secret add \
  --repository acme-corp/api-service \
  --event push --event tag \
  --image woodpeckers/plugin-docker-buildx \
  registry_password

# Read value from stdin (avoids shell history)
woodpecker-cli secret add --repository acme-corp/api-service \
  deploy_ssh_key < ~/.ssh/deploy_ed25519

# Organization-wide
woodpecker-cli secret add --organization acme-corp sonar_token

# Global (admin only)
woodpecker-cli secret add --global harbor_pull_token

Rotation is delete + re-add (values are write-only):

woodpecker-cli secret rm --repository acme-corp/api-service registry_password
woodpecker-cli secret add --repository acme-corp/api-service registry_password

Consuming Secrets in Pipelines

steps:
  build:
    image: woodpeckers/plugin-docker-buildx
    settings:
      username:
        from_secret: registry_user
      password:
        from_secret: registry_password

  deploy:
    image: alpine:3.20
    environment:
      DEPLOY_KEY:
        from_secret: deploy_ssh_key
    commands:
      - echo "$DEPLOY_KEY" > /tmp/key && chmod 600 /tmp/key
      - ssh -i /tmp/key deploy@prod.example.com ./deploy.sh $CI_COMMIT_TAG

Log Masking

Secrets injected via from_secret are automatically masked as ***** in step logs. Know the limits:

Mitigations:

commands:
  # Never print transformed secrets; redirect to files instead
  - echo "$DEPLOY_KEY" > /tmp/key && chmod 600 /tmp/key
  # Scrub files before any debug output
  - set +x

Image Restrictions

Adding --image ... pins a secret so only that plugin/image receives it. Even if another step is compromised (malicious image, script injection), it cannot read the restricted secret. Restrict at minimum: registry passwords, deploy keys, cloud keys.

Bridging to Vault

For teams already running Vault (vault-secrets), prefer short-lived dynamic credentials over long-lived Woodpecker secrets:

  1. Store exactly one long-lived secret in Woodpecker: a Vault role ID / limited-scope token pair.
  2. In the pipeline, authenticate and fetch short-lived secrets at runtime:
steps:
  fetch-secrets:
    image: hashicorp/vault:1.17
    environment:
      VAULT_ADDR: https://vault.internal:8200
      VAULT_ROLE_ID:
        from_secret: vault_role_id
      VAULT_SECRET_ID:
        from_secret: vault_secret_id
    commands:
      - export VAULT_TOKEN=$(vault write -field=token auth/approle/login role_id=$VAULT_ROLE_ID secret_id=$VAULT_SECRET_ID)
      - vault kv get -field=pg_password secret/ci/api-service > .pg_password
      - chmod 600 .pg_password   # consumed by later steps via workspace file
  1. Give the Vault policy read access only to that app's path, with TTLs โ€” a leaked CI secret then expires quickly.

Audit regularly:

woodpecker-cli secret ls --global
woodpecker-cli secret ls --org acme-corp
# For each entry ask: still used? correct scope? restricted to images? rotate date?

Practical Examples

Example 1: Registry credentials, properly scoped

openssl rand -hex 24 > /tmp/regpass   # generate strong value
woodpecker-cli secret add --org acme-corp \
  --image woodpeckers/plugin-docker-buildx \
  --event push --event tag \
  registry_password < /tmp/regpass && shred -u /tmp/regpass

Pipeline consumption shown above under "Consuming Secrets". Verify behavior with a deliberate bad value: logs should show *** not the typo.

Example 2: Quarterly rotation script skeleton

#!/usr/bin/env bash
set -euo pipefail
REPO="acme-corp/api-service"
new=$(openssl rand -hex 24)
printf '%s' "$new" | woodpecker-cli secret add --repository "$REPO" registry_password
echo "rotated $(date -Isec)" >> ~/ci-secret-rotation.log
# update the same value in Harbor/Vault UI out-of-band

Common Pitfalls & Troubleshooting

Problem Cause Fix
from_secret yields empty value Secret name mismatch, wrong scope, or event filter excludes this run Check secret ls output names/events vs pipeline metadata
Secret visible in logs Transformed/derived value, or echoed file content Avoid printing; use file indirection; keep values long
New pipeline can't see org secret Org secret limited to certain repos/images Edit secret in UI: Repositories/Images tabs
CLI says forbidden Token lacks admin rights for org/global scopes Use an admin account or stay within repo scope
Secret leaked in PR from fork Fork PRs shouldn't get secrets Ensure secrets have event filters excluding forked PRs; Woodpecker blocks secrets on fork PRs by default โ€” don't override
Vault AppRole login fails from agent Network egress blocked from runner Allow agent โ†’ Vault 8200; verify VAULT_ADDR TLS trust
Rotation broke deploys mid-run Value changed while old run cached env Rotate between releases; re-run failed pipelines after rotation

Next Steps / Ops Actions

Sources & Related Articles

External references consulted:

Related knowledge-base articles:

Change Log

2026-08-26

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse