feat(cmd): S04-B re-encrypt root scope

What

Implements the re-encrypt --scope=root operator command — root key rotation — the fourth step of the S04-B rotation-tooling plan. Replaces Step 3's reEncryptRootNotImplemented stub with the real action:

  • Provider derivation from the decoded key file: an active provider that wraps under the first key, and a single-key provider per retained key whose URI selects the rows still wrapped under it. A retained line whose URI equals the active key's is skipped with a log line (a duplicated key-file line is an authoring mistake, not a rotation input).
  • Orientation refusal before any re-wrap, naming both URIs, when the decoded file's first key does not carry --active-key-uri (a stale mount would otherwise rotate in reverse and still pass verification). The decoded active and retained URIs are logged at startup.
  • Serial RootKeyRotator.RotateAll from each retained provider to the active provider, with a Progress callback feeding per-batch liveness logs.
  • Whole-live-set verification via ListLiveKeysNotUnderRootKeyURI(<active URI>, 1), run unconditionally (even for a no-retained-keys file): a non-empty result exits 1 naming the first offending root_key_uri.

Composition seam growth

The one-shot operator composition is extended for the root scope (the plan's Step 4 file list intentionally omits operator.go): rotationStore embeds crypto.RotationKeyStore so the injected store drives NewRootKeyRotator, and operatorRuntime carries the infrastructure config rather than any key material — the action resolves the root-key secret itself, builds the providers, and zeroizes the decoded keys before the first database round trip, so no other subcommand ever holds root-key bytes. A command-level preflight validates the secret (resolve, decode, wipe) before the composition opens the database pool, so a broken secret is reported without a database round trip.

Spec coverage

Spec: docs/specs/S04-b-encryption-key-rotation-tooling.md, "Root key rotation (--scope=root)".

Criterion Test
AC 1 — re-wraps every live row onto the active key; re-run processes zero TestReEncryptRootScope_RewrapsEveryLiveRow
AC 2 — single-key file rotates nothing but still verifies (exit 0) TestReEncryptRootScope_SingleKeyFileVerifies
AC 3 — verification empty=success; non-empty exits 1 naming the offender (incl. a URI absent from the file) TestReEncryptRootScope_StrandedURIFailsVerification, TestReEncryptRootScope_SingleKeyFileVerifiesOffender
AC 4 — orientation refusal before any re-wrap naming both URIs; startup URI logging TestReEncryptRoot_OrientationRefusalNamesBothURIs, TestReEncryptRootScope_OrientationRefusalLeavesRowsUntouched
AC 14 — no key material in command-flow logs TestReEncryptRootScope_RewrapsEveryLiveRow (AC-14 assertion)
Root error cases — unwrap failure aborts and persists; duplicate line skipped; transient failure resumes TestReEncryptRootScope_UnwrapFailureAbortsAndPersists, _DuplicateLineSkipped, _TransientFailureResumes

Testing

The action is covered end-to-end by cmd/artifact-registry/reencrypt_root_integration_test.go, driven through cmd.Run against a per-test isolated database.

No e2e scenario catalog entry is added or affected: re-encrypt --scope=root is an operator tool with no user-facing request path, so it has no place in the format/protocol e2e catalogs. The operator-facing runbook for the command lands with the documentation step (Step 9) of this plan.

Stacking

Targets sahmed/s04b-step3-commands (Step 3, !1345 (merged)); GitLab retargets this MR down the stack as each predecessor merges. Part of the S04-B rotation-tooling plan.

Related to https://gitlab.com/gitlab-org/ops/artifact-registry/-/issues/496

Edited by Suleimi Ahmed

Merge request reports

Loading
Loading