---
title: "Configuration"
manual: "nr-vault"
version: "1.0"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-vault:configuration@1.0"
source: "Configuration/Index.rst"
rendered: "2026-09-18T07:37:50+00:00"
---

# Configuration {#configuration-1}

## Extension configuration {#extension-configuration}

Configure nr-vault in **Admin Tools > Settings > Extension Configuration**.

-   **storageAdapter**

    -   *Type:* string
    -   *Default:* local
    -   *Options:* local

    Where secrets are stored.

    -   **local**

        Store secrets in the TYPO3 database (default). Secrets are encrypted
        with envelope encryption before storage.

    > [!NOTE]
    > **External vault adapters (HashiCorp Vault, AWS Secrets Manager) are
    > planned for future releases.** The adapter architecture is designed to
    > support external backends, but currently only the local database adapter
    > is implemented. See [Custom storage adapters](https://docs.typo3.org/permalink/netresearch/nr-vault:developer-custom-adapters@1.0) for information on
    > implementing custom adapters.

-   **securityProfile**

    -   *Type:* string
    -   *Default:* standard
    -   *Options:* standard, hardened

    The vault's operating profile — a single, internally consistent policy
    rather than a bag of independent toggles. Enforcement happens in code
    (provider selection, access control, audit anchoring), never only in
    documentation.

    -   **standard**

        Secure defaults with zero-configuration TYPO3 integration.

    -   **hardened**

        Fail-closed and audit-ready. It refuses the `typo3` provider and
        nothing else: `file`, `env`, `transit` and a provider another
        extension registers are all permitted, because the demand is that
        the master key lives outside
        `config/system/settings.php`. It disables provider
        auto-detection and any fallback to the TYPO3 encryption key, and
        makes vault operations refuse to run on a misconfigured or
        unavailable provider. It is also the prerequisite for
        [disableAdminOverride](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-disableadminoverride@1.0).

    An unrecognised value throws rather than degrading to `standard` — a
    typo in a hardened deployment must never weaken the effective policy.

-   **masterKeyProvider**

    -   *Type:* string
    -   *Default:* typo3
    -   *Options:* typo3, file, env, transit, or an identifier registered by another extension

    How to retrieve the master encryption key.

    -   **typo3**

        Derive from TYPO3's encryption key. This is the recommended default
        as it requires no additional configuration and works out of the box.

    -   **file**

        Read from a file on the filesystem.

    -   **env**

        Read from an environment variable.

    -   **transit**

        Unwrap through HashiCorp Vault's transit secrets engine. Only the
        Vault-encrypted ciphertext is stored locally — see
        [HashiCorp Vault Transit provider](https://docs.typo3.org/permalink/netresearch/nr-vault:configuration-master-key-transit@1.0).

    Any other value names a provider that an installed extension registered
    with the `nr_vault.master_key_provider` tag. An identifier no provider
    claims is refused with exception code `1703800015`; the vault does not
    fall back to a different key source in the hardened profile. See
    [Custom master key providers](https://docs.typo3.org/permalink/netresearch/nr-vault:developer-custom-key-providers@1.0).

-   **masterKeySource**

    -   *Type:* string
    -   *Default:* NR_VAULT_MASTER_KEY

    Source location for the master key. Interpretation depends on the provider:

    -   **file**: Path to the key file (e.g., `/secure/path/vault.key`).
    -   **env**: Environment variable name (e.g., `NR_VAULT_MASTER_KEY`).
    -   **typo3**: Not used (key derived from TYPO3's encryption key).
    -   **transit**: Not used (configured via the `hashicorp.*` settings below).

-   **hashicorp.address**

    -   *Type:* string
    -   *Default:* (empty)

    Vault server base address, e.g. `https://vault.example.com:8200`.
    Required by the `transit` master key provider.

-   **hashicorp.authMethod**

    -   *Type:* string
    -   *Default:* token
    -   *Options:* token, kubernetes, approle

    Vault authentication method. The `transit` master key provider implements
    `token` only and refuses to start on the other values rather than silently
    downgrading. See [HashiCorp Vault Transit provider](https://docs.typo3.org/permalink/netresearch/nr-vault:configuration-master-key-transit@1.0).

-   **hashicorp.tokenEnvVar**

    -   *Type:* string
    -   *Default:* VAULT_TOKEN

    Name of the environment variable holding the Vault token. Read in preference
    to [hashicorp.token](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-hashicorp-token@1.0).

-   **hashicorp.token**

    -   *Type:* string
    -   *Default:* (empty)

    Vault token stored in the extension configuration. Development fallback only
    — a token stored here is readable in the Install Tool and ends up in
    configuration exports. Prefer the environment variable.

-   **hashicorp.transitMount**

    -   *Type:* string
    -   *Default:* transit

    Mount path of the transit secrets engine, without the `/v1/` prefix.
    Nested mounts such as `platform/transit` are supported.

-   **hashicorp.transitKeyName**

    -   *Type:* string
    -   *Default:* nr-vault-master

    Name of the transit key that wraps the vault master key.

-   **hashicorp.transitWrappedKeyPath**

    -   *Type:* string
    -   *Default:* (empty)

    File holding the Vault-wrapped master key. Empty resolves to
    `<var-path>/secrets/vault-master.key.transit`. The file contains
    ciphertext only, never key material.

-   **hashicorp.path**

    -   *Type:* string
    -   *Default:* secret/data/typo3

    Path prefix for secrets in Vault. **Reserved** — it belongs to the
    not-yet-implemented HashiCorp storage adapter and has no effect on the
    transit master-key provider, which uses the `transit*` settings above.

## AWS Secrets Manager {#aws-secrets-manager}

> [!NOTE]
> The AWS Secrets Manager adapter is **planned but not implemented**. Both
> settings below are reserved; setting them changes nothing today. They are
> documented so an operator who finds them in the Settings module knows they
> are inert rather than misconfigured.

-   **aws.region**

    -   *Type:* string
    -   *Default:* (empty)

    AWS region for Secrets Manager, for example `eu-central-1`.

-   **aws.secretPrefix**

    -   *Type:* string
    -   *Default:* typo3/

    Prefix for secret names in AWS Secrets Manager.

## CLI access {#cli-access}

-   **allowCliAccess**

    -   *Type:* boolean
    -   *Default:* false

    Allow CLI commands to access secrets without a backend user session.

-   **cliAccessGroups**

    -   *Type:* string
    -   *Default:* empty

    Comma-separated list of backend user group UIDs that CLI can access.
    Empty means all secrets are accessible when CLI access is enabled.

-   **cliAllowedOperations**

    -   *Type:* string
    -   *Default:* `secret.use,secret.create,secret.rotate`

    Operation permissions the unattributed CLI actor may hold while
    [allowCliAccess](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-allowcliaccess@1.0) is on.
    The default covers deployment automation (store, rotate, consume).
    High-risk operations — `secret.reveal` (`vault:retrieve`
    printing plaintext), `secret.delete`, `secret.manage_policy`,
    `audit.view`, `audit.export`, `master_key.rotate`
    (`vault:rotate-master-key`), `vault.configure` — are
    **excluded by default** and must be added explicitly where a workflow
    genuinely needs them. Prefer a named
    technical actor (`TechnicalActorContext::runAs()`) over widening
    this list: the audit trail then names the responsible identity.
    Note that the scheduled orphan cleanup deletes secrets and therefore
    needs `secret.delete` when it runs as the bare CLI actor.

    Five of the seven change what a CLI command can do: `secret.reveal`
    (`vault:retrieve`), `secret.delete` (`vault:delete`),
    `master_key.rotate` (`vault:rotate-master-key`), `audit.export`
    (`vault:audit --export`) and `vault.configure`
    (`vault:audit-anchor` and `vault:audit --reset-anchor`).
    `audit.view` covers three more — `vault:audit` for plain
    listing, `vault:audit --verify` and `vault:audit-verify` —
    so reading or verifying the audit log from an unattributed shell needs
    it added here. `secret.manage_policy` gates the **backend** policy
    edit rather than a command, and it is listed because it is the widest
    entry over time: it edits `allowed_groups` and `write_groups`, so a
    shell holding it can widen its own per-secret reach and the widened
    tiers read as ordinary configuration afterwards.

    The scheduler is a different actor and is not affected by this allowlist:
    `scheduler:run` authenticates the `_cli_` administrator, so the
    `AuditVerifyTask` / `AuditAnchorTask` gates resolve through the admin
    bypass instead. They bite only under `disableAdminOverride`, where that
    identity needs the permissions on one of its groups — see
    [Step 6 — Withdraw the administrator override, and pin it](https://docs.typo3.org/permalink/netresearch/nr-vault:operations-hardened-deployment-step6@1.0).

-   **frontendPlaceholderLegacyCli**

    -   *Type:* boolean
    -   *Default:* false

    Restore the pre-hardening command-line behaviour, in which *every*
    frontend-accessible `%vault(id)%` placeholder resolves on
    the CLI, whoever authored the string it appears in.

    Off by default: the CLI enforces the same allow-set as a frontend
    request, so an identifier has to be published through an admin-only
    source — TypoScript setup, site configuration,
    `plugin.tx_nrvault.frontendResolvableIdentifiers`, or
    `FrontendPlaceholderPolicyInterface::allowIdentifier()`. 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).

    > [!WARNING]
    > Turning this on re-opens a real path, not a theoretical one.
    > `scheduler:run` authenticates the `_cli_` administrator, so
    > the admin bypass grants the read and this allow-set is the only
    > remaining gate between an editor-authored `tt_content` field and
    > a secret in the output of a scheduled newsletter or export job.
    >
    > Pin the value out of admin reach in
    > `config/system/additional.php`, or a compromised admin can
    > simply tick the box in the backend Settings module. The example pins
    > the flag **off** — the recommended state; a deployment that
    > deliberately runs with the legacy behaviour pins `true` instead,
    > which keeps the *decision* out of admin reach either way:
    >
    > ```php
    > // Pin the strict default so no backend admin can enable the bypass.
    > $GLOBALS['TYPO3_CONF_VARS']['SYS']['nrVault']['frontendPlaceholderLegacyCli'] = false;
    > ```

    Enable it only for a deployment whose internal render jobs genuinely
    need the old behaviour and cannot publish their identifiers. The
    narrower remedy is one `frontendResolvableIdentifiers`
    line, or one `allowIdentifier()` call in the job itself.

    The flag is CLI-only: it never weakens a web request, and it never
    changes *which* secrets exist — a secret without
    `frontend_accessible` stays unreadable either way.

-   **auditLogRetention**

    -   *Type:* integer
    -   *Default:* 365

    Number of days to retain audit log entries. Set to 0 for unlimited retention.

-   **auditReads**

    -   *Type:* boolean
    -   *Default:* true

    Log every secret *read* to the audit log. Disable only for
    high-throughput scenarios where read logging is not required.

    > [!WARNING]
    > Disabling read logging means secret retrievals leave no audit
    > trail. The toggle is configurable by design
    > (see [ADR-019: Configurable audit read logging](https://docs.typo3.org/permalink/netresearch/nr-vault:adr-019-configurable-audit-read-logging@1.0)), but flipping
    > it does not itself emit a sentinel entry. For tamper-resistant
    > deployments, pin the value filesystem-only via
    > `config/system/additional.php` so it cannot be changed from
    > the backend Settings module:
    >
    > ```php
    > $GLOBALS['TYPO3_CONF_VARS']['SYS']['nrVault']['auditReads'] = true;
    > ```

-   **disableAdminOverride**

    -   *Type:* boolean
    -   *Default:* false

    Remove the unconditional "administrators and system maintainers may do
    anything" bypass — both the operation permissions and the per-secret
    read/write/delete tiers. Administrators then hold exactly what their
    groups were granted and reach only the secrets they own or share a
    group with.

    **Only effective when** [securityProfile](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-securityprofile@1.0) **is** `hardened`. In the standard
    profile the flag is inert — a lockout guard, since setting it without
    the rest of the hardened policy is more likely a misunderstanding than
    a decision. `vault:break-glass --status` reports
    `adminOverrideDisabledEffective` so the mismatch is visible.

    > [!WARNING]
    > Removing the override without an escape hatch turns the first
    > genuine incident into an outage. The hatch is
    > [break-glass mode](https://docs.typo3.org/permalink/netresearch/nr-vault:security-break-glass@1.0)
    > ([vault:break-glass](https://docs.typo3.org/permalink/netresearch/nr-vault:command-break-glass@1.0)) — a justified,
    > audited, time-boxed window that restores full admin power.
    >
    > Pin the value out of admin reach in
    > `config/system/additional.php`, or a compromised admin can
    > simply untick it in the backend Settings module:
    >
    > ```php
    > $GLOBALS['TYPO3_CONF_VARS']['SYS']['nrVault']['disableAdminOverride'] = true;
    > ```

    See [Disabling the admin override](https://docs.typo3.org/permalink/netresearch/nr-vault:security-disable-admin-override@1.0) for what exactly is removed.

-   **auditHmacEpoch**

    -   *Type:* integer
    -   *Default:* 3

    Hash-algorithm version marker for the audit log hash chain:

    -   **0**

        Legacy SHA-256 without HMAC.

    -   **1**

        HMAC-SHA256 over identity fields only.

    -   **2**

        HMAC-SHA256 over identity **and** forensic fields (`success`,
        `error_message`, `reason`, `ip_address`, `user_agent`,
        `context`).

    -   **3**

        Additionally binds the epoch selector `hmac_key_epoch` itself plus the
        attribution fields `actor_type`, `actor_username`, `actor_role`
        and `request_id`.

    Epoch 2 binds the forensic surface into the chain, so a DB-write attacker
    cannot flip `success` or rewrite `error_message` without breaking it.
    The shipped default (`3`) additionally closes the algorithm-downgrade
    forgery — with the epoch selector inside the hash, an attacker can no
    longer relabel a row to epoch 0, re-sign it with keyless SHA-256 and keep
    the chain consistent — and makes actor attribution tamper-evident.

    After raising this value, run the Install Tool wizard *Migrate audit hash
    chain* (or the [vault:audit-migrate-hmac](https://docs.typo3.org/permalink/netresearch/nr-vault:command-audit-migrate-hmac@1.0)
    command) to re-hash existing rows. See
    [ADR-023: Audit hash chain HMAC consideration](https://docs.typo3.org/permalink/netresearch/nr-vault:adr-023-audit-hash-chain-hmac@1.0).
    On the command line the wizard runs as
    `vendor/bin/typo3 upgrade:run nrVaultAuditHmacMigration`, on TYPO3 13
    and 14 alike.

    `vault:doctor` grades the three states apart under
    `audit.hmac_epoch`: pass at 3 and above, **warning** at 1 and 2 naming the
    columns that epoch leaves outside the MAC, and **critical** at 0. A stalled
    or partially applied migration is the usual way an installation ends up at
    1 or 2, so treat that warning as "the migration did not finish", not as a
    pending nice-to-have.

    The setting is not the evidence. It decides how the *next* row is signed and
    says nothing about the rows already stored, so raising it to 3 without
    running the migration leaves every historical row at its old epoch while the
    configuration reads as fully protected. `audit.hmac_epoch` therefore also
    **warns** when the oldest stored row is below the configured epoch, naming
    both numbers and exposing the stored minimum as `details.storedMinEpoch`.
    `vault:audit-verify` reports the full per-epoch distribution, so the
    migration can be confirmed rather than assumed.

-   **auditAnchorRequired**

    -   *Type:* boolean
    -   *Default:* false

    Treat a missing audit chain tip anchor as an **error** instead of a
    warning.

    The anchor pins "audit row `uid = A` still exists with
    `entry_hash = H`" in `sys_registry`, signed with a key that is not
    in the database. It is what makes removal of the *end* of the audit
    log detectable — a truncation leaves no UID gap and no broken link,
    so the chain walk alone reports it as valid. See
    [ADR-034: Audit chain tip anchor](https://docs.typo3.org/permalink/netresearch/nr-vault:adr-034-audit-chain-tip-anchor@1.0).

    The anchor arms itself on the next audit log write. Leave this
    setting off until that has happened: before the first write after the
    upgrade, an installation that never had an anchor is
    indistinguishable from one whose anchor an attacker deleted, and
    every chain would report invalid.

    Once enabled, an attacker with database write access can no longer
    silence the control by deleting the anchor row, because this setting
    lives in a configuration file rather than the database. It does two
    things, not one: verification reports a missing anchor as an error,
    **and** ordinary audit writes stop arming an anchor that is not
    there. Without the second half the error would clear itself within
    seconds — one audited read is enough to mint a fresh anchor on a
    truncated log.

    The second half applies whatever the log contains, including an empty
    one. "The log still holds an earlier entry" cannot stand in for "this
    anchor was never armed": the audit write that would arm the anchor
    has just inserted the only row the chain has, so a log emptied
    outright is indistinguishable from a new installation.

    Arming is therefore always explicit while this is on:
    [vault:audit --reset-anchor](https://docs.typo3.org/permalink/netresearch/nr-vault:command-audit@1.0) arms the anchor and
    writes the reset into the chain. That includes the first arming — one
    more reason to enable the setting only after the anchor exists.

    Requires [auditHmacEpoch](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-audithmacepoch@1.0) >= 1; at epoch 0 the
    chain is keyless and the anchor is disabled.

-   **encryptionAlgorithm**

    -   *Type:* string
    -   *Default:* (empty)

    AEAD algorithm recorded per secret at encrypt time. Empty selects
    XChaCha20-Poly1305, which is the recommended value: it is available in
    every libsodium build, and its 24-byte nonce makes random-nonce collisions
    a non-concern, so vault contents stay portable across hosts with differing
    CPU capabilities.

    Set it to `aes256gcm` only on hosts with hardware AES support. An unknown
    or host-unavailable value makes encryption fail loudly at the crypto
    boundary rather than silently falling back.

-   **preferXChaCha20**

    -   *Type:* boolean
    -   *Default:* false

    Prefer XChaCha20-Poly1305 over AES-256-GCM **for legacy secrets only** —
    those stored under encryption version 1, before the per-secret algorithm
    marker existed. New secrets record their algorithm explicitly and take it
    from [encryptionAlgorithm](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-encryptionalgorithm@1.0); this setting has no effect
    on them.

## External audit sinks {#external-audit-sinks}

The database table `tx_nrvault_audit_log` is the chain-authoritative audit
sink. External sinks are additional, best-effort *copies* whose purpose is to
put audit evidence somewhere a database-write attacker cannot reach.

Two properties are worth stating up front:

-   A sink failure **never** fails the audited vault operation. Fan-out happens
    after the chain row has committed and after the audit lock has been released,
    so a slow or broken destination cannot roll back a secret operation or
    serialise every other vault call behind itself. Failures are logged, counted,
    and reported by [vault:audit-verify](https://docs.typo3.org/permalink/netresearch/nr-vault:command-audit-verify@1.0).
-   Only an external sink makes a **full audit table reset** detectable. See
    [vault:audit-anchor](https://docs.typo3.org/permalink/netresearch/nr-vault:command-audit-anchor@1.0) for why the in-database hash
    chain structurally cannot catch a truncate-and-rebuild.

Under the hardened security profile (`securityProfile = hardened`), having no
usable external sink is reported as a `NO_EXTERNAL_SINK` finding.

-   **auditSinkSyslogEnabled**

    -   *Type:* boolean
    -   *Default:* false

    Mirror every audit entry, chain-tip anchor and integrity alert to the local
    syslog as an RFC 5424 structured-data message on facility `local0`. The
    cheapest useful sink: on any host with a log shipper the audit trail leaves
    the TYPO3 database with no extra infrastructure.

-   **auditSinkSyslogIdent**

    -   *Type:* string
    -   *Default:* 'nr-vault'

    `openlog()` ident, which becomes RFC 5424's APP-NAME. Vary it when several
    TYPO3 instances share a host. An empty value falls back to the default — an
    unattributable syslog line would defeat the purpose of the sink.

-   **auditSinkFileEnabled**

    -   *Type:* boolean
    -   *Default:* false

    Append audit evidence to newline-delimited JSON files (one JSON object per
    line). This is also the sink that writes the chain-tip anchors
    [vault:audit-verify](https://docs.typo3.org/permalink/netresearch/nr-vault:command-audit-verify@1.0) reads back, so enabling it is
    the minimum for table-reset detection.

    Files are created with mode `0600` and only ever appended to.

-   **auditSinkFilePath**

    -   *Type:* string
    -   *Default:* '' (resolves to `var/log/nr-vault-audit.ndjson`)

    Absolute path of the append-only audit entry stream.

    > [!WARNING]
    > A path inside the public web root **disables** the sink rather than
    > writing there: the stream names every secret identifier, actor, IP address
    > and chain hash, so publishing it over HTTP would be worse than having no
    > external sink at all. The refusal is logged with the resolved path.
    >
    > The default is outside the document root on every Composer-based
    > installation. A legacy (non-Composer) layout, where `var/` lives under
    > `typo3temp/`, must configure an explicit path.

-   **auditSinkAnchorPath**

    -   *Type:* string
    -   *Default:* '' (resolves to `var/log/nr-vault-audit-anchor.ndjson`)

    Absolute path of the append-only chain-tip anchor stream, written by
    [vault:audit-anchor](https://docs.typo3.org/permalink/netresearch/nr-vault:command-audit-anchor@1.0) and read back by
    [vault:audit-verify](https://docs.typo3.org/permalink/netresearch/nr-vault:command-audit-verify@1.0). Integrity alerts are
    appended here too, so this one file tells the whole chain-health story.

    Deliberately separate from `auditSinkFilePath`: this is the evidence that
    survives a table reset, so point it at append-only or off-host storage.
    Verification always takes the anchor with the **highest sequence**, never the
    last line — appending a low-sequence anchor therefore cannot weaken the
    baseline.

    The public-web-root refusal described above applies to this path as well.

-   **auditSinkWebhookEnabled**

    -   *Type:* boolean
    -   *Default:* false

    POST every audit entry, anchor and integrity alert as JSON to an HTTP
    endpoint — typically a SIEM collector. Each payload carries a `type`
    discriminator (`entry`, `anchor`, `alert`) so one endpoint can route all
    three.

    When enabled, the webhook also receives integrity alerts by default through
    the built-in `nr-vault/audit-integrity-alert-sinks` event listener.

-   **auditSinkWebhookUrl**

    -   *Type:* string
    -   *Default:* ''

    `https://` (or `http://`) endpoint receiving the payloads. Only
    `http`/`https` are accepted, so a `file://` or `php://` value cannot
    turn audit fan-out into a local write.

-   **auditSinkStaleDeliveryHours**

    -   *Type:* integer
    -   *Default:* 24

    Hours after which the last successful external delivery of an enabled
    sink counts as **stale** for `vault:doctor` (finding
    `audit.sink_state.<sink>`: warning under the standard profile, critical
    under hardened). The per-sink delivery state — last success, last
    failure, consecutive failures — is persisted in `sys_registry` by the
    sink registry, so a freshly started process still knows a collector has
    been unreachable for days. Use
    `vault:doctor --active-probes` to verify delivery end-to-end.

    > [!NOTE]
    > Outbound calls go through the hardened HTTP client, inheriting the
    > extension-wide SSRF and DNS-rebinding defences. A collector on a
    > private/RFC1918 address is therefore **refused** unless the host is
    > allow-listed literally in
    > `$GLOBALS['TYPO3_CONF_VARS']['HTTP']['allowed_hosts']`.
    >
    > That is intentional. This URL is settable from the backend Settings
    > module, so without the guard a compromised administrator could repoint it
    > at a cloud metadata service and use the vault as an SSRF pivot.
    > `allowed_hosts` is filesystem-bound and out of the backend's reach,
    > which keeps that pivot closed while leaving the legitimate on-premise path
    > available. A refusal is not silent — it is logged, counted, and reported
    > as a `SINK_FAILURE` finding.
    >
    > The same entry is needed for a collector whose name is not in DNS,
    > whether its address is public or not: the guard resolves with
    > `dns_get_record()` and refuses any host it cannot resolve to a checked
    > address, because the transport would otherwise resolve the name through
    > `/etc/hosts`, NSS or mDNS and connect to an address nothing checked.

### Scheduling {#scheduling}

Two scheduler tasks accompany the CLI commands:

-   **Vault Audit Chain Anchoring**

    Publishes the current chain tip (`vault:audit-anchor`). The interval is the
    blind window — entries written since the last anchor are what an attacker who
    resets the table can still hide. Hourly is a reasonable starting point.
    Asserts `vault.configure`, the same permission as the command.

-   **Vault Audit Integrity Verification**

    Verifies the chain against the anchor (`vault:audit-verify`) and dispatches
    an integrity alert event per finding. Set *Fail on tamper evidence only*
    while sinks are still being rolled out, so a pending integration does not keep
    the task permanently red and train operators to ignore it. Asserts
    `audit.view`, the same permission as the command.

> [!NOTE]
> Both tasks run under `scheduler:run`, which authenticates the
> `_cli_` administrator, so on a default installation the admin bypass
> grants them and nothing needs configuring. Under
> [disableAdminOverride](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-disableadminoverride@1.0) there is no bypass by design:
> give the identity the scheduler runs as a group carrying
> `tx_nrvault:vault.configure` and `tx_nrvault:audit.view`, or both tasks
> fail — loudly, but they fail.

## Master key providers {#master-key-providers}

### TYPO3 provider (default) {#typo3-provider-default}

Uses TYPO3's built-in encryption key to derive the master key. This is the
recommended default because:

-   **Zero configuration**: Works immediately after installation.
-   **No server access required**: Ideal for users without shell access.
-   **Unique per installation**: Each TYPO3 instance has its own key.
-   **Already secured**: TYPO3's encryption key is already protected.

The master key is derived from the encryption key using HKDF-SHA256 with a
nr-vault-specific context, ensuring it cannot be used to compromise other
TYPO3 functionality.

**Master key derivation (internal)**

```php
// How it works internally
$masterKey = hash_hkdf(
    'sha256',
    $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'],
    32,
    'nr-vault-master-key'
);
```

> [!NOTE]
> If you rotate TYPO3's encryption key, all secrets will need to be
> re-encrypted. Use the key rotation command before changing the
> encryption key.

### File provider {#file-provider}

Store the master key in a file with restrictive permissions:

**Create master key file**

```bash
# Generate a new key
openssl rand -base64 32 > /secure/path/vault-master.key
chmod 0400 /secure/path/vault-master.key
```

Configure in extension settings:

-   [masterKeyProvider](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-masterkeyprovider@1.0): file
-   [masterKeySource](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-masterkeysource@1.0): /secure/path/vault-master.key

> [!WARNING]
> The key file must be:
>
> -   Outside the web root.
> -   Readable only by the web server user.
> -   Not in version control.
> -   Backed up separately from the database.

### Environment provider {#environment-provider}

Store the master key in an environment variable:

**Set master key via environment**

```bash
export NR_VAULT_MASTER_KEY="base64-encoded-key"
```

Configure in extension settings:

-   [masterKeyProvider](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-masterkeyprovider@1.0): env
-   [masterKeySource](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-masterkeysource@1.0): NR_VAULT_MASTER_KEY

This is ideal for containerized deployments where secrets are injected
via environment variables.

### HashiCorp Vault Transit provider {#hashicorp-vault-transit-provider}

The master key is generated once, wrapped by Vault's transit secrets engine,
and only the resulting ciphertext (`vault:v1:…`) is stored on the local
filesystem. Every start-up unwraps it through a Vault API call, so no key
material sits at rest next to the database.

Set up the transit engine and a key:

**Enable transit and create the wrapping key**

```bash
vault secrets enable transit
vault write -f transit/keys/nr-vault-master
```

Grant the TYPO3 instance encrypt and decrypt on that one key — nothing else:

**Vault policy nr-vault-transit.hcl (HCL)**

```text
path "transit/encrypt/nr-vault-master" {
  capabilities = ["update"]
}

path "transit/decrypt/nr-vault-master" {
  capabilities = ["update"]
}
```

**Apply the policy and issue a token**

```bash
vault policy write nr-vault-transit nr-vault-transit.hcl
vault token create -policy=nr-vault-transit -period=768h
```

Provide the token through the environment, never in the extension
configuration:

**Vault token via environment**

```bash
export VAULT_TOKEN="hvs...."
```

Configure in extension settings:

-   [masterKeyProvider](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-masterkeyprovider@1.0): transit
-   [hashicorp.address](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-hashicorp-address@1.0): [https://vault.example.com:8200](https://vault.example.com:8200)
-   [hashicorp.authMethod](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-hashicorp-authmethod@1.0): token
-   [hashicorp.tokenEnvVar](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-hashicorp-tokenenvvar@1.0): VAULT_TOKEN
-   [hashicorp.transitMount](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-hashicorp-transitmount@1.0): transit
-   [hashicorp.transitKeyName](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-hashicorp-transitkeyname@1.0): nr-vault-master

Then create and wrap the master key:

**Initialize the vault with a Vault-wrapped master key**

```bash
vendor/bin/typo3 vault:init
```

The wrapped key is written to
[hashicorp.transitWrappedKeyPath](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-hashicorp-transitwrappedkeypath@1.0)
with `0600` permissions. Back that file up together with the Vault key: the
blob is worthless without Vault, and Vault is worthless without the blob.

Rotating the transit key (`vault write -f transit/keys/nr-vault-master/rotate`)
re-wraps future ciphertexts without touching any secret in the TYPO3 database.
Rotating the vault master key itself is unchanged — the new key is wrapped
through Vault and the local blob replaced:

**Rotate the vault master key**

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

> [!NOTE]
> Only `token` authentication is implemented. `approle` and `kubernetes`
> are rejected with a clear error instead of being treated as token auth;
> AppRole login support is a planned follow-up.

> [!WARNING]
> What Transit does and does not protect.
>
> It protects **key custody**: the master key can be rotated, centrally
> audited and revoked in Vault, a stolen database plus a stolen webroot is
> useless without Vault access, and there is no key file on disk to
> exfiltrate.
>
> It does **not** protect against a fully compromised PHP process. Such a
> process holds the same Vault token the extension holds and can simply call
> `decrypt` itself. Transit raises the cost of offline attacks and makes
> access observable — it is not a sandbox around a live intruder.

> [!NOTE]
> The provider talks to Vault through TYPO3's HTTP client, so
> `$GLOBALS['TYPO3_CONF_VARS']['HTTP']` settings (proxy, TLS verification,
> timeouts) apply. Use an `https` address: the Vault token travels in the
> `X-Vault-Token` request header.

## Access control {#access-control}

Access to secrets is controlled by:

1.  **Ownership**: The user who created the secret has full access.
1.  **Group membership**: Secrets can be shared with backend user groups, in
    two tiers — `allowed_groups` grants read, `write_groups` grants read
    and write. Neither grants delete.
1.  **Admin access**: Backend administrators have access to all secrets —
    unless the hardened profile withdrew that bypass via
    [disableAdminOverride](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-disableadminoverride@1.0), after
    which an administrator holds only what their own groups grant.
1.  **CLI access**: Configurable via [allowCliAccess](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-allowcliaccess@1.0),
    narrowed by [cliAllowedOperations](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-cliallowedoperations@1.0).
1.  **Operation permissions**: independently of all of the above, every
    privileged operation (`secret.create`, `secret.rotate`,
    `secret.delete`, `secret.manage_policy`, …) is granted per backend user
    group and asserted centrally. Passing the per-secret tiers never implies
    holding the operation. See [Operation permissions](https://docs.typo3.org/permalink/netresearch/nr-vault:security-operation-permissions@1.0).

## Analytics thresholds {#analytics-thresholds}

These decide when the usage-analytics dashboard and the `secrets.dead` /
`secrets.never_rotated` readiness controls flag a secret. They are
reporting thresholds only — nothing expires or is deleted because of them.

-   **staleNeverReadDays**

    -   *Type:* integer
    -   *Default:* 30

    A secret that has **never** been read and is older than this many days is
    flagged *dead* — a strong candidate for redaction, since nothing has ever
    consumed it.

-   **staleNotReadDays**

    -   *Type:* integer
    -   *Default:* 90

    A secret not read for this many days is flagged *dead*.

-   **staleNeverRotatedDays**

    -   *Type:* integer
    -   *Default:* 180

    A secret not rotated — or, if it never was, not created — within this many
    days is flagged *never rotated*.

## Context-based scoping {#context-based-scoping}

Organize secrets by context for easier management:

-   `payment` \- Payment gateway credentials.
-   `email` \- Email service API keys.
-   `api` \- Third-party API tokens.
-   `database` \- External database credentials.

Contexts are user-defined strings that help organize and filter secrets.

## Site configuration integration {#site-configuration-integration}

Use the `%vault(identifier)%` syntax in site configuration files:

**config/sites/main/config.yaml**

```yaml
settings:
  payment:
    stripeSecretKey: '%vault(stripe_api_key)%'
  email:
    mailchimpKey: '%vault(mailchimp_key)%'
```

References are resolved on demand, in the reading context, via
`SiteConfigurationVaultProcessor` — not automatically when the site
configuration is loaded:

**Resolve at read time**

```php
use Netresearch\NrVault\Configuration\SiteConfigurationVaultProcessor;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$site = $request->getAttribute('site');
$processor = GeneralUtility::makeInstance(SiteConfigurationVaultProcessor::class);
$config = $processor->processConfiguration($site->getConfiguration(), $site);
$stripeKey = $config['settings']['payment']['stripeSecretKey'];
```

This keeps sensitive values out of version control while allowing configuration
through the standard TYPO3 site settings.

> [!NOTE]
> Resolution is deliberately caller-driven. TYPO3 caches the loaded site
> configuration to an on-disk file; resolving `%vault()%` references
> eagerly at load time would persist the decrypted secrets there in cleartext
> and would enforce access control only once, at cache-warm time. Read-time
> resolution avoids both. See [Site configuration](https://docs.typo3.org/permalink/netresearch/nr-vault:usage-site-configuration@1.0)
> in the Usage chapter for the full example.

## Frontend-accessible secrets {#frontend-accessible-secrets}

By default, secrets cannot be resolved in frontend context (TypoScript).
To allow a secret to be used in TypoScript:

1.  Create the secret with `frontend_accessible` metadata.
1.  Use the `%vault(identifier)%` syntax in TypoScript.

`frontend_accessible` says the secret *may* appear in a page; it does not say
which placeholders get expanded. In a frontend request — and on the command
line, unless [frontendPlaceholderLegacyCli](https://docs.typo3.org/permalink/netresearch/nr-vault:confval-ext-nrvault-frontendplaceholderlegacycli@1.0) is on — the identifier must also
be published through an admin-only source — the TypoScript setup array, the site
configuration, `plugin.tx_nrvault.frontendResolvableIdentifiers`, or
`FrontendPlaceholderPolicyInterface::allowIdentifier()`. Step 2 satisfies
that on its own; an identifier used only in a Fluid template file or an eID
handler needs one of the last two. See
[Which placeholders resolve in the frontend](https://docs.typo3.org/permalink/netresearch/nr-vault:usage-typoscript-frontend-scope@1.0).

**Store frontend-accessible secret**

```php
$this->vaultService->store(
    'google_maps_key',
    $apiKey,
    [
        'metadata' => [
            'frontend_accessible' => true,
        ],
    ],
);
```

> [!WARNING]
> Frontend-accessible secrets may be exposed in rendered HTML output.
> Only use this for secrets that are intended to be public (like
> client-side API keys).
