OnCallReady

Lesson 32.16 · Vault & Secrets Management · 20 min read

AppRole: logins for CI jobs and services

In plain words

Imagine a parcel locker at a train station. The locker number is printed on the order email; that alone does not open it. The one-time code is sent separately, works once, and expires in an hour. You need both, and if someone else used the code first, the locker tells you so.

AppRole is login for machines built the same way. The role ID is like the locker number: not secret, it can sit in the job's configuration. The secret ID is the one-time code: short-lived, limited uses, and ideally delivered response-wrapped, so what travels through logs and systems is a single-use wrapping token, not the secret itself. The job unwraps it, logs in with both halves, gets a short token with a narrow policy, and the secret ID is worthless afterwards.

Why this lesson exists

People log in with a password or SSO. A CI job, a cron job or a service on a VM cannot type a password - and if you give it a long-lived token in a CI variable, you are back to the static secret Vault was supposed to replace. AppRole is Vault's login for machines: two pieces, delivered separately, one of them short-lived and single-use. This lesson is how it works and how to deliver the pieces without either leaking.

What you need to know already: tokens, TTLs and orphans (the tokens lesson); policies and testing them with a token (the policies lesson); CI variables and secrets (25.7).

The words you need first

A role for a CI job

$ vault auth enable approle
Success! Enabled approle auth method at: approle/
$ vault write auth/approle/role/ci-deploy token_policies=ci-deploy token_ttl=10m token_max_ttl=15m secret_id_ttl=1h secret_id_num_uses=1 secret_id_bound_cidrs=10.0.5.0/24
Success! Data written to: auth/approle/role/ci-deploy

Read it back - the role is the whole contract between Vault and the job:

$ vault read auth/approle/role/ci-deploy
Key                        Value
---                        -----
bind_secret_id             true
local_secret_ids           false
max_ttl                    15m
period                     0s
policies                   [ci-deploy]
secret_id_bound_cidrs      [10.0.5.0/24]
secret_id_num_uses         1
secret_id_ttl              1h
token_bound_cidrs          []
token_explicit_max_ttl     0s
token_max_ttl              15m
token_no_default_policy    false
token_num_uses             0
token_period               0s
token_policies             [ci-deploy]
token_ttl                  10m
token_type                 default
ttl                        10m

(max_ttl, period, policies, ttl are the old names of the token_* fields; Vault shows both.)

The two halves

$ vault read auth/approle/role/ci-deploy/role-id
Key        Value
---        -----
role_id    65c26ebb-4844-5f3b-af7e-54e3f3a4c447
$ vault write -f auth/approle/role/ci-deploy/secret-id
Key                   Value
---                   -----
secret_id             7084e0fa-db56-bb55-f4dd-fb66e3752d7c
secret_id_accessor    8c55aa7c-179c-8d7f-3bf5-1f5960d73e65
secret_id_num_uses    1
secret_id_ttl         1h

-f (force) because the request has no data. The secret ID accessor, like a token's, lets you look up and destroy a secret ID without knowing it: vault list auth/approle/role/ci-deploy/secret-id lists accessors.

Logging in

The login is a write to auth/approle/login - there is no vault login -method=approle in the CLI, because machines use the API:

$ vault write auth/approle/login role_id=$RID secret_id=$SID
Key                     Value
---                     -----
token                   hvs.CAESIh1Ux9mWvfaUz3jDvjmfJVhzSuPJSuf3Wn8TaaGh4KHGh2cy4O4xZ40g4YDYSPLQxOsYns8oHRpDq0BFc
token_accessor          9Fx1xI4JNU65baruS4InFfTz
token_duration          10m
token_renewable         true
token_policies          ["ci-deploy" "default"]
identity_policies       []
policies                ["ci-deploy" "default"]
token_meta_role_name    ci-deploy

The same request over HTTP, as a pipeline step would send it:

# an illustration (no ▶)
curl -s --request POST --data "{\"role_id\":\"$RID\",\"secret_id\":\"$SID\"}" $VAULT_ADDR/v1/auth/approle/login | jq -r .auth.client_token

The errors are deliberately vague: a wrong role ID, a wrong, expired or used-up secret ID all give the same answer, so an attacker learns nothing:

$ vault write auth/approle/login role_id=nope secret_id=nope
Error writing data to auth/approle/login: Error making API request.

URL: PUT https://127.0.0.1:8200/v1/auth/approle/login
Code: 400. Errors:

* invalid role or secret ID

Only the CIDR check says what it is - because by then the caller has a valid secret ID:

* source address "127.0.0.1" unauthorized through CIDR restrictions on the secret ID

Delivering the secret ID: response wrapping

The job needs a fresh secret ID each run, and whoever passes it along must not be able to use it. Response wrapping solves both: ask for the secret ID wrapped.

