Loading
feat(cmd): add the encryption tier seam and skip path
What
Step 1 of the S04-C encryption wiring plan: the wireEncryption tier's boot seam and skip path.
cmd/artifact-registry/wire_encryption.go(new): skip-iff-absent (Config.Encryption.Local != nilis the presence predicate), the present-block/nil-client boot failure, refusing production factory defaults behind the named interim sentinel, and the skip-outcome boot log.cmd/artifact-registry/wire.go: the two interface-typedbootDepsfactories, the wiring-bundle enablement-signal fields (keyManager,rowEncryptor), and the tier call afterwireDatabase, outside theclient != nilgate.- The present-block path boots to
errEncryptionNotWired, pinned byTestWireEncryption_PresentBlockNotYetWiredso Step 2 flips exactly that one test. Inert on merge: no fixture, example, or boot test carries anencryptionblock.
Spec coverage
Spec: docs/specs/S04-c-column-level-encryption-wiring.md
Acceptance criteria
| # | Criterion | Tests |
|---|---|---|
| AC-1 | Absent block: boot completes via the production entry point, zero secret reads, nothing constructed | TestWireEncryption_SkipsWhenAbsent, TestWireEncryption_SkipsWhenAbsentWithClient, TestWireEncryption_SkipLeavesEnablementSignalNil |
| AC-2 | Single loader TTL to both constructors | Not in this MR — owned by Step 2 (!1297 (merged)) |
| AC-3 | Present block + no database client: boot fails | TestWireEncryption_PresentBlockWithoutDatabaseFailsBoot |
| AC-4 | Secret-read failure names the key, no key material | Not in this MR — owned by Step 2 (!1297 (merged)) |
| AC-5 | Malformed-secret sentinels in the error chain | Not in this MR — owned by Step 2 (!1297 (merged)) |
| AC-6 | Fallback-unwrap increment + coercion | Not in this MR — owned by Step 3 (!1298 (merged)) |
| AC-7 | Cardinality audit with the counter registered | Not in this MR — owned by Step 3 (!1298 (merged)) |
| AC-8 | Sweeper exits on both shutdown paths | Not in this MR — owned by Step 2 (!1297 (merged)) |
| AC-9 | All tests pass with -race |
Standard race lanes cover the new tests |
| AC-10 | Boot-outcome log, exactly once | Skip half: TestLogEncryptionTierSkipped + the app-backed skip assertion. Wired half owned by Step 2 (!1297 (merged)) |
Error cases
| # | Condition | Tests |
|---|---|---|
| E-1 | Absent block: not an error, tier skipped | TestWireEncryption_SkipsWhenAbsent (+ variants) |
| E-2 | Present block, no database client: boot failure | TestWireEncryption_PresentBlockWithoutDatabaseFailsBoot |
| E-3 | Secret read fails | Not in this MR — owned by Step 2 (!1297 (merged)) |
| E-4 | Zero usable keys (ErrNoKeysFound) |
Not in this MR — owned by Step 2 (!1297 (merged)) |
| E-5 | Wrong key size / invalid base64 | Not in this MR — owned by Step 2 (!1297 (merged)) |
| E-6 | Empty key slice (ErrNoKeysProvided) |
Spec-noted defense in depth, unreachable after a successful decode; owned by internal/crypto on main |
| E-7 | TTL outside bounds | Loader-owned (internal/config, on main); the tier never sees the value |
| E-8 | Unknown record URI: coerce, not an error | Not in this MR — owned by Step 3 (!1298 (merged)) |
Security considerations
| # | Concern | Tests |
|---|---|---|
| S-1 | Root key read once, zeroized, never in config/logs/errors | No secret read exists in this step; the zero-Get assertions in AC-1's tests pin that |
| S-2 | Enabled path fails closed, no degraded mode | TestWireEncryption_PresentBlockWithoutDatabaseFailsBoot, TestWireEncryption_PresentBlockNotYetWired (interim sentinel) |
| S-3 | Counter label coercion bounds attacker-influenced value | Not in this MR — owned by Step 3 (!1298 (merged)) |
| S-4 | Key-material confinement, zeroization limits | S04-A properties; not owned by this spec's MRs |
| S-5 | FIPS posture | Service-wide compliance work, outside S04 (spec Deployment posture) |
| S-6 | &8 review gates before block enablement | Process gate (#513), runs over the completed implementation |
| S-7 | Merged-but-disabled wiring is inert | Verified in-tree: no fixture, example, or boot test carries an encryption block. Verified deployed: neither the staging nor the production ar-config Vault secret carries a top-level encryption: key (checked 2026-08-05) |
Notes
- Stacked on !1278 (merged) (the plan) per the sequential step DAG; Draft until the plan merges, then retargets to
main. - E2e scenario impact: none — boot wiring only, no client-visible behavior (the catalogs cover format protocol journeys).
- Deployed-config inertness: verified 2026-08-05 that neither the staging nor the production
ar-configVault secret carries a top-levelencryption:key, so merging this step cannot fail a deploy's boot. - Documentation window: between this step and Step 2 (!1297 (merged)) landing, a present
encryptionblock fails boot witherrEncryptionNotWiredwhiledocs/dev/configuration-reference.mdandconfig.example.yamldescribe the block's wired behavior. The user-facing reference stays free of transitional language (it documents as-is behavior only, per the configuration-reference guardrail); the stacked merge sequence keeps the window short. - Size: ~420 LOC, inside the ceiling.
Related to https://gitlab.com/gitlab-org/ops/artifact-registry/-/work_items/491
Edited by Suleimi Ahmed