Secrets Management, External Secret Providers, Protected Data, Rotation, and Least-Privilege Patterns: Guided Hands-On Workflow and Core Operations
Turn the secret-lifecycle model into a disposable experiment. You will compare a masked/protected synthetic variable with a local external-provider simulation, request secret material only inside an authorized job, verify presence by metadata and digest rather than content, rotate the synthetic secret, and prove that the old version is rejected after revocation.
Learning objectives
- Use only generated synthetic values and temporary paths in a disposable project/local environment.
- Compare a protected/masked synthetic variable with a job-scoped provider simulation without logging either raw value.
- Prove authorized retrieval using version, byte count, digest, principal, and pipeline/job identity rather than secret content.
- Rotate a synthetic secret from v1 to v2 and prove the provider refuses the revoked v1 reference after rotation.
- Capture cleanup evidence showing temporary secret material and synthetic project settings were removed.
1. Scenario: two storage models, one safe evidence rule
Use a throwaway GitLab project or run the shell-only portions locally. The chapter compares:
- a synthetic project variable, optionally marked masked/protected, to show GitLab’s built-in variable controls;
- a local provider simulation that generates v1/v2 material outside the repository, applies a simple principal/version authorization policy, and returns a temporary file only when authorized.
The simulation is deliberately not presented as cryptographic security. Its purpose is to make the authorization/retrieval/rotation states observable without requiring Premium features, a cloud subscription, or a real Vault server.
2. Preflight: prove the project and source context before adding synthetic data
LAB_BRANCH="ch07/secrets-lab"
EVIDENCE_DIR="ch07-evidence"
mkdir -p "$EVIDENCE_DIR"
git status --short --branch
git rev-parse HEAD | tee "$EVIDENCE_DIR/head-before.txt"
git remote -v | tee "$EVIDENCE_DIR/remotes-before.txt"
# In a CI job, print only an allowlist:
printf 'source=%s\n' "$CI_PIPELINE_SOURCE"
printf 'ref=%s\n' "$CI_COMMIT_REF_NAME"
printf 'sha=%s\n' "$CI_COMMIT_SHA"
If you create a project variable for comparison, name it
LAB_FAKE_CREDENTIAL and use a generated dummy value
unrelated to any real system. Mark it masked; optionally protect it
on an isolated protected lab branch. Record only its metadata and
presence, never its value.
3. Build the local provider simulation outside the repository
PROVIDER_DIR="$(mktemp -d)"
trap 'rm -rf "$PROVIDER_DIR"' EXIT
mkdir -p "$PROVIDER_DIR/versions"
umask 077
openssl rand -hex 32 > "$PROVIDER_DIR/versions/v1"
openssl rand -hex 32 > "$PROVIDER_DIR/versions/v2"
printf 'v1\n' > "$PROVIDER_DIR/active"
provider_fetch() {
principal="$1"
requested="$2"
output="$3"
active="$(cat "$PROVIDER_DIR/active")"
if [ "$principal" != 'project/ch07:deploy' ]; then
printf 'provider_result=denied principal=%s\n' "$principal" >&2
return 77
fi
if [ "$requested" != "$active" ]; then
printf 'provider_result=revoked requested=%s active=%s\n' "$requested" "$active" >&2
return 78
fi
cp "$PROVIDER_DIR/versions/$requested" "$output"
chmod 600 "$output"
printf 'provider_result=allowed version=%s\n' "$requested"
}
The generated bytes never enter Git, project settings, artifacts, or logs. The function models two decisions: “is this principal allowed?” and “is this requested version active?” A real provider replaces those shell comparisons with cryptographic authentication and authorization policy.
5. Denied retrieval: authorization failure is successful evidence
set +e
provider_fetch 'fork/untrusted' 'v1' "$PROVIDER_DIR/denied" 2> "$EVIDENCE_DIR/denied.txt"
rc=$?
set -e
test "$rc" -eq 77
test ! -e "$PROVIDER_DIR/denied"
printf 'denied_job_assertion=passed rc=%s\n' "$rc"
A denial is not a broken pipeline if denial is the expected security outcome. The job can assert the provider returned the expected denial and still complete successfully, while preserving the non-secret denial reason.
6. Compare a masked/protected synthetic variable without printing it
if test -n "${LAB_FAKE_CREDENTIAL:-}"; then
printf 'project_variable_present=yes\n'
printf 'project_variable_length=%s\n' "${#LAB_FAKE_CREDENTIAL}"
else
printf 'project_variable_present=no\n'
fi
7. Rotate v1 → v2, then prove v1 is revoked
# Activate v2.
printf 'v2\n' > "$PROVIDER_DIR/active"
# Old reference must fail.
set +e
provider_fetch 'project/ch07:deploy' 'v1' "$PROVIDER_DIR/old" 2> "$EVIDENCE_DIR/revoked-v1.txt"
old_rc=$?
set -e
test "$old_rc" -eq 78
test ! -e "$PROVIDER_DIR/old"
# New active reference succeeds.
provider_fetch 'project/ch07:deploy' 'v2' "$PROVIDER_DIR/new"
printf 'rotation_new_sha256=%s\n' "$(sha256sum "$PROVIDER_DIR/new" | awk '{print $1}')"
rm -f "$PROVIDER_DIR/new"
printf 'rotation_status=v2-active-v1-revoked\n'
The key proof is not the v2 digest. It is the pair of outcomes: v2 retrieval succeeds for the authorized principal and v1 retrieval is denied after rotation.
8. Map each simulation state to a real provider integration
| Simulation | Real integration analogue |
|---|---|
project/ch07:deploy principal |
GitLab job identity represented by an ID token and provider-validated claims. |
active file |
Provider-side active version/alias/secret metadata. |
provider_fetch authorization |
Vault/cloud-secret-manager policy evaluating identity and requested secret. |
| Copied temp file | Runner materializes requested secret, commonly as a temporary file by default. |
| Return 77/78 | Provider authorization denial or missing/revoked version response. |
rm -rf cleanup |
Runner/job teardown plus provider-side revocation/retention policy. |
9. Optional architecture exercise: Vault-style request with OIDC
Do not execute this unless you own a disposable Vault/GitLab integration. The point is to identify the control surfaces:
vault_example:
id_tokens:
VAULT_ID_TOKEN:
aud: https://vault.example.invalid
secrets:
LAB_PASSWORD:
vault: lab/app/password@ops
token: $VAULT_ID_TOKEN
script:
- test -f "$LAB_PASSWORD"
- printf 'vault_secret_materialized=yes\n'
Provider configuration must bind the audience and meaningful GitLab claims. A successful GitLab job token minting event does not prove Vault authorization; the provider policy owns that decision.
10. Challenge: choose the correct repair layer
An authorized job has the correct secret reference but receives a provider denial. Which layer should you inspect first?
- Confirm source/ref/SHA and that the intended job ran.
- Confirm the token/audience configuration without logging the token.
- Confirm provider trust-policy claims and requested secret path/version.
- Only then investigate network/runtime issues.
Do not “fix” a 403 by widening the provider policy to every project or every ref. Authorization failures should be repaired by reconciling intended identity with the narrow policy.
11. Cleanup and verification
-
The
trapremoves the temporary provider directory automatically; verify it no longer exists. -
Remove only
LAB_FAKE_CREDENTIALand any protected lab branch/rule you created for this disposable project. - Preserve only non-secret evidence: pipeline/job IDs, source/ref/SHA, provider/principal/version labels, result codes, synthetic digests, and cleanup confirmation.
- Never archive the generated secret files themselves.
Knowledge check
Why does the local provider store live outside the repository checkout?
To model the key property that secret material is not versioned with application source/configuration.
What does return code 77 prove in the simulation?
The provider policy rejected the unauthorized principal and did not create an output secret file.
What two observations complete the rotation proof?
The authorized principal can retrieve v2 and the same principal can no longer retrieve revoked v1.
Why is the simulation not a replacement for Vault or a cloud secrets manager?
Its authorization is simple shell logic and provides no cryptographic identity, durable audit, HA, provider policy engine, or production lifecycle guarantees.
Why must a real provider 403 not be repaired by broadening policy blindly?
The denial may be correctly enforcing least privilege; first reconcile the intended job identity/claims/path with the narrow trust policy.
Official references and version notes
- Pipeline security — current guidance that CI/CD variables are less secure than dedicated secret-management providers and that sensitive values should use stronger secret-management controls where possible.
- CI/CD variables — masking, hiding, protection, fork/MR behavior, file variables, and explicit warnings that masking is not a defense against malicious job code.
- External secrets in CI/CD — current supported provider integrations and tier/offering requirements.
- OIDC authentication using ID tokens — job-scoped ID tokens, audiences, claims, and third-party trust boundaries.
-
HashiCorp Vault secrets
—
id_tokens,secrets:vault, file materialization, and provider-side authorization. - GitLab Secrets Manager — current limited/beta availability, permissions, branch/environment scoping, Runner requirements, rotation reminders, and file-by-default behavior.
- Secret detection — defense-in-depth for accidentally committed credentials; detection is not a substitute for rotation after exposure.
Secret-management behavior in this chapter was rechecked against current primary GitLab documentation on 2026-09-11. External-secret integrations documented by GitLab are currently Premium/Ultimate; ID tokens are available on Free/Premium/Ultimate. GitLab Secrets Manager is availability/billing sensitive and currently requires GitLab Runner 19.0 or later for CI access, so it is discussed as an optional current-platform path rather than a mandatory lab dependency. The mandatory exercises use generated synthetic values and a local provider simulation.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.