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 != nil is 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-typed bootDeps factories, the wiring-bundle enablement-signal fields (keyManager, rowEncryptor), and the tier call after wireDatabase, outside the client != nil gate.
  • The present-block path boots to errEncryptionNotWired, pinned by TestWireEncryption_PresentBlockNotYetWired so Step 2 flips exactly that one test. Inert on merge: no fixture, example, or boot test carries an encryption block.

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-config Vault secret carries a top-level encryption: key, so merging this step cannot fail a deploy's boot.
  • Documentation window: between this step and Step 2 (!1297 (merged)) landing, a present encryption block fails boot with errEncryptionNotWired while docs/dev/configuration-reference.md and config.example.yaml describe 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

Merge request reports

Loading
Loading