# Secrets vault (/docs/guides/secrets-and-keys/secrets-vault)

> Versioned secrets with scheduled rotation, check-outs of shared credentials, dynamic per-caller credentials, and values under your own KMS keys.



Every application runs on secrets: database passwords, API keys for upstream services, signing keys, connection
strings. They usually end up in a secrets manager with an access model of its own, while shared administrator
passwords sit in a separate password vault, and nobody can say in one place who used which credential. When someone
leaves, their account is closed, but the passwords they read keep working.

The vault keeps a tenant's secrets next to the identities and policies that decide who may use
them. It covers four jobs that usually need separate products:

* **A secrets manager.** Named secrets with versions and stage labels (`current`, `previous`, `pending`), scheduled
  rotation with generated values, and rotators that apply a new value to the system it unlocks.
* **A password vault for shared privileged accounts.** Check-outs hand out a credential for a limited time, to one
  person at a time if you like, with a reason, and can rotate it when it comes back.
* **Dynamic secrets.** An engine mints a fresh credential per caller (a short-lived database role, a cloud token) with a
  lease, and takes it back when the lease ends.
* **Encryption you control.** Values are sealed with the deployment secret, or under the organization's own
  [KMS key](/docs/guides/secrets-and-keys/key-management), so disabling that key makes them unreadable.

Access is decided like everything else in Better IAM: the `iam:vault:*` actions on `iam/vault/secrets/{name}`, with
the secret's tags available to policy conditions.

## Secrets and names [#secrets-and-names]

A secret lives in one tenant under a path-like name that is unique in the tenant:

```ts
await iam.api.vault.create(admin, {
  tenantId,
  name: 'prod/payments/db-password',
  description: 'Primary payments database, app user',
  tags: { environment: 'prod', team: 'payments' },
  value: 'correct horse battery staple',
});

const { value, version } = await iam.api.vault.reveal(session, {
  tenantId,
  name: 'prod/payments/db-password',
});
```

* **Names** are 1 to 16 segments separated by `/`, at most 256 characters in all. Each segment has up to 64 letters,
  digits and `_.-`, and does not start with `.` or `-`.
* **Tags** are up to 20 per secret. Tag names start with a letter and use letters, digits, `_` and `-`; values are 1 to
  256 characters. Policies see them (below), and `list` filters by them.