$ vault write -wrap-ttl=120s -f auth/approle/role/ci-deploy/secret-id
Key                              Value
---                              -----
wrapping_token:                  hvs.CAESIpfHbp4pDrsFrFhQzVknndh6QohXAENvcgEFvKGh4KHGh2cy4cJDPbDVS6xC2rii4u2mPhU1djiDeQl9G
wrapping_accessor:               LgPu54LAJcE2KJCfiPNUoy5J
wrapping_token_ttl:              2m
wrapping_token_creation_time:    2026-09-22 20:00:03.802908038 +0000 UTC
wrapping_token_creation_path:    auth/approle/role/ci-deploy/secret-id

The secret ID is not in that output - it is inside Vault, behind a token that:

If the job's unwrap fails, someone else unwrapped it first: the secret ID was intercepted, and that is an incident, not a retry.

The trusted orchestrator pattern

CI platform (trusted orchestrator)                pipeline job
  has a token allowed to create secret IDs          knows the ROLE ID (pipeline config)
  for role ci-deploy - and nothing else
       |                                                    ^
       |  vault write -wrap-ttl=120s -f .../secret-id       |
       '---- wrapping token (useless in 2 min) ------------>|
                                                    vault unwrap -> secret ID
                                                    auth/approle/login -> token (10m)
                                                    read what ci-deploy allows

Each half alone is not enough: the role ID sits in config, the secret ID arrives wrapped, for one run, from the CI runners' network only. The orchestrator's own token needs a policy like:

path "auth/approle/role/ci-deploy/secret-id" {
  capabilities = ["update"]
  min_wrapping_ttl = "1s"
  max_wrapping_ttl = "300s"
}

min_wrapping_ttl forces it to ask for wrapped responses only: it cannot even see the secret IDs it creates.

AppRole or OIDC?

Modern build platforms (GitLab, GitHub, the cloud providers' own pipeline services) can give each job a signed OIDC token that says which repository, branch and pipeline it is. Vault's JWT/OIDC auth method verifies that token directly: no secret ID to deliver at all. Prefer that when the CI platform supports it; AppRole remains the tool for VMs, cron jobs, and platforms without workload identity - and it is what the Vault Agent lesson uses.

In an interview: "How does a build job get secrets from Vault without a long-lived token in a pipeline variable?" - an AppRole (or the JWT auth method with the platform's OIDC token): the role ID is in the pipeline config, the trusted build platform fetches a single-use, short-lived secret ID with -wrap-ttl, the job unwraps it, logs in for a 10-minute token with a narrow policy, and the secret ID is worthless afterwards.

What you can now do

Why it helps

Pipelines, cron jobs and VMs need secrets too, and the usual shortcut - a long-lived Vault token in a CI variable - is exactly the kind of shared, never-expiring credential Vault is meant to remove. AppRole with short, single-use secret IDs and response wrapping is the standard answer, and "how does a pipeline get secrets without a long-lived token?" is a common interview question.

It also introduces the trusted orchestrator pattern and response wrapping, which reappear whenever a credential has to be handed to something new: a bootstrapping VM, a job runner, a Vault Agent on its first start.

Commands in this lesson

vault

FAQ

Is the role ID a secret?

Not really: it identifies the role, like a username, and is useless without a valid secret ID. It can live in the job's configuration or repository. Treat it as internal information, but the real protection is the secret ID's short TTL, use limit and delivery path.

What is response wrapping?

Instead of the real response, Vault stores it in the cubbyhole of a new single-use token and returns only that wrapping token, with a short TTL (-wrap-ttl=60s). The receiver runs vault unwrap once to get the original. If the unwrap fails because it was already used, someone intercepted it, and you know.

Who should be allowed to create secret IDs?

A trusted orchestrator: the system that starts the job, such as the pipeline platform or a deploy service, with a policy that may only request wrapped secret IDs for specific roles. The job itself should not hold that permission, otherwise anything that compromises the job can mint new credentials.

What do secret_id_num_uses and secret_id_ttl do?

secret_id_num_uses=1 makes each secret ID work for one login only; secret_id_ttl=10m makes it expire if unused. Together they limit a leaked secret ID to a small window and one use. token_ttl and token_max_ttl on the role control the token that the login returns.

When would I not use AppRole?

When the platform can prove identity itself: Kubernetes pods (the Kubernetes auth method), cloud VMs (cloud IAM auth methods) and build platforms that issue signed job tokens (the JWT/OIDC method). Those remove the secret ID entirely. AppRole remains the tool for machines without such an identity.

In an interview Mid

How does an automated job get secrets from Vault without a long-lived token stored in its configuration?

With AppRole: the job's configuration contains only the role ID, which is not secret on its own. A trusted orchestrator - the system that launches the job - requests a short-lived, single-use secret ID with -wrap-ttl, so the job receives a wrapping token instead of the value. The job runs vault unwrap, logs in with vault write auth/approle/login role_id=... secret_id=..., and gets a token of about ten minutes with a narrow policy. Afterwards the secret ID is worthless.

Where the platform signs its own job tokens, the JWT/OIDC auth method removes the secret ID entirely.

Also asked: What does the trusted orchestrator pattern protect against? · How would you detect that a wrapped secret was intercepted? · Which settings would you put on an AppRole role for a nightly job?

Practise this lesson in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.