Removing long-lived secrets from CI/CD: fetching temporary credentials via OIDC or AppRole in GitHub Actions, and the native GitLab CI integration with the secrets keyword in .gitlab-ci.yml.

In episode 12 you connected OpenBao to Kubernetes via ServiceAccount auth and the sidecar injector. Episode 13 moves to the pipeline world: you will see how OpenBao keeps CI/CD free of permanent API keys, by fetching temporary credentials every time a pipeline runs. The two platforms covered are GitHub Actions and GitLab CI/CD.
Often repositories store API keys in Repository Secrets or variables — those values persist for a long time, are shared across all jobs, and one leak has a big impact. Not to mention the annoying rotation: change one key, and all configurations must be changed along with it.
A better way is to eliminate those permanent credentials altogether. The pipeline only stores the information on how to fetch credentials, not the credentials themselves. When a job runs, the pipeline requests a temporary token from OpenBao, uses that token for the duration of the job, and then the token expires on its own.
| Aspect | Long-Lived Secrets | Dynamic Secrets |
|---|---|---|
| Credential lifetime | Months to years | Minutes to hours |
| Rotation | Manual and easy to miss | Automatic via TTL |
| Leak impact | Very large | Limited and soon expired |
GitHub Actions has two ways to use OpenBao: OIDC for trust-based authentication between GitHub and OpenBao, or AppRole for a more classic approach.
With OIDC, GitHub issues a token containing claims about the repository, environment, and ref that runs the job. OpenBao accepts that token after verifying its signature. No secret needs to be stored in the repository at all — the pipeline's own identity is the credential.
If OIDC is not yet possible, AppRole remains a legitimate choice. The job uses role_id and secret_id to log in; secret_id should be one-time-use and short-lived, so even if it leaks, the value is useless outside that pipeline.
The hashicorp/vault-action action, known as vault-action, is compatible with OpenBao because their APIs are equivalent. Example workflow that fetches credentials before running a job:
name: deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Ambil kredensial dari OpenBao
uses: hashicorp/vault-action@v6
with:
url: https://bao.internal.example.com
method: approle
roleId: ${{ secrets.OPENBAO_ROLE_ID }}
secretId: ${{ secrets.OPENBAO_SECRET_ID }}
exportToken: true
secrets: secret/data/app db_password | DB_PASSWORD
- name: Jalankan deploy
run: ./deploy.sh
env:
DB_PASSWORD: ${{ env.DB_PASSWORD }}Note that only role_id and secret_id are stored in the repository, not the real database password. The actual value is fetched when the job runs and only lives within that job's environment. For OIDC, replace the with block with a method: jwt configuration plus a jwt_oidc_audience matching the OpenBao role.
GitLab offers a native integration via the secrets: keyword in .gitlab-ci.yml. The GitLab Runner contacts the OpenBao server, logs in according to the designated role, fetches the value, then injects it as an environment variable into the job.
vault:
variables:
VAULT_SERVER_URL: https://bao.internal.example.com
VAULT_AUTH_METHOD: approle
VAULT_ROLE_ID: $OPENBAO_ROLE_ID
VAULT_SECRET_ID: $OPENBAO_SECRET_ID
deploy:
stage: deploy
secrets:
DB_PASSWORD:
vault: secret/data/app/db_password@secret
script:
- ./deploy.shThe VAULT_SERVER_URL, VAULT_AUTH_METHOD, VAULT_ROLE_ID, and VAULT_SECRET_ID variables tell the Runner where and how to log in. The secrets: keyword then maps the secret value to the DB_PASSWORD environment variable before the script runs.
The advantage is that the integration happens at the Runner level — secrets are injected before the script runs, so the script itself does not need to know where the value came from. This also means secrets are not visible in the log output.
Note
CI/CD credentials are best fetched as dynamic secrets. For databases, use the database engine that issues a unique user with a TTL — not a static secret — so that role_id and secret_id are only the entrance, not the secret content itself.
| Aspect | GitHub Actions | GitLab CI/CD |
|---|---|---|
| Mechanism | hashicorp/vault-action action | Native secrets: keyword |
| Authentication | OIDC or AppRole | OIDC, JWT, or AppRole |
| Secret injection | Environment variable on the step | Environment variable before the script |
| Configuration | Done per action step | Keyword in .gitlab-ci.yml |
| Server config | Via the action's with block | VAULT_SERVER_URL variable |
Choose the platform that fits your ecosystem; the same principle applies: store only how to fetch credentials, let OpenBao issue the values when needed.
In this episode 13, you understood the danger of long-lived secrets in CI/CD, GitHub Actions integration via the hashicorp/vault-action action with OIDC or AppRole, and the native GitLab CI integration using the secrets: keyword in .gitlab-ci.yml.
Key takeaways:
secrets: injects values before the script runs, without touching the logs.In the next episode, episode 14, we automate infrastructure: integrating OpenBao with OpenTofu or Terraform and Ansible to fetch dynamic database credentials and PKI certificates during provisioning.