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:
- โ
Create and rotate Woodpecker secrets via
woodpecker-cliand the UI - โ Choose the right scope (repo vs org vs global) for each credential
- โ Restrict secrets to specific images to limit blast radius
- โ Understand log masking โ and its limits
- โ Bridge Woodpecker to HashiCorp Vault for short-lived credentials
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:
- Masking matches the exact string; base64/hex transformations of a secret will print.
- Values shorter than a few characters may not mask reliably.
- Anything you
echofrom a file containing the secret (e.g. a written kubeconfig) isn't masked unless the raw literal appears verbatim.
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:
- Store exactly one long-lived secret in Woodpecker: a Vault role ID / limited-scope token pair.
- 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
- 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
- Apply least privilege to triggers too: branch-protection
- Scan repos/pipelines for accidental hard-coded secrets: security-scanning
- Deepen Vault integration: vault-secrets, vault-authentication
Sources & Related Articles
External references consulted:
- https://woodpecker-ci.org/docs/usage/secrets
- https://developer.hashicorp.com/vault/docs/auth/approle
Related knowledge-base articles:
Change Log
2026-08-26
- Initial creation covering Woodpecker secret scopes, CLI management, masking limits, image restrictions, and Vault bridging.