Control mapping 

Implemented controls mapped to BSI IT-Grundschutz modules and OWASP ASVS chapters, each with an implementation pointer and an evidence source.

Cryptography 

BSI IT-Grundschutz: CON.1 (Kryptokonzept). OWASP ASVS v4: chapter 6 (Stored Cryptography), chapter 8 (Data Protection).

Control Implementation Evidence source
Secrets encrypted at rest with authenticated encryption EncryptionService::encrypt() — XChaCha20-Poly1305 or AES-256-GCM, identifier as associated data tx_nrvault_secret contains no plaintext column; Envelope scheme
Per-secret key separation (bounded blast radius) One DEK per secret, wrapped by the master key (encrypted_dek, dek_nonce) Schema; ADR-002: Envelope encryption
Master key never stored with the ciphertext Typo3MasterKeyProvider , FileMasterKeyProvider , EnvironmentMasterKeyProvider (and transit) masterKeyProvider setting; Key custody
Domain-separated key derivation hash_hkdf() with three distinct info strings HKDF usages
Recorded, not inferred, algorithm selection encryption_version + encryption_algorithm markers; unknown marker is a hard error Algorithm agility
Cryptographically secure randomness for keys and nonces random_bytes() ; fresh nonce per operation and per re-wrap Key and nonce lengths
Constant-time comparison of secrets and integrity tags hash_equals() in chain verification, anchor comparison and token handling Constant-time comparison
Minimised plaintext lifetime in memory sodium_memzero() on success and in finally; #[SensitiveParameter] Memory scrubbing policy
Key rotation without plaintext exposure EncryptionService::reEncryptDek() — DEK layer only vault:rotate-master-key output; Key rotation

Access control and authorization 

BSI IT-Grundschutz: ORP.4 (Identitäts- und Berechtigungsmanagement). OWASP ASVS v4: chapter 2 (Authentication), chapter 4 (Access Control).

Control Implementation Evidence source
Authentication required for every vault surface TYPO3 backend authentication; frontend requests hold no operation permission AccessControlService::isGranted() ; Frontend and page-cache caveats
Least privilege by operation VaultPermission — ten distinct permissions as TYPO3 custom permission options be_groups.custom_options; Operation permissions
Separation of machine consumption from human disclosure secret.use and secret.reveal, neither implying the other Permission table; Verification procedures
Separation of duties for privileged operations Distinct master_key.rotate, secret.manage_policy, vault.configure, audit.view / audit.export Group grants per backend group
Object-level authorization independent of operation permission Per-secret owner / group tiers via canRead() / canWrite() / canDelete() ADR-005: Access control
Server-side enforcement at every entry point Operation permissions are enforced centrally in VaultService (store() requires secret.create / secret.rotate and — for access-policy changes — secret.manage_policy; rotate() requires secret.rotate; delete() requires secret.delete), so DataHandler/FormEngine requests and programmatic callers face the same gate as the module controllers. Controllers re-assert the permissions as defense-in-depth. SecretTcaHook does so too, and is the sole enforcement point for one case the service cannot see: creating a tx_nrvault_secret record without a value never calls store(), so the hook asserts secret.create in processDatamap_preProcessFieldArray() and refuses the record before it is inserted. VaultService::assertOperationGranted() ; SecretTcaHook::isCreationGranted() ; route configuration plus controller code
No privileged short-circuit in the grant lookup Grant evaluation deliberately avoids BackendUserAuthentication::check() , which returns true unconditionally for admins hasCustomPermissionOption()
Administrative override is a single, withdrawable seam adminBypassActive() ; disableAdminOverride in the hardened profile, pinnable outside the backend vault:break-glass --status reporting adminOverrideDisabledEffective
Emergency access is time-boxed, justified and evidenced BreakGlassService — mandatory reason, TTL clamped to 1–60 minutes, audit written before the grant break_glass_activated / break_glass_deactivated rows; Break-glass mode
Disabled accounts cannot act on a stale session Defence-in-depth disable checks in AccessControlService and BreakGlassService Code review
Attributable impersonation for headless code TechnicalActorContext::runAs() — validates the target user, restores scope on exceptions, never mutates $GLOBALS['BE_USER'] actor_type = 'technical' audit rows; ADR-029: Scoped technical-actor identity for headless use
Unattended CLI access closed by default allowCliAccess defaults to 0 Configuration; Configuration
Frontend placeholder resolution restricted to published identifiers FrontendPlaceholderPolicy — a %vault(id)% placeholder resolves only if the identifier was published from an admin-only source (TypoScript setup, site configuration, frontendResolvableIdentifiers, or an explicit allowIdentifier() grant). frontend_accessible alone is no longer sufficient, and the rule is strict on the CLI too unless frontendPlaceholderLegacyCli opts out ADR-035: Per-request allow-set of frontend-resolvable identifiers
Record copy and delete fail closed across vault fields DataHandlerHook — a record delete asserts every vault field's delete gate before removing the first secret and is cancelled outright if any fails; a copy that cannot clone every secret deletes the ones it made and blanks every vault field. The control is a preflight plus best-effort compensation, not atomicity: a failure the preflight cannot predict leaves the secrets already deleted unrestorable, a failed rollback delete leaves an orphaned clone, and a failed blanking leaves the copy still referencing the source record's identifiers. The record is preserved either way, the delete and blanking residuals are named to the editor, and every failure — the orphaned clone included — is logged under a correlation reference TCA integration; ADR-018: FlexForm secret lifecycle management
CLI grant narrowed to low-risk operations cliAllowedOperations defaults to secret.use,secret.create,secret.rotate; reveal, delete, audit export, master-key rotation and vault configuration must be added explicitly vault:doctor finding cli.allowed_operations

