---
title: "CLI commands"
manual: "nr-vault"
version: "1.0"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-vault:developer-commands@1.0"
source: "Developer/Commands.rst"
rendered: "2026-09-18T07:37:50+00:00"
---

# CLI commands {#cli-commands}

nr-vault provides several CLI commands for DevOps automation and management.

## vault:init {#vault-init}

Initialize the vault by creating a master key.

**Command syntax**

```bash
vendor/bin/typo3 vault:init [options]
```

### Options {#options}

-   **--output=PATH, -o PATH**

    Path to store the master key file (default: configured path or `var/vault/master.key`).

-   **--force, -f**

    Overwrite existing master key (dangerous - existing secrets become unrecoverable!).

-   **--env, -e**

    Output key as environment variable format instead of file.

### Example {#example}

**vault:init examples**

```bash
# Initialize with default location
vendor/bin/typo3 vault:init

# Specify custom key file location
vendor/bin/typo3 vault:init --output=/secure/path/vault.key

# Output as environment variable
vendor/bin/typo3 vault:init --env
```

> [!WARNING]
> The master key file should be stored outside the webroot with restricted
> permissions (0400 or 0600). Never commit it to version control.

## vault:store {#vault-store}

Store a secret in the vault.

**Command syntax**

```bash
vendor/bin/typo3 vault:store <identifier> [options]
```

### Arguments {#arguments}

-   **identifier**

    Unique identifier for the secret.

### Options {#options-1}

-   **--value=SECRET**

    The secret value (will prompt if not provided).

-   **--stdin**

    Read the secret value from stdin.

-   **--file=PATH, -f PATH**

    Read the secret value from a file.

-   **--metadata=KEY=VALUE, -m KEY=VALUE**

    Additional metadata as `key=value`. **Repeatable** — pass the option once
    per pair. Recognised keys: `description`, `context`, `expiresAt`,
    `owner`, `groups`, `scopePid`. There are no separate
    `--description` / `--context` / `--expires` options.

-   **--groups=UID, -g UID**

    Backend user group ID that may access this secret. **Repeatable** — pass
    the option once per group (`--groups=1 --groups=2`). A comma-separated
    `--groups="1,2"` is *not* split: it is read as a single non-numeric value
    and becomes group 0.

-   **--as-provisioner**

    Write as the configured provisioning backend user instead of the
    unattributed CLI actor, so the stored secret is owned and audited under a
    real account.
    The user comes from the `provisioningBeUserUid` extension setting —
    never from this command line — and its group must carry the
    `tx_nrvault:secret.create` permission option.
    The command aborts before storing anything when the setting is unset.

### Example {#example-1}

**vault:store examples**

```bash
# Interactive (prompts for secret)
vendor/bin/typo3 vault:store stripe_api_key

# With options (arbitrary metadata via repeatable --metadata key=value)
vendor/bin/typo3 vault:store payment_key \
  --value="sk_live_..." \
  --metadata="description=Stripe production key" \
  --metadata="context=payment" \
  --groups=1 --groups=2
```

## vault:retrieve {#vault-retrieve}

Retrieve a secret from the vault.

**Command syntax**

```bash
vendor/bin/typo3 vault:retrieve <identifier> [options]
```

### Options {#options-2}

-   **--output=PATH, -o PATH**

    Write the secret to a file instead of stdout.

-   **--no-newline**

    Do not append a newline to the output. This is the option to use when
    capturing into a shell variable.

-   **--reason=TEXT, -r TEXT**

    Reason for retrieving this secret, recorded in the audit log.

