GitHub Actions
Replace GitHub repository / organization Secrets with ScaiVault reads at job start. Lets you rotate credentials without re-editing GitHub Secrets, keep a unified audit trail (who-read-what including CI), and apply path-pattern policies that GitHub Secrets can't express.
Authentication: OIDC, not static tokens#
Don't store a long-lived ScaiVault token as a GitHub Secret. Use OIDC-to-ScaiKey token exchange — every workflow run gets a short-lived JWT scoped to that run.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
Behind the scenes the action:
- Requests an OIDC token from GitHub with
aud=scaivault. - Calls
POST /v1/auth/exchangeon ScaiVault with the OIDC token. - Exports
SCAIVAULT_TOKENfor subsequent steps. Token TTL: 1 hour (job time).
You need to register GitHub's OIDC issuer with ScaiKey once. See the Authentication reference.
Read secrets at job start#
After auth, secrets are normal API calls:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
::add-mask:: tells GitHub Actions to redact the value if it ever appears in logs. Always add it after reading a sensitive value.
Better: an action that handles the masking#
1 2 3 4 5 6 | |
The action reads each secret, extracts the named field, exports it as an env var, and adds it to the mask list. Behaves like aws-actions/configure-aws-credentials does for AWS but for ScaiVault.
Pattern: deploy keys with policy scoping#
Each repo / environment gets its own ScaiKey service account. The corresponding ScaiVault policy is narrow:
1 2 3 4 5 6 7 8 9 10 11 12 | |
When sa:github-deployer-billing is compromised, you rotate one binding and revoke that one identity. No one else is affected.
For the OIDC trust to be safe, scope the trust relationship in ScaiKey to specific GitHub repos and branches:
1 2 3 4 5 6 | |
Without subject scoping, any GitHub Actions workflow in your org could claim the identity.
Pattern: dynamic credentials per deploy#
For deploys that need short-lived database access (running migrations), use dynamic secrets:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | |
15-minute TTL covers a typical migration. The if: always() revoke ensures the user goes away even if the migration crashes.
Pattern: caching for matrix workflows#
Workflows with many parallel jobs hitting the same secrets can exceed rate limits. Use a single auth job that fetches once and passes secrets to downstream jobs via outputs (encrypted by GitHub Actions runtime):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | |
The GPG dance is necessary because GitHub outputs aren't encrypted between jobs the same way Secrets are. For most cases, just re-authenticating per job is simpler and cheap enough.
Audit observation#
Every workflow run shows up in the ScaiVault audit log under the service account identity. Filter:
1 | |
Each entry has the workflow's request ID, which you can correlate with GitHub Actions' run ID via the X-GitHub-Run-ID header (ScaiVault stashes it in extra_data).
Common pitfalls#
Forgetting ::add-mask::. If you echo a fetched secret, GitHub Actions doesn't know it's sensitive. Logs leak. Always mask immediately after reading.
Long-lived tokens stored as Secrets. GitHub Secrets aren't a great place for ScaiVault credentials. OIDC fixes this. If you really can't use OIDC (self-hosted runner, weird network), at least rotate the static token quarterly and bind it to one specific role.
Loose OIDC trust. subject_pattern: "repo:acme-org/*" lets any repo in the org claim the identity. Scope to the specific repo and branch.
Reading in plan, applying in apply. Don't read secrets during terraform plan (it runs on PRs from forks too — leakage path). Read only in terraform apply jobs running on protected branches.
What's next#
- Kubernetes integration — same patterns for in-cluster workloads.
- Authentication — token-exchange endpoint details.
- Dynamic Postgres tutorial — the engine that makes the migrations pattern above work.