* **Values** are at most 64 KiB by default (`vault.maxValueBytes`), and a tenant holds at most 1000 secrets, pending
  deletions included (`vault.maxSecretsPerTenant`). A static secret may also start with a generated value
  (`generate: true`, see [Rotation](#rotation)) or with no value yet.

A secret's `format` is `text` (the default) or `json`, which holds an object. Pass a json secret's value as `fields`
(or as a JSON string in `value`); `reveal` returns both `value` and the parsed `fields`.

```ts
await iam.api.vault.create(admin, {
  tenantId,
  name: 'prod/smtp',
  format: 'json',
  fields: { host: 'smtp.acme.test', username: 'mailer', password: 'mail-password' },
});
```

## Who may do what [#who-may-do-what]

| Action             | Allows                                                                                                   |
| ------------------ | -------------------------------------------------------------------------------------------------------- |
| `iam:vault:read`   | Metadata, never values: `get`, `listVersions`, `listLeases`, `accessLog`.                                |
| `iam:vault:reveal` | `reveal`: a stored value.                                                                                |
| `iam:vault:write`  | New versions and stage labels: `put`, `promote`, `setStage`.                                             |
| `iam:vault:rotate` | `rotate`.                                                                                                |
| `iam:vault:lease`  | `checkout` of a static secret and `lease` of a dynamic one.                                              |
| `iam:vault:manage` | `create`, `update`, `delete`, `restore`, `setVersionState`, `destroyVersion`, and ending others' leases. |

`list` returns only the secrets the caller may read (each one is decided like `iam:vault:read`), and needs no
permission of its own; neither do `listMine` (the caller's own live check-outs and leases) and `generate`.

Every action applies to `iam/vault/secrets/{name}`, so wildcards scope access by path, and a
policy variable gives each person a folder of their own:

```json
{
  "version": 1,
  "statements": [
    {
      "sid": "PaymentsTeamReadsItsSecrets",
      "effect": "allow",
      "actions": ["iam:vault:read", "iam:vault:reveal"],
      "resources": ["iam/vault/secrets/prod/payments/*"]
    },
    {
      "sid": "PersonalSecrets",
      "effect": "allow",
      "actions": ["iam:vault:*"],
      "resources": ["iam/vault/secrets/users/${principal.id}/*"]
    }
  ]
}
```

Conditions see the secret's attributes: `resource.name`, `resource.kind` (`static` or `dynamic`), `resource.format`,
`resource.status`, `resource.createdBy`, `resource.checkoutRequired`, `resource.rotationEnabled`,
`resource.customerManagedKey`, `resource.engine`, and each tag as `resource.tag.{key}`:

```json title="Reveal staging secrets only, with MFA"
{
  "effect": "allow",
  "actions": ["iam:vault:reveal"],
  "resources": ["iam/vault/secrets/*"],
  "conditions": {
    "StringEquals": { "resource.tag.environment": "staging" },
    "Bool": { "principal.mfa": true }
  }
}
```

A name nobody uses yet has no attributes, so a tag condition never lets anyone create a secret: grant `manage` by path
for that. A new secret is decided again with its attributes (tags, kind, engine, settings), so a deny on
`resource.engine` refuses creating a secret bound to that engine. Every `update` is decided again too: the caller must
still be allowed to manage the secret afterwards, and the change may not open any vault action on it to them that they
did not have before. Re-tagging a `prod` secret as `staging` is refused for someone who may reveal only staging
secrets, however broad their `manage`.

### Values stay with the person asking [#values-stay-with-the-person-asking]

Values never leave through "view as": `reveal`, `checkout` and `lease` are refused in
impersonation sessions, whatever the member may do. Agents acting for a person are
decided like any delegated session, so a delegation's `confirm` list can hold `iam:vault:reveal` back for the person to
[approve call by call](/docs/guides/ai-agents#confirming-sensitive-actions-one-at-a-time). Each approval opens one
call, including the `iam:kms:decrypt` that a secret under a [customer-managed key](#customer-managed-keys) needs.
Plugins' `afterOperation` hooks see the answers of `reveal`, `checkout` and `rotate` without
their values.

## Versions and stages [#versions-and-stages]

Every change of value is a new version. Stage labels point at versions:

* `current` is what callers get by default. `put` makes the new version current, and the old one becomes `previous`.
* `pending` holds a value that is not live yet: `put({ stage: 'pending' })`, then `promote({ version })`. Rotation
  uses it too.
* Up to 8 custom labels, such as `canary`, set with `setStage` (and removed with `version: null`). Labels are 1 to 32
  lowercase letters, digits or hyphens, starting with a letter. `current` and `previous` move only through `put`,
  `promote` and `rotate`.

```ts
await iam.api.vault.put(admin, { tenantId, name, value: 'new value' }); // version 2 is current
await iam.api.vault.reveal(session, { tenantId, name, stage: 'previous' }); // version 1
await iam.api.vault.promote(admin, { tenantId, name, version: 1 }); // roll back
```

`maxVersions` (1 to 100, default 10) keeps the newest versions; older ones that no label points at are deleted. A
version can also be:

* **disabled** with `setVersionState`: kept, but not revealed (`VERSION_DISABLED`) until it is enabled again;
* **destroyed** with `destroyVersion`, only from a session with
  recent authentication (a sign-in within the last five minutes by default):
  its value is erased for good, labels on it are removed, and its record stays as history (`VERSION_DESTROYED`).

The current version can be neither: promote another version first.

## Rotation [#rotation]

`rotate` replaces a static secret's value in three steps: it stages a new version as `pending`, lets the secret's
rotator apply it to the system the secret unlocks, and then makes it current.

```ts
await iam.api.vault.create(admin, {
  tenantId,
  name: 'prod/payments/db-password',
  generate: true,
  rotation: {
    intervalDays: 30,
    rotator: 'postgres',
    generator: { length: 32, charset: 'ascii', exclude: '%' },
  },
});
```

`rotation` takes `intervalDays` (1 to 365; without it, rotation runs only on demand), `rotator` (a name from the
deployment's `vault.rotators`), and `generator`. Without a generator, rotation uses the default one. A json secret
names the field rotation replaces (`rotation.field`), and its other fields are carried over; a json secret with a
generator or a rotator must name it.

Generators draw from the operating system's CSPRNG. `vault.generate` returns a value without storing it, for a
password form, say.

<TypeTable
  type="{
  length: { type: 'number', description: 'Characters in the value, 8 to 256.', default: '32' },
  charset: {
    type: &#x22;'alphanumeric' | 'ascii' | 'hex' | 'base64url' | 'numeric'&#x22;,
    description: 'The characters to draw from. ascii is printable ASCII without space, quotes, backslash or backtick, which break shells and connection strings.',
    default: &#x22;'alphanumeric'&#x22;,
  },
  exclude: {
    type: 'string',
    description: 'Characters a target system rejects (up to 64). The generator must keep at least 10 characters.',
  },
  eachClass: {
    type: 'boolean',
    description: 'At least one lowercase letter, uppercase letter, digit and symbol, of the classes the charset keeps.',
    default: 'true',
  },
}"
/>

### Rotators [#rotators]

Rotators are functions you configure on the deployment. A rotator receives the pending `value` (and `fields` for a
json secret), the value being replaced as `previous` (and `previousFields`), the new version number, and the secret's
`tenantId`, `name` and `tags`. Throwing fails the rotation.

```ts
const iam = betterIam({
  // ...
  vault: {
    rotators: {
      postgres: {
        async rotate({ tenantId, name, value, tags }) {
          await admin.query(`ALTER ROLE ${ident(tags.role!)} PASSWORD ${literal(value)}`);
        },
      },
    },
  },
});
```

A failed rotation keeps the pending version, records the error (with the new and old values, each field of a json
secret, and their JSON-, URL- and base64-encoded spellings replaced by `[redacted]`), and fails with
`ROTATION_FAILED`. The next attempt, by hand or on schedule, gives the rotator the same value, so **rotators must be
idempotent**. Scheduled retries back off from one hour, doubling up to a day.

Only a version the rotation itself staged is ever retried. A value someone put under the `pending` label by hand is
never handed to the rotator, so `iam:vault:write` cannot choose the credential a rotator sets on the target system.
`rotate({ value })` (or `fields`) chooses the new value and needs `iam:vault:write` and `iam:vault:reveal` besides
`iam:vault:rotate`, since the caller knows it. It is refused for secrets that only check-outs hand out, which always
rotate to generated values. A version disabled while the rotator runs is not made current.

### Scheduled rotation [#scheduled-rotation]

With `intervalDays`, a secret is due that many days after its last new value, or after its creation. A `put` counts as
a new value; rolling back with `promote` does not. `iam.vault.rotateDue()`, run hourly, rotates due secrets that have a
generator or a rotator, acting as `deployment-operator`. A due secret with neither, such as a key a vendor issues, is
recorded once per due date as `vault:rotation-due`: subscribe a webhook to it to remind the
secret's owners. `get` and `list` show `rotation.due`, `nextRotationAt` and the last failure.

> **Outside the transaction, with a timeout.** 
  Rotators and engines are called outside any database transaction, with a 30 second timeout (`vault.callTimeoutMs`).

## Check-outs for shared credentials [#check-outs-for-shared-credentials]

Some credentials are shared by a team: a break-glass root password, a vendor portal account. A check-out policy makes
the vault hand them out one use at a time:

```ts
await iam.api.vault.create(admin, {
  tenantId,
  name: 'break-glass/db-root',
  generate: true,
  checkout: {
    required: true, // reveal is refused (CHECKOUT_REQUIRED); only check-outs hand the value out
    exclusive: true, // one holder at a time (SECRET_CHECKED_OUT)
    maxDurationMs: 2 * 3_600_000,
    rotateOnCheckin: true, // a returned password stops working
    requireReason: true,
  },
});

const out = await iam.api.vault.checkout(operator, {
  tenantId,
  name: 'break-glass/db-root',
  reason: 'INC-1234: primary database unresponsive',
});
// out.value, out.leaseId, out.expiresAt
await iam.api.vault.checkin(operator, { tenantId, leaseId: out.leaseId });
```

<TypeTable
  type="{
  required: {
    type: 'boolean',
    description: 'The value is handed out only through check-outs: reveal is refused with CHECKOUT_REQUIRED, except for the holder of a live check-out.',
    default: 'true',
  },
  exclusive: {
    type: 'boolean',
    description: 'One holder at a time; others get SECRET_CHECKED_OUT until it is returned or expires.',
    default: 'false',
  },
  maxDurationMs: {
    type: 'number',
    description: 'The longest check-out, 1 minute to 24 hours. A check-out that names no durationMs lasts this long.',
    default: '3600000',
  },
  rotateOnCheckin: {
    type: 'boolean',
    description: 'Rotate the value once the last holder returns it, so a returned password stops working.',
    default: 'false',
  },
  requireReason: {
    type: 'boolean',
    description: 'A check-out must give a reason (up to 512 characters), kept with the lease and in the audit log.',
    default: 'false',
  },
}"
/>

Only secrets with a check-out policy can be checked out, so an `iam:vault:lease` grant meant for dynamic secrets never
hands out stored values. Nobody holds two check-outs of the same secret. While a check-out lasts, its holder may:

* reveal the version they checked out;
* renew it with `renewLease`, by the length it was issued for, never more than `maxDurationMs` after it began, and
  only while they still hold `iam:vault:lease`;
* return it with `checkin`.

The session that took a check-out may always return it, even after the holder's access was taken away. The holder's
other sessions (another sign-in, an agent acting for them, a narrower API key) act on it only while they may lease the
secret themselves. Anyone with `iam:vault:manage` can end someone else's check-out with `revokeLease`.

With `rotateOnCheckin`, the secret rotates once its last holder returns it, however the check-out ended: checked in,
ended with `revokeLease`, or expired (`iam.vault.expireLeases`, which also ends the check-outs of holders who are no
longer active). A json secret that rotates on check-in must name `rotation.field`. A rotation that fails after a return
is recorded and retried by `iam.vault.rotateDue`, and a scheduled rotation of such a secret waits while it is checked
out. `get` shows who holds a secret and until when.

## Dynamic secrets [#dynamic-secrets]

A dynamic secret stores no value. Each `lease` asks an engine, configured on the deployment, for a credential minted
for the caller:

```ts
const iam = betterIam({
  // ...
  vault: {
    engines: {
      postgres: {
        async issue({ leaseId, config, ttlMs, holder }) {
          const username = `v_${holder.id.slice(0, 8)}_${leaseId.slice(0, 8)}`;
          const password = randomPassword();
          const until = new Date(Date.now() + ttlMs).toISOString();
          await admin.query(
            `CREATE ROLE ${ident(username)} LOGIN PASSWORD ${literal(password)} VALID UNTIL ${literal(until)} IN ROLE ${ident(config.role)}`,
          );
          return { fields: { username, password }, handle: username };
        },
        async revoke({ handle }) {
          await admin.query(`DROP ROLE IF EXISTS ${ident(handle!)}`);
        },
      },
    },
  },
});

await iam.api.vault.create(admin, {
  tenantId,
  name: 'prod/analytics/readonly',
  kind: 'dynamic',
  format: 'json',
  engine: 'postgres',
  engineConfig: { role: 'analytics_readonly' },
  lease: { defaultTtlMs: 3_600_000, maxTtlMs: 8 * 3_600_000 },
});

const lease = await iam.api.vault.lease(analyst, { tenantId, name: 'prod/analytics/readonly' });
// lease.fields.username, lease.fields.password, lease.expiresAt
```

An engine's `issue` receives the `leaseId`, the secret's `engineConfig` (any JSON up to 8 KiB), the lease length
`ttlMs`, and the `holder` (`id`, `kind`, `name`, `email`). It returns the credential as `value`, or as `fields` for a
json secret, and optionally a `handle`. `revoke` and `renew` are optional. A dynamic secret's `lease` settings are
`defaultTtlMs` (1 minute to 30 days, default one hour) and `maxTtlMs` (1 minute to 30 days, default one day, or
`defaultTtlMs` when that is longer). Dynamic secrets take no `value`, `rotation`, `checkout` or `kmsKey`.

* The credential is returned once and never stored. The engine's `handle` (at most 4096 characters) is sealed and
  handed back to `revoke` and `renew`.
* `renewLease` extends a lease up to `lease.maxTtlMs` from its start, telling the engine when it has `renew`.
* `revokeLease` (the holder, or `iam:vault:manage`) revokes it at the engine. A revocation the engine refuses stays
  `revoking` and is retried by `iam.vault.expireLeases`, one minute later and then doubling up to six hours, for seven
  days. After that the lease is given up and recorded as `vault:revoke-failed`.
* `iam.vault.expireLeases()` (every few minutes) revokes expired leases and the leases of holders who may no longer
  have them: people and agents who were deactivated or deleted, agents whose sponsor left, and leases an agent took for
  a person under a delegation that was revoked or ran out. It looks at every live lease on each run, however many
  there are, and ends at most `limit` (default 1000) of each kind per run.
* An engine failure fails the call with `ENGINE_FAILED`. Once `issue` was called, whatever went wrong afterwards (an
  error, a timeout, a credential or handle the vault refuses, the lease being revoked while it was issued, or a process
  that stopped before the issue finished), the lease is revoked at the engine by its `leaseId`. Engines must therefore
  be able to find a credential by lease id when no handle came back.
* Deleting a dynamic secret for good waits until its live leases are revoked.

> **Give credentials an expiry of their own.** 
  Engines should create credentials that expire by themselves (like `VALID UNTIL` above), so a revocation that keeps
  failing cannot leave them working forever.

## Customer-managed keys [#customer-managed-keys]

By default, values are sealed with the deployment secret (AES-256-GCM, bound to the tenant, secret and version, so a
sealed value cannot be moved to another record). `kmsKey` (a key id or `alias/{name}`) puts a static secret's values
under one of the organization's own [KMS](/docs/guides/secrets-and-keys/key-management) encryption keys instead:

```ts
await iam.api.vault.create(admin, {
  tenantId,
  name: 'prod/signing-key',
  value: pem,
  kmsKey: 'alias/vault',
});
// Move an existing secret under the key (every kept version is re-encrypted), or back with kmsKey: null.
await iam.api.vault.update(admin, { tenantId, name: 'prod/smtp', kmsKey: 'alias/vault' });
```

As with AWS KMS, the key's owner keeps a say over the values:

* Binding a secret to a key needs `iam:kms:encrypt` on it as well as `iam:vault:manage`. Moving a secret off its key
  (to another key or back to the deployment secret) needs `iam:kms:decrypt` on the current one.
* `reveal` and `checkout` need `iam:kms:decrypt` on the key besides the vault permission; `put` needs
  `iam:kms:encrypt`, and `rotate` both. The scheduler jobs and `iam.vault` are not asked.
* Every use appears in the key's audit trail as `iam:kms:encrypt` or `iam:kms:decrypt` with `metadata.via: 'vault'`.
* Disabling the key makes the values unreadable (`KEY_STATE_INVALID`) until it is enabled again. Deleting it destroys
  them for good.

Customer-managed keys are enabled `aes-256-gcm` encryption keys of the tenant; values of any size are
envelope-encrypted with them.

## Deleting secrets [#deleting-secrets]

`delete` schedules the deletion after a recovery window of 7 to 30 days (`recoveryDays`, default 30). Meanwhile the
secret cannot be read, changed or leased, its check-outs end, its name stays taken, and `restore` brings it back.
`iam.vault.purgeDeleted()` (daily) deletes secrets whose window has ended, with their versions, leases and access
records, once their live dynamic leases are revoked at their engine; a refused revocation keeps the secret for the next
run. `recoveryDays: 0` deletes at once, and only from a session with recent authentication (`RECENT_AUTH_REQUIRED`
otherwise).

## Server code [#server-code]

The deployment's own code reads secrets without a credential through `iam.vault`:

```ts
const db = await iam.vault.get(tenantId, 'prod/payments/db-password');

// Replace vault:// references in a configuration object; each secret is read once.
const config = await iam.vault.resolve(tenantId, {
  smtp: { host: 'vault://prod/smtp#host', password: 'vault://prod/smtp#password' },
  stripeKey: 'vault://prod/stripe/secret-key',
});
```

`get` returns the current version of a static secret, or the `version` or `stage` you pass; a secret pending deletion
is `NOT_FOUND`. `resolve` replaces `vault://{name}` with a secret's value and `vault://{name}#{field}` with one field of
a json secret, anywhere in a JSON value.

> **Trusted code only.** 
  `iam.vault` is not reachable over HTTP. It skips check-out rules and records nothing, neither in the audit log nor in
  the access log, so keep it to trusted code.

## From the command line [#from-the-command-line]

With `BETTER_IAM_TOKEN` (and `BETTER_IAM_TENANT` or `--tenant`), the [CLI](/docs/reference/cli) reads and writes
secrets and starts programs with them:

```bash
better-iam vault-get prod/payments/db-password
printf %s "$NEW_PASSWORD" | better-iam vault-put prod/payments/db-password
better-iam vault-run --prefix prod/payments/ --env STRIPE_KEY=prod/stripe#secret -- node server.js
```

* [`vault-get`](/docs/reference/cli#vault-get) prints the current value, or `--version`, `--stage`, or one `--field`
  of a json secret.
* [`vault-put`](/docs/reference/cli#vault-put) takes the value from standard input or `--value-env` (the name of an
  environment variable), never from the command line, or generates one with `--generate`. `--stage pending` stages it
  instead of making it current.
* [`vault-run`](/docs/reference/cli#vault-run) reveals secrets and runs the command after `--` with them as
  environment variables, so they never touch disk or shell history. `--env VAR=name[#field]` maps variables one by one
  (comma-separated), and `--prefix` adds every static secret under a path, named after the rest of its path:
  `prod/payments/db-password` becomes `DB_PASSWORD`, and a json secret adds one variable per field. `--env` wins over
  `--prefix`. The command inherits the terminal, its exit status becomes `vault-run`'s, and `BETTER_IAM_TOKEN` is
  removed from its environment unless `--keep-token`.

## Deployment options and jobs [#deployment-options-and-jobs]

The `vault` option of `betterIam()` holds the rotators and engines, and the limits:

<TypeTable
  type="{
  rotators: {
    type: 'Record<string, VaultRotator>',
    description: 'Rotators by name, which rotation.rotator refers to. Names are lowercase identifiers.',
  },
  engines: {
    type: 'Record<string, VaultEngine>',
    description: &#x22;Dynamic secret engines by name, which a dynamic secret's engine refers to.&#x22;,
  },
  maxValueBytes: { type: 'number', description: 'Largest value in UTF-8 bytes, 16 to 1048576.', default: '65536' },
  maxSecretsPerTenant: {
    type: 'number',
    description: 'Secrets per tenant, pending deletions included, 1 to 100000.',
    default: '1000',
  },
  accessRetentionDays: {
    type: 'number',
    description: 'How long access records and the history of ended leases are kept, 1 to 3650 days.',
    default: '90',
  },
  callTimeoutMs: {
    type: 'number',
    description: 'How long a rotator or engine call may take, 1 second to 5 minutes.',
    default: '30000',
  },
}"
/>

A bad value fails construction with `INVALID_CONFIG`. Schedule the three jobs with the other
[scheduled jobs](/docs/operations/jobs). They act as `deployment-operator` and need no credential:

| Job                        | CLI                                                              | How often         | Does                                                                                                  |
| -------------------------- | ---------------------------------------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------- |
| `iam.vault.rotateDue()`    | [`vault-rotate-due`](/docs/reference/cli#vault-rotate-due)       | hourly            | Rotates due secrets and retries failed rotations; records `vault:rotation-due` for manual ones.       |
| `iam.vault.expireLeases()` | [`vault-expire-leases`](/docs/reference/cli#vault-expire-leases) | every few minutes | Ends expired check-outs and leases, retries failed revocations, deletes lease history past retention. |
| `iam.vault.purgeDeleted()` | [`vault-purge-deleted`](/docs/reference/cli#vault-purge-deleted) | daily             | Deletes secrets past their recovery window.                                                           |

`iam.sweepExpired()` (the retention sweep) deletes access records older than
`vault.accessRetentionDays`, and `iam.rotateSecrets()` [re-seals](/docs/operations/deployment/secrets#re-seal) vault
values and engine handles when the deployment secret changes. Values under a customer-managed key are unaffected.

## Audit and the access log [#audit-and-the-access-log]

Every call is audited as its `iam:vault:*` action, and calls that change or return values also as `vault:create`,
`vault:update`, `vault:delete`, `vault:restore`, `vault:purge`, `vault:reveal`, `vault:put`, `vault:promote`,
`vault:stage`, `vault:version-state`, `vault:destroy-version`, `vault:rotate` (outcome `deny` when the rotator failed),
`vault:rotation-due`, `vault:checkout`, `vault:checkin`, `vault:checkout-expired`, `vault:lease` (`deny` when the
engine failed), `vault:renew`, `vault:revoke`, `vault:lease-expired` and `vault:revoke-failed`. Refused calls are
audited with outcome `deny` like any other.

`accessLog` answers "who used this secret" without searching the audit log: reveals,
check-outs and returns, leases, renewals, revocations, new versions and rotations, newest first, with the person's
name, the session kind and the agent, if one acted.

## Errors [#errors]

| Code                                                                        | Status | Meaning                                                                                |
| --------------------------------------------------------------------------- | ------ | -------------------------------------------------------------------------------------- |
| [`SECRET_PENDING_DELETION`](/docs/reference/errors#secret_pending_deletion) | 409    | The secret is scheduled for deletion; restore it first.                                |
| [`CHECKOUT_REQUIRED`](/docs/reference/errors#checkout_required)             | 409    | The secret is handed out only through check-outs.                                      |
| [`SECRET_CHECKED_OUT`](/docs/reference/errors#secret_checked_out)           | 409    | Someone else holds an exclusive secret, or the caller already holds a check-out of it. |
| [`VERSION_DISABLED`](/docs/reference/errors#version_disabled)               | 409    | The version is disabled.                                                               |
| [`VERSION_DESTROYED`](/docs/reference/errors#version_destroyed)             | 410    | The version's value was destroyed.                                                     |
| [`ROTATION_FAILED`](/docs/reference/errors#rotation_failed)                 | 502    | The rotator failed; the pending version waits for the next attempt.                    |
| [`ENGINE_FAILED`](/docs/reference/errors#engine_failed)                     | 502    | The dynamic secret engine failed to issue or renew a credential.                       |
| [`KEY_STATE_INVALID`](/docs/reference/errors#key_state_invalid)             | 409    | The secret's customer-managed key is disabled or pending deletion.                     |

Other refusals use the common codes: `ACCESS_DENIED`, `NOT_FOUND`, `CONFLICT` for a name in use (pending deletions
included), `LIMIT_EXCEEDED` past the secret or stage label limits, `RECENT_AUTH_REQUIRED`, and `INVALID_INPUT`. The API
reference lists them per method.

  - [vault API reference](/docs/reference/api/vault): Every method with its permission, audit events, and errors.

  - [Key management](/docs/guides/secrets-and-keys/key-management): The tenant KMS keys that customer-managed secrets are encrypted under.

  - [CLI reference](/docs/reference/cli#vault-run): vault-get, vault-put, vault-run, and the vault job commands.
