netresearch/nr-vault
Secure secrets management for TYPO3 with envelope encryption, access control, and audit logging
v0.15.0
82K downloads/mo
#3629 most downloaded on Packagist
netresearch/t3x-nr-vault
What this package is like to depend on
Last release 13 days ago
10 Aug 2026
Ships fairly regularly
a new release about every 3 weeks
Nearly every release is documented
notes for 32 of 32 stable releases
Nothing withdrawn
no release was ever pulled
7 months old
32 releases · first in 2026
32 releases in the last 12 months
see the full history below
Release timeline
32 releases · Jan 2026 to Aug 2026Releases
latest 32-
v0.15.010 Aug 2026Release notes
Open source →Changes
- chore(release): 0.15.0 (#299)
- Let vault:store write as a configured provisioning actor (#298)
- Update actions/attest-build-provenance action to v4.2.2 (#297)
- Update step-security/harden-runner action to v2.20.1 (#296)
Installation
composer require netresearch/nr-vault
Publication status
- TER: extensions.typo3.org/extension/nr_vault — 0.15.0 verified
- Packagist: packagist.org/packages/netresearch/nr-vault — 0.15.0 verified
- Documentation: docs.typo3.org/p/netresearch/nr-vault/0.15/en-us/ — render verified
Security
All release artifacts are signed with Sigstore keyless signing.
Verify signatures
cosign verify-blob \ --bundle nr-vault-0.15.0.zip.sigstore.json \ --certificate-identity-regexp "https://github.com/netresearch/.*" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ nr-vault-0.15.0.zip
Verify checksums
sha256sum -c checksums.txt
Software Bill of Materials (SBOM)
SBOMs are provided in both SPDX and CycloneDX formats for supply chain transparency.
Release notes
Open source →One feature, aimed at a gap that only shows up in unattended deployments: there was no way to write a secret from a pipeline without switching on
allowCliAccess, which grants the operation to every process holding a shell in that container and attributes the write to nobody.Added
-
A named actor for unattended writes (#298).
vault:store --as-provisionerperforms the store insideTechnicalActorContext::runAs()as the backend user named by the newprovisioningBeUserUidoption. The actor needs no admin flag — a group carryingtx_nrvault:secret.createis enough, because a technical actor's grants are read from its groups' custom permission options. The grant is therefore one operation on one identity, and every write it makes is attributable in the audit log.The UID comes from configuration and never from the command line. A flag that accepted a UID as an argument would be a general impersonation primitive, strictly worse than the switch it replaces.
Fail-closed at both ends: a non-numeric, negative or absent value reads as no provisioning actor rather than uid 0, which names the unauthenticated CLI placeholder the option exists to avoid; and the flag without a configured actor is an error, not a silent fallback to the unattributed write the caller explicitly asked not to make.
ext_conf_template.txtalready recommended "prefer a named technical actor (TechnicalActorContext::runAs())" overallowCliAccess. Until now no command could enter such a scope, so the advice had nothing behind it.
Fixed
- The closure handed to
runAs()captured the plaintext by value, sosodium_memzero()separated the copy-on-write pair instead of wiping both and the secret survived in memory in the copy the command believed it had cleared. Caught in review before release, so no shipped version is affected.
Changed
- CI actions updated:
step-security/harden-runnerto v2.20.1,actions/attest-build-provenanceto v4.2.2.
-
v0.14.006 Aug 2026Release notes
Open source →Highlights
Fifty-eight pull requests since v0.13.0, most of them one hardening programme carried out in four rounds of adversarial review. Several changes are breaking, and several close paths that produced successful-looking audit entries for operations that were never authorised — in an extension whose promise is auditability, that is the part to read first. Read the 0.14.0 changelog end to end before upgrading; its Migration section lists everything an operator has to do by hand.
Access control is no longer admin-or-nothing
Ten grantable operation permissions replace the coarse admin-only model:
secret.use,secret.reveal,secret.create,secret.rotate,secret.delete,secret.manage_policy,audit.view,audit.export,master_key.rotateandvault.configure. They are granted per backend group and compose with — never replace — the per-secret owner/group ACL tiers; both must hold. The operationally important split is use ≠ reveal: an application or technical actor can consume a credential no human is allowed to look at, and audit viewing is separated from secret access entirely.The admin bypass itself can now be switched off.
disableAdminOverrideremoves it from both layers and can be pinned out of admin reach inconfig/system/additional.php. The way back in isvault:break-glass --activate --reason "…", a time-boxed window (1–60 minutes, default 15) whose audit row is written before the power is granted, so an open window without evidence is impossible. PSR-14 events fire for SIEM consumption and the backend modules carry a danger banner while a window is open.A security profile the code enforces
securityProfile(standard|hardened) is policy, not a documentation label. Underhardened,masterKeyProvider = typo3is a configuration error — vault secrets must not be protected by the same key TYPO3 uses for everything else — and provider selection stops auto-detecting, so a missing provider fails loudly instead of silently degrading down the oldtypo3 → env → filechain. An unknown profile value throws rather than falling back, because a typo must never weaken the effective policy.New in this release: a HashiCorp Vault Transit master key provider, and
vault:doctor, a readiness command with stable exit codes (0 clean, 1 warnings, 2 critical) that an operator or a pipeline can gate on.vault:doctor --profile=hardenedreports what would fail before you change any configuration;--active-probesis the only check that proves audit records actually arrive rather than that a URL parses.Tamper evidence that survives a truncated table
The audit chain tip is now anchored both inside the database and outside it, so deleting the audit table no longer leaves a chain that verifies as valid. A truncated log verifies as INVALID and blocks
vault:rotate-master-keyand both HMAC re-seal paths, which would otherwise launder it. Audit sinks gained active verification and per-sink delivery state.vault:audit --reset-anchoris the deliberate way to re-arm the anchor after a wipe you performed on purpose, and it writes the reset into the chain so it cannot be done invisibly.Breaking changes
- The plaintext secret cache is gone, and with it
VaultServiceInterface::clearCache()and thecacheEnabledsetting. Every read is now oneSELECT, one decrypt and one audit row — which is the point. VaultServiceInterface,SecretRepositoryInterface,VaultAdapterInterfaceandVaultDoctorServiceInterfacegained members. Third-party implementations need updating; the two new parameters are optional, so only the new methods are a hard break.undeleteis gone fortx_nrvault_secret, for everyone including administrators. Restoring a soft-deleted secret is now a database operation, where the change is visible as what it is. Update any runbook that promised the delete was reversible.- Frontend
%vault()%resolution only serves published identifiers. Every documented TypoScript and site-configuration form keeps working; an identifier used only in a Fluid file, auserFuncor a DataProcessor must be published once per site viaplugin.tx_nrvault.frontendResolvableIdentifiers. Scheduled render jobs are covered by the same allow-set — check them before upgrading. - CLI access is narrowed to an explicit operation allowlist (
cliAllowedOperations, defaultsecret.use,secret.create,secret.rotate). Anything that reveals, deletes, exports, rotates the master key or touches the audit log needsallowCliAccess = 1and the operation added to that list. Prefer a named technical actor viaTechnicalActorContext::runAs()so the audit trail names an identity rather than an unattributable shell. - Grant
secret.usebefore you upgrade, not after. Non-admin editors working with vault-backed FormEngine or FlexForm fields need it, and that is the one that bites quietly.
Fixes worth naming
- Master-key rotation skipped disabled secrets, leaving their DEK wrapped under the key the command then tells you to destroy — a permanently undecryptable ciphertext on re-enable. With only disabled secrets present it even reported "No secrets found" and exited 0. Reported by @CybotTM in #286, fixed by @CybotTM in #287.
reseal()skipped its anti-truncation guard on the rotation path, because it authenticated the stored anchor under the key it was about to sign with; rotation is the one path that passes a different key. Reported by @CybotTM in #283, fixed by @CybotTM in #284.store()carriedversionandcrdateforward but resetlast_rotated_at,read_countandlast_read_aton every update, so a rotated secret read as never rotated — fixed by @CybotTM in #281.- Record copy and delete now fail closed across vault fields (#259), and FormEngine mutations are audit-atomic (#252) — a write that cannot be audited does not land, including the MM ACL tiers written alongside it. Both by @CybotTM.
Every issue in this release was reported by @CybotTM and every pull request authored by @CybotTM; #283 and #286 are the two that were filed as issues first.
Full Changelog: v0.13.0...v0.14.0
Installation
composer require netresearch/nr-vault
Publication status
- TER: extensions.typo3.org/extension/nr_vault — 0.14.0 verified
- Packagist: packagist.org/packages/netresearch/nr-vault — 0.14.0 verified
- Documentation: docs.typo3.org/p/netresearch/nr-vault/0.14/en-us/ — render verified
Security
All release artifacts are signed with Sigstore keyless signing.
Verify signatures
cosign verify-blob \ --bundle nr-vault-0.14.0.zip.sigstore.json \ --certificate-identity-regexp "https://github.com/netresearch/.*" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ nr-vault-0.14.0.zip
Verify checksums
sha256sum -c checksums.txt
Software Bill of Materials (SBOM)
SBOMs are provided in both SPDX and CycloneDX formats for supply chain transparency.
Release notes
Open source →Fifty-eight merged pull requests since 0.13.0, most of them a hardening programme carried out in four rounds of adversarial review: a technically enforced security profile, ten grantable operation permissions in place of the admin-only model, a tamper-evidence anchor inside and outside the database, and a readiness command an operator or a pipeline can gate on. Several changes are breaking, and several close paths that produced successful-looking audit entries for operations that were never authorised — in an extension whose promise is auditability, that is the part to read first. Start with Changed and end with Migration, which collects everything you actually have to do.
Added
- Security profiles (#236).
securityProfile(standard|hardened, defaultstandard) is a technically enforced policy, not a documentation label. Underhardened,masterKeyProvider = typo3is a configuration error — vault secrets must not be protected by the same key TYPO3 uses for everything else — and provider selection stops auto-detecting: the configured provider is returned even when it is unavailable, sogetMasterKey()fails loudly instead of silently degrading down the oldtypo3 → env → filechain. An unknown profile value throws rather than falling back tostandard, because a typo must never be able to weaken the effective policy.VaultHealthServiceandvault:rotate-master-keyresolve through the same factory, so a misconfiguration surfaces in the system status and blocks rotation instead of being discovered at decryption time. - Ten grantable operation permissions (#238) replace the coarse admin-only
model:
secret.use,secret.reveal,secret.create,secret.rotate,secret.delete,secret.manage_policy,audit.view,audit.export,master_key.rotate,vault.configure. They are carried as TYPO3 custom permission options (be_groups.custom_options, namespacetx_nrvault) and granted per group in the Backend Users module, and they compose with — never replace — the per-secret owner/group ACL tiers: both must hold. The split that matters operationally is use ≠ reveal:secret.usegates every interactive plaintext read, while displaying a secret additionally needssecret.reveal, so an application or a technical actor can consume a credential no human is allowed to look at. Audit viewing is separated from secret access entirely (audit.view/audit.export), and the "admins may do anything" override lives in exactly one seam so it can be switched off as a whole (see break-glass, below). - The admin override can be switched off, with an audited break-glass window
as the way back in (#241).
disableAdminOverride(hardened profile only; inert understandardas a lockout guard) removes the admin and system-maintainer bypass from both layers — the operation permissions and the per-secret read/write/delete tiers — and can be pinned out of admin reach inconfig/system/additional.phpvia$GLOBALS['TYPO3_CONF_VARS']['SYS']['nrVault']['disableAdminOverride'].vault:break-glass --activate --reason "…" [--minutes N](1–60, default 15) opens a time-boxed window; the audit row is written before the power is granted, so an open window without evidence is impossible, and a logged failed opening is harmless. While a window is open the vault backend modules carry a danger banner,--statusis machine-readable, and PSR-14BreakGlassActivatedEvent/BreakGlassDeactivatedEventfire for SIEM pickup. ArunAs()technical actor may not open one — break-glass is not an authentication boundary. - HashiCorp Vault Transit master key provider (#239). The 32-byte master key
is generated once, wrapped by Transit, and only the
vault:v1:…ciphertext is stored locally (0600, atomic write); every unwrap is a live, centrally audited Vault call, so revoking the token or its policy locks the vault out immediately and a stolen database plus webroot is useless on its own. Token auth only —approleandkubernetesare rejected rather than silently downgraded. New settingshashicorp.transitMount,hashicorp.transitKeyName,hashicorp.transitWrappedKeyPath,hashicorp.tokenEnvVar. The trust model is documented honestly: Transit protects custody, rotation and central auditability; it does not stop a fully compromised PHP process from callingdecryptwith the token it legitimately holds. - The audit log can be anchored outside the database (#240).
AuditSinkInterfaceplus three sinks — syslog (RFC 5424 structured data, control characters stripped against log forging), an append-only NDJSON file (0600,LOCK_EX, refuses any path inside the public web root, resolved against the nearest existing ancestor so..and symlinks cannot bypass it) and an SSRF-guarded HTTP webhook. Fan-out happens after commit and after the advisory lock is released, so a slow sink can never serialize vault operations behind the lock and a sink failure never rolls back the audited operation.vault:audit-anchorpublishes{sequence, chainTip, timestamp, hmacEpoch}through every enabled sink and the reader takes the highest anchored sequence, so appending a low anchor cannot weaken the baseline;vault:audit-verifythen checks the chain and compares it against that anchor, reporting a table that was truncated and re-seeded with a valid-but-different chain asTABLE_RESET. Machine-readable reason codes (HASH_MISMATCH,UID_GAP,TABLE_RESET,EPOCH_DOWNGRADE,SINK_FAILURE,NO_EXTERNAL_SINK,BREAK_GLASS), scheduler tasks for both commands, and a PSR-14AuditIntegrityAlertEventforwarded through the sinks. Seven new settings under "Audit Sinks", all off by default. vault:doctor(#244) — one readiness check across the whole security posture, with exit codes a pipeline can gate on: 0 clean, 1 warnings, 2 any critical. One class per control (provider explicitness, availability and key-file permissions; profile consistency; break-glass window; audit reads, retention, chain, external sink and anchor; CLI exposure; secret expiry and rotation hygiene; production context and HTTPS; version sanity), each declaring which profiles it applies to, each producing a typedFindingwith id, severity, risk, remediation and a documentation link. A crashing check is contained as acheck.crashedcritical finding rather than taking the run down with it.--profile=hardenedasks "would this installation pass as hardened?" without changing any configuration, which is what makes it usable as a deployment gate. The backend overview shows a profile badge and "N/M controls passed" to any vault user; the detailed findings are gated behindvault.configure.- Audit sinks are now checked by delivery, not by configuration (#255).
"Enabled" used to mean a switch plus a syntactically valid URL, and the only
telemetry was a per-process failure counter — so a freshly started
vault:doctoralways saw zero failures and reported a collector that had been unreachable for days as healthy. Per-sink delivery state (last success, last failure, consecutive failures, lifetime failures, last error) is now persisted insys_registry, written fail-safe on every dispatch outcome and throttled to one healthy write per minute per sink, so the bookkeeping can never fail or slow the audited operation. Newaudit.sink_state.<sink>findings grade consecutive failures and a last success older thanauditSinkStaleDeliveryHours(new setting, default 24) as a warning understandardand critical underhardened.vault:doctor --active-probesgoes further and pushes the current chain tip through every enabled sink end to end — a refused probe is critical in both profiles. Probes never run implicitly: not from the passive checks, not from the backend status panel. The anchor is re-publishable evidence by design, so a probe pollutes nothing and even refreshes the external anchor. - Four further doctor controls, each closing a case where the runtime was
correct and the readiness report was not.
audit.hmac_epoch(#260, graded further in #268 and #277): critical at epoch 0 — one setting that simultaneously drops row hashes to keyless SHA-256, makes the chain-level downgrade guard vacuous and disables the in-DB anchor, silently overridingauditAnchorRequired; warning at epochs 1–2, where 13 and 5 columns respectively sit outside the signed payload (at epoch 1successitself is forgeable, so a recorded denial can be flipped into a recorded grant without touching a signed byte); pass only at ≥ 3. The check now reads the stored minimum epoch from the oldest row rather than grading the configuration alone, and reports it asdetails.storedMinEpochfor CI.audit.db_anchor(#260) loads thesys_registryanchor directly, which no check did before at any epoch.cli.frontend_placeholder_legacy(#268) reports thefrontendPlaceholderLegacyCliopt-in — emitted from both return paths of the CLI check, because the obvious placement would have skipped it on exactly the default installations where the bypass is fully live. Andcli.allowed_operations(#277) stops callingsecret.manage_policyandaudit.viewharmless: the first governs the permissions themselves, so a shell can widen its own per-secret reach; the second maps out the credential topology and is where that shell's own activity is recorded. - A secret's availability is a vault operation with a name (#278).
VaultServiceInterface::setEnabled(string $identifier, bool $enabled, string $reason = ''): void— absolute, not a toggle, so two concurrent disables converge instead of cancelling out and leaving two audit entries claiming opposite outcomes. It resolves the secret through a disabled-visible lookup, assertscanWrite()andsecret.manage_policy, no-ops when the state already matches (the gates run first, so a refused no-op is still audited), and reverts onAuditWriteExceptionexactly as the other compensating paths do, including the CRITICAL escalation when the revert itself fails. It is audited asmetadata_update, matching what the FormEngine path already writes for the same column, so "who disabled this secret" has one answer regardless of the write path.list()and the repository filters gained$includeDisabled(defaultfalse) so the management surfaces can see what the read paths no longer return. vault:audit --verifyreports the anchor state on aTip anchor:line, and the backend verification view shows it too.vault:audit-verifyadditionally reports theStored HMAC epochsdistribution in text and JSON (#277) — the count is free, sinceverifyHashChain()already walks every row.vault:audit --reset-anchorclears the anchor after a wipe or purge you performed deliberately, writes the reset into the chain so it cannot be done invisibly, and re-arms the anchor on that entry. New audit actionaudit_anchor_reset.auditAnchorRequiredextension setting (default off). A missing anchor becomes an error, and ordinary audit writes stop arming an anchor that is not there — whatever the log currently contains, an emptied one included — so deleting the anchor and truncating or wiping the log can no longer be laundered back to a valid verdict by ordinary traffic. Enable it after the first audit write following the upgrade; while it is on,vault:audit --reset-anchoris the only way to arm the anchor.- Security, operations and auditor documentation (#243) — 17 new pages that
turn the implemented hardening into an auditable, reproducible state.
Documentation/Security/gains a threat model, the profile comparison, the trust boundaries, the cryptography chapter, what the audit chain does and does not prove, and a prominentKnownLimitationspage.Documentation/Operations/covers hardened deployment, key custody, backup and restore, key rotation, monitoring and alerting, incident response and decommissioning.Documentation/Auditor/states the target of evaluation, maps controls to BSI IT-Grundschutz and OWASP ASVS with a declared-gaps table so an assessment credits no absent control, and gives reproducible evidence and verification procedures (the staging-only ones marked as such). The language is deliberately unmarketed throughout — tamper-evident, not tamper-proof; minimized exposure, not secure deletion. - ADR-036 (#272) records the rule four hardening rounds had been
implementing without writing down: a mutation and its audit entry commit
together, and a change that cannot be audited must not persist. It exists
because two of its mechanics are non-obvious enough that a future refactor
would plausibly break them silently — why the ACL MM relations need a snapshot
captured in
processDatamap_preProcessFieldArray()(DataHandler writes them duringcheckValue(), before any audit hook runs, and the row'sallowed_groupscolumn holds only a relation count, so restoring the column restores nothing), and why the create path is asymmetric (for aNEWrecord the MM writes are deferred past the hook, so a reverted creation deletes the row before its relations exist and leaves orphans to purge). It also states where the guarantee stops rather than leaving the boundary implied. - A release evidence bundle (#245).
Build/Scripts/collect-evidence.phpassembles whatever exists at release time — test results, line and security-directory coverage, whole-codebase and security-scoped mutation summaries, PHPStan level,composer audit,vault:doctor --format=json— into a flat, stableevidence-manifest.jsonplus a human-readableEVIDENCE.md. An absent producer isabsentand exit 0; only a present-but-malformed artifact is an error..github/workflows/release-evidence.ymlruns it on a tag and publishes the bundle as a run artifact with a build-provenance attestation over the tarball, verifiable withgh attestation verify.CONTRIBUTING.mdnow requires two-person review forClasses/Crypto|Security|Audit(the author cannot approve; one approver must be a code owner) plus a threat-model delta, andSECURITY.mdcarries a 7-day Critical/High patch SLA, down from 30. - Quality gates that block rather than inform. Codecov is blocking at 90%
patch coverage for
Classes/Crypto|Security|Auditand 80% by default, with the ignore list mirroring the PHPUnit excludes so the reported number is the one the suite actually measures (#242, #258). A security-scoped mutation gate ships asinfection-security.json5with a raise-only ratchet, now at MSI 86 after a pass that killed 273 escaped mutants by pinning previously unasserted semantics across the audit, crypto and security trees — test-only, no production code touched (#242, #257). The gate treats its own inputs as security-critical, so a pull request lowering the ratchet is measured rather than waved through. Alongside: PHPStan now analyses the wholeTeststree instead ofTests/Architectureonly (#233), the CLI documentation guard checks each command's option lists against the realaddOption()calls and not merely the shell examples (#273), sixteen previously untested classes reached full line coverage (#258), and the envelope fuzz probe spans two full base64 block periods and reports every observed length when it fails (#256).
Changed
-
The request-scoped plaintext cache is gone, and
VaultServiceInterface::clearCache()with it (breaking) (#250). See Security for what the cache did; what changes for integrators is that the method no longer exists — there is nothing left to clear — and thecacheEnabledextension setting,ExtensionConfiguration(Interface)::isCacheEnabled()and theext_conf_template.txtentry are removed too. The saved work was a single-rowSELECTplus one decrypt; an actor-keyed cache was considered and rejected, because permission state, break-glass windows and expiry all change within a request and a cache that has to track them is a second authorization implementation. -
allowCliAccessno longer grants every operation (breaking) (#254). With the switch on — which deployment automation genuinely requires — a shell on the host implicitly heldsecret.reveal,secret.delete,audit.export,master_key.rotateandvault.configure, becauseisGranted()returned the trust switch regardless of which permission was asked for. The newcliAllowedOperationssetting defaults tosecret.use,secret.create,secret.rotate, and both CLI branches now require the trust switch and the allowlist. Everything else needs an explicit opt-in, sovault:retrieve,vault:delete, the scheduled orphan cleanup,vault:audit --exportandvault:rotate-master-keystop working on installations that relied on the blanket grant. Under the hardened profile, unattributed CLI access is now a critical doctor finding rather than a warning. -
The frontend placeholder allow-set applies on the CLI too (breaking) (#262). ADR-035 scoped
%vault()%resolution to published identifiers in web requests, but the CLI branch returned early with a blankettrue. That looked defensible while plain unauthenticated CLI still failed closed on theallowCliAccess = 0default — exceptscheduler:runauthenticates the_cli_admin user, so the admin bypass grants vault reads regardless of that switch. For editor-authored content rendered by a scheduled job — a newsletter, a static export, a search indexer — the allow-set was therefore the only remaining gate, and it was the one being skipped. The CLI is now strict like everything else.frontendPlaceholderLegacyCli(default0) restores the previous behaviour byte for byte; it is CLI-scoped so it cannot weaken a web request, read per call rather than memoised, fails closed on unreadable configuration, and honours the$TYPO3_CONF_VARSpin. Installations whose scheduler jobs resolve unpublishedfrontend_accessibleidentifiers must publish those identifiers or set the flag. -
Every audit CLI entry point now asserts an operation permission (breaking) (#274).
vault:audit,vault:audit-verifyandvault:audit-anchorgated nothing at all, while the same capabilities were gated in the backend module all along: anyone who could invoke them could read the audit log — who touched which secret when, which maps out the credential topology — carry an unchained copy of it off with--export, clear the tamper-evidence tip anchor, or re-attest a truncated chain to the external sinks. The permission now follows the operation's effect, so the same operation answers to the same permission through every entry point:Operation Permission Entry points Read audit entries audit.viewaudit module, vault:auditVerify the chain audit.viewaudit module, vault:audit --verify,vault:audit-verify,AuditVerifyTaskExport to a file audit.exportaudit module, vault:audit --exportPublish the chain tip vault.configurevault:audit-anchor,AuditAnchorTaskReset the tip anchor vault.configurevault:audit --reset-anchorVerification is a read of the chain — it recomputes and compares, it mutates nothing — so it shares
audit.viewwith the listing rather than taking the administrative permission. Anchoring and resetting the anchor do 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 before any query, file write, chain read or anchor change happens, and writes noaccess_deniedentry — the same shape as every other operation-permission gate (vault:retrieve,vault:rotate-master-key, the backend modules), and for--verifyand--reset-anchorthe deciding argument is recursion: a denial entry would append a row and advance the tip anchor, mutating the very state the operator is about to inspect. In--format=jsona refusedvault:audit-verifyreportsvalid: false, so a monitor never reads it as a clean chain.OrphanCleanupTaskis deliberately not gated at task level: its effect already answers tosecret.deleteone layer deeper. -
undelete,copyandmoveare refused fortx_nrvault_secret(breaking) (#276). Each is marked handled so core never reaches its cmdmap branch, writes anaccess_deniedaudit entry and emits a DataHandler error naming the reason.localize,copyToLanguage,inlineLocalizeSynchronize,discardandversionare deliberately left alone — each was verified inert for this schema, and a gate with nothing behind it is noise. Administrators are refused too, and that is a product rule rather than a permission tier: an exemption would make "the vault cannot restore it" mean "unless an administrator says otherwise", and would write a restore into the HMAC chain that the vault never performed. An operator who must resurrect a row still has the database, where the change is visible as what it is. The user-facing wording follows: the confirm dialogs, the TCA field clear andvault:deletenow all say "The vault cannot restore it. The encrypted record is retained in the database until it is removed there.", replacing a documentation passage that called the soft delete "auditable and reversible". -
The
ext_emconf.phpTYPO3 constraint is capped at14.3.99(breaking for anyone installing via the Extension Manager against a future 14.x) (#271). The previous13.4.0-14.99.99claimed compatibility with 14.4 through 14.99, which do not exist as supported releases — v14.3 is the LTS. This does not reopen the 0.7.0 decision to keep a coarse range: that reasoning was about the gap a single continuous range cannot express (the unsupported 14.0–14.2 sprint releases, still spanned), not about the ceiling.composer.jsonremains authoritative for a Composer-based installation. -
Six interfaces gained members, which is breaking for third-party implementations (#259, #263, #278, #280, #281). Each addition exists because a caller needed a narrower or a wider operation than the interface could express; none changes an existing signature's meaning, and the two optional parameters default to today's behaviour so existing callers and test doubles bind unchanged.
Interface Addition Why VaultServiceInterfaceassertDeletable(string $identifier): voidruns delete()'s gates without deleting, so a record spanning several vault fields fails closed before the first irreversible deletionVaultServiceInterfacesetEnabled(string $identifier, bool $enabled, string $reason = ''): voidthe single audited write path for a secret's availability VaultServiceInterfacelist(?string $pattern = null, bool $includeDisabled = false): arraylets the management surfaces see secrets the read paths no longer return SecretRepositoryInterfacefindByIdentifierIncludingDisabled(),findByUidIncludingDisabled()lift HiddenRestrictionby name for administrative lookups;removeAll()was rejected because it would also discardDeletedRestrictionand resurrect soft-deleted rowsSecretRepositoryInterfacesetHidden(int $uid, bool $hidden): void,setMetadata(int $uid, array $metadata): voidcolumn-scoped writes, so a metadata or availability change stops rewriting the whole row SecretRepositoryInterface,VaultAdapterInterfacesave()andstore()gainbool $persistGroupRelations = truelets the FormEngine completion path keep MM rows and their count columns consistent instead of zeroing the tiers VaultDoctorServiceInterfacerun()gainsbool $activeProbes = falserequired by --active-probes -
The backend modules and AJAX routes moved from
admintouser(#238), and every controller action asserts its own operation instead: the overview filters its submodule cards by permission and the templates render only the actions the user holds. Non-admin editors working with vault-backed FormEngine or FlexForm fields therefore needsecret.use, which they did not need before.vault:rotate-master-keynow needsallowCliAccess = 1like every other secret command — the_cli_user is never logged in, so group grants cannot apply to it — and, since #254,master_key.rotateincliAllowedOperationsas well. -
Technical actors resolve their grants from their backend groups (#251). They were previously hard-coded to an implicit
secret.use, which central enforcement would have turned into "norunAs()worker can ever mutate anything". The other operation permissions are now read straight from thetx_nrvault:<permission>custom options on the actor'sbe_groupsrows — never throughBackendUserAuthentication::check(), which core short-circuits totruefor admins — and fail closed: no groups, no grant.secret.usestays implicit. -
A truncated audit log now verifies as INVALID. This also blocks
vault:rotate-master-keyand both HMAC re-seal paths, which already refuse to run on any other chain error — re-sealing a truncated chain would launder it. Usevault:audit --reset-anchorfor a truncation you performed on purpose. Installations upgrade into a populated chain with no anchor row; that is a warning, not an error, and the anchor arms itself on the next audit write. -
Frontend
%vault()%resolution is restricted to frontend requests and to any web request whose type cannot be established — eID among them, where$GLOBALS['TYPO3_REQUEST']does not exist. CLI and backend requests were unchanged by ADR-035 and are now covered separately (see the CLI entry above); a backend request is recognised by the request the content object renderer carries, never by what an earlier request left in the superglobal, and a renderer built without a request of its own is restricted wherever it runs. Log volume on the rejection path is bounded by a latch that is per request and only engages in a frontend or unknown web context: 100 injected placeholders naming a withheld secret used to produce 100 warnings and 100AccessDeniedaudit rows, and now produce at most one record and no rows. The latch cannot carry into the next request and never engages on the CLI, so a long-runningscheduler:runor Messenger consumer keeps every warning it emitted. -
Detection trade-off. Because a rejected identifier is refused before the vault is touched, it no longer produces the
AccessDeniedaudit row it used to. Outside Development a rejection is written nowhere, so probing for a site's published identifiers leaves no trace; the signal is the literal%vault(...)%surviving in the output. This is deliberate — any per-rejection record is a write an anonymous visitor can drive — and is recorded as a residual in ADR-035. -
The documentation was audited against the shipped code and roughly seventy drift items corrected (#266, #267, #246, #264). The corrections worth naming are the ones that ran the wrong way: the auditor documentation claimed a secret-scanning control does not exist when it runs on every pull request, while both
AGENTS.mdfiles claimed a gitleaks scan that did not run at all — a false control claim in a secrets extension changes behaviour, because a contributor adding fixtures relaxes their own care trusting a scan that never happens. Verification procedure 6c expected aTRUNCATEto leave a valid chain, which the ADR-034 anchor now correctly reports as invalid. ADR-034 prescribed the oneentry_namespacevalue the code deliberately avoids, ADR-005's code sample taught the inlinedisAdmin()pattern the codebase forbids,vault:storeandvault:retrievedocumented options that do not exist, andTcaIntegration.rst's resolver examples were fatal errors. Three passages claimed multi-field copy and delete are atomic, where the real guarantee is preflight plus best-effort compensation with three named residuals; the code was honest about this in its own log messages, only the prose said "never". -
Dependencies and CI, grouped:
actions/upload-artifactto v7 (#247), the zizmor policy first added locally and then removed once the shared reusable started serving it centrally (#253, #265), the labeler moved into its ownpull_request_targetworkflow so fork pull requests stop failing withResource not accessible by integration(#275), and the unit tests moved offexpectExceptionMessage(), which PHPUnit 13.2 deprecated — via a trait routing toexpectExceptionMessageMatches()with apreg_quoted needle, because the official replacement landed in 13.2.0 and four of eight matrix cells resolve PHPUnit below that (#279).
Fixed
- Master-key rotation skipped disabled secrets (#286). The rotation inventory, the pre-flight smoke test and the per-secret loop all used the hidden-restricted repository lookups, so a disabled secret's DEK stayed wrapped under the old master key — the very key the command's next-steps output tells the operator to destroy. Re-enabling the secret later would surface a permanently undecryptable ciphertext; with only disabled secrets in the vault the command even reported "No secrets found" and exited successfully. All three lookups now use the disabled-visible repository paths, and a functional test rotates a vault holding an active and a disabled secret and proves both plaintexts survive the key switch.
reseal()skipped its anti-truncation guard on the master-key rotation path (#283). The guard refuses to re-sign a shortened chain, but it can only check a stored anchor it has authenticated — and it authenticated under the key it was about to sign with. Rotation is the one path that passes a different key, so the old-key MAC never verified, the guard was skipped, andreseal()minted a fresh anchor over whatever the tip was at that moment. The command's pre-flight chain verification kept this theoretical (the rows would have to disappear inside the rotate transaction), which is why it was tracked as a follow-up rather than an advisory.reseal()now falls back to the provider's current key to authenticate the stored anchor, putting the rotation path behind the same guard as the two migration paths.- A rotated secret read as never rotated (#281).
store()carriedversion,crdateandcruser_idforward from the existing record but notlast_rotated_at,read_countorlast_read_at, so all three fell back to their0constructor defaults and were written on every update — from the module's edit form,vault:store,vault:migrate-field, the FormEngine completion path and any programmatic call alike.rotate()was never affected. This is not cosmetic: those are the columns the module's Reads and Last read display, that rotation-age reporting consults, that the orphan-cleanup heuristics act on, and that an audit reads. The whole suite was green while the bug was live, which is itself part of the finding. updateMetadata()wrote the whole row (#281), carrying the same concurrency exposuresetEnabled()was fixed for, while its own docblock promised a write "without changing the secret value". It is narrowed rather than removed — there is no production caller, but it is declared onVaultAdapterInterface, the documented extension point for third-party adapters (ADR-007), so deleting it would be a public-API break for a defect that is fixable in place. The merge stays in the adapter; the newsetMetadata()primitive writes the metadata column andtstampand nothing else.- Two fail-open lookups on the FormEngine write path (#280).
isUpdateAuthorized()andenforcePrivilegedColumnPolicy()both treated "record not found" as "allowed". A null lookup is not hypothetical: core reads its datamap target with the delete clause off and skips only on an empty record, so a soft-deleted tombstone has a pid, is processed by core, and was waved straight through by the vault. Both now refuse. They report differently, because only one of them has an identifier — a tombstone gets anaccess_deniedentry under its own identifier plus a DataHandler error, an absent record gets the error alone, so nothing enters the tamper-evident chain anonymously.enforcePrivilegedColumnPolicy()returns a bool and the caller nulls$fieldArrayrather than dropping the privileged columns: the read that failed is the same read that would supply the stored values to compare against, so dropping columns is not fail-closed there. - Disabling a secret was a one-way door (#278). A disabled secret vanished
from every repository query, so the row left the list, the re-enable button —
rendered per listed row — became unreachable,
editActionthrewSecretNotFoundException, and a freshstore()classified as a creation and collided with the unique key as a raw database error. Administrative operations (delete(),assertDeletable(),rotate(),store(),getMetadata()) now use the disabled-visible lookup, andbuildSecretEntity()carrieshiddenforward — without which routingstore()through the wider lookup would have silently re-enabled a disabled secret on any value write.findByIdentifier()itself is untouched, so the read path is unchanged. The list template's "Disabled" badge, row class and active/disabled filter had been dead against a hardcoded'hidden' => falseand a comment claiming secrets have no hidden state; both are gone. LocalEncryptionAdapter::delete()did nothing at all for a disabled secret (#278) — a silent no-op, because it went through the restriction-honouring lookup.- An update replaced unsubmitted options with defaults (#252), wiping
description, context,
allowed_groups,write_groups, expiry, scope and frontend availability that DataHandler had just persisted; a plain programmaticstore('id', $value)silently reset policy fields whose change is gated bysecret.manage_policy. Updates preserve unsubmitted fields now. In the same area, a FormEngine create was classified as an update because the row exists beforestore()is called, so it was gated bysecret.rotateand audited asupdate; creation is now classified by the value — a record without an encrypted value is a creation in progress. Secretparsed theallowed_groups/write_groupscount columns as a list of group uids when the MM load came back empty (#261), so a count of 3 would have read as "group 3 is allowed". The fallback had no legitimate producer and is gone; the write side emits the relation count consistently.EncryptionService::resolveAlgorithm()accepted anyencryption_version >= 2and opened it under version-2 rules (#242) — forward compatibility by accident, so an envelope claiming version 99 decrypted "successfully". Unimplemented versions are refused loudly; versions 1 and 2 are unchanged.wipeCredentials()raised instead of being idempotent (#258).sodium_memzero()nulls the zval after zeroing, so a second call threwSodiumException— on a failure path, where it replaced the real error with its own. A guard flag makes repeat calls the no-ops the docblock always promised.- The TypoScript examples in
Documentation/Usage/Index.rstshowedTEXTobjects whose only property wasvalue.TEXTremovesvaluefrom its configuration before rendering, so such an object never callsstdWrap()and the placeholder never resolved. The examples now carry thestdWrap.sub-array that makes them work.
Security
- The plaintext read cache bypassed every control on the read path (#250,
High).
VaultServiceheld a request-scoped plaintext cache consulted before the record load, the per-secretcanRead()tier, the interactivesecret.usegate, the expiry check and the read audit entry — andVaultServiceis a shared singleton. In a long-running worker the concrete failure is three steps: technical actor A reads secret X and the plaintext lands in the cache; therunAs()scope switches to actor B; actor B retrieves the same identifier and gets the plaintext back with no authorization, no expiry check and no audit row. The cache key carried neither actor nor context nor permission state, and the cache was only reliably emptied in the destructor. It is removed entirely; see Changed for the API consequence. - The operation permissions could be bypassed through the paths that matter
most (#251, High). Only the module controllers checked them.
VaultService::store(),rotate()anddelete()checked the per-secret tiers alone, andtx_nrvault_secretis an ordinary visible TCA table — so a backend user with generic table rights could create, rotate or delete secrets through a direct FormEngine or DataHandler request without ever meetingsecret.create,secret.rotateorsecret.delete, and change policy columns withoutsecret.manage_policy. Enforcement now sits at the business boundary with audited denials:store()requiressecret.createfor a new identifier andsecret.rotatefor an existing one, plussecret.manage_policywhen the call actually changes owner, group tiers or frontend availability — compared on the effective values after the existing coercions, not on what was submitted.SecretTcaHookcarries the same gates for the two FormEngine paths that do not pass through the service. - FormEngine mutations were fail-open on audit errors (#252, High for audit
purposes). The delete success entry was written before core deleted the
record and a failing audit write was swallowed outright (
catch (Throwable) {}); a metadata update kept its database change when the audit write failed, on a "don't fail the save" rationale. The auditor documentation's claims — every mutation logged, delete and store compensate a failed audit write — simply did not hold for the DataHandler path. The honest failure contract came first:AuditLogService::log()now wraps any chain-write failure inAuditWriteException, where previously only the advisory-lock timeout was wrapped, so a genuine INSERT failure bypassed every compensating rollback that catches that type. On top of it, thetx_nrvault_secretdelete command runs throughVaultService::delete()and core'sdeleteActionis skipped in every outcome; metadata changes revert their captured pre-change values when the audit write fails; and a failed vault delete now cancels the record delete on foreign tables instead of proceeding and orphaning the secret behind an apparently successful removal. - The rollback restored the row but not the ACL relations (#261, High).
SecretRepositoryloads effective groups from the MM tables, and the audit-failure rollback only restored thetx_nrvault_secretrow — so an ACL widening whose audit write failed persisted unaudited, while the restored count column actively contradicted the MM state. Both tiers are now snapshotted inprocessDatamap_preProcessFieldArray(), which is the only viable moment because DataHandler'swriteMM()runs insidecheckValue(), before the audit hook exists to fail. A tier that legitimately had no groups is restored to empty, and if a tier cannot be repaired the DataHandler log says NOT revertible instead of falsely reporting success. Forstatus='new'the MM writes are deferred past the hook, so the revert used to leave orphaned rows against the deleted uid; a newprocessDatamap_afterAllOperations()pass purges them. - A refused create left a squatted row and a false success entry in the HMAC
chain (#263). The hook classified creates by outcome — "was a value stored"
— so "no value submitted" and "value submitted but refused" were
indistinguishable, and a create by a user lacking
secret.createfell into the value-less branch: the row survived withowner_uidforced to the denied user, the identifier was reserved against later legitimate creators, and acreate success=1entry went into the tamper-evident chain next to the truthfulaccess_deniedone. Classification is now explicit (RecordCreationOutcome::classify()→ValueLess/Stored/Rejected), a rejected create deletes the fresh row and joins the MM purge, and no success entry is written — theaccess_deniedentry is the record. - Value-less creates bypassed
secret.createentirely (#270). Both create gates live insideVaultService::store(), and the value-less path is by definition the one wherestore()is never called;secret_inputis optional in the TCA, so a backend user withtables_modifybut withoutsecret.createcould deliberately leave the value empty, create a vault record, become its owner, reserve the identifier, and produce a successfulcreateentry in the chain for an operation they were not permitted to perform. No race, no misconfiguration. The gate now sits inprocessDatamap_preProcessFieldArray()and refuses before the row exists, using core's documented abort contract ($fieldArray = null→if (!is_array($incomingFieldArray)) { continue 2; }, present in v12.4, v13.4 and v14.3), so nothing is inserted and nothing needs compensating. - Every non-privileged update to an existing secret went through with no
per-secret ACL check (#269, High), and the hook then audited it as a
successful
metadata_updateattributed to the editor — the log asserting that a change was authorised when it was not. Two concrete abuses followed. Backdatingexpires_attakes a foreign secret out of service for every consumer, and setting it to0revives a retired one. Writingmetadatais worse:OrphanCleanupTaskreads table and uid straight out of that column andrecordExists()answered false for a table that does not exist, which the caller reads as "source record gone, retire the secret" — so a crafted payload on a secret past the retention cutoff made the scheduler delete it, with the task as the recorded actor. That is destruction, not denial of service. The hook now resolves theSecretand requirescanWrite()before anything is written, refusing the whole record;expires_atandmetadatajoin the privileged column set; orphan cleanup fails closed, so only a successful lookup against an existing table returning no row may answer "gone"; andSecretsController::toggleAction, which gated the operation permission alone and let any holder hide or unhide any secret, checkscanWrite()too. Reachability was narrower than "any editor" — vault-created secrets live atpid 0, which core refuses to non-admins before the hook runs, so the defect needed a secret row on a real page — and the control gap and the false audit entry are real regardless. - Record copy and delete were not fail-closed across multiple vault fields
(#259). A failed per-field clone was only logged, so the copied record kept
the DataHandler-duplicated source identifier and silently aliased the
source's secret: rotating the copy mutated the source, deleting the copy
destroyed it. A unit test had pinned that as expected behaviour. Deletes ran
sequentially with no break, so a mid-sequence failure still deleted the
remaining fields and left the record pointing at dead secrets — and
SecretNotFoundExceptioncounted as failure, which made a record referencing a missing secret permanently undeletable through the backend, self-reinforcing with the copy bug. Copy now compensates every already-cloned secret and blanks all vault fields of the copy on any failure; delete pre-flights every field through the new non-mutatingassertDeletable(), which sharesdelete()'s private gate so the two cannot drift, and a missing secret counts as success. The residual is stated rather than implied: this is preflight plus best-effort compensation, not atomicity (#267). undeleterestored a soft-deleted secret with no vault check of any kind (#276) — no ACL, no operation permission, no audit entry. The vault delete writes onlydeleted = 1, so ciphertext, DEK,frontend_accessible,hiddenand both MM ACL tiers survive intact, and restoring the row brings all of it back; a previously frontend-accessible secret is immediately resolvable again. The prerequisites were an authenticated non-admin withtables_modify, workspace 0 and one uid — zero vault permissions — because core'sundeleteRecord()gates page permissions behindif ($recordPid > 0), skipped entirely atpid 0, andSimpleDataHandlerControllerpassescmdthrough with no allow-list. Core does writesys_logandsys_history, so the restore was invisible specifically to the chain auditors are pointed at.copyandmoveare refused alongside it: a copied secret is always value-less while carrying the original's identifier, andfindByIdentifier()has noORDER BY, so the empty clone can win the lookup and break an intact secret;moveis the only command that takes a secret off root level into the page tree, wheredeleteSpecificPage()removes records by pid through a path that reaches neither the vault ACL nor the audit log.- Truncating the audit log is no longer invisible (ADR-034, #234). The hash
chain only ever checked rows that were still present, so
DELETE FROM tx_nrvault_audit_log WHERE uid > N— or a fullTRUNCATE— left a self-consistent chain that every tamper-evidence control reported as valid. nr_vault now records one signed assertion outside that table, in the core tablesys_registry: "rowuid = Astill exists and itsentry_hashis stillH", authenticated with a key derived from the master key under its own HKDF context. Tail truncation, deletion of the last row, a full wipe, and a wipe followed by refilling the same UIDs are all reported as an invalid chain now. There is no database schema change. What an attacker without the master key cannot do is forge the assertion, so the one-statement invisible truncation becomes a two-target attack whose second target can only be destroyed — and destruction changes the reported verdict. - Frontend
%vault()%placeholders are now scoped to published identifiers (ADR-035, #235).TypoScriptVaultListenerruns on the output of everystdWrap()call, so an editor-writtentt_contentfield (stdWrap.field = bodytext) or a reflected request parameter (data = GP:q) was a resolution site for any secret flaggedfrontend_accessible— the plaintext landed in output shared through the page cache. In a frontend request the extension now resolves an identifier only when it was published through a source an editor cannot write: the TypoScript setup array, the site configuration or settings,plugin.tx_nrvault.frontendResolvableIdentifiers, orFrontendPlaceholderPolicyInterface::allowIdentifier(). The check runs before the vault is touched, so a rejected identifier reaches neither the vault nor the audit log. - Nine findings from a security scan of the extension (#232). Secrets
entered into a
vaultSecretFlexForm field were stored in cleartext (HIGH): the hook resolved the data structure with an empty table name, an empty field name and the submitted array where the record row belongs, which throws on both v13 and v14 — and the exception was swallowed, so the editor's plaintext fell through to DataHandler and into the record XML with no vault ACL and no audit entry. Frontend authorization was inferred from the absence of$GLOBALS['BE_USER'], which TYPO3 populates for any visitor carrying a backend session, so an editor could put a placeholder for a secret they cannot read into a published page, have an admin review it in the frontend, and the decrypted value went into the shared page cache and out to anonymous visitors. The SSRF DNS-pinning middleware returned "allowed, nothing to pin" for canonical IP literals, trusting a caller-side check that redirect hops never pass, so a302to169.254.169.254reached cloud metadata. An unboundedX-Request-Idwent into avarchar(100)column while the HMAC covered the untruncated value, which either aborted the audit write or left a row permanently disagreeing with its own hash. Both CSV exports emitted the proprietaryfputcsvescape, letting an attacker-controlled field close its own cell and synthesize a formula cell past the sanitizer; both now emit strict RFC 4180. And vault exception text reached editors through the flash message andDataHandler::log(), giving an existence oracle for secrets outside their ACL — failures now report a generic message plus a correlation reference, with the cause logged server-side, while the TSconfigedit/readOnlyfield permissions are re-checked on the write path instead of only rendered as areadonlyattribute. - The plaintext exposure window in the browser is bounded, and the reveal
endpoint is uncacheable (#237). A shared reveal lifecycle auto-hides after
30 seconds with a visible countdown and wipes immediately on
visibilitychangeandpagehide; the reveal modal wipes its input on every close path, including ESC and backdrop.AjaxController::revealActionsendsCache-Control: no-storeon success and error. Under the hardened profile copy-to-clipboard is disabled outright, because the clipboard outlives the dialog and cannot be reliably cleared from JavaScript. The guarantee is documented for what it is — a bounded exposure window, not memory clearing, since JavaScript strings cannot be zeroized. An orphanedSecretReveal.js, whose DOM ids appeared in no template, PHP file, configuration or test, was deleted rather than hardened.
Migration
Everything below is something an operator has to do; nothing here happens by itself.
- Grant the operation permissions before upgrading, not after. Backend users
need the matching
tx_nrvault:<permission>custom option on one of their groups (Backend Users module). The one that bites quietly: non-admin editors who work with vault-backed FormEngine or FlexForm fields now needsecret.use. Admins are unaffected unless you also setdisableAdminOverride. - Re-check every CLI workflow against
cliAllowedOperations. The default issecret.use,secret.create,secret.rotate. Anything that reveals (vault:retrieve), deletes (vault:delete, the scheduled orphan cleanup), exports (vault:audit --export), rotates the master key, reads or verifies the audit log (vault:audit,vault:audit-verify), or publishes the tip anchor (vault:audit-anchor) needsallowCliAccess = 1and the operation added to that list. Prefer a named technical actor viaTechnicalActorContext::runAs(): grants then come from its provisioned groups and the audit trail names the identity that read, exported or anchored, rather than recording an unattributable shell. - Scheduled operation is unaffected on a default installation —
scheduler:runauthenticates the_cli_administrator, who passes through the admin bypass. UnderdisableAdminOverridethat bypass is gone by design, so the identity running the scheduler needs a group carryingtx_nrvault:audit.view(verify) andtx_nrvault:vault.configure(anchor); without it both tasks fail loudly rather than skipping quietly. That is the intended trade: a red scheduler entry is recoverable, an anchoring run that did not happen but looks like one that did is exactly what the anchor exists to rule out. - Publish any frontend identifier that is not already in TypoScript or site
configuration. Every documented TypoScript and site-configuration form keeps
working: writing
lib.apiKey.value = %vault(my_api_key)%publishesmy_api_key. An identifier used only in a Fluid template file, auserFuncor a DataProcessor is not in the setup array — publish it once per site withplugin.tx_nrvault.frontendResolvableIdentifiers = my_api_key. A rejected placeholder is left literal in the output, and the Development context emits onenoticeper request naming it. - In an eID handler, call
allowIdentifier()with the PSR-7 request as its second argument:GeneralUtility::makeInstance(FrontendPlaceholderPolicyInterface::class)->allowIdentifier('my_api_key', $request). The policy is a shared service that outlives a request, so the grant is stored against that request object in aWeakMap— unreachable from any later request, which in a worker SAPI (FrankenPHP, RoadRunner) is what stops one request's grant from authorising the next one's anonymous, page-cached render. Pass the request you are handling andsetRequest()the same object on the content object renderer you render with; the grant is matched by object identity.$GLOBALS['TYPO3_REQUEST']is never used for it. A renderer carrying a different request, or none, resolves nothing. - Check your scheduled render jobs before upgrading if any of them resolve
placeholders. A newsletter, static export or indexing job that renders
editor-authored content now enforces the same allow-set as a frontend request.
Publish the identifiers it needs, or set
frontendPlaceholderLegacyCli = 1to restore the old CLI behaviour — and expectvault:doctorto report that flag as a warning understandardand a critical underhardened, because no workflow needs it that publishing the identifier would not also serve. - Drop any
VaultServiceInterface::clearCache()call and anycacheEnabledconfiguration. Both are gone. Reads are no longer cached at all, so a consumer that read the same identifier in a tight loop now performs oneSELECTand one decrypt per read — and produces one audit row per read, which is the point. - Third-party implementations of
VaultServiceInterface,SecretRepositoryInterface,VaultAdapterInterfaceorVaultDoctorServiceInterfaceneed the new members listed in the interface table under Changed. The two new parameters are optional and default to today's behaviour, so only the new methods are a hard break. undeleteis no longer available fortx_nrvault_secret, for anyone, administrators included. If you rely on restoring soft-deleted secrets, that path is now the database — where the change is visible as what it is. Update any runbook that promised the delete was reversible.- Enable
auditAnchorRequiredafter the first audit write following the upgrade, not before: installations arrive with a populated chain and no anchor row, which is a warning while the flag is off and an error once it is on. While it is on,vault:audit --reset-anchoris the only way to arm the anchor, and it writes the reset into the chain so the operation cannot be performed invisibly. - If you configure the webhook audit sink against an on-premises RFC 1918
collector, add an explicit
HTTP.allowed_hostsentry. The sink uses the SSRF-guarded client and the refusal is loud — logged, counted and raised as a finding — but it is a refusal. - Run
vault:doctor --profile=hardenedbefore switching a profile, andvault:doctor --active-probesafter configuring sinks. The first tells you what would fail without changing any configuration; the second is the only check that proves records actually arrive rather than that a URL parses. Exit codes are stable (0 clean, 1 warnings, 2 critical), so both are usable as pipeline gates. - If
auditHmacEpochis below 3, raise it and run the migration — in that order matters less than doing both. Raising the setting without runningvault:audit-migrate-hmacleaves older rows signed at the lower epoch and is the silent case doctor now reports as a warning; at epoch 1 thesuccesscolumn itself is outside the signed payload, so a recorded denial can be flipped to a recorded grant without touching a signed byte. Epoch 0 is critical: it drops row hashes to keyless SHA-256, makes the downgrade guard vacuous, and disables the in-DB anchor even whenauditAnchorRequiredis set.
- The plaintext secret cache is gone, and with it
-
v0.13.030 Jul 2026Release notes
Open source →Highlights
Two pull requests, one theme: making the vault usable by other extensions without each of them re-inventing the parts that must not be re-invented. Read the 0.13.0 changelog for the full detail.
A consuming extension can now seal its own payloads
EncryptionServiceInterfaceprotects a secret across seven arguments and a column per field. That shape is right fortx_nrvault_secretand wrong for a consumer that keeps one encrypted payload in one column — so consumers were inventing framing around it. The new portable envelope codec (EnvelopeCodecInterface, ADR-032) gives themseal()/open()/isSealed()/rewrap()against a self-describing format (nrv1:+ base64 JSON) instead.…and master-key rotation no longer strands those payloads
This is the part to read if you already have a consumer sealing data. Rotation re-wrapped the data keys in
tx_nrvault_secretand nothing else. A consumer's wrapped data key lives in its own table, so rotating left those envelopes under exactly the key the command's next-steps output tells the operator to destroy — silently, because the rotation succeeded at everything it knew about.Consumer-owned envelope rotation (
ForeignEnvelopeRotatorInterface, ADR-033), found and fixed by @CybotTM in #230, closes it: an extension tags an implementationnrvault.foreign_envelope_rotator, andvault:rotate-master-keyre-wraps its envelopes inside the same transaction as its own secrets, handing over keys as anEnvelopeRotationContextso the consumer never holds key material. The command now reports every participant and refuses to run when it cannot inventory one, when a consumer's table sits on a different database connection, or when a rotator re-wraps fewer envelopes than it reported.If you maintain an extension that seals payloads with nr-vault, registering a rotator is required from this release on.
One catalogue of what a secret looks like
The knowledge of what a secret looks like was maintained in four places — this extension's plaintext scanner and three separate redactors in nr-llm — and the copies had drifted apart in both directions.
SecretPatternLibrary(ADR-031) merges them, carrying an anchored form per shape for classifying a whole value and an inline form for masking a secret embedded in free text;SecretRedactoris the consumer-facing API.The merge immediately fixed redaction bugs that had been live in the drifted copies:
client_secretwas never redacted. The parameter-name alternation had to match immediately after?or&, so the name RFC 6749 §2.3.1 actually defines passed through untouched — as didpasswordand the hyphenatedapi-key.- Redaction patterns ran past the end of the URL. Bounded only at
&and whitespace, a?token=…inside a JSON payload swallowed the closing quote and the following key; a URL carrying a port followed later by an unrelated address was read as one userinfo component, losing the port and fabricating a credentialled URL to a host that was never contacted. - The plaintext scanner now classifies OpenAI keys,
ghs_tokens and fine-grained GitHub PATs asCriticalinstead of leaving them unlabelled.
Also fixed
MasterKeyRotatedEventis dispatched. It was declared, documented inApi.rstand listed as step 3 of the rotation procedure in ADR-003, but fired from nowhere. It now dispatches after the rotation commits, carrying the consumer-envelope count alongside the secret count.The documented
decrypt()andreEncryptDek()signatures inApi.rsthad drifted five parameters behind the interface, and were corrected.Full Changelog: v0.12.2...v0.13.0
Installation
composer require netresearch/nr-vault
Publication status
- TER: extensions.typo3.org/extension/nr_vault — 0.13.0 verified
- Packagist: packagist.org/packages/netresearch/nr-vault — 0.13.0 verified
- Documentation: docs.typo3.org/p/netresearch/nr-vault/0.13/en-us/ — render verified
Security
All release artifacts are signed with Sigstore keyless signing.
Verify signatures
cosign verify-blob \ --bundle nr-vault-0.13.0.zip.sigstore.json \ --certificate-identity-regexp "https://github.com/netresearch/.*" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ nr-vault-0.13.0.zip
Verify checksums
sha256sum -c checksums.txt
Software Bill of Materials (SBOM)
SBOMs are provided in both SPDX and CycloneDX formats for supply chain transparency.
Release notes
Open source →Added
- Shared secret-shape catalogue (
Netresearch\\NrVault\\Secret, ADR-031). The knowledge of what a secret looks like was maintained in four places — this extension's plaintext scanner and three separate redactors in nr-llm — and the copies had drifted apart in both directions.SecretPatternLibrarymerges them, carrying an anchored form per shape for whole-value classification and an inline form for masking a secret embedded in free text.SecretRedactoris the consumer-facing API. Consuming extensions can read the catalogue statically or resolveSecretRedactorInterfacefrom the container. - Portable envelope codec (
EnvelopeCodecInterface, ADR-032).seal()/open()/isSealed()/rewrap()protect a payload a consumer keeps in ONE column, so it no longer has to invent framing aroundEncryptionServiceInterface's seven-argument, column-per-field shape. The sealed form isnrv1:+ base64 JSON. - Consumer-owned envelope rotation (
ForeignEnvelopeRotatorInterface, ADR-033). A consuming extension tags an implementationnrvault.foreign_envelope_rotator;vault:rotate-master-keythen re-wraps its envelopes inside the same transaction as its own secrets, handing over the keys as anEnvelopeRotationContextso the consumer never holds key material.
Fixed
- Master-key rotation no longer strands a consumer's envelopes. Rotation
re-wrapped the data keys in
tx_nrvault_secretonly. A consuming extension's wrapped data key lives in its own table, so rotating left those envelopes under a key the operator was told to destroy — silently, because the rotation succeeded at everything it knew about. Any consumer that seals payloads must now register a rotator; the command reports each participant and refuses to run when it cannot inventory one, when a consumer's table sits on a different database connection, or when a rotator re-wraps fewer envelopes than it reported. MasterKeyRotatedEventis dispatched. It was declared, documented inApi.rst, and listed as step 3 of the rotation procedure in ADR-003, but was never dispatched from anywhere. It now fires after the rotation commits and carries the consumer-envelope count alongside the secret count.- Credential query parameters with a vendor prefix are redacted. The
parameter-name alternation had to match immediately after the
?or&, soclient_secret— the name RFC 6749 §2.3.1 defines — never matched and passed through untouched.passwordand the hyphenatedapi-keywere missing for the same reason. - The URL redaction patterns no longer run past the end of the URL. Bounded
only at
&and whitespace, a?token=…inside a JSON payload swallowed the closing quote and the following key, and a URL carrying a port followed later by an unrelated address was read as one userinfo component — losing the port and the following field, and fabricating a credentialled URL to a host that was never contacted. - The plaintext scanner classifies three more shapes. OpenAI API keys,
ghs_tokens and fine-grained GitHub PATs in a scanned column or configuration key are now reported asCriticalinstead of unlabelled. - Corrected the documented
decrypt()andreEncryptDek()signatures inApi.rst, which had drifted five parameters behind the interface.
-
v0.12.227 Jul 2026Release notes
Open source →Patch release fixing the audit-logging error that appeared on every re-save of an existing secret record.
Fixed
- "Audit logging failed: Unknown audit action "metadata_update"" on metadata-only saves (#227). Opening a secret record and saving it without re-entering the secret classified the save as a
metadata_updateaudit action, which the audit log rejected as unknown — the editor saw the error on every such save, and the audit entry for the metadata change was silently never written. The action is now a valid audit action: metadata-only saves are sealed into the tamper-evident audit chain and appear as "Metadata Update" in the audit-module action filter. The path became reachable with the v0.12.1 fix that preserves an untouched secret on re-save. (#228)
Regression coverage was added at both levels: a functional test asserting the clean save plus the sealed audit entry, and a Playwright E2E test replaying the backend flow from the report.
Thanks to @lradloff for reporting #227.
Installation
composer require netresearch/nr-vault
Publication status
- TER: extensions.typo3.org/extension/nr_vault — 0.12.2 verified
- Packagist: packagist.org/packages/netresearch/nr-vault — 0.12.2 verified
- Documentation: docs.typo3.org/p/netresearch/nr-vault/0.12/en-us/ — render verified
Security
All release artifacts are signed with Sigstore keyless signing.
Verify signatures
cosign verify-blob \ --bundle nr-vault-0.12.2.zip.sigstore.json \ --certificate-identity-regexp "https://github.com/netresearch/.*" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ nr-vault-0.12.2.zip
Verify checksums
sha256sum -c checksums.txt
Software Bill of Materials (SBOM)
SBOMs are provided in both SPDX and CycloneDX formats for supply chain transparency.
Release notes
Open source →Fixed
- Saving an existing secret record without re-entering the secret no longer
shows "Audit logging failed: Unknown audit action "metadata_update"". The
DataHandler hook classified such metadata-only saves as
metadata_update, but theAuditActionenum had no such case, so the audit write was rejected — the save succeeded, yet the audit entry for the metadata change was silently never written. The action is now a valid enum case and the change is sealed into the tamper-evident audit chain; it also appears as "Metadata Update" in the audit-module action filter (#227).
- "Audit logging failed: Unknown audit action "metadata_update"" on metadata-only saves (#227). Opening a secret record and saving it without re-entering the secret classified the save as a
-
v0.12.125 Jul 2026Release notes
Open source →nr-vault 0.12.1
A patch release fixing a data-loss bug in vault secret fields.
Fixed
- Vault secrets are no longer wiped on an untouched save. Saving a record that holds a vault secret (for example a provider's API key) without re-entering it previously removed the secret from both the record and the vault — the stored secret is never rendered back into the edit form, so an untouched re-save submitted an empty value that the DataHandler hooks treated as "delete." Empty saves now keep the existing secret; deletion requires an explicit clear (which also repairs the previously no-op clear control). Applies to both regular TCA fields and FlexForm-embedded vault fields (#223, fixed in #225).
Thanks to @lradloff for reporting #223.
All changes since v0.12.0- fix(hook): preserve vault secret on an untouched re-save (#223) (#225)
- Update traefik:v3.7 Docker digest to 652929a (#224)
- chore: release v0.12.1 (#226)
Installation
composer require netresearch/nr-vault
Publication status
- TER: extensions.typo3.org/extension/nr_vault — 0.12.1 verified
- Packagist: packagist.org/packages/netresearch/nr-vault — 0.12.1 verified
- Documentation: docs.typo3.org/p/netresearch/nr-vault/0.12/en-us/ — render verified
Security
All release artifacts are signed with Sigstore keyless signing.
Verify signatures
cosign verify-blob \ --bundle nr-vault-0.12.1.zip.sigstore.json \ --certificate-identity-regexp "https://github.com/netresearch/.*" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ nr-vault-0.12.1.zip
Verify checksums
sha256sum -c checksums.txt
Software Bill of Materials (SBOM)
SBOMs are provided in both SPDX and CycloneDX formats for supply chain transparency.
Release notes
Open source →Fixed
- A vault secret (for example a provider API key) is no longer wiped when its record is saved without re-entering the secret. Because the stored secret is never rendered back into the edit form, an untouched re-save submitted an empty value, which the DataHandler hooks treated as "delete" — removing the secret from both the record and the vault. Empty saves now keep the existing secret; deletion requires an explicit clear (which also repairs the previously no-op clear control). Applies to both regular TCA fields and FlexForm-embedded vault fields (#223).
-
v0.12.023 Jul 2026Release notes
Open source →Security release
Six findings from a full security review — one HIGH and five MEDIUM. Two carry behaviour changes; see Upgrade notes below before upgrading.
Fixes
- Vault secrets no longer cached in cleartext (HIGH, #216).
%vault(id)%references in site configuration were resolved eagerly when TYPO3 loaded the site configuration, and TYPO3 persists that array into its on-diskcorecache — so decrypted secrets landed invar/cachein cleartext, with the per-principal access check applied only once, at cache-warm time. Resolution is now caller-driven, at read time. - CSV formula injection in audit-log exports (#218). Attacker-controlled audit fields (
User-Agent, request id, identifiers) reached CSV exports without neutralizing spreadsheet formula leaders (= + - @, tab, CR); all four export sinks now neutralize them. - Secret leaked into the audit log (#217). A query-parameter-placed secret surfaced in an outbound-request error message that was logged verbatim; the URL query is now stripped before the message is sealed into the audit row.
- Audit hash chain hardened against downgrade forgery (#221). Verification accepted a uniform relabel to keyless epoch-0 SHA-256, and the HMAC migration re-sealed the chain without verifying it first. Verification now enforces a configured-epoch floor, and the migration refuses to re-seal a chain that fails verification.
- Delete access control on
tx_nrvault_secret(#220). Deleting a secret via DataHandler (FormEngine, list module) enforced no vault ACL; it now requires owner / admin / system-maintainer. - Secret and master-key files created
0600from the start (#219).vault:retrieve --outputandvault:initleft a world/group-readable window between the write and thechmod.
Upgrade notes
- Site-configuration
%vault()%references now resolve at read time, not automatically on load (#216, ADR-030). Code that read$site->getConfiguration()[…]and received a decrypted value must now callSiteConfigurationVaultProcessor::processConfiguration($config, $site)explicitly. AuditLogServiceInterfacegainedverifyChainForReseal()and averifyHashChain()$minEpochparameter (#221); external implementers of the interface must implement the new method.vault:retrieve/vault:initnow abort on a failedchmod(#219), and a vault-ACL-denied delete now preserves the record and logsaccess_deniedinstead of soft-deleting (#220).
Installation
composer require netresearch/nr-vault
Publication status
- TER: extensions.typo3.org/extension/nr_vault — 0.12.0 verified
- Packagist: packagist.org/packages/netresearch/nr-vault — 0.12.0 verified
- Documentation: docs.typo3.org/p/netresearch/nr-vault/0.12/en-us/ — render verified
Security
All release artifacts are signed with Sigstore keyless signing.
Verify signatures
cosign verify-blob \ --bundle nr-vault-0.12.0.zip.sigstore.json \ --certificate-identity-regexp "https://github.com/netresearch/.*" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ nr-vault-0.12.0.zip
Verify checksums
sha256sum -c checksums.txt
Software Bill of Materials (SBOM)
SBOMs are provided in both SPDX and CycloneDX formats for supply chain transparency.
Release notes
Open source →Security release. Six findings from a full security review, one HIGH and five MEDIUM. Two carry behaviour changes — read Changed and Removed before upgrading.
Security
- Vault secrets no longer persisted in cleartext in the site-configuration
cache (#216, HIGH).
%vault(id)%references inconfig/sites/*/config.yamlwere resolved eagerly when TYPO3 loaded the site configuration, and TYPO3 writes that resolved array into its on-diskcorecache — so decrypted secrets landed invar/cachein cleartext, and the per-principal access check ran only once, at cache-warm time. Resolution is now caller-driven at read time (see Removed). - CSV formula injection in audit-log exports (#218). Attacker-controlled
audit fields (
User-Agent, request id, identifiers) were written to CSV exports without neutralizing spreadsheet formula leaders (= + - @, tab, CR). All four export sinks now route cells through a shared neutralizer. - Secret leaked into the audit log via transport errors (#217). A query-parameter-placed secret surfaced in the outbound-request exception message and was logged verbatim; the URL query is now stripped before the message is sealed into the tamper-evident row.
- Audit hash chain hardened against downgrade forgery (#221). Chain verification accepted a uniform relabel to keyless epoch-0 SHA-256, and the HMAC migration re-sealed the chain without verifying it first (laundering prior tampering). Verification now enforces a configured-epoch floor, and the migration refuses to re-seal a chain that fails verification.
- Delete access control on
tx_nrvault_secret(#220). Deleting a secret via DataHandler (FormEngine, list module) enforced no vault ACL; it now requires owner / admin / system-maintainer, mirroringVaultService::delete(). - Secret and master-key files created with
0600from the start (#219).vault:retrieve --outputandvault:initwrote the file with the process umask and onlychmod-ed afterward, leaving a world/group-readable window.
Changed
- Site-configuration
%vault()%references resolve at read time, not automatically on load (#216). Consumers that relied on transparent resolution — reading$site->getConfiguration()[…]and receiving a decrypted value — must now callSiteConfigurationVaultProcessor::processConfiguration($config, $site)at the point of use. See ADR-030. AuditLogServiceInterface:verifyHashChain()gains an optional?int $minEpoch = nullparameter and a newverifyChainForReseal()method (#221). External implementers of the interface must implement the new method.vault:retrieve/vault:initabort on a failedchmod(#219) instead of silently continuing.- A vault-ACL-denied delete preserves the record (#220) and records an
access_deniedaudit entry, instead of being soft-deleted.
Removed
SiteConfigurationVaultListener— the event listener that eagerly resolved site-configuration vault references on load (#216). See Security above for why, and Changed for the read-time replacement.
- Vault secrets no longer cached in cleartext (HIGH, #216).
-
v0.11.422 Jul 2026Release notes
Open source →nr_vault 0.11.4
A corrective patch that restores the extension documentation pipeline.
- Repaired
Documentation/guides.xml— the file was corrupted and broke rendering on docs.typo3.org. The documentation now builds and renders again. - Added a docs render CI job (
.github/workflows/docs.yml) so a broken or malformedguides.xmlis caught in CI before it reaches a release.
The backend dashboard widgets and the dark-mode fix introduced in 0.11.3 remain in place; this release adds no new features and changes no behaviour.
This is an additive 0.x patch release, so projects requiring
^0.11pick it up automatically. - Repaired
-
v0.11.321 Jul 2026Release notes
Open source →What changed
- Dashboard widgets (new): two admin-only widgets —
nrvault-secrets(active-secret count) andnrvault-audit-activity(14-day audit-event bar chart). Registered only whentypo3/cms-dashboardis installed (guarded, no hard dependency). (#212) - Dark mode: the vault backend module — including the code examples in the overview/help — now uses adaptive backend color tokens instead of pinned colors, fixing low-contrast rendering in the v14 dark scheme; dead CSS removed. (#213)
- Icons: full v14 3-color icon set including per-submodule icons, with v13 legacy tiles.
Additive/non-breaking 0.x patch so
^0.11consumers pick it up. - Dashboard widgets (new): two admin-only widgets —
-
v0.11.221 Jul 2026Release notes
Open source →What's in 0.11.2
Completes the 0.11.1 translation fix: copy-mode localization now also works for records with FlexForm vault fields.
Fixed
0.11.1 fixed the error thrown when translating a content element into another language in copy mode — but only for regular vault fields. The same error still occurred for records carrying vault secrets inside FlexForm fields (reported as a follow-up on #207). Both paths now work; this was the last occurrence of the underlying issue.
Drop-in upgrade: no configuration change, no database migration. Affects all versions up to and including 0.11.1.
Fixes #207 (#210). Thanks to @lradloff for reporting.
Installation
composer require netresearch/nr-vault
Publication status
- TER: extensions.typo3.org/extension/nr_vault — 0.11.2 verified
- Packagist: packagist.org/packages/netresearch/nr-vault — 0.11.2 verified
- Documentation: docs.typo3.org/p/netresearch/nr-vault/0.11/en-us/ — render verified
Security
All release artifacts are signed with Sigstore keyless signing.
Verify signatures
cosign verify-blob \ --bundle nr-vault-0.11.2.zip.sigstore.json \ --certificate-identity-regexp "https://github.com/netresearch/.*" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ nr-vault-0.11.2.zip
Verify checksums
sha256sum -c checksums.txt
Software Bill of Materials (SBOM)
SBOMs are provided in both SPDX and CycloneDX formats for supply chain transparency.
Release notes
Open source →Fixed
- Copy-mode translation TypeError also fixed for FlexForm vault fields
(#207, #210). The 0.11.1 fix covered only
DataHandlerHook; the samebool-typed$pasteUpdateparameter remained onFlexFormVaultHook::processCmdmap_postProcessand threw the identicalTypeErrorwhen translating records with FlexForm vault fields in copy mode. The parameter now acceptsbool|array— the last remainingbool-typed$pasteUpdatein the extension.
-
v0.11.120 Jul 2026Release notes
Open source →What's in 0.11.1
Translating content into another language in copy mode works again.
Fixed
Using the Translate button to copy a content element into another language failed with an error, and no translation was created. Copy-mode localization now completes.
Drop-in upgrade: no configuration change, no database migration. Affects all versions up to and including 0.11.0.
Fixes #207 (#208). Thanks to @lradloff for reporting.
Installation
composer require netresearch/nr-vault
Publication status
- TER: extensions.typo3.org/extension/nr_vault — 0.11.1 verified
- Packagist: packagist.org/packages/netresearch/nr-vault — 0.11.1 verified
- Documentation: docs.typo3.org/p/netresearch/nr-vault/0.11/en-us/ — render verified
Security
All release artifacts are signed with Sigstore keyless signing.
Verify signatures
cosign verify-blob \ --bundle nr-vault-0.11.1.zip.sigstore.json \ --certificate-identity-regexp "https://github.com/netresearch/.*" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ nr-vault-0.11.1.zip
Verify checksums
sha256sum -c checksums.txt
Software Bill of Materials (SBOM)
SBOMs are provided in both SPDX and CycloneDX formats for supply chain transparency.
Release notes
Open source →Fixed
DataHandlerHookTypeError when translating content in copy mode (#207, #208). TYPO3 core reassigns the$pasteUpdatehook argument from itsfalsedefault to an array on the localize / copy-to-language path, so thebooltype onprocessCmdmap_preProcess()andprocessCmdmap_postProcess()raised aTypeErrorbefore the command guard ran, breaking content-element translation via therecords/localizeAJAX endpoint. The parameter now acceptsbool|array, matching core's runtime contract.
-
v0.11.017 Jul 2026Release notes
Open source →Added
TechnicalActorContext::runAs()(#202, #205). Headless consumers (Symfony Messenger workers, scheduler tasks) can evaluate vault access as a named technical backend user without mutating$GLOBALS['BE_USER']: the scoped context is consulted directly byAccessControlServicewith the same user semantics an authenticated backend login gets (owner, admin, group ACLs with subgroup expansion). Actor validation is fail-closed (deleted, disabled, time-restricted and non-rootLevel users are rejected before the scope starts), nesting stacks innermost-wins, the identity is always restored on scope exit, and audit rows record the actor astechnical. Ambient behavior without an active scope is unchanged.
-
v0.10.217 Jul 2026Release notes
Open source →Security
- CLI access control reachable from queue workers and scheduler (#201). The
TYPO3 CLI bootstrap places an unauthenticated
CommandLineUserAuthenticationin$GLOBALS['BE_USER'];AccessControlServicetreated it as a backend user, which shadowed the configured CLI access rules — making them unreachable from Symfony Messenger workers and scheduler runs — and let its default uid 0 matchownerUid=0secrets, granting read and delete even with CLI access disabled. The unauthenticated placeholder is now routed to the CLI access rules; authenticated_cli_users keep their user-based semantics and web requests are unaffected.
- CLI access control reachable from queue workers and scheduler (#201). The
TYPO3 CLI bootstrap places an unauthenticated
-
v0.10.103 Jul 2026Release notes
Open source →Security
-
Legacy numeric IP-literal SSRF bypass closed (#192). curl's resolver accepts
inet_aton()forms — dword (2130706433), octal (0177.0.0.1), hex (0x7f.0.0.1) and partial-dot (127.1) — that all reach127.0.0.1, but PHP'sinet_pton()/FILTER_VALIDATE_IPreject them, so they slipped through the dangerous-IP guard as pseudo-hostnames (no DNS record, no pin) and curl derived the internal IP itself. Such forms are now rejected both inisHostAllowed()and in the request-time SSRF middleware unless the operator allowlists the exact literal; the canonical dotted-quad / IPv6 form is unaffected. -
Privileged secret ACL columns are now authorization-gated (#186). On the secret FormEngine write path, the
owner_uid,allowed_groups,write_groups,frontend_accessibleandscope_pidcolumns oftx_nrvault_secretcarried noexcludeflag and no admin gate, so a non-admin editor could widen a secret's ACL or reassign ownership (privilege escalation, CWE-639 / CWE-269). The write path now enforces owner/admin authorization on those privileged columns (also closes a minor in-memory cleartext exposure, CWE-316). -
Audit hash-chain tamper-evidence hardened (#185). Two forgery paths reachable by the documented in-scope database attacker (a principal with
UPDATE/DELETEontx_nrvault_audit_log) are closed: an epoch-downgrade that allowed the tail row to be rewritten under a keyless SHA-256 epoch because thehmac_key_epochwas not bound into any hashed payload (CWE-345 / CWE-757), and an attribution forgery. Both are now detected byverifyHashChain().
Fixed
-
The
vaultSecretfield description no longer renders twice on v14 (#189). TYPO3 v14'sAbstractFormElement::renderLabel()emits the TCAdescriptionitself (viarenderDescription()), soVaultSecretElement's own copy became a duplicate. The element now renders its own description only on v13, where the label does not — so it appears exactly once on both v13 and v14. -
The SSRF middleware no longer fatals without ext-curl (#192). It referenced the curl-only
CURLOPT_RESOLVEconstant unconditionally, so the first pinned request on a curl-less install raisedError: Undefined constant "CURLOPT_RESOLVE"— contradictingcreate()'s documented StreamHandler-fallback warning. The pin is now attached only whencurl_init()exists; the dangerous-IP rejections still run. -
Dual-stack hosts are reachable again from IPv6-less environments (#190). The DNS-rebinding defence pinned each resolved address as a separate
CURLOPT_RESOLVEentry, but curl keeps only the last entry perhost:port— so effectively only the final DNS record (typically the AAAA) was pinned. On hosts without IPv6 connectivity every request to a dual-stack host (e.g.api.github.com) failed withcURL error 7and no IPv4 fallback; all-IPv4 multi-record hosts silently lost their fallback addresses too. All safe resolved addresses now travel comma-joined in a single resolve entry (curl's multi-address form, curl ≥ 7.59), restoring curl's native cross-family/cross-address connect fallback while keeping the pin: every usable address is still one the defence resolved and vetted.
-
-
v0.10.011 Jun 2026Release notes
Open source →Added
- Per-instance request timeout on the secure HTTP client.
VaultHttpClientInterface::withTimeout(int $seconds)is a new immutable wither (likewithReason()): the override is baked into the hardened inner Guzzle client via the shared factory, so it applies to every send path — plain and authenticated — whileconnect_timeoutstays platform-managed. Non-positive values keep the platform default. Long-running upstream calls (large image generations) no longer die at the instance-wide HTTP timeout.
Fixed
- CLI and worker reads are classified as automated again. The TYPO3 CLI
bootstrap installs a
CommandLineUserAuthentication(aBackendUserAuthenticationsubclass), so the backend-user-first check inAccessControlService::getCurrentActorType()stamped every CLI/worker access asbackend. Analytics then counted 0 automated reads and flagged busy automation secrets "Automation-stale". CLI detection now runs first, and a latent constant-case fatal in the legacy CLI check was removed.
Changed
runTests.shdefaults to the upper supported PHP bound (8.5) instead of 8.2; CI pins its matrix explicitly and is unaffected.
- Per-instance request timeout on the secure HTTP client.
-
v0.9.010 Jun 2026Release notes
Open source →Added
- Per-secret encryption-algorithm marker. Each secret now records how it
was encrypted (
encryption_version2 + explicit algorithm in the newtx_nrvault_secret.encryption_algorithmcolumn). Decryption dispatches on the stored marker instead of re-deriving the algorithm from the decrypting host's CPU capabilities, so the same data decrypts identically on any PHP host and future algorithm migrations become possible. Legacy rows (version 1, no marker) keep decrypting byte-identically via the old host-derived path; value rotation upgrades them to version 2. New secrets default to XChaCha20-Poly1305;aes256gcmcan be pinned via the newencryptionAlgorithmextension setting (invalid or host-unavailable values fail loudly). - Audit-chain re-keying during master-key rotation. The tamper-evident
audit chain is HMAC-keyed from the master key;
vault:rotate-master-keynow re-keys the whole chain inside the same transaction as the DEK re-encryption (newAuditChainRekeyService, keyset-paginated for bounded memory), preserving per-row epochs and refusing to re-key a chain that does not verify under the current key. A second-rotation (epoch 2) functional test covers consecutive rotations end to end.
Changed
verifyHashChain()streams rows instead of materialising the whole audit log; rotation pre-flight on large installs no longer risks OOM.dek_nonce/value_noncecolumns widened varchar(24) → varchar(32): the base64 form of a 24-byte XChaCha20 nonce is 32 characters (latent truncation bug on the XChaCha20 path).encrypt()/decrypt()no longersodium_memzero()the master key — it is the provider's shared request-lifetime cache entry; the provider owns its lifecycle. Per-secret key material (DEK, MAC key, plaintext) is still wiped on every path, including exception paths.
Fixed
- Backend column migration built
table__column__{{uid}}identifiers whose literal braces failed validation — every backend-module column migration was rejected. - OAuth token cache key now includes the refresh-token secret identifier and the (order-normalised) additional parameters, preventing cross-audience token confusion between configurations that differ only in audience/resource/tenant.
- OAuth error messages additionally redact JSON-body and quoted-prose echoes
of
client_secret,refresh_token, andaccess_token. OAuthConfigrejects unknown grant types and arefresh_tokengrant without a refresh-token secret at construction time.- Secure HTTP client rejects non-object JSON request bodies up front instead of silently dropping the payload (or fataling on scalars).
- UUID v7 generation now sets all 14 random bits of the variant field.
- Per-secret encryption-algorithm marker. Each secret now records how it
was encrypted (
-
v0.8.009 Jun 2026Release notes
Open source →Added
prefixoption forwithAuthentication()Header placement — prepends an auth scheme/prefix to the secret before injection, so non-BearerAuthorization: <scheme> <secret>schemes can use the audited, memory-scrubbed secure HTTP client instead of building the header manually with a plaintext key. TYPO3 FAL providers useKey, DeepL usesDeepL-Auth-Key. The combined prefixed value is zeroed alongside the raw secret; the no-prefix path keeps a single secret buffer (no extra allocation). The option is threaded throughwithReason(); the OAuth builder leaves it unset.
Documentation
Api.rst: documented theprefixoption and added a custom-Authorization-scheme example; corrected the DeepL usage example, which previously documentedBearer(a scheme DeepL never used).
-
v0.7.002 Jun 2026Release notes
Open source →Changed
- Require TYPO3 v14.3 LTS instead of v14.0 for the v14 line
(
typo3/cms-* : ^13.4 || ^14.3). 14.0/14.1/14.2 were unsupported sprint releases; 14.3 is the LTS. The CI matrix, README, and bug-report template are aligned to the same constraint. (ext_emconf.phpkeeps its coarse13.4.0-14.99.99range — a single continuous range cannot express the^13.4 || ^14.3gap. Because nr-vault requires a composer-based TYPO3 installation,composer.jsonis the authoritative version constraint.)
Fixed
SecretRepository::findIdentifiers()now skips non-string identifier rows instead of coercing them to an empty string. A driver/schema anomaly that returned a non-stringidentifierpreviously injected a bogus empty identifier into list views and rotation loops; such rows are now dropped (an empty identifier is unreachable for valid data).
Added
- CLI documentation drift guard (
Tests/scripts/check-cli-docs.php, wired intocomposer ciasci:test:php:doc-cli). It verifies every documentedvault:*example acrossREADME.mdand the wholeDocumentation/tree against the command classes — unknown commands, unknown options, and excess positional arguments fail the build. Backslash line-continuations are joined so options on continuation lines are checked, and both#[AsCommand(name: …)]and positional#[AsCommand(…)]forms are recognised. .gitattributes(export-ignoredev-only paths for smaller composer/TER packages),.ddev/.gitignore, and a canonical.ddev/commands/web/setupentry point.- Vault Analytics backend module — a new "Analytics" submodule under the Vault module showing usage KPIs (total / expired / frontend-accessible / never-rotated secrets and reads in the selected window) and, most usefully, a redaction-candidates table that flags secrets which appear unused and may be safe to remove. Candidates are graded into delete-candidates (never read, not read for a configurable period, or expired) and review-candidates (revealed manually but never read by automation; never rotated). A time-window selector (30/90/180/365 days) drives the usage signals, and each flagged secret links straight to its edit view. Thresholds are configurable under the extension's Analytics settings.
vault:seed-democommand — populates a development instance with realistic, historic demo secrets and a matching audit-log history so the Analytics module has lifelike data to show. Idempotent, refuses to run in Production, and reseeds with--force.
Documentation
- README CLI reference expanded from 5 to all 12
vault:*commands with corrected argument signatures (vault:store --value=…,vault:audit --since=…). - Corrected
vault:*examples across the documentation that drifted from the actual command signatures:vault:storevalue via--value/--metadata(not--description/--context/--expiresor a positional),vault:audit--since/--until(not--days),vault:migrate-fieldpositional<table> <field>(not--table/--field),vault:rotate-master-key--confirm/--new-key, the fullvault:auditoption reference, andtx_vault_secret→tx_nrvault_secret. - Documented the dev-only
vault:seed-democommand in the CLI reference.
- Require TYPO3 v14.3 LTS instead of v14.0 for the v14 line
(
-
v0.6.131 May 2026Release notes
Open source →Fixed
SecureHttpClientFactory's request-time SSRF middleware now honours literalallowed_hostsentries. In 0.6.0 the per-request DNS-rebinding middleware rejected every host that resolved into a private/loopback range regardless ofallowed_hosts, so the documented on-prem opt-in ("LITERAL allowlist entries can opt back in") only applied to theisHostAllowed()gate, not to clients built bycreate(). Consumers that reach an internal/self-hosted endpoint through acreate()client — e.g. an LLM provider talking to a local Ollama at a private-resolving hostname — were silently blocked with no way to opt back in. The middleware now applies the same literal-allowlist check asisHostAllowed(); an allowlisted host whose DNS answer is private is pinned viaCURLOPT_RESOLVEinstead of rejected, so rebinding to a different address stays blocked. Wildcardallowed_hostsentries still never bypass the guard.
-
v0.6.031 May 2026Release notes
Open source →Security
- VaultService::store() now requires authorization. Previously any
backend user with write rights on a host table carrying a vault field
could create or overwrite arbitrary vault identifiers, bypassing the
per-secret ACL.
store()now distinguishes new vs. update and callscanCreate()/canWrite($existing); denied paths emit anaccess_deniedaudit entry and throwAccessDeniedException. Non-admin backend actors that attempt to set or changeowner_uidare silently coerced to the default (existing owner on update, current actor on create). CLI / scheduler / API actors retain full control. #[SensitiveParameter]rolled out across the crypto / DTO / audit boundaries (0 → 35 occurrences). Plaintext secrets, master keys, DEKs, OAuth tokens, refresh tokens and vault tokens no longer surface in stack traces, error handlers, monolog payloads, orvar_dump(). Applied toEncryptionService(Interface),MasterKeyProviderInterfaceand all three providers,VaultService(Interface)::store/rotate,AuditLogServiceInterface::log$hashBefore/$hashAfter,PendingSecret::$value,FlexFormPendingSecret::$value,VaultServerConfig::$token, and the privateencryptWithKey/decryptWithKeylocals.- SSRF defence-in-depth in
SecureHttpClientFactory::isHostAllowed(). Regardless ofallowed_hostsconfiguration, IP literals and DNS-resolved hostnames pointing into private / RFC1918 / RFC6598 CGNAT / loopback / link-local / cloud-metadata (169.254.169.254) / multicast / class-E / IPv6 ULA / IPv6 link-local / IPv6 multicast / NAT64 / discard ranges are rejected. The check normaliseshost:port,[ipv6]:port, bare::1(whichparse_urlmisparses), bracketed[2001:db8::1], trailing dots, mixed case, and whitespace. LITERAL allowlist entries can opt back in for on-prem deployments (e.g.'10.0.0.42'); wildcards (*.example.com) cannot — a wildcard owner could otherwise pivot via DNS rebinding. The check resolves hostnames at filter time; full DNS-rebind protection viaCURLOPT_RESOLVEpinning is a follow-up. - Master-key rotation is now audit-logged.
VaultRotateMasterKeyCommandemitsmaster_key_rotate_startbefore the re-encryption loop andmaster_key_rotate_end(success or failure) afterwards, both with a sanitised reason — error messages are scrubbed of libsodium internals before persistence. auditReadsfilesystem-only override.$TYPO3_CONF_VARS[SYS][nrVault][auditReads], if set, takes precedence over the BE-toggleable extension configuration. Pin the value inLocalConfiguration.php/additional.phpon production so a compromised admin cannot silence read logging via the BE Settings module.Typo3MasterKeyProviderentropy gate. The default master-key provider now rejects TYPO3encryptionKeyvalues shorter than 32 characters (would otherwise produce a weak HKDF output). Add a request-lifetime static cache (ADR-020) so HKDF runs once per request instead of on every crypto operation.FileMasterKeyProviderchmod race closed.storeMasterKey()wraps thefile_put_contents()call inumask(0o077)so the file is created0600, thenchmod 0400tightens further — no more world-readable window under permissive umasks.
Changed
MasterKeyProviderInterface::storeMasterKey(),EncryptionServiceInterface::encrypt/decrypt/reEncryptDek/ calculateChecksum(),VaultServiceInterface::store/rotate(), andAuditLogServiceInterface::log()now annotate sensitive parameters with#[SensitiveParameter]. This is a signature change visible to downstream implementers: PHP does not enforce the attribute on implementations, but implementers should mirror it on their overrides to keep the protection.AccessControlServiceInterfacegainsisCurrentActorAdmin(): bool. New method delegating BE-admin check to the service instead of$GLOBALS['BE_USER']lookup. Returnsfalsefor CLI / scheduler / API actor types — callers that need to bypass admin gates must handle actor type explicitly.- Pre-commit hook moves PHPStan from pre-push to pre-commit. Type
errors are now caught at commit time on top of the existing
php-cs-fixer+ lint actions. Pre-push retains unit-test execution. Note: captainhook's installer does not currently support worktree gitdirs (git clone --bare+worktree add); operators in worktrees need to runvendor/bin/captainhook install -g <gitdir>manually.
- VaultService::store() now requires authorization. Previously any
backend user with write rights on a host table carrying a vault field
could create or overwrite arbitrary vault identifiers, bypassing the
per-secret ACL.
-
v0.5.021 Apr 2026Release notes
Open source →Added
- OAuth fallback:
OAuthTokenManager::fetchTokenWithFallback()falls back toclient_credentialswhen a storedrefresh_tokenis rejected with HTTP 400/401 +invalid_grant/invalid_token. 5xx / 429 /invalid_clienterrors re-throw so outages are not masked. Both the failed refresh and the fallback are audit-logged. - Internationalization: Translate all backend module templates to use XLF translation keys
- Help Page: Add help page with docheader tab menu to backend module
- Security: HMAC-SHA256 keyed audit hash chain (ADR-023)
- CLI:
vault:audit-migrate-hmaccommand for migrating legacy SHA-256 audit entries
Security
- AccessControlService now denies vault access to backend users
whose
disableflag is set, even when a stale session somehow reaches the vault layer. Any non-zero integer / numeric-string value is treated as disabled (matches TYPO3 DataHandler semantics). - AccessControlService filters stale group IDs from user sessions
against the live
be_groupstable before intersecting with a secret'sallowedGroups. A deleted group whose UID still lingers in a session no longer grants access. Lookup is cached per request. - AuditLogService::verifyHashChain() detects UID gaps in the
stored chain — an attacker who deletes entry N and patches
entry N+1's
previous_hashcan no longer hide the deletion. NewmissingUids/missingUidCountfields on the verification result.
Changed
- Test coverage driver: PCOV → Xdebug. PCOV only emits line coverage; Xdebug adds branch and path coverage, which Infection mutation testing and audit-flow analysis both need. ~2× CI runtime cost accepted for the signal quality.
- Testing pyramid overhaul: unit tests 1298 → 1705 (assertions
3045 → 6949), fuzz tests 1 file → 10 files (1514 methods,
2255 assertions), functional tests 12 files → 24 files, E2E specs
8 → 14 including a new
Tests/E2E/security/bundle (XSS, audit tamper, CSRF, cookie attributes, full CRUD lifecycle). SeeTests/E2E/USER_PATHWAY_COVERAGE.mdfor the full pathway audit matrix. - Infection mutation testing enabled end-to-end after years of
being blocked by PHPUnit 12's
failOnWarning=true. First measured baseline MSI: 72.35 % (thresholds set to 72 / 72 with a documented ratchet plan toward 85 / 95 by Q4). SeeDocumentation/Developer/mutation-baseline.md. - CI: reusable workflows pinned to commit SHAs (no more floating
@main), concurrency block cancels stale PR runs, on-demand mutation testing via therun-mutationPR label. - Dev dependencies consolidated via
netresearch/typo3-ci-workflowsmeta-package: 14 direct require-dev entries reduced to 4 (mikey179/vfsstream,netresearch/typo3-ci-workflows,roave/security-advisories,typo3/cms-scheduler). The meta-package also bringsphpstan/phpstan-deprecation-rules,saschaegerer/phpstan-typo3,nikic/php-fuzzer,overtrue/phplint, anddg/bypass-finalsthat we did not previously have. - Test infrastructure: extract
AbstractVaultFunctionalTestCase,TcaSchemaMockTrait,BackendUserMockTrait,SecretFixtureBuilder, and a projectTests/Unit/TestCase.phpbase class. 100 tests migrated. Architecture check script enforces the base on new unit tests. - PHPStan: add strict-rules + deprecation-rules + phpunit + saschaegerer/phpstan-typo3 extensions (via meta-package auto- installer). Baseline refreshed.
- runTests.sh: dual
SIGINT/SIGTERM/EXITtrap, collision- resistant container suffix, Alpine base bumped 3.8 → 3.20, newunitCoveragePathsuite. - Performance: Fix N+1 queries in
VaultService::list() - Performance: Optimize frontend rendering and database operations
- Refactoring: Extract duplicated
generateUuidandlooksLikeVaultIdentifiermethods
Fixed
- CLI:
vault:migrate-field --uid-field=''now fails fast with a clear error instead of emitting an "Undefined array key" warning mid-batch. - OAuthException now carries
httpStatusandoauthError(parsed from the RFC 6749 §5.2 error body) so callers can distinguish refresh-token rejection from server outage. - Symfony 7.4: migrate
Application::add()→addCommand()across command tests (eliminates a deprecation warning). - DOM-XSS: eliminate
innerHTMLsinks in frontend JS and insecure test randomness - Security: Address critical and high-severity security findings
- Accessibility: Improve frontend accessibility and error handling
- Secret Reveal: Fix
SecretReveal.jsGET to POST andEnvironmentMasterKeyProvidercopy-on-write bug - Gitleaks: Allowlist test fixtures and docs in gitleaks config
- OAuth fallback:
-
v0.4.607 Mar 2026Release notes
Open source →Added
- Help Page: Add help page with docheader tab menu to backend module
-
v0.4.506 Mar 2026Release notes
Open source →Fixed
- TCA Element: Implement AJAX reveal and copy for vault secret TCA element
-
v0.4.406 Mar 2026Release notes
Open source →Fixed
- VaultSecretElement: Fix missing label, broken form submission, and silent errors
- CI: Add
merge_grouptrigger to CI workflow - README: Correct broken badges
Changed
- Repo Hygiene: Clean up files that should be gitignored
-
v0.4.302 Mar 2026Release notes
Open source →Fixed
- TYPO3 v13: Add Overview submodule for v13 module overview compatibility
-
v0.4.201 Mar 2026Release notes
Open source →Fixed
- TYPO3 v13: Use integer values for
f:be.infoboxstate for v13 compatibility
- TYPO3 v13: Use integer values for
-
v0.4.101 Mar 2026Release notes
Open source →Fixed
- TYPO3 v13: Use standard TYPO3 XLF label keys for backend modules
- TYPO3 v13: Use
toolsparent module for v13 compatibility - Documentation: Fix documentation issues found by analysis
Changed
- CI: Consolidate caller workflows into 4 grouped files
-
v0.4.028 Feb 2026Release notes
Open source →Added
- Compatibility: Widen support to PHP 8.2+ and TYPO3 v13.4+
- CI: Enable coverage uploads to Codecov
- CI: Expand test matrix to PHP 8.2-8.5 and TYPO3 v13.4/v14
- CodeQL: Add CodeQL security scanning for actions and JavaScript
Changed
- CI: Migrate to centralized reusable workflows
- CI: Harmonize composer script naming to
ci:test:php:*convention - Build: Move build configs (
phpunit.xml,phpstan-baseline.neon) intoBuild/ - Licensing: Add SPDX copyright and license headers to all PHP files
- OpenSSF: Improve Scorecard compliance
Fixed
- PHP 8.2: Remove
#[Override], typed class constants, andarray_any()for PHP 8.2 compatibility - TYPO3 v13: Replace TYPO3 v14-only APIs with v13-compatible equivalents
- TYPO3 v13: Use
LLL:EXT:module labels for v13 compatibility - PHP 8.5: Fix MockObject property declarations for PHP 8.5 compatibility
- i18n: Localize user-facing hardcoded strings in controllers
- CI: Fix SLSA provenance generation and Renovate auto-merge configuration
-
v0.3.126 Jan 2026Release notes
Open source →Added
- Documentation: Add Secure Outbound HTTP Client PRD and ADRs
- CI: Add dedicated fuzzing workflow for OpenSSF Scorecard
Changed
- Code of Conduct: Update to Contributor Covenant v3.0 and standardize contact methods
Fixed
- Security: Fix scorecard workflow permissions for branch protection check
- CI: Use
workflow_runtrigger for SLSA provenance generation - OpenSSF: Improve Scorecard token-permissions and pinned-dependencies
-
v0.3.011 Jan 2026Release notes
Open source →Added
- CI: Add TER upload to release workflow
- Testing: Enhance
runTests.shwith mock OAuth, E2E DDEV support, and parallel tests - Testing: Add coverage and E2E test suites to
runTests.sh - Testing: Support
MOCK_OAUTH_URLenv var in OAuth integration tests - Playwright: Update to Playwright 1.57.0 with parallel execution
Changed
- Type Safety: Replace shaped arrays with typed DTOs throughout codebase
- Performance: Enable opcache CLI and JIT for faster test execution
- Performance: Enable parallel execution for php-cs-fixer
- Build: Simplify Makefile with comprehensive test commands
Fixed
- PHPStan: Add type guards and annotations for PHPStan level 10
- Tests: Update functional tests for DTO property access
- PHPUnit 12: Add
AllowMockObjectsWithoutExpectationsfor PHPUnit 12 - Codecov: Improve integration with verification step
-
v0.2.009 Jan 2026Release notes
Open source →Added
- Documentation: Document all master key options in Installation
- Documentation: Add backend module screenshots
- CI: Add SLSA provenance workflow and badge
- CI: Add PR quality gates for Code-Review scorecard
- Badges: Add Contributor Covenant badge
Changed
- Type Safety: Replace array returns with typed DTOs
- Documentation: Improve introduction with compelling value proposition
Fixed
- CI: Remove duplicate Scorecard job from
security.yml - DDEV: Resolve
network_modeconflict in mock-oauth-router
-
v0.1.108 Jan 2026Release notes
Open source →Added
- Testing: Add comprehensive unit tests to reach 80% coverage
- Testing: Add OAuth2 integration tests with mock server
- Testing: Add XChaCha20 encryption tests for algorithm coverage
- Testing: Add functional tests for repositories and services
- CI: Add OpenSSF Scorecard workflow
- CI: Add auto-merge workflow for dependency PRs
- Badges: Add OpenSSF Scorecard, Best Practices, and Codecov badges
Changed
- Supply Chain: Update cosign to use bundle format for signing
- OpenSSF: Improve Scorecard compliance
- Documentation: Clarify external vault adapters are planned, not implemented
Fixed
- Tests: Use SQLite-compatible SQL syntax in functional tests
- Tests: Resolve test failures and add interfaces for final class mocking
-
v0.1.005 Jan 2026Release notes
Open source →Added
- Core Vault Service: Secure secrets storage with CRUD operations
- Envelope Encryption: AES-256-GCM encryption with per-secret Data Encryption Keys (DEK)
- Master Key Management: Support for file-based, environment variable, and derived master keys
- Access Control: Backend user and group-based permission system
- Context-based Scoping: Organize secrets by context (e.g., "payment", "email")
- Audit Logging: Tamper-evident hash chain for all secret operations
- CLI Commands: Command-line tools for secret management and key rotation
- Backend Module: TYPO3 backend interface for secret management
- TCA Integration: Custom
vaultSecretrenderType for TCA fields - FlexForm Support: Vault secrets in FlexForm configurations
- Vault HTTP Client: Make authenticated API calls without exposing secrets
- OAuth 2.0 Support: Token management with automatic refresh
- Secret Versioning: Track secret changes with version history
- Expiration Support: Optional expiration dates for secrets
- Memory Safety: Automatic wiping of sensitive data with
sodium_memzero()
Security
- Envelope encryption prevents master key exposure during normal operations
- Per-secret DEKs limit blast radius of key compromise
- Integrity verification with checksums on encrypted data
- Secure random nonce generation for each encryption operation
- Backend user group-based access control
- Audit trail with tamper-evident hash chain
Technical
- PHP 8.2+ required
- TYPO3 v13.4 / v14 compatible
- PER Coding Style (latest)
- PHPStan level 10 (maximum)
- PHPat architecture tests
- Mutation testing with Infection
- Readonly classes and properties throughout
- Constructor property promotion
- Modern PHP 8.x patterns (match, named arguments, attributes)