> [!IMPORTANT]
> This command asserts `secret.reveal`. For the unattributed CLI actor that
> means `allowCliAccess` must be on **and** `secret.reveal` must be in
> [cliAllowedOperations](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-cliallowedoperations@1.0), which excludes it by default.
> Without both, the command exits 1. Prefer a named technical actor
> ([Technical actor context](https://docs.typo3.org/permalink/netresearch/nr-vault:developer-technical-actor-context@1.0)) over widening the allowlist.

### Example {#example-2}

**vault:retrieve examples**

```bash
# Display with metadata
vendor/bin/typo3 vault:retrieve stripe_api_key

# For use in scripts. Do NOT use -q: that is Symfony's global quiet flag
# and suppresses the value entirely, yielding an empty string.
API_KEY=$(vendor/bin/typo3 vault:retrieve --no-newline stripe_api_key)
```

## vault:list {#vault-list}

List all accessible secrets.

**Command syntax**

```bash
vendor/bin/typo3 vault:list [options]
```

### Options {#options-3}

-   **--pattern=PATTERN, -p PATTERN**

    Filter by identifier pattern (supports `*` wildcard).

-   **--format=FORMAT**

    Output format: table (default), json, csv. No short form.

-   **--limit=N, -l N**

    Maximum number of results (default: 100).

### Example {#example-3}

**vault:list examples**

```bash
# List all secrets
vendor/bin/typo3 vault:list

# Filter by pattern
vendor/bin/typo3 vault:list --pattern="payment_*"

# JSON output for automation
vendor/bin/typo3 vault:list --format=json
```

## vault:rotate {#vault-rotate}

Rotate a secret with a new value.

**Command syntax**

```bash
vendor/bin/typo3 vault:rotate <identifier> [options]
```

### Options {#options-4}

-   **--value=SECRET**

    The new secret value (will prompt if not provided).

-   **--stdin**

    Read the new secret value from stdin.

-   **--file=PATH, -f PATH**

    Read the new secret value from a file.

-   **--reason=TEXT, -r TEXT**

    Reason for rotation, logged in the audit trail. Defaults to
    `Manual rotation via CLI`, so the field is never empty — pass a real
    reason rather than relying on the placeholder.

### Example {#example-4}

**vault:rotate example**

```bash
vendor/bin/typo3 vault:rotate stripe_api_key \
  --reason="Scheduled quarterly rotation"
```

## vault:delete {#vault-delete}

Delete a secret from the vault.

**Command syntax**

```bash
vendor/bin/typo3 vault:delete <identifier> [options]
```

### Options {#options-5}

-   **--reason=TEXT, -r TEXT**

    Reason for deletion, logged in the audit trail. Defaults to
    `Manual deletion via CLI`.

-   **--force, -f**

    Skip confirmation prompt.

> [!IMPORTANT]
> This command asserts `secret.delete`, which
> [cliAllowedOperations](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-cliallowedoperations@1.0) excludes by default. On the CLI
> it needs `allowCliAccess` **and** that operation in the allowlist.

### Example {#example-5}

**vault:delete example**

```bash
vendor/bin/typo3 vault:delete old_api_key \
  --reason="Service deprecated" \
  --force
```

## vault:audit {#vault-audit}

View the audit log.

**Command syntax**

```bash
vendor/bin/typo3 vault:audit [options]
```

### Options {#options-6}

-   **--identifier=ID, -i ID**

    Filter by secret identifier.

-   **--action=ACTION, -a ACTION**

    Filter by action — any `AuditAction` value. The common ones are
    `create`, `read`, `update`, `delete`, `rotate` and
    `access_denied`; the enum also covers `metadata_update`, `http_call`,
    `http_call_cancelled` (an in-flight outbound call stopped by its
    cancellation signal, after the credential went out) and
    `http_call_cancelled_before_send` (refused before the send; no secret was
    read — see [ADR-037: A cancellable send is a method, not an exported handle](https://docs.typo3.org/permalink/netresearch/nr-vault:adr-037-cancellable-outbound-send@1.0)), the master-key and chain
    lifecycle (`master_key_rotate_start` / `_end`, `audit_chain_rekey`,
    `audit_anchor_reset`), break-glass (`break_glass_activated` /
    `_deactivated`) and the OAuth actions.

-   **--actor=UID**

    Filter by actor (backend user UID).

-   **--since=DATE**

    Show entries since the given date (`Y-m-d` or `Y-m-d H:i:s`).

-   **--until=DATE**

    Show entries up to the given date (`Y-m-d` or `Y-m-d H:i:s`).

-   **--success=BOOL**

    Filter by success status (`true`/`false`).

-   **--limit=N, -l N**

    Maximum number of results (default: 50).

-   **--format=FORMAT, -f FORMAT**

    Output format: `table` (default), `json`, `csv`.

-   **--verify**

    Verify hash chain integrity instead of listing entries. The output includes
    a `Tip anchor:` line reporting the state of the truncation anchor (see
    [Tip anchor (truncation detection)](https://docs.typo3.org/permalink/netresearch/nr-vault:security-audit-chain-anchor@1.0)): `ok`, `NOT ARMED`, `VIOLATED`,
    `UNREADABLE`, `disabled` or `inconclusive`.

-   **--reset-anchor**

    Clear the audit chain tip anchor, record the reset in the chain, and re-arm
    the anchor on that entry. Only after a wipe or purge of
    `tx_nrvault_audit_log` that you performed deliberately — the anchor
    otherwise reports a violation permanently, which is exactly what makes an
    undeclared truncation visible. Asks for confirmation. This is the only path
    that arms an anchor at all while
    [auditAnchorRequired](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-auditanchorrequired@1.0) is enabled.

-   **--force**

    Skip the interactive confirmation of `--reset-anchor` (for unattended runs).

-   **--export=FILE, -e FILE**

    Export results to a file (format taken from `--format`).

> [!IMPORTANT]
> Every mode of this command asserts an operation permission: listing,
> console output and `--verify` assert `audit.view`, `--export` asserts
> `audit.export`, and `--reset-anchor` asserts `vault.configure`. A
> refusal exits 1 before any work happens: nothing is queried, no chain is
> read, no export file is written, and the tip anchor is left untouched.
>
> `--verify` shares `audit.view` with the listing because verification
> recomputes and compares — it mutates nothing, so it is a read of the chain,
> and the audit module gates the same operation the same way.
> `--reset-anchor` is the outlier: it clears the truncation anchor and
> writes into the chain, which is vault administration.
>
> [cliAllowedOperations](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-cliallowedoperations@1.0) excludes all three permissions
> by default, so for the unattributed CLI actor this command needs
> `allowCliAccess` **and** the operation in that allowlist. Prefer a named
> technical actor ([Technical actor context](https://docs.typo3.org/permalink/netresearch/nr-vault:developer-technical-actor-context@1.0)) over widening the
> allowlist — the audit trail then names the identity that read or exported
> the log.

### Example {#example-6}

**vault:audit examples**

```bash
# View audit log since a given date
vendor/bin/typo3 vault:audit --since=2026-05-01

# Filter by secret
vendor/bin/typo3 vault:audit --identifier=stripe_api_key

# Export to JSON
vendor/bin/typo3 vault:audit --format=json > audit.json

# Verify the chain, including the truncation tip anchor
vendor/bin/typo3 vault:audit --verify

# Re-arm the anchor after a deliberate wipe of the audit log
vendor/bin/typo3 vault:audit --reset-anchor --force
```

## vault:audit-anchor {#vault-audit-anchor}

Publish the current audit log chain tip to the enabled external audit sinks.

The in-database hash chain proves that no stored row was altered, but it cannot
prove that the chain is still the *same* chain: an attacker with `DELETE`
rights can truncate `tx_nrvault_audit_log` and let the service build a fresh,
internally consistent chain from uid 1. Nothing inside the database
distinguishes that from a young installation.

Each anchoring run records — outside the database — that the chain had reached a
given sequence with a given tip hash. [vault:audit-verify](https://docs.typo3.org/permalink/netresearch/nr-vault:command-audit-verify@1.0) compares the
live chain against the newest anchor and reports `TABLE_RESET` when they
disagree.

> [!IMPORTANT]
> The anchoring interval is the blind window: an attacker who resets the table
> can only hide entries written since the last anchor. Schedule it hourly for a
> vault under audit; daily is the loosest defensible setting. Use the
> *Vault Audit Chain Anchoring* scheduler task for this.

**Command syntax**

```bash
vendor/bin/typo3 vault:audit-anchor [options]
```

### Options {#options-7}

-   **--dry-run**

    Show the anchor that would be published without writing it to any sink.

-   **--format=FORMAT, -f FORMAT**

    Output format: `text` (default) or `json`.

> [!IMPORTANT]
> This command asserts `vault.configure` — the same permission
> `vault:audit --reset-anchor` asserts, because both mutate tamper evidence.
> An actor who truncates the log and then anchors makes the external sink
> attest the truncated chain, which is the laundering the anchor exists to
> prevent. A refusal exits 1 without reading the chain tip and without
> publishing anything; in `--format=json` the body carries the reason.
>
> `--dry-run` is gated identically rather than as a read: it publishes
> nothing, but it prints the current chain tip — the value a forged anchor has
> to reproduce — and it is the rehearsal of an administrative operation.
>
> [cliAllowedOperations](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-cliallowedoperations@1.0) excludes `vault.configure` by
> default, so from a shell this command needs `allowCliAccess` **and** that
> operation in the allowlist. **Scheduled runs are unaffected on a default
> installation**: the *Vault Audit Chain Anchoring* task runs under
> `scheduler:run`, which authenticates the `_cli_` administrator, and
> the admin bypass grants it. Under `disableAdminOverride` the bypass is gone
> by design and that identity needs a group carrying
> `tx_nrvault:vault.configure`; without it the task fails rather than
> skipping quietly, because an anchoring run that did not happen must never
> look like one that did.

### Exit codes {#exit-codes}

-   **0**

    The anchor was accepted by at least one external sink.

-   **1**

    The chain tip could not be read, or **no sink accepted the anchor**. An
    anchor that reached nothing outside the database provides no table-reset
    protection, so this is a failure rather than a no-op — enable at least one of
    `auditSinkFileEnabled`, `auditSinkSyslogEnabled` or
    `auditSinkWebhookEnabled`.

### Example {#example-7}

**vault:audit-anchor examples**

```bash
# Publish the current chain tip
vendor/bin/typo3 vault:audit-anchor

# Preview without writing
vendor/bin/typo3 vault:audit-anchor --dry-run

# Machine-readable output for a monitoring wrapper
vendor/bin/typo3 vault:audit-anchor --format=json
```

## vault:audit-verify {#vault-audit-verify}

Verify audit log integrity against both the hash chain and the external
chain-tip anchor.

This complements `vault:audit --verify`, which runs the in-database hash-chain
pass only. In addition to that pass, this command compares the chain against the
newest anchor published by [vault:audit-anchor](https://docs.typo3.org/permalink/netresearch/nr-vault:command-audit-anchor@1.0) and classifies every
finding under a machine-readable reason code.

**Command syntax**

```bash
vendor/bin/typo3 vault:audit-verify [options]
```

### Options {#options-8}

-   **--format=FORMAT, -f FORMAT**

    Output format: `text` (default) or `json`. The JSON form carries the full
    finding list, the compared anchor, the enabled sinks and their failure counts.

-   **--tamper-only**

    Only fail on tamper evidence; treat configuration and delivery findings
    (`NO_EXTERNAL_SINK`, `SINK_FAILURE`) as warnings. Useful while external
    sinks are still being rolled out, so a pending integration does not keep the
    check permanently red and train operators to ignore a real alarm.

> [!IMPORTANT]
> This command asserts `audit.view`, the same permission
> `vault:audit --verify` and the audit module's *Verify chain* action
> assert: verification recomputes and compares, it mutates nothing, so it is a
> read of the chain wherever it is invoked from. A refusal exits 1 without
> running any verification, and in `--format=json` reports `valid: false`
> — a verifier that was not allowed to run must never read as a verifier that
> found nothing. `--tamper-only` does not soften a refusal: that is not a
> finding about the chain.
>
> [cliAllowedOperations](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-cliallowedoperations@1.0) excludes `audit.view` by
> default, so from a shell this command needs `allowCliAccess` **and** that
> operation in the allowlist. **Scheduled runs are unaffected on a default
> installation**: the *Vault Audit Integrity Verification* task runs under
> `scheduler:run`, which authenticates the `_cli_` administrator, and
> the admin bypass grants it. Under `disableAdminOverride` that identity
> needs a group carrying `tx_nrvault:audit.view`; without it the task fails,
> for the same reason a verification that threw does — a check that did not
> run must never report success.

### Reason codes {#reason-codes}

-   **HASH_MISMATCH**

    A stored `entry_hash` or `previous_hash` does not match the recomputed
    value. Tamper evidence.

-   **UID_GAP**

    The uid sequence is not contiguous — rows were deleted from the chain. Tamper
    evidence (may also be a retention purge; confirm against your purge log).

-   **TABLE_RESET**

    The chain no longer contains the anchored tip: it is shorter than the anchored
    sequence, or the row at that sequence hashes differently. The signature of a
    truncate-and-rebuild. Tamper evidence.

-   **EPOCH_DOWNGRADE**

    The `hmac_key_epoch` was relabelled downward, moving rows onto a weaker or
    keyless verification algorithm. Tamper evidence.

-   **NO_EXTERNAL_SINK**

    The hardened security profile is active but no external audit sink is enabled
    and usable, or no anchor could be read. Configuration finding, not tamper
    evidence.

-   **SINK_FAILURE**

    An external sink could not be delivered to during this process. Availability
    finding, not tamper evidence.

### Alerting {#alerting}

Every finding is dispatched as
`\Netresearch\NrVault\Event\AuditIntegrityAlertEvent` before the command
returns, so SIEM and notification listeners fire whether or not anyone reads this
output. When the webhook sink is enabled it receives the alerts by default via
the built-in `nr-vault/audit-integrity-alert-sinks` listener.

### Exit codes {#exit-codes-1}

-   **0**

    No findings — or, with `--tamper-only`, no tamper evidence.

-   **1**

    At least one finding (see `--tamper-only`), or verification could not run at
    all. A verifier that cannot run never reports success.

### Example {#example-8}

**vault:audit-verify examples**

```bash
# Full verification
vendor/bin/typo3 vault:audit-verify

# Machine-readable output for monitoring
vendor/bin/typo3 vault:audit-verify --format=json

# Only alarm on tamper evidence while sinks are being rolled out
vendor/bin/typo3 vault:audit-verify --tamper-only
```

## vault:rotate-master-key {#vault-rotate-master-key}

Rotate the master encryption key. Re-encrypts all DEKs with a new master key.

**Command syntax**

```bash
vendor/bin/typo3 vault:rotate-master-key [options]
```

### Options {#options-9}

-   **--old-key=PATH**

    Path to file containing the old master key (defaults to current configured key).

-   **--new-key=PATH**

    Path to file containing the new master key (defaults to current configured key).

-   **--dry-run**

    Simulate the rotation without making changes.

-   **--confirm**

    Required for actual execution (safety measure).

> [!IMPORTANT]
> This command asserts `master_key.rotate`, which
> [cliAllowedOperations](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-cliallowedoperations@1.0) excludes by default. On the CLI
> it needs `allowCliAccess` **and** that operation in the allowlist,
> otherwise it aborts with exit code 1. Granting it to the unattributed CLI
> actor hands the whole vault's key envelope to anyone with a shell; a named
> technical actor is the better carrier.

> [!WARNING]
> Master key rotation re-encrypts all Data Encryption Keys (DEKs).
> Ensure you have a backup of the old key before proceeding.

### Example {#example-9}

**vault:rotate-master-key examples**

```bash
# Old key from file, new key from current config
vendor/bin/typo3 vault:rotate-master-key \
  --old-key=/secure/path/old-master.key \
  --confirm

# Both keys from files
vendor/bin/typo3 vault:rotate-master-key \
  --old-key=/path/to/old.key \
  --new-key=/path/to/new.key \
  --confirm

# Dry run to verify before actual rotation
vendor/bin/typo3 vault:rotate-master-key \
  --old-key=/path/to/old.key \
  --dry-run
```

## vault:scan {#vault-scan}

Scan for potential plaintext secrets in database and configuration.

**Command syntax**

```bash
vendor/bin/typo3 vault:scan [options]
```

### Options {#options-10}

-   **--format=FORMAT, -f FORMAT**

    Output format: table (default), json, or summary.

-   **--exclude=TABLES, -e TABLES**

    Comma-separated list of tables to exclude (supports wildcards).

-   **--severity=LEVEL, -s LEVEL**

    Minimum severity to report: critical, high, medium, low (default: low).

-   **--database-only**

    Only scan database tables.

-   **--config-only**

    Only scan configuration files.

The command detects:

-   Database columns with secret-like names (password, api_key, token, etc.).
-   Known API key patterns (Stripe, AWS, GitHub, Slack, etc.).
-   Extension configuration secrets.
-   LocalConfiguration secrets (SMTP password, etc.).

### Severity levels {#severity-levels}

-   **critical**

    Known API key pattern detected (Stripe, AWS, etc.).

-   **high**

    Password or private key column with non-empty value.

-   **medium**

    Token or API key column with suspicious value.

-   **low**

    Secret-like column name detected.

### Example {#example-10}

**vault:scan examples**

```bash
# Scan all sources
vendor/bin/typo3 vault:scan

# Output as JSON for CI/CD
vendor/bin/typo3 vault:scan --format=json

# Exclude cache tables
vendor/bin/typo3 vault:scan --exclude=cache_*,cf_*

# Only show critical issues
vendor/bin/typo3 vault:scan --severity=critical
```

## vault:migrate-field {#vault-migrate-field}

Migrate existing plaintext database field values to vault storage.

**Command syntax**

```bash
vendor/bin/typo3 vault:migrate-field <table> <field> [options]
```

### Arguments {#arguments-1}

-   **table**

    Database table name (e.g., `tx_myext_settings`).

-   **field**

    Field name containing plaintext values to migrate.

### Options {#options-11}

-   **--dry-run**

    Show what would be migrated without making changes.

-   **--batch-size=N, -b N**

    Number of records to process per batch (default: 100).

-   **--where=SQL, -w SQL**

    Additional WHERE clause to filter records (e.g., `pid=1`).

-   **--force, -f**

    Migrate even if field already contains vault identifiers.

-   **--clear-source**

    Clear the source field after migration (set to empty string).

-   **--uid-field=NAME**

    Name of the UID field (default: uid).

> [!WARNING]
> **Attention**
>
> Always backup your database before running migrations.

### Example {#example-11}

**vault:migrate-field examples**

```bash
# Preview migration
vendor/bin/typo3 vault:migrate-field tx_myext_settings api_key --dry-run

# Migrate with specific records
vendor/bin/typo3 vault:migrate-field tx_myext_settings api_key --where="pid=1"

# Migrate and clear source field
vendor/bin/typo3 vault:migrate-field tx_myext_settings api_key --clear-source
```

## vault:cleanup-orphans {#vault-cleanup-orphans}

Clean up orphaned vault secrets from deleted TCA records.

When records with vault-backed fields are deleted, the corresponding vault
secrets may become orphaned. This command identifies and removes such orphaned
secrets.

**Command syntax**

```bash
vendor/bin/typo3 vault:cleanup-orphans [options]
```

### Options {#options-12}

-   **--dry-run**

    Show what would be deleted without making changes.

-   **--retention-days=DAYS, -r DAYS**

    Only delete orphans older than this many days (default: 0).

-   **--table=TABLE, -t TABLE**

    Only check secrets for this specific table.

-   **--batch-size=N, -b N**

    Number of secrets to check per batch (default: 100).

### Example {#example-12}

**vault:cleanup-orphans examples**

```bash
# Preview orphan cleanup
vendor/bin/typo3 vault:cleanup-orphans --dry-run

# Only clean up orphans older than 30 days
vendor/bin/typo3 vault:cleanup-orphans --retention-days=30

# Clean up orphans for specific table only
vendor/bin/typo3 vault:cleanup-orphans --table=tx_myext_settings
```

## vault:audit-migrate-hmac {#vault-audit-migrate-hmac}

Migrate existing audit log entries from plain SHA-256 (epoch 0) to
HMAC-SHA256 (target epoch configured via `auditHmacEpoch`). This command
rehashes all audit log entries using an HMAC key derived from the master key,
upgrading the hash chain from tamper detection to adversarial tamper resistance.

See [ADR-023: Audit hash chain HMAC consideration](https://docs.typo3.org/permalink/netresearch/nr-vault:adr-023-audit-hash-chain-hmac@1.0) for the architectural decision
behind this migration.

**Command syntax**

```bash
vendor/bin/typo3 vault:audit-migrate-hmac [options]
```

### Options {#options-13}

-   **--dry-run**

    Show what would be migrated without making changes.

### Example {#example-13}

**vault:audit-migrate-hmac examples**

```bash
# Preview migration
vendor/bin/typo3 vault:audit-migrate-hmac --dry-run

# Run the migration
vendor/bin/typo3 vault:audit-migrate-hmac
```

> [!WARNING]
> **Attention**
>
> This command requires a valid master key to derive the HMAC key.
> Always backup your database before running the migration. Once migrated,
> entries cannot be reverted to plain SHA-256 without restoring the backup.

## vault:seed-demo {#vault-seed-demo}

Populate a development instance with realistic, historic demo secrets and a
matching audit-log history (useful for exploring the Analytics module).

**Command syntax**

```bash
vendor/bin/typo3 vault:seed-demo [options]
```

### Options {#options-14}

-   **--force, -f**

    Delete existing demo data and reseed.

### Example {#example-14}

**vault:seed-demo examples**

```bash
# Seed demo data (no-op if already seeded)
vendor/bin/typo3 vault:seed-demo

# Wipe existing demo data and reseed
vendor/bin/typo3 vault:seed-demo --force
```

> [!WARNING]
> Development only. The command refuses to run in a Production application
> context and creates dummy secrets with obviously-fake values.

## vault:break-glass {#vault-break-glass}

Open, close or inspect a time-boxed break-glass window that temporarily
restores the administrator override removed by
[disableAdminOverride](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-disableadminoverride@1.0).

Only a real backend administrator or system maintainer — or an operator with
CLI access to the host — may open or close a window. A justification is
mandatory, both transitions are written to the tamper-evident audit log, and
the window expires on its own. See [Break-glass mode](https://docs.typo3.org/permalink/netresearch/nr-vault:security-break-glass@1.0) for the full
operational contract.

**Command syntax**

```bash
vendor/bin/typo3 vault:break-glass [options]
```

### Options {#options-15}

-   **--activate, -a**

    Open a break-glass window. Requires `--reason`.

-   **--deactivate, -d**

    Close the open window early. Requires `--reason`. A no-op when no window
    is open.

-   **--status, -s**

    Show the current state. This is the default when no action is given.

-   **--reason=TEXT, -r TEXT**

    Justification recorded in the audit log and shown in the backend warning
    banner. Mandatory for `--activate` and `--deactivate`; an empty or
    whitespace-only value is rejected.

-   **--minutes=N, -m N**

    Window length in minutes (default: 15). Values are clamped to the range
    1..60 rather than rejected.

### Example {#example-15}

**vault:break-glass examples**

```bash
# Is a window open, and is the override disabled at all?
vendor/bin/typo3 vault:break-glass --status

# Open a 30-minute window for an incident
vendor/bin/typo3 vault:break-glass --activate --reason="INC-4711 rotate leaked deploy key" --minutes=30

# Close it as soon as the work is done
vendor/bin/typo3 vault:break-glass --deactivate --reason="INC-4711 closed"
```

The `--status` output is line-oriented for monitoring probes and always
exits `0` — a closed window is a successful answer, not a failure:

**--status output while a window is open**

```text
securityProfile                  hardened
disableAdminOverride             yes
adminOverrideDisabledEffective   yes
status: active
activatedBy                      admin (uid 1)
reason                           INC-4711 rotate leaked deploy key
activatedAt                      2026-07-31T09:12:04+00:00
expiresAt                        2026-07-31T09:42:04+00:00
remainingSeconds                 1738
```

> [!WARNING]
> **Attention**
>
> While a window is open, administrators hold **every** vault permission and
> full access to **every** secret. Close it as soon as the work is done
> rather than waiting for the expiry.

## vault:doctor {#vault-doctor}

Evaluate every deployment-readiness control and report each one with the risk it
carries and the command that fixes it.

Designed as the last step of a deployment pipeline: the exit code is the
contract, so the command can gate a release without anything parsing its prose.
See [Deployment gate](https://docs.typo3.org/permalink/netresearch/nr-vault:security-deployment-gate@1.0) for how to wire it in.

**Command syntax**

```bash
vendor/bin/typo3 vault:doctor [options]
```

### Options {#options-16}

-   **--profile=PROFILE, -p PROFILE**

    Security profile to check against: `standard` or `hardened`. Defaults to
    the configured profile.

    **This never changes any configuration.** `--profile=hardened` on a standard
    installation answers "would this pass if we hardened it?", so a hardening
    migration can be planned from the real finding list instead of by flipping the
    switch on production and seeing what breaks. The report states both the profile
    it checked and the profile actually in force.

-   **--format=FORMAT, -f FORMAT**

    Output format: `text` (default) or `json`. The JSON form carries every
    control — passing ones included — under stable ids.

-   **--active-probes**

    Additionally push the current chain-tip anchor through every enabled audit
    sink to verify end-to-end delivery (the webhook collector must answer 2xx;
    the file sink must actually append; syslog must accept the message). Adds
    one `audit.sink_probe.<sink>` finding per enabled sink; a refused probe
    is critical. Talks to external systems, so it is never run implicitly —
    neither by the passive checks nor by the backend status panel.

### Exit codes {#exit-codes-2}

The verdict is the **worst severity present**, never an average: a long list of
passing controls can never offset a critical finding.

-   **0**

    Every control passed. The configuration is audit-ready for the checked profile.

-   **1**

    Warnings only. Deployable; fix before an audit.

-   **2**

    At least one critical finding — or the profile value was unusable, or the run
    could not complete at all. A gate that cannot run never reports success.

### Control catalogue {#control-catalogue}

Control ids are **stable API**: they appear in `--format=json`, in CI gate
allow-lists and in monitoring rules. A new control gets a new id rather than
reusing an old one.

Severities below are given as `standard` / `hardened` where they differ.

-   **provider.configured**

    A master-key provider is chosen and permitted by the profile. `typo3` (the
    zero-configuration default) is *warning* / *critical* — the hardened profile
    forbids it, and its factory refuses to boot. An empty value is critical in
    both.

-   **provider.known**

    The configured provider identifier resolves to something this installation can
    build. Critical when it does not — a provider from a later release, or from an
    extension that is not installed here.

-   **provider.available**

    A key source is reachable at runtime. Critical when none is. *Warning* when
    standard-profile auto-detection resolved a different provider than the one
    configured: the vault works, but not the way the configuration says, and
    hardening removes the fallback.

-   **provider.master_key_readable**

    A non-empty key was actually read and envelope encryption is operational.
    Critical otherwise.

-   **provider.key_permissions**

    File provider only. Critical when the key file is readable, writable or
    executable beyond its owner. Only the octal mode is reported, never the path.

-   **profile.valid**

    The configured `securityProfile` is a known profile. Critical otherwise —
    the extension refuses to guess, so an unknown value is an outage.

-   **profile.admin_override**

    `disableAdminOverride` agrees with the profile. Warning in both mismatch
    directions: the flag set under `standard` (where it is inert, so the
    configuration implies a control that does not exist), and `hardened` without
    it (a hardened deployment that kept its widest bypass).

-   **breakglass.window_open**

    Warning while a [break-glass window](https://docs.typo3.org/permalink/netresearch/nr-vault:security-break-glass@1.0) is open,
    naming who opened it, why, and when it closes. Warning rather than critical on
    purpose: an open window is a justified deliberate act, and a red gate would
    push operators to close it mid-incident just to deploy.

-   **audit.reads_logged**

    `auditReads` is enabled. *Warning* / *critical* — a stolen credential is
    *read*, not written, so an unlogged read defeats what the hardened profile
    exists for.

-   **audit.retention**

    `auditLogRetention` is 0 (keep forever) or at least 365 days. Warning for a
    shorter window, which cannot cover the previous review cycle.

-   **audit.hash_chain**

    The newest 1000 audit entries verify against the hash chain. Critical on any
    hash error or uid gap. **Bounded on purpose** — a full-table HMAC
    recomputation does not belong on a page load — so a pass means "the recent
    tail verifies", never "the chain is intact". [vault:audit-verify](https://docs.typo3.org/permalink/netresearch/nr-vault:command-audit-verify@1.0) is
    the authoritative full-range verifier and belongs on a schedule.

    `details` carries `errorCount`, `warningCount` and
    `missingUidCount`. The warnings are epoch boundaries — non-fatal by
    design, and the sign that a migration covered only part of the verified
    range, which is context a CI gate wants beside a green `errorCount`.

-   **audit.hmac_epoch**

    [auditHmacEpoch](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-audithmacepoch@1.0) is at least 3 — the shipped default,
    and the only epoch whose HMAC spans the whole audit row. Three-way, because
    the epoch integer selects which payload `AuditLogService` signs, and
    "keyed" and "trustworthy" are not the same state:

    **Critical** at 0. The one integer switches off three controls at once —
    rows are hashed with keyless SHA-256, the epoch-downgrade floor equals the
    configured epoch and so can never be undercut, and the in-DB tip anchor is
    disabled. The finding names all three, because "epoch is 0" on its own reads
    like a version marker rather than a disabled chain.

    **Warning** at 1 and 2. The chain is keyed there, so entries cannot be
    rewritten wholesale — but the MAC does not cover the whole row, and the
    finding names the columns it leaves out (also as
    `details.unsignedFields`). At **epoch 1** the payload is six identity
    fields only (uid, secret_identifier, action, actor_uid, crdate,
    previous_hash), so `success` itself is forgeable: a recorded denial can be
    flipped into a recorded grant, and `error_message`, `reason`,
    `ip_address`, `user_agent`, `hash_before`, `hash_after` and
    `context` can be rewritten the same way. At **epoch 2** the forensic
    columns are signed, but `hmac_key_epoch` — the algorithm selector itself —
    is not, so a row can be relabelled to a lower epoch and re-signed under the
    weaker algorithm without the key; neither are `actor_type`,
    `actor_username`, `actor_role` and `request_id`, the fields the backend
    audit list and the CSV export present as the responsible actor, so blame can
    be reassigned on any row.

    Both intermediate epochs are live states, not hypotheticals: a stalled or
    partial [vault:audit-migrate-hmac](https://docs.typo3.org/permalink/netresearch/nr-vault:command-audit-migrate-hmac@1.0) run (or its install-tool wizard)
    leaves an installation exactly there, which is why the remediation says to
    check that the migration completed rather than only raising the setting.

    **Warning** when the setting is at 3 or above but the STORED rows are not.
    [auditHmacEpoch](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-audithmacepoch@1.0) decides how the *next* row is signed;
    it does not reach back over the ones already written. Raise it without
    running [vault:audit-migrate-hmac](https://docs.typo3.org/permalink/netresearch/nr-vault:command-audit-migrate-hmac@1.0) and every historical row keeps its
    narrower signature while the configuration reads as fully protected — the
    chain still verifies, because those rows are keyed, just under a smaller
    payload. Nothing else catches it: the epoch-downgrade floor in
    `verifyHashChain()` compares the chain HIGH-WATER epoch against the
    configured one, so a single row at the current epoch satisfies it, and it
    only runs on a full-range pass, which `audit.hash_chain` never requests.

    The check reads the `hmac_key_epoch` of the OLDEST row — one
    PRIMARY-ordered row, because that column carries no index and a `GROUP BY`
    over it would be a full scan on a control that also renders the backend
    status panel. That equals the stored minimum on any chain that verifies: a
    *decrease* between adjacent uids is already recorded as an error, so a
    legitimate chain is epoch-monotonic. The value is reported as
    `details.storedMinEpoch` (`-1` when the audit log is empty) for a CI
    gate, and the pass text now states the configured epoch and — where there
    was a stored row to check it against — the stored minimum it confirmed,
    rather than asserting a property of rows it never looked at.

    The interrupted-migration case is *not* the silent one. Both migration paths
    rewrite row by row without a transaction, so a half-finished run leaves
    newer rows behind older migrated ones — a decrease, which is already an
    error. The state that arrives without any signal is the setting raised while
    the migration was never run.

    [vault:audit-verify](https://docs.typo3.org/permalink/netresearch/nr-vault:command-audit-verify@1.0) reports the FULL distribution rather than only
    the minimum: its walk already reads every row's epoch to pick the hash
    algorithm, so the per-epoch counts cost nothing. They appear as
    `Stored HMAC epochs` in the text report and as `epochCounts` /
    `minEpoch` / `maxEpoch` in the JSON body — on a clean run too, because
    "the chain is valid" and "the whole chain is signed at the configured epoch"
    are different statements and only the second is answered there.

-   **audit.db_anchor**

    The IN-DATABASE tip anchor in `sys_registry` — a different control from
    `audit.anchor`, which covers the copy published to the external sinks. Pass
    when the anchor is present and its MAC verifies; *warning* when it is not
    armed yet on a non-empty chain, when `sys_registry` and
    `tx_nrvault_audit_log` are mapped to different database connections (no
    vault-side action fixes that), and when the anchor is disabled by
    `auditHmacEpoch = 0`. **Critical** when it is missing while
    [auditAnchorRequired](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-auditanchorrequired@1.0) is enabled, when the stored anchor
    is present but unreadable (tampered value or a master key changed without a
    re-seal), and for the contradiction `auditAnchorRequired = 1` together with
    `auditHmacEpoch = 0` — verification reports `Disabled` and returns before
    the requirement is ever consulted, so that combination protects nothing while
    reading as the stricter configuration. An empty audit log is a pass: the
    anchor arms itself on the first audit write. Like `audit.hash_chain` this
    control states its scope — a pass means the anchor authenticates, not that
    the anchored row still carries the anchored hash; that comparison is
    [vault:audit-verify](https://docs.typo3.org/permalink/netresearch/nr-vault:command-audit-verify@1.0).

-   **audit.external_sink**

    At least one external audit sink is enabled. *Pass* / *critical*: sinks are
    documented as opt-in under `standard`, and flagging a default installation
    for having no SIEM would train operators to ignore the hardened finding.
    Carries `details.reasonCode = NO_EXTERNAL_SINK`, the same code
    `vault:audit-verify` uses.

-   **audit.anchor**

    A chain-tip anchor exists and the chain has not shrunk below it. Missing
    anchor is *pass* / *critical*. A chain shorter than the anchored sequence is
    critical in both (`TABLE_RESET`) — an append-only chain cannot get shorter.
    Only the shrinkage comparison is done here; the tip-hash comparison is
    [vault:audit-verify](https://docs.typo3.org/permalink/netresearch/nr-vault:command-audit-verify@1.0).

-   **audit.sink_delivery**

    No sink refused delivery in this process. Warning with the per-sink counts
    otherwise. Zero means "not in this run" — the cross-process question is
    answered by `audit.sink_state.<sink>`.

-   **audit.sink_state.\<sink>**

    One finding per enabled sink, based on the PERSISTED delivery state
    (`sys_registry`): consecutive failures or a last successful delivery
    older than `auditSinkStaleDeliveryHours` are *warning* / **critical**
    (hardened); an enabled sink with no recorded delivery yet is *pass* /
    *warning* (hardened). A freshly started `vault:doctor` can therefore no
    longer report a collector that has been unreachable for days as healthy.

-   **audit.sink_probe.\<sink>**

    Emitted only with `--active-probes`: the current chain-tip anchor is
    pushed through every enabled sink end-to-end (webhook: the collector must
    answer 2xx). A refused probe is **critical** in both profiles — the sink
    is enabled but demonstrably not accepting evidence.

    When `--active-probes` runs with **no** sink enabled there is nothing to
    probe, and a single literal `audit.sink_probe.none` finding is emitted
    instead of the per-sink family, so an empty probe run cannot be mistaken
    for a clean one.

-   **cli.access**

    `allowCliAccess`. Pass when off. When on: *pass* / **critical**
    (hardened) — deployment automation legitimately needs it under the
    standard profile, but the hardened profile promises attributability and a
    bare CLI actor breaks that promise.

-   **cli.access_groups**

    Emitted only when CLI access is on. Warning when `cliAccessGroups` is empty,
    leaving the grant unscoped with no group boundary left to review.

-   **cli.allowed_operations**

    Emitted only when CLI access is on. Reports which operations
    [cliAllowedOperations](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-cliallowedoperations@1.0) actually grants the unattributed
    CLI actor. Warning when the list contains a high-risk operation
    (`secret.reveal`, `secret.delete`, `secret.manage_policy`,
    `audit.view`, `audit.export`, `master_key.rotate`,
    `vault.configure`) or an unknown value. Unknown
    values are called out because they are silently inert — a typo revokes the
    grant the operator believes is configured rather than failing loudly.

    Of the seven high-risk entries, four currently change what a CLI command
    can do: `secret.reveal` (`vault:retrieve`), `secret.delete`
    (`vault:delete` and the orphan cleanup), `master_key.rotate`
    (`vault:rotate-master-key`) and `audit.view` (`vault:audit`,
    `vault:audit --verify`, `vault:audit-verify`). `secret.manage_policy`,
    `audit.export` and `vault.configure`
    gate the corresponding **backend** actions; `vault:audit --export`
    asserts no operation permission of its own. They are still called out
    here, because the allowlist is the record of what the CLI actor has been
    granted, not only of what it can currently reach.

    The pass wording is deliberately not "low-risk operation(s)". What remains
    after the seven are excluded is the deployment-automation default, and it
    is not inert either: `secret.create` makes the CLI actor the *owner* of
    what it creates, and `secret.rotate` substitutes a credential the
    operator's own systems then use. The pass states that nothing in the list
    needs an explicit opt-in — not that the list is harmless.

-   **cli.frontend_placeholder_legacy**

    [frontendPlaceholderLegacyCli](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-frontendplaceholderlegacycli@1.0). Pass when off — the
    command line then enforces the same frontend placeholder allow-set as a web
    request. *Warning* / **critical** (hardened) when on.

    **Always emitted, unlike the two controls above.** The setting is not part
    of the `allowCliAccess` grant and is not gated by it:
    `FrontendPlaceholderPolicy` consults this flag and nothing else, so the
    bypass is fully live on a default installation with CLI access off. It
    shares the `cli.` prefix because it widens what a shell reaches, not
    because it belongs to that grant.

    Warning rather than pass under `standard`, which is where it differs from
    `cli.access`: a deployment pipeline genuinely needs `allowCliAccess`,
    whereas there is no workflow that needs this flag and cannot be served by
    publishing the identifier instead. What it re-opens is concrete —
    `scheduler:run` authenticates the `_cli_` administrator, so the admin
    bypass grants the read whatever the per-secret tiers say, and a scheduled
    newsletter or export job rendering editor-authored `tt_content` through
    `stdWrap()` substitutes any frontend-accessible secret an editor can name.
    See [ADR-035: Per-request allow-set of frontend-resolvable identifiers](https://docs.typo3.org/permalink/netresearch/nr-vault:adr-035-frontend-placeholder-allow-set@1.0).

-   **secrets.expired**

    No stored secret is past its expiry. Warning with the count otherwise: an
    expired secret still decrypts, so the credential stays recoverable from a
    database dump.

-   **secrets.never_rotated**

    No secret has gone unrotated beyond `staleNeverRotatedDays`. Warning with
    the count otherwise.

-   **secrets.dead**

    No stored secret shows zero read activity. Warning with the count otherwise.
    No identifier appears in any of these three findings — an identifier names a
    credential and the JSON report travels into CI logs. Use the Analytics module
    to see which secrets.

-   **inventory.missing_secrets**

    Every identifier the audit log records as created still has a row in
    `tx_nrvault_secret`. Critical with the count otherwise. A delete is a soft
    delete — the row stays with `deleted = 1` — and nothing removes a secret
    row outright, so a create entry with nothing behind it is a lost row rather
    than a deleted one, and the plaintext exists nowhere else. The comparison is
    zero against zero on a fresh installation, which is why an empty vault does
    not raise it. See [Restore verification: the probe decrypt](https://docs.typo3.org/permalink/netresearch/nr-vault:operations-backup-and-restore-verification@1.0) for what
    it does *not* catch.

-   **inventory.orphan_permissions**

    Every row in the two MM tables belongs to a stored secret. Warning with the
    count otherwise: the permission tables and the secret table came from
    different moments. The opposite half — a secret whose permission rows are
    gone — cannot be detected from the data and locks legitimate users out; the
    restore procedure covers it with a non-admin read check.

-   **environment.production_context**

    `Environment::getContext()` is Production. *Pass* / *warning* — a
    Development context is the normal state of a developer machine, and only a
    hardened deployment contradicts itself by running in one.

-   **environment.backend_lock_ssl**

    `[BE][lockSSL]` is set. Warning otherwise, in both profiles: the reveal
    endpoint returns secret plaintext to a browser.

-   **version.extension**

    The extension version was read from `ext_emconf.php`. Warning otherwise — a
    report that cannot state which version produced it is not evidence.

-   **version.typo3_supported**

    The running core is inside the range the extension declares. Warning
    otherwise.

-   **check.crashed**

    Not a control but a containment result: emitted as **critical** when a check
    throws, naming the failing check in `details.check`. A diagnostic whose
    output degrades to "no findings" when part of it breaks is worse than none,
    because the silence reads as a pass — so a crashed check is louder than a
    failing one.

### JSON output {#json-output}

**vault:doctor --format=json (abridged)**

```json
{
  "profile": "hardened",
  "configuredProfile": "standard",
  "profileOverridden": true,
  "auditReady": false,
  "highestSeverity": "critical",
  "exitCode": 2,
  "summary": { "total": 22, "pass": 19, "warning": 2, "critical": 1 },
  "findings": [
    {
      "id": "provider.configured",
      "severity": "pass",
      "summary": "Master-key provider \"file\" is explicitly configured.",
      "risk": "",
      "remediation": "",
      "docsUrl": "https://docs.typo3.org/p/netresearch/nr-vault/main/en-us/Configuration/Index.html#configuration-master-key-providers",
      "details": { "provider": "file" }
    },
    {
      "id": "audit.external_sink",
      "severity": "critical",
      "summary": "The hardened profile requires an external audit sink, but none is enabled and usable.",
      "risk": "The audit trail exists only in the database it is meant to protect. …",
      "remediation": "Enable at least one of \"auditSinkFileEnabled\", \"auditSinkSyslogEnabled\" or \"auditSinkWebhookEnabled\", then schedule the anchoring command. …",
      "docsUrl": "https://docs.typo3.org/p/netresearch/nr-vault/main/en-us/Configuration/Index.html#configuration-audit-sinks",
      "details": { "sinks": "", "reasonCode": "NO_EXTERNAL_SINK" }
    }
  ]
}
```

Field notes for anything parsing this:

-   `findings` lists **every** evaluated control, passing ones included, so
    `summary.pass` / `summary.total` needs no second source of truth.
-   `severity` is one of `pass`, `warning`, `critical`.
-   `risk` and `remediation` are empty strings for passing controls, never
    absent.
-   `docsUrl` may be an empty string.
-   `details` holds scalars only, and never key material, a master-key path or a
    secret identifier.
-   `exitCode` duplicates the process exit code, for wrappers that swallow it.
-   On a rejected `--profile` value or a run that could not start, the payload is
    `{"error": "…", "exitCode": 2}` instead — check for `error` before reading
    `findings`.

### Example {#example-16}

**vault:doctor examples**

```bash
# Is this deployment ready for the profile it claims?
vendor/bin/typo3 vault:doctor

# Would it pass as hardened? Changes nothing.
vendor/bin/typo3 vault:doctor --profile=hardened

# Machine-readable, for a CI gate or a monitoring probe
vendor/bin/typo3 vault:doctor --format=json

# Verify end-to-end sink delivery (talks to the collector)
vendor/bin/typo3 vault:doctor --active-probes
```
