Vault Error: 'invalid role or secret ID' on AppRole Login
Fix Vault AppRole 'invalid role or secret ID' 400 errors: mismatched role_id, expired secret_id_ttl, exhausted num_uses, CIDR binds, wrong mount path, and CI whitespace.
- #vault
- #secrets
- #security-hardening
- #troubleshooting
- #errors
Stuck on this HashiCorp Vault error? Get the free incident triage checklist
A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.
Exact Error Message
$ vault write auth/approle/login role_id="$ROLE_ID" secret_id="$SECRET_ID"
Error writing data to auth/approle/login: Error making API request.
URL: PUT https://vault.example.com:8200/v1/auth/approle/login
Code: 400. Errors:
* invalid role or secret ID
When the role name itself does not resolve, you get a different 400 instead:
Error writing data to auth/approle/login: Error making API request.
URL: PUT https://vault.example.com:8200/v1/auth/approle/login
Code: 400. Errors:
* invalid role ID
What It Means
AppRole authentication takes two values: a role_id, which identifies the role and is effectively a username, and a secret_id, which is the password-equivalent and is issued separately. Vault validates them together and returns a single, deliberately vague message — invalid role or secret ID — whenever the pair does not authenticate. It will not tell you which half failed, because doing so would let an attacker enumerate valid role IDs.
That vagueness is why this error is frustrating to debug: the same string covers a mistyped role ID, a secret ID that expired, a secret ID whose secret_id_num_uses budget is spent, a caller whose source IP falls outside secret_id_bound_cidrs, a wrapped secret ID that was unwrapped twice, and a stray trailing newline injected by a CI variable. All of these are legitimate rejections; none of them are a Vault bug. The path to a fix is to eliminate each cause in order, starting with the ones you can verify without touching the role.
Common Causes
- The
secret_idhas passed itssecret_id_ttland has been expired by Vault. secret_id_num_usesis exhausted, so the secret ID authenticated successfully earlier and is now spent.- The caller’s source IP is not permitted by
secret_id_bound_cidrsortoken_bound_cidrson the role. - The role was deleted and recreated, which regenerates the
role_idand invalidates all old secret IDs. - AppRole is mounted at a custom path but the client is logging in against the default
auth/approle. - A response-wrapped secret ID was already unwrapped (or the wrapping token expired), so the value never arrived.
- CI injected a trailing newline or surrounding whitespace into the variable holding the ID.
Diagnostic Commands
Confirm AppRole is enabled and find the path it is actually mounted at:
vault auth list -detailed
Read the role’s configuration; every constraint that can cause this error is visible here:
vault read auth/approle/role/ci-deployer
Fetch the role’s canonical role ID and compare it byte-for-byte with what the client sends:
vault read -field=role_id auth/approle/role/ci-deployer
Check for invisible whitespace in the values your client is using — this catches the CI newline case without printing the secret:
printf '%s' "$ROLE_ID" | wc -c
printf '%s' "$SECRET_ID" | wc -c
Inspect a specific secret ID’s remaining TTL and use count without consuming it:
vault write auth/approle/role/ci-deployer/lookup-secret-id \
secret_id="$SECRET_ID"
List outstanding secret ID accessors for the role to see whether any are still live:
vault list auth/approle/role/ci-deployer/secret-id
Confirm the source address Vault sees, which is what CIDR binds are evaluated against:
curl -s https://vault.example.com:8200/v1/sys/health | jq .
Step-by-Step Resolution
- Verify the mount path first. If AppRole was enabled with
-path=ci, the login endpoint isauth/ci/loginand the default path will always reject:
vault auth list | grep approle
vault write auth/ci/login role_id="$ROLE_ID" secret_id="$SECRET_ID"
- Re-fetch the role ID from Vault rather than trusting a stored copy. Role IDs change if the role is deleted and recreated:
vault read -field=role_id auth/approle/role/ci-deployer
- Issue a fresh secret ID and test the login immediately. If a fresh one works, the old one had expired or exhausted its uses:
vault write -f -field=secret_id auth/approle/role/ci-deployer/secret-id
- Inspect the role’s constraints.
secret_id_ttlandsecret_id_num_usesare the two most common silent killers; widen them deliberately if the workload needs it:
vault write auth/approle/role/ci-deployer \
token_policies="deploy" \
secret_id_ttl=60m \
secret_id_num_uses=10 \
token_ttl=20m \
token_max_ttl=1h
- Check the CIDR binds. If
secret_id_bound_cidrsortoken_bound_cidrsis set, a login from an unlisted runner IP fails with exactly this message. Update the list to the real egress ranges rather than removing the bind:
vault write auth/approle/role/ci-deployer \
secret_id_bound_cidrs="10.20.0.0/16,203.0.113.64/28"
- If you use response wrapping, remember the wrapping token is single-use. Unwrap once, in the consumer, and pass the wrapping token — never the raw secret ID — through your pipeline:
# Issuer (trusted broker):
vault write -f -wrap-ttl=120s auth/approle/role/ci-deployer/secret-id
# Consumer (the job that will log in):
SECRET_ID=$(VAULT_TOKEN="$WRAPPING_TOKEN" vault unwrap -field=secret_id)
Keep secret IDs out of build logs entirely: mask the variables in your CI provider, never echo them, and prefer -field= so the CLI emits only the value into a variable. If unwrapping fails because the wrapping token is spent or expired, the error will be about the wrapping token rather than the role — a distinct case from Vault error: “missing client token”.
Prevention
- Set
bind_secret_id=true(the default) and treat secret IDs as short-lived, single-use credentials. - Deliver secret IDs by response wrapping so a leaked pipeline variable is detectably burned, not silently reusable.
- Store the role ID in configuration and the secret ID only in memory or a tmpfs file, never side by side.
- Trim your CI variables (
${VAR//[$'\r\n']/}) or use a provider feature that guarantees no trailing newline. - Scope
secret_id_bound_cidrsto real runner egress ranges and update it when networking changes. - Use Vault Agent auto-auth with the AppRole method so login, retry, and token refresh are handled for you.
Related Errors
invalid role ID— the role ID does not resolve at all; usually a wrong or recreated role.missing client token— the login succeeded but the resulting token was never passed to the next call.failed to validate SecretID: invalid secret_id— a malformed value, often truncation or a shell-quoting bug.permission deniedafter a successful login — authentication worked, but the token’s policies are too narrow.
Frequently Asked Questions
Which half is actually invalid — the role ID or the secret ID? Vault will not say, by design. Narrow it yourself: fetch a fresh role ID and a fresh secret ID and try again. If that works, re-introduce the stored values one at a time to find which one is stale.
Why does the login work locally but fail in CI? Two usual suspects. First, whitespace: many CI systems append a newline when writing a variable to a file. Second, CIDR binds: your laptop is inside an allowed range and the runner is not. Compare the byte lengths of the values and check secret_id_bound_cidrs on the role.
Do I need a secret ID at all? Only if bind_secret_id is true, which it is by default. A role with bind_secret_id=false logs in with the role ID alone, but then the role ID is the entire credential — only reasonable when paired with strict secret_id_bound_cidrs. Do not turn it off to make an error go away.
Can I reuse one secret ID across many jobs? You can raise secret_id_num_uses, but the safer pattern is one wrapped, single-use secret ID per job, issued by a trusted broker at dispatch time. That way a leaked value is either already consumed or expires in seconds. For more on AppRole and token lifetimes, see Vault error: “permission denied” from an expired token.
Fixed it? Get 500 HashiCorp Vault & DevOps AI prompts — free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.
Did this fix your issue?
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.