Join our Newsletter — 33% off our NHI Course
Home› Guides› How to audit Vault AppRole and secret ids
Guide Secrets Management

How to audit Vault AppRole and secret ids

← All guides
By NHI Mgmt Group, edited by Lalit Choda Updated 11 November 2026 13 min read
On this page

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_ttl defaults to empty (no expiry), secret_id_num_uses to 0 (unlimited) and secret_id_bound_cidrs to 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.

ElementWhat it isSecret?Audit question
RoleA named set of Vault policies and login constraints under auth/<mount>/role/<name>NoAre the policies least privilege, and are TTL, use count and CIDR constraints set?
Role IDIdentifier selecting the role at login; a UUID by default, behaves like a usernameNo, but should not travel with the secret IDIs it stored in the same place as the secret ID?
Secret IDThe password-equivalent credential required at login when bind_secret_id is trueYesHow many exist, how old are they, do they expire, are they bound to a network range?
Secret ID accessorA reference to a secret ID that can be listed, looked up and destroyedNoWhich accessors are stale, unbounded or orphaned?
Issued tokenThe Vault token returned by auth/approle/login, carrying token_policiesYesAre 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.

  1. Find every AppRole mount. AppRole can be enabled more than once at different paths.
    vault auth list -format=json | jq -r 'to_entries[] | select(.value.type=="approle") | .key'
    Record each mount path (for example approle/, ci-approle/) and the namespace it lives in.
  2. List roles on each mount. The API is LIST /auth/approle/role; the CLI equivalent is:
    vault list auth/approle/role
  3. Read each role's configuration.
    vault read -format=json auth/approle/role/<role_name>
    Capture 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_type and token_no_default_policy. Flag the findings in the table below.
  4. List secret ID accessors for each role. LIST /auth/approle/role/:role_name/secret-id returns accessors, not secret values:
    vault list -format=json auth/approle/role/<role_name>/secret-id
    A role with hundreds or thousands of accessors is usually generating a secret ID per deployment without expiry; a role with zero accessors and bind_secret_id=true cannot currently be logged in to and may be dead.
  5. Look up each accessor.
    vault write -format=json auth/approle/role/<role_name>/secret-id-accessor/lookup \
        secret_id_accessor=<accessor>
    The response includes creation_time, expiration_time, last_updated_time, secret_id_num_uses, secret_id_ttl, cidr_list, token_bound_cidrs and metadata. An expiration_time of 0001-01-01T00:00:00Z means 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.
  6. 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:
    vault 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"
    Save the previous vault read output first so you can write the old values back.
  7. 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:
    vault write -wrap-ttl=120s -f auth/approle/role/<role_name>/secret-id
    Give the broker a narrow policy that can only create wrapped secret IDs for its own roles.
  8. Destroy stale or unbounded secret IDs (destructive). After the owner confirms, destroy by accessor:
    vault write auth/approle/role/<role_name>/secret-id-accessor/destroy \
        secret_id_accessor=<accessor>
    Then run the tidy endpoint (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 fieldDefaultFindingSuggested target
bind_secret_idtruefalse means the role ID alone logs intrue unless a documented alternative constraint exists
secret_id_ttl"" (no expiry)Empty or zeroMinutes to hours, matched to the delivery window
secret_id_num_uses0 (unlimited)01 where the workload logs in once and renews its token
secret_id_bound_cidrs[] (any source)Empty on production rolesThe real source ranges of the consuming hosts
token_policies[]Wildcard or shared admin policiesOne role per application with its own policy
token_ttl / token_max_ttl0 (mount or system default)Long system defaults inheritedShort TTLs with renewal, for example through Vault Agent
expiration_time (lookup)n/a0001-01-01T00:00:00ZA real expiry
creation_time (lookup)n/aOlder than your review thresholdDestroy after owner confirmation
metadata (lookup)n/aEmpty, so nobody knows who requested itBroker 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-empty secret_id_ttl, a non-zero secret_id_num_uses and bound CIDRs on every production role.
  • Accessors: a fresh vault list .../secret-id shows only accessors that the owner has confirmed; repeat lookups show real expiration_time values 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

SituationFix
A workload fails after a secret ID was destroyedGenerate 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_cidrsThe 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 reachedThe 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 failsHashiCorp notes that if the issued token must create child tokens, token_num_uses should be 0
A role was deleted but is neededRecreate 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 accessorCheck the mount path and namespace; accessors are scoped to the role and mount that issued them
Accessor lists keep growingSecret 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

MetricDirection
Roles with secret_id_ttl empty or zeroZero
Roles with secret_id_num_uses of 0Falling to documented exceptions
Production roles without secret_id_bound_cidrsZero
Live accessors older than the review thresholdZero
Locations holding both role ID and secret IDZero
Share of workloads on platform auth instead of AppRoleRising

Framework mapping

FrameworkReferenceRelevance
NIST SP 800-53 Rev 5IA-5 Authenticator ManagementLifetime, distribution and revocation of secret IDs
NIST SP 800-53 Rev 5AC-2 Account Management; AC-6 Least PrivilegeRole inventory, ownership and narrow token policies
NIST SP 800-53 Rev 5AU-2 Event Logging; AU-6 Audit Record ReviewAudit device enabled and login records reviewed
NIST CSF 2.0PR.AA-01; PR.AA-05Service credentials managed; permissions reviewed
MITRE ATT&CKT1528 Steal Application Access TokenStolen 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, zero secret_id_num_uses and 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

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

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Researched and written by the NHI Mgmt Group editorial team using AI tools, under the editorial direction of Lalit Choda. Facts are drawn from the sources listed on this page and checked by our automated quality process. Last updated 11 November 2026. Spotted an error? Use our contact page.
NHI Mgmt Group, the independent authority on Non-Human Identity, IAM and Agentic AI security. nhimg.org