Report when older db_key_base salts can be retired
What does this MR do and why?
Adds Gitlab::Encryption::DbKeyBaseRotation::TokenDigestsStatus. It reports when the old db_key_base salt can be retired for token digests. EE adds runner controller tokens.
Token digests don't tell which key salted them. Rehashing on use (!259034 (merged)) rewrites a digest found under an older salt when the token is used. So only active tokens created before the rotation and not used since may still need an older salt.
.new(rotated_at:)raisesArgumentErrorunlessrotated_atis a time in the past. It must be when the last node switched to the new key, plus the rehash lease timeout.#executereturns one result per digest token model withremaining,expired,without_expiryandlatest_expiry. It iterates each table in batches of 10,000 over the primary key, with one aggregate query per batch. The result is memoized.#retirement_datereturns today when none remain, the day after the latest expiry date when all remaining tokens expire, or nil while a remaining token never expires. Runner controller tokens have no expiry, so only use, revocation or rotation clears them.
Nothing calls it yet. The gitlab:db_key_base:status rake task (https://gitlab.com/gitlab-org/gitlab/-/work_items/634003) will, for runbook step 5. It doesn't depend on that code, but is only meaningful once the rehash ships. That task should refuse to report while the rehash isn't enabled, and warn about expired tokens.
When the report can over-count or under-count
- Over-counts (safe):
last_used_atupdates are throttled (10 minutes for personal access tokens), so a recently used token may still be counted. Tokens created or used exactly atrotated_atare counted. rotated_attoo early (under-counts): a node still on the old key creates old-salt tokens after it, and can rehash tokens back to the old key. Hence "last node switched, plus the rehash lease timeout".- Failed rehash (accepted, under-counts): a token whose rehash failed on its last use since the rotation is missed, because
last_used_atstill moves. A later use retries the rehash once its 10-minute lease expires. The remaining risk is a token used once since the rotation, with that one rehash failing. - Expired tokens: they are reported as
expiredbut don't affectretirement_date. Extending their expiry after retirement (for example withgitlab:tokens:edit) would break them. - Day after the latest expiry: a personal access token stops working at 00:00 UTC on its
expires_atdate. The extra day keeps instances ahead of UTC from retiring a few hours early.
What retiring the salt doesn't cover
- Replaying a revoked (rotated) personal access token revokes its token family. After retirement, a revoked token from before the rotation can't be found, so it gets a plain 401 instead.
db_key_basealso saltsUsers::CreditCardValidationhashes andweb_hook_logs.url_hash. This report covers token digests only.
Database
Each table is iterated with each_batch over its primary key, because no index serves these filters. The aggregate for each batch:
SELECT COUNT(*) FILTER (WHERE expires_at IS NULL OR expires_at > NOW()),
COUNT(*) FILTER (WHERE expires_at <= NOW()),
COUNT(*) FILTER (WHERE expires_at IS NULL),
MAX(expires_at)
FROM "personal_access_tokens"
WHERE "personal_access_tokens"."id" >= 1 AND "personal_access_tokens"."id" < 10001
AND ("personal_access_tokens"."revoked" = FALSE OR "personal_access_tokens"."revoked" IS NULL)
AND "personal_access_tokens"."created_at" <= '2026-10-01 00:00:00'
AND ("personal_access_tokens"."last_used_at" IS NULL OR "personal_access_tokens"."last_used_at" <= '2026-10-01 00:00:00')
LIMIT 1Query plan: TODO
EE, on the CI database:
SELECT COUNT(*)
FROM "ci_runner_controller_tokens"
WHERE "ci_runner_controller_tokens"."id" >= 1 AND "ci_runner_controller_tokens"."id" < 10001
AND "ci_runner_controller_tokens"."status" = 0
AND "ci_runner_controller_tokens"."created_at" <= '2026-10-01 00:00:00'
AND ("ci_runner_controller_tokens"."last_used_at" IS NULL OR "ci_runner_controller_tokens"."last_used_at" <= '2026-10-01 00:00:00')Query plan: TODO
References
- Task: https://gitlab.com/gitlab-org/gitlab/-/work_items/631693
- Phase 4: https://gitlab.com/gitlab-org/gitlab/-/work_items/631458
- Phase 4 rake task: https://gitlab.com/gitlab-org/gitlab/-/work_items/634003
- Phase 2 MR: !258947 (merged)
- Epic: https://gitlab.com/groups/gitlab-org/-/work_items/23829
- Rehash token digests on use: https://gitlab.com/gitlab-org/gitlab/-/work_items/631700
Screenshots or screen recordings
Not applicable, backend only.
How to set up and validate locally
-
Run the specs:
bin/rspec spec/lib/gitlab/encryption/db_key_base_rotation ee/spec/lib/ee/gitlab/encryption/db_key_base_rotation
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.