AppRole is the HashiCorp Vault auth method most often left running with secret IDs that never expire, never run out of uses and can log in from anywhere. A role ID and a secret ID together are a machine credential, and in many estates they sit side by side in CI variables, configuration files or container images, which defeats the separation AppRole was designed around. This runbook shows how to inventory every AppRole mount and role, read each role's constraints, list and look up live secret ID accessors, find the ones that are stale or unbounded, and then tighten or destroy them with a rollback. It is for platform, security and secrets engineers who already operate Vault and need a repeatable audit. For the wider Vault audit of tokens, policies and every auth method, see How to audit HashiCorp Vault tokens, policies and auth methods; this page goes deeper on AppRole only.
This is an independent guide from NHI Mgmt Group, based on HashiCorp's public Vault documentation as of November 2026. It is not sponsored or reviewed by HashiCorp. Parameters and defaults change between Vault versions, so confirm them against the documentation for the version you run.
Key takeaways
- The defaults are permissive.
secret_id_ttldefaults to empty (no expiry),secret_id_num_usesto 0 (unlimited) andsecret_id_bound_cidrsto empty, so an untuned role issues secret IDs that live for ever. - Audit by accessor, never by value. Listing a role's secret IDs returns accessors; the lookup-by-accessor endpoint returns creation, expiry and last update times without exposing the secret.
- Check the separation, not just the settings. HashiCorp's guidance is that the role ID and secret ID are only ever together on the system that consumes the secret; finding both in one CI variable store is a finding in itself.
- Destroy by accessor and keep a record. Destroying a secret ID cannot be undone, but the rollback is cheap: generate a new wrapped secret ID and deliver it through the broker.
- Prefer platform auth where it exists. HashiCorp positions AppRole as a fallback when no trusted platform method (Kubernetes, cloud IAM, JWT/OIDC) is available, so the audit should also ask whether each role needs to exist.
When you need this
- Periodic access review: quarterly review of machine credentials under NIST SP 800-53 IA-5 Authenticator Management and AC-2 Account Management.
- After a CI or build system incident: AppRole credentials are frequently stored as pipeline variables. When a build platform is compromised, every secret ID it could read must be treated as exposed. The Codecov breach showed how a tampered CI script can harvest every environment variable a build can see.
- Before migrating to platform auth: moving workloads to Kubernetes, cloud IAM or JWT auth is easier when you know which roles are live and who uses them.
- When Vault storage or login volume grows unexpectedly: unlimited secret IDs that are generated per deployment and never expire accumulate in storage and are a sign of a broken lifecycle.
Before you start
AppRole has three moving parts you will audit separately. The table summarises what each is and where its risk sits.
| Element | What it is | Secret? | Audit question |
|---|---|---|---|
| Role | A named set of Vault policies and login constraints under auth/<mount>/role/<name> | No | Are the policies least privilege, and are TTL, use count and CIDR constraints set? |
| Role ID | Identifier selecting the role at login; a UUID by default, behaves like a username | No, but should not travel with the secret ID | Is it stored in the same place as the secret ID? |
| Secret ID | The password-equivalent credential required at login when bind_secret_id is true | Yes | How many exist, how old are they, do they expire, are they bound to a network range? |
| Secret ID accessor | A reference to a secret ID that can be listed, looked up and destroyed | No | Which accessors are stale, unbounded or orphaned? |
| Issued token | The Vault token returned by auth/approle/login, carrying token_policies | Yes | Are token_ttl, token_max_ttl and token_bound_cidrs short and bounded? |
Permissions: run the discovery from a dedicated audit policy that grants list and read on auth/+/role/* and update on the two lookup paths (.../secret-id-accessor/lookup uses POST, which Vault maps to the update capability). Keep .../secret-id-accessor/destroy and role writes in a separate change policy that needs approval. Never give the audit identity create on .../secret-id, because generating a secret ID is the one action that hands out credentials.
Namespaces: on Vault Enterprise and HCP Vault Dedicated, repeat each step per namespace by setting VAULT_NAMESPACE or the -namespace flag; an audit of the root namespace alone misses child namespaces.
Audit device: confirm an audit device is enabled before you start (vault audit list). A new cluster starts with auditing disabled, and without it you cannot tell when a secret ID was last used to log in. Note that Vault writes HMAC-SHA256 hashes of most string values to the audit log, so you correlate by hash, not by plain value.
Step-by-step
Steps 1 to 5 are read-only. Steps 6 and 7 change configuration and are reversible. Step 8 destroys credentials.
- Find every AppRole mount. AppRole can be enabled more than once at different paths.
Record each mount path (for examplevault auth list -format=json | jq -r 'to_entries[] | select(.value.type=="approle") | .key'approle/,ci-approle/) and the namespace it lives in. - List roles on each mount. The API is
LIST /auth/approle/role; the CLI equivalent is:vault list auth/approle/role - Read each role's configuration.
Capturevault read -format=json auth/approle/role/<role_name>bind_secret_id,secret_id_ttl,secret_id_num_uses,secret_id_bound_cidrs,local_secret_ids,token_policies,token_ttl,token_max_ttl,token_num_uses,token_bound_cidrs,token_typeandtoken_no_default_policy. Flag the findings in the table below. - List secret ID accessors for each role.
LIST /auth/approle/role/:role_name/secret-idreturns accessors, not secret values:
A role with hundreds or thousands of accessors is usually generating a secret ID per deployment without expiry; a role with zero accessors andvault list -format=json auth/approle/role/<role_name>/secret-idbind_secret_id=truecannot currently be logged in to and may be dead. - Look up each accessor.
The response includesvault write -format=json auth/approle/role/<role_name>/secret-id-accessor/lookup \ secret_id_accessor=<accessor>creation_time,expiration_time,last_updated_time,secret_id_num_uses,secret_id_ttl,cidr_list,token_bound_cidrsandmetadata. Anexpiration_timeof0001-01-01T00:00:00Zmeans the secret ID does not expire. Export the full result per accessor to a file; this is your evidence and your input to step 8. - Tighten role constraints (reversible). Update the role with explicit limits, which apply to secret IDs generated after the change. For example, the HashiCorp pattern documentation uses a 60 minute secret ID TTL with five uses, and recommends a single use for best security:
Save the previousvault write auth/approle/role/<role_name> \ secret_id_ttl=60m \ secret_id_num_uses=1 \ secret_id_bound_cidrs="10.20.0.0/24" \ token_ttl=20m \ token_max_ttl=30m \ token_bound_cidrs="10.20.0.0/24"vault readoutput first so you can write the old values back. - Move secret ID delivery to response wrapping (reversible). The broker should request a wrapped secret ID and pass only the wrapping token to the workload, which unwraps it:
Give the broker a narrow policy that can only create wrapped secret IDs for its own roles.vault write -wrap-ttl=120s -f auth/approle/role/<role_name>/secret-id - Destroy stale or unbounded secret IDs (destructive). After the owner confirms, destroy by accessor:
Then run the tidy endpoint (vault write auth/approle/role/<role_name>/secret-id-accessor/destroy \ secret_id_accessor=<accessor>POST /auth/approle/tidy/secret-id) to clean up expired secret ID entries and their accessors. Delete whole roles (vault delete auth/approle/role/<role_name>) only when no workload uses them and the export from step 3 is stored.
What to flag in step 3 and step 5
| Setting or field | Default | Finding | Suggested target |
|---|---|---|---|
bind_secret_id | true | false means the role ID alone logs in | true unless a documented alternative constraint exists |
secret_id_ttl | "" (no expiry) | Empty or zero | Minutes to hours, matched to the delivery window |
secret_id_num_uses | 0 (unlimited) | 0 | 1 where the workload logs in once and renews its token |
secret_id_bound_cidrs | [] (any source) | Empty on production roles | The real source ranges of the consuming hosts |
token_policies | [] | Wildcard or shared admin policies | One role per application with its own policy |
token_ttl / token_max_ttl | 0 (mount or system default) | Long system defaults inherited | Short TTLs with renewal, for example through Vault Agent |
expiration_time (lookup) | n/a | 0001-01-01T00:00:00Z | A real expiry |
creation_time (lookup) | n/a | Older than your review threshold | Destroy after owner confirmation |
metadata (lookup) | n/a | Empty, so nobody knows who requested it | Broker writes job or pipeline identifiers as metadata |
Checking where the secret IDs live
Vault cannot tell you where a secret ID was copied to. Pair the accessor inventory with a search of the places they usually end up: CI/CD variable stores, Kubernetes Secrets, configuration management data, container images and repositories. See How to Audit CI/CD Secrets Scope and How to find hard-coded secrets in container images. Any location holding both a role ID and a secret ID for the same role breaks the broker model and should be treated as a finding even if the values are current.
Using the audit log for last use
The accessor lookup shows when a secret ID was created and last updated, not every login. For login history, search the audit device for requests to auth/<mount>/login. Because values are hashed, compute the keyed hash of a known value with the /sys/audit-hash/<device path> endpoint for the same audit device and search for that hash. Hashes only match entries written under the same hashing key: if the device was disabled and re-enabled at the same path, Vault created a new key and older entries will not match.
Verify it worked
- Roles:
vault read auth/approle/role/<role_name>shows non-emptysecret_id_ttl, a non-zerosecret_id_num_usesand bound CIDRs on every production role. - Accessors: a fresh
vault list .../secret-idshows only accessors that the owner has confirmed; repeat lookups show realexpiration_timevalues for new secret IDs. - Delivery: the broker's audit trail shows wrapped secret ID requests matched to job runs, and no unwrap failures.
- No breakage: the audit log shows no rise in failed logins on the roles you changed during the observation window, and application teams confirm renewals.
Rollback and common errors
| Situation | Fix |
|---|---|
| A workload fails after a secret ID was destroyed | Generate a new wrapped secret ID for that role and deliver it through the broker; record the real consumer and add metadata |
Logins fail after adding secret_id_bound_cidrs | The source address seen by Vault differs (NAT, load balancer, proxy); widen to the observed range or write back the saved role values |
| Jobs fail with secret ID use limit reached | The workload logs in more than once per secret ID; let it renew its token instead, or raise secret_id_num_uses to the measured need |
| Child token creation fails | HashiCorp notes that if the issued token must create child tokens, token_num_uses should be 0 |
| A role was deleted but is needed | Recreate it from infrastructure as code or the step 3 export; a new role ID is generated unless you set the old value, so update the consumer |
| Lookups return nothing for a known accessor | Check the mount path and namespace; accessors are scoped to the role and mount that issued them |
| Accessor lists keep growing | Secret IDs are generated per run with no TTL; set secret_id_ttl and schedule tidy/secret-id |
Hardening afterwards
- Use a trusted broker and response wrapping: the CI system or orchestrator requests a wrapped secret ID per run; the role ID is delivered through a different channel, such as the image or deployment configuration. HashiCorp lists as anti-patterns a CI worker passing the role ID and secret ID together to a runner, and a CI system logging in to Vault and passing its client token to the application.
- Alert on the two broker signals HashiCorp names: a wrapped secret ID requested when no job is running, and an unwrap attempt rejected because the wrapping token was already used. Either may indicate interception.
- Replace AppRole with platform auth where possible: Kubernetes, cloud IAM and JWT/OIDC auth methods remove the secret ID altogether. See the Secrets Zero Problem Guide and the HashiCorp Vault Guide.
- One role per application: shared roles make it impossible to attribute a login or a leaked secret ID to a single workload.
- Use batch tokens for high-volume logins: HashiCorp recommends batch tokens, which are issued from memory and do not consume storage, for very high login rates.
Small estate or large platform
A single cluster with a handful of roles can run steps 1 to 5 in a short script on a schedule and review the output by hand. A platform team with many namespaces and hundreds of roles should generate roles from infrastructure as code with mandatory TTL, use count and CIDR values, enforce them in code review, and treat the accessor inventory as a daily export into the NHI inventory. See NHI Inventory Reconciliation.
Metrics
| Metric | Direction |
|---|---|
Roles with secret_id_ttl empty or zero | Zero |
Roles with secret_id_num_uses of 0 | Falling to documented exceptions |
Production roles without secret_id_bound_cidrs | Zero |
| Live accessors older than the review threshold | Zero |
| Locations holding both role ID and secret ID | Zero |
| Share of workloads on platform auth instead of AppRole | Rising |
Framework mapping
| Framework | Reference | Relevance |
|---|---|---|
| NIST SP 800-53 Rev 5 | IA-5 Authenticator Management | Lifetime, distribution and revocation of secret IDs |
| NIST SP 800-53 Rev 5 | AC-2 Account Management; AC-6 Least Privilege | Role inventory, ownership and narrow token policies |
| NIST SP 800-53 Rev 5 | AU-2 Event Logging; AU-6 Audit Record Review | Audit device enabled and login records reviewed |
| NIST CSF 2.0 | PR.AA-01; PR.AA-05 | Service credentials managed; permissions reviewed |
| MITRE ATT&CK | T1528 Steal Application Access Token | Stolen secret IDs and Vault tokens used to obtain access |
Practitioner checklist
- List every AppRole mount in every namespace and confirm an audit device is enabled.
- Read every role and flag empty
secret_id_ttl, zerosecret_id_num_usesand missing CIDR bindings. - List secret ID accessors per role and export a lookup for each one.
- Find locations that store a role ID and secret ID together and break them apart.
- Set explicit TTL, use count and CIDR values on each role, saving the previous config.
- Move secret ID delivery to wrapped responses from a narrowly scoped broker.
- Destroy stale accessors after owner confirmation and schedule
tidy/secret-id. - Plan each remaining role's move to a platform auth method where one exists.
Standards and references
- HashiCorp: AppRole auth method (API)
- HashiCorp: Use AppRole authentication
- HashiCorp: AppRole usage best practices
- HashiCorp: Audit devices
- HashiCorp: /sys/audit-hash
- NIST: SP 800-53 Rev. 5 Security and Privacy Controls
- NIST: Cybersecurity Framework 2.0
Related NHI Mgmt Group resources: HashiCorp Vault Guide · How to audit HashiCorp Vault tokens, policies and auth methods · How to Audit CI/CD Secrets Scope · NHI Dormancy Detection