CLI commands
nr-vault provides several CLI commands for DevOps automation and management.
vault:init
Initialize the vault by creating a master key.
vendor/bin/typo3 vault:init [options]
Options
- --output, -o
- 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
# 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
Store a secret in the vault.
vendor/bin/typo3 vault:store <identifier> [options]
Arguments
- identifier
- Unique identifier for the secret.
Options
- --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/--expiresoptions. - --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.
Example
# 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
Retrieve a secret from the vault.
vendor/bin/typo3 vault:retrieve <identifier> [options]
Options
- --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, which excludes it by default.
Without both, the command exits 1. Prefer a named technical actor
(Technical actor context) over widening the allowlist.
Example
# 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
List all accessible secrets.
vendor/bin/typo3 vault:list [options]
Options
- --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
# 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
Rotate a secret with a new value.
vendor/bin/typo3 vault:rotate <identifier> [options]
Options
- --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
vendor/bin/typo3 vault:rotate stripe_api_key \
--reason="Scheduled quarterly rotation"
vault:delete
Delete a secret from the vault.
vendor/bin/typo3 vault:delete <identifier> [options]
Options
- --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 excludes by default. On the CLI
it needs allowCliAccess and that operation in the allowlist.
Example
vendor/bin/typo3 vault:delete old_api_key \
--reason="Service deprecated" \
--force
vault:audit
View the audit log.
vendor/bin/typo3 vault:audit [options]
Options
- --identifier=ID, -i ID
- Filter by secret identifier.
- --action=ACTION, -a ACTION
- Filter by action — any
Auditvalue. The common ones areAction create,read,update,delete,rotateandaccess_denied; the enum also coversmetadata_update,http_call, 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-dorY-m-d H:i:s). - --until=DATE
- Show entries up to the given date (
Y-m-dorY-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)):ok,NOT ARMED,VIOLATED,UNREADABLE,disabledorinconclusive. - --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_logthat 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 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).
Example
# 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
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 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.
vendor/bin/typo3 vault:audit-anchor [options]
Options
- --dry-run
- Show the anchor that would be published without writing it to any sink.
- --format=FORMAT, -f FORMAT
- Output format:
text(default) orjson.
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,auditSinkSyslogEnabledorauditSinkWebhookEnabled.
Example
# 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
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 and classifies every
finding under a machine-readable reason code.
vendor/bin/typo3 vault:audit-verify [options]
Options
- --format=FORMAT, -f FORMAT
- Output format:
text(default) orjson. 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.
Reason codes
- HASH_MISMATCH
- A stored
entry_hashorprevious_hashdoes 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_epochwas 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
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
- 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
# 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
Rotate the master encryption key. Re-encrypts all DEKs with a new master key.
vendor/bin/typo3 vault:rotate-master-key [options]
Options
- --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 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
# 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
Scan for potential plaintext secrets in database and configuration.
vendor/bin/typo3 vault:scan [options]
Options
- --format, -f
- Output format: table (default), json, or summary.
- --exclude, -e
- Comma-separated list of tables to exclude (supports wildcards).
- --severity, -s
- 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
- 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
# 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
Migrate existing plaintext database field values to vault storage.
vendor/bin/typo3 vault:migrate-field <table> <field> [options]
Arguments
- table
- Database table name (e.g.,
tx_myext_settings). - field
- Field name containing plaintext values to migrate.
Options
- --dry-run
- Show what would be migrated without making changes.
- --batch-size, -b
- Number of records to process per batch (default: 100).
- --where, -w
- 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 of the UID field (default: uid).
Attention
Always backup your database before running migrations.
Example
# 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
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.
vendor/bin/typo3 vault:cleanup-orphans [options]
Options
- --dry-run
- Show what would be deleted without making changes.
- --retention-days, -r
- Only delete orphans older than this many days (default: 0).
- --table, -t
- Only check secrets for this specific table.
- --batch-size, -b
- Number of secrets to check per batch (default: 100).
Example
# 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
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 for the architectural decision behind this migration.
vendor/bin/typo3 vault:audit-migrate-hmac [options]
Options
- --dry-run
- Show what would be migrated without making changes.
Example
# Preview migration
vendor/bin/typo3 vault:audit-migrate-hmac --dry-run
# Run the migration
vendor/bin/typo3 vault:audit-migrate-hmac
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
Populate a development instance with realistic, historic demo secrets and a matching audit-log history (useful for exploring the Analytics module).
vendor/bin/typo3 vault:seed-demo [options]
Options
- --force, -f
- Delete existing demo data and reseed.
Example
# 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
Open, close or inspect a time-boxed break-glass window that temporarily restores the administrator override removed by disableAdminOverride.
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 for the full operational contract.
vendor/bin/typo3 vault:break-glass [options]
Options
- --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, -r
- Justification recorded in the audit log and shown in the backend warning
banner. Mandatory for
--activateand--deactivate; an empty or whitespace-only value is rejected. - --minutes, -m
- Window length in minutes (default: 15). Values are clamped to the range 1..60 rather than rejected.
Example
# 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:
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
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
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 for how to wire it in.
vendor/bin/typo3 vault:doctor [options]
Options
- --profile=PROFILE, -p PROFILE
-
Security profile to check against:
standardorhardened. Defaults to the configured profile.This never changes any configuration.
--profile=hardenedon 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) orjson. 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
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 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
securityProfileis a known profile. Critical otherwise — the extension refuses to guess, so an unknown value is an outage. - profile.admin_override
disableAdminOverrideagrees with the profile. Warning in both mismatch directions: the flag set understandard(where it is inert, so the configuration implies a control that does not exist), andhardenedwithout it (a hardened deployment that kept its widest bypass).- breakglass.window_open
- Warning while a break-glass window 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
auditReadsis enabled. Warning / critical — a stolen credential is read, not written, so an unlogged read defeats what the hardened profile exists for.- audit.retention
auditLogRetentionis 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 is the authoritative full-range verifier and belongs on a schedule.
- audit.hmac_epoch
-
auditHmacEpoch 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
AuditLogServicesigns, 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), sosuccessitself is forgeable: a recorded denial can be flipped into a recorded grant, anderror_message,reason,ip_address,user_agent,hash_before,hash_afterandcontextcan be rewritten the same way. At epoch 2 the forensic columns are signed, buthmac_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 areactor_type,actor_username,actor_roleandrequest_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 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.
- audit.db_anchor
- The IN-DATABASE tip anchor in
sys_registry— a different control fromaudit.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, whensys_registryandtx_nrvault_audit_logare mapped to different database connections (no vault-side action fixes that), and when the anchor is disabled byauditHmacEpoch = 0. Critical when it is missing while auditAnchorRequired is enabled, when the stored anchor is present but unreadable (tampered value or a master key changed without a re-seal), and for the contradictionauditAnchorRequired = 1together withauditHmacEpoch = 0— verification reportsDisabledand 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. Likeaudit.hash_chainthis 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. - 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. Carriesdetails.reasonCode = NO_EXTERNAL_SINK, the same codevault:audit-verifyuses. - 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. - 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 thanauditSinkStaleDeliveryHoursare warning / critical (hardened); an enabled sink with no recorded delivery yet is pass / warning (hardened). A freshly startedvault:doctorcan 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-probesruns with no sink enabled there is nothing to probe, and a single literalaudit.sink_probe.nonefinding 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
cliAccessGroupsis 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 actually grants the unattributed CLI actor. Warning when the list contains a high-risk operation (
secret.reveal,secret.delete,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 five high-risk entries, three currently change what a CLI command can do:
secret.reveal(vault:retrieve),secret.delete(vault:deleteand the orphan cleanup) andmaster_key.rotate(vault:rotate-master-key).audit.exportandvault.configuregate the corresponding backend actions;vault:audit --exportasserts 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. - cli.frontend_placeholder_legacy
-
frontendPlaceholderLegacyCli. 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
allowCliAccessgrant and is not gated by it:FrontendPlaceholderPolicyconsults this flag and nothing else, so the bypass is fully live on a default installation with CLI access off. It shares thecli.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 fromcli.access: a deployment pipeline genuinely needsallowCliAccess, 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:authenticates therun _cli_administrator, so the admin bypass grants the read whatever the per-secret tiers say, and a scheduled newsletter or export job rendering editor-authoredtt_contentthroughstdWrap()substitutes any frontend-accessible secret an editor can name. See ADR-035: Per-request allow-set of frontend-resolvable identifiers. - 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.
- 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
{
"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:
findingslists every evaluated control, passing ones included, sosummary.pass/summary.totalneeds no second source of truth.severityis one ofpass,warning,critical.riskandremediationare empty strings for passing controls, never absent.docsUrlmay be an empty string.detailsholds scalars only, and never key material, a master-key path or a secret identifier.exitCodeduplicates the process exit code, for wrappers that swallow it.- On a rejected
--profilevalue or a run that could not start, the payload is{"error": "…", "exitCode": 2}instead — check forerrorbefore readingfindings.
Example
# 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