Logging and audit 

BSI IT-Grundschutz: OPS.1.1.5 (Protokollierung), DER.1 (Detektion von sicherheitsrelevanten Ereignissen). OWASP ASVS v4: chapter 7 (Error Handling and Logging).

Control Implementation Evidence source
Every access, mutation and denial is logged AuditLogService::log() on read, create, update, rotate, delete, metadata change and access_denied tx_nrvault_audit_log; ADR-006: Audit logging
Log entry precedes the effect where order matters Break-glass audits before granting; delete/store compensate a failed audit write by rolling the data change back. The FormEngine/DataHandler paths share this contract: the tx_nrvault_secret delete command runs through VaultService::delete() , and a metadata change (or value-less record creation) whose audit write fails is reverted by SecretTcaHook — no mutation persists without its audit entry. BreakGlassService::activate() , VaultService::delete() , SecretTcaHook
Complete attribution actor_uid, actor_type, actor_username, actor_role, ip_address, user_agent, request_id Schema; bound into the hash from epoch 3
Only known actions can enter the chain AuditAction::tryFrom() rejects unknown actions loudly Code review
Tamper detection on stored entries HMAC-SHA256 hash chain; verification with hash_equals() vault:audit-verify; ADR-023: Audit hash chain HMAC consideration
Adversarial resistance against a database writer Chain key derived from the master key, which the database does not contain What the log proves — and against whom
Deletion detection uid-sequence gap analysis → UID_GAP Verification output
Algorithm-downgrade detection Per-row epoch comparison, chain-level epoch floor, and the epoch bound into the hash from epoch 3 EPOCH_DOWNGRADE findings
Reset detection without external evidence AuditChainAnchorStore — MAC-signed tip in sys_registry under a key derived from the master key, so truncating the audit table alone leaves an anchor naming a row that is gone. auditAnchorRequired promotes a missing anchor from warning to critical from a configuration file vault:doctor finding audit.db_anchor; vault:audit --verify tip-anchor line; ADR-034: Audit chain tip anchor
Reset detection through external evidence ChainTipAnchorService — shrinkage, substitution and epoch regression against a published anchor TABLE_RESET findings; anchor file / SIEM records
Log copies outside the protected store Syslog (RFC 5424), append-only NDJSON, webhook — fan-out after commit, contained per sink Sinks
Availability of the audit pipeline is observable Persisted per-sink delivery state (last success / last failure / consecutive failures, sys_registry) surfaced as audit.sink_state.<sink> findings; active end-to-end verification via vault:doctor --active-probes (audit.sink_probe.<sink>); process-local failure counters; SINK_FAILURE and NO_EXTERNAL_SINK reason codes Sink failure counters
Machine-readable alerting AuditIntegrityAlertEvent with stable reason codes and isTamperEvidence() AuditIntegrityAlertEvent
Scheduled, unattended detection AuditAnchorTask , AuditVerifyTask Scheduler records; Scheduler tasks
No secrets in logs or exceptions [REDACTED] placeholders; token redaction in transport errors; response bodies not surfaced Code review; SecretRedactor

Data protection and secret handling 

BSI IT-Grundschutz: CON.1 (Kryptokonzept), CON.3 (Datensicherungskonzept). OWASP ASVS v4: chapter 8 (Data Protection).

Control Implementation Evidence source
Disclosure responses are never cached Cache-Control: no-store and Pragma: no-cache on every reveal response, success and error alike AjaxController::withNoStore() ; HTTP response inspection
Client-side exposure is bounded startRevealLifecycle() — 30 s auto-hide, wipe on visibilitychange and pagehide vault-reveal-lifecycle.js; Revealed plaintext in the browser cannot be zeroized
No client-side secret cache Every reveal re-hits vault_reveal, so every reveal is audited Two reveals produce two audit rows
Clipboard exposure removed where it cannot be controlled copyAllowed = false under the hardened profile; no copy button Reveal response payload
Frontend disclosure is a property of the secret, not the visitor frontend_accessible; frontend requests hold no operation permission Frontend and page-cache caveats
Change detection without a plaintext oracle value_checksum is a keyed MAC over the ciphertext, keyed per secret from the DEK The change-detection token
Recoverability of encrypted data Documented separation of database and key-material backups, with a probe-decrypt verification step Backup and restore
Detection of plaintext secrets elsewhere in the installation SecretDetectionService , vault:scan Scan output

Configuration, fail-closed behaviour and supply chain 

BSI IT-Grundschutz: CON.8 (Software-Entwicklung), OPS.1.1.5 (Protokollierung), DER.2.1 (Behandlung von Sicherheitsvorfällen). OWASP ASVS v4: chapter 10 (Malicious Code), chapter 14 (Configuration).

Control Implementation Evidence source
Security policy is enforced in code, not documentation SecurityProfile consulted by provider selection, access control and audit verification Exact technical differences
Fail closed on misconfiguration Unknown profile → 1753900001; forbidden provider → 1753900002; no hardened fallback or auto-detection MasterKeyProviderFactory ; Verification procedures
Security-critical settings can be placed out of admin reach $GLOBALS['TYPO3_CONF_VARS']['SYS']['nrVault'] pins config/system/additional.php; --status output
Deployment-time policy gate vault:doctor --profile=hardened; exit 0 pass, 1 warnings, 2 critical Pipeline logs and --format=json artefact
Outbound requests hardened against SSRF and DNS rebinding SecureHttpClientFactory ; scheme restricted to http/https; private targets need allowed_hosts ADR-026: DNS-rebinding defence via CURLOPT_RESOLVE
Static analysis, security scanning and code review in CI Shared security.yml (composer audit + Opengrep SAST on registry rules, blocking on WARNING+), codeql.yml, license-check.yml, dependency-review.yml, fuzz.yml. The in-repo semgrep.yml is not referenced by any workflow — see Development and release evidence .github/workflows/checks.yml; Evidence collection
Secret scanning on every pull request gitleaks job against the in-repo .gitleaks.toml, reporting to GitHub code scanning .github/workflows/checks.yml
Workflow-definition auditing zizmor job over .github/workflows/, configured by .github/zizmor.yml .github/workflows/checks.yml
Broad compatibility test matrix PHP 8.2–8.5 × TYPO3 ^13.4 and ^14.3, unit and functional, coverage uploaded .github/workflows/ci.yml
Release provenance actions/attest-build-provenance over the .zip and .tar.gz — ungated. Requested at the call site with id-token: write + attestations: write .github/workflows/release.yml; verify with gh attestation verify
Release evidence is itself tamper-evident actions/attest-build-provenance over release-evidence-<version>.tar.gz, in the bundle job with id-token: write + attestations: write. The attestation outlives the 90-day run-artifact retention .github/workflows/release-evidence.yml; verify with `gh attestation verify release-evidence-<version>.tar.gz --repo netresearch/t3x-nr-vault`
Software bill of materials Two SBOMs per tagged release via anchore/sbom-action: <prefix>-<version>.sbom.spdx.json (SPDX) and .sbom.cdx.json (CycloneDX). Delivered by the shared reusable; include-sbom defaults true and this repository's release.yml does not opt out Release assets *.sbom.spdx.json / *.sbom.cdx.json
Artefact signing Keyless Sigstore signing via sigstore/cosign-installer and cosign sign-blob --bundle, over every file in dist/, producing <file>.sigstore.json (that extension is chosen so OpenSSF Scorecard recognises the artefacts as signed). sign-artifacts defaults true; not opted out here Release assets *.sigstore.json; verify with cosign verify-blob --bundle
Artefact integrity checksums.txtsha256sum over the whole dist/ directory, including the SBOMs Release asset; verify with sha256sum -c checksums.txt
Third-party actions pinned by digest Every third-party uses: in the release path is pinned to a full commit SHA (sbom-action, cosign-installer, attest-build-provenance, upload-artifact). Netresearch-owned reusables intentionally stay on @main so upstream fixes propagate Reusable workflow source; Development and release evidence
Continuous security posture measurement OpenSSF Scorecard on schedule and on default-branch pushes .github/workflows/checks.yml; Scorecard results
Documented incident and emergency procedures Runbooks for exposure, tampering and break-glass review Incident response

Declared gaps 

Stated here so an assessment does not have to discover them, and does not credit them as controls.

Gap Status
No protection against a compromised PHP process By design. Architecturally unfixable in-process; the alternative is recorded in ADR-016: Sidecar daemon option and is not implemented.
No HSM or cloud-KMS integration Only indirectly, via HashiCorp Vault Transit. No provider exists for AWS KMS, Azure Key Vault or GCP KMS.
Audit chain is tamper-evident, not tamper-proof By design. Detection only; prevention would require append-only external storage, which is an operator responsibility.
Metadata and the audit trail are unencrypted Accepted. Identifiers, ownership, timestamps and the access history are readable to anyone with database access; audit.view / audit.export gate the application path only.
vault:delete is a soft delete Accepted. The ciphertext row remains until removed at the database level; crypto-erasure is the disposal mechanism — Decommissioning.
Revealed plaintext in the browser cannot be zeroized Accepted, mitigated by a bounded exposure window and by disabling clipboard copy under the hardened profile.
runAs() is not an authentication boundary Explicitly documented as such. Any extension code can act as any enabled backend user; the control provided is attribution, not prevention.
Supply-chain controls are delivered by a shared reusable pinned to @main, not by in-repo workflow steps Accepted, by netresearch convention. SBOM generation, Cosign signing and build-provenance attestation live in netresearch/typo3-ci-workflows (referenced at @main so upstream fixes propagate), not in this repository's release.yml. An assessment verifies them against the resolved reusable and the produced release assets, not against this repository's workflow files alone.