From 1502603048f82348fa727e1941e283c24469972e Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Wed, 15 Oct 2025 06:24:06 +1300 Subject: [PATCH 01/17] Add rake method --- ee/lib/tasks/gitlab/secrets_management/openbao.rake | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ee/lib/tasks/gitlab/secrets_management/openbao.rake b/ee/lib/tasks/gitlab/secrets_management/openbao.rake index 8eee2e50961653..3f5aea69dbe750 100644 --- a/ee/lib/tasks/gitlab/secrets_management/openbao.rake +++ b/ee/lib/tasks/gitlab/secrets_management/openbao.rake @@ -61,6 +61,11 @@ Usage: rake "gitlab:secrets_management:openbao:clone[/installation/dir]") ) end end + + desc 'GitLab | Secrets Management | Retrieve recovery keys from OpenBao' + task :recovery_key_generate do + puts "hello, world!" + end end end end -- GitLab From 2b3a316c620ab71f6f9dd98eccf255a92777b2d2 Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Wed, 15 Oct 2025 14:04:52 +1300 Subject: [PATCH 02/17] Allow access to sys/rotate without user parameter --- ee/lib/secrets_management/secrets_manager_jwt.rb | 7 +++++-- .../tasks/gitlab/secrets_management/openbao.rake | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/ee/lib/secrets_management/secrets_manager_jwt.rb b/ee/lib/secrets_management/secrets_manager_jwt.rb index 763db6782564c4..65a41b30582d04 100644 --- a/ee/lib/secrets_management/secrets_manager_jwt.rb +++ b/ee/lib/secrets_management/secrets_manager_jwt.rb @@ -16,6 +16,8 @@ def initialize(current_user: nil, project: nil, old_aud: nil) def payload now = Time.now.to_i + sub = 'gitlab_secrets_manager' + { iss: Gitlab.config.gitlab.url, iat: now, @@ -23,9 +25,10 @@ def payload exp: now + DEFAULT_TTL.to_i, jti: SecureRandom.uuid, aud: aud, - sub: 'gitlab_secrets_manager', + sub: sub, + user_id: sub, correlation_id: Labkit::Correlation::CorrelationId.current_id - }.merge(project_claims) + } end def project_claims diff --git a/ee/lib/tasks/gitlab/secrets_management/openbao.rake b/ee/lib/tasks/gitlab/secrets_management/openbao.rake index 3f5aea69dbe750..8c00df869fb6ac 100644 --- a/ee/lib/tasks/gitlab/secrets_management/openbao.rake +++ b/ee/lib/tasks/gitlab/secrets_management/openbao.rake @@ -63,8 +63,18 @@ Usage: rake "gitlab:secrets_management:openbao:clone[/installation/dir]") end desc 'GitLab | Secrets Management | Retrieve recovery keys from OpenBao' - task :recovery_key_generate do - puts "hello, world!" + task :recovery_key_generate, [] => :gitlab_environment do + privileged_jwt = SecretsManagement::SecretsManagerJwt.new.encoded + + # root = User.where(username: 'root').take + # project = root.projects.first + # privileged_jwt = SecretsManagement::SecretsManagerJwt.new(current_user: root, project: project).encoded + + smc = SecretsManagement::SecretsManagerClient.new(jwt: privileged_jwt) + + result = smc.send(:make_request, :get, "/v1/sys/rotate/root/verify", {}, optional: true) + # result = smc.read_jwt_role('gitlab_rails_jwt', 'app') + p result end end end -- GitLab From fe4230166a2adcafd3736361361a6947db4ad04b Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Wed, 15 Oct 2025 14:09:34 +1300 Subject: [PATCH 03/17] Re-add project claims --- ee/lib/secrets_management/secrets_manager_jwt.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ee/lib/secrets_management/secrets_manager_jwt.rb b/ee/lib/secrets_management/secrets_manager_jwt.rb index 65a41b30582d04..b4ac00d6b993d1 100644 --- a/ee/lib/secrets_management/secrets_manager_jwt.rb +++ b/ee/lib/secrets_management/secrets_manager_jwt.rb @@ -28,13 +28,17 @@ def payload sub: sub, user_id: sub, correlation_id: Labkit::Correlation::CorrelationId.current_id - } + }.merge(project_claims) end def project_claims - ::JSONWebToken::UserProjectTokenClaims + if project + return ::JSONWebToken::UserProjectTokenClaims .new(project: project, user: current_user) .generate + end + + {} end private -- GitLab From 7fa40f6f3f7ee84f1b1e503ab6ec2eaf18aa7edf Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Fri, 17 Oct 2025 12:28:02 +1300 Subject: [PATCH 04/17] End-to-end PoC for rake task --- .../gitlab/secrets_management/openbao.rake | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/ee/lib/tasks/gitlab/secrets_management/openbao.rake b/ee/lib/tasks/gitlab/secrets_management/openbao.rake index 8c00df869fb6ac..8b899a8ce14880 100644 --- a/ee/lib/tasks/gitlab/secrets_management/openbao.rake +++ b/ee/lib/tasks/gitlab/secrets_management/openbao.rake @@ -66,15 +66,29 @@ Usage: rake "gitlab:secrets_management:openbao:clone[/installation/dir]") task :recovery_key_generate, [] => :gitlab_environment do privileged_jwt = SecretsManagement::SecretsManagerJwt.new.encoded - # root = User.where(username: 'root').take - # project = root.projects.first - # privileged_jwt = SecretsManagement::SecretsManagerJwt.new(current_user: root, project: project).encoded - smc = SecretsManagement::SecretsManagerClient.new(jwt: privileged_jwt) - result = smc.send(:make_request, :get, "/v1/sys/rotate/root/verify", {}, optional: true) - # result = smc.read_jwt_role('gitlab_rails_jwt', 'app') - p result + recovery_already_exists = true + begin + result = smc.send(:make_request, :get, "/v1/sys/rotate/recovery/verify", {}, optional: true) + rescue + recovery_already_exists = false + end + + puts "recovery_already_exists: #{recovery_already_exists}" + if !recovery_already_exists + puts "init recovery:" + result = smc.send(:make_request, :post, "/v1/sys/rotate/recovery/init", { + secret_shares: 1, secret_threshold: 1}) + + p result + p "recovery" + p result["data"]["keys"][0] + else + puts "existing recovery:" + p result + end + end end end -- GitLab From ec491e5a54f95660fba28cc768e5869a7730c06c Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Mon, 20 Oct 2025 10:40:36 +1300 Subject: [PATCH 05/17] Tidy up SecretsManagerJwt In this commit, I've resolved issues raised by my team around SecretsManagementJwt changes. Additionally I've added some basic exception handling. --- .../secrets_management/secrets_manager_jwt.rb | 16 ++++++------ .../gitlab/secrets_management/openbao.rake | 26 +++++++------------ 2 files changed, 17 insertions(+), 25 deletions(-) diff --git a/ee/lib/secrets_management/secrets_manager_jwt.rb b/ee/lib/secrets_management/secrets_manager_jwt.rb index b4ac00d6b993d1..8d85e39bb266d8 100644 --- a/ee/lib/secrets_management/secrets_manager_jwt.rb +++ b/ee/lib/secrets_management/secrets_manager_jwt.rb @@ -18,7 +18,7 @@ def payload sub = 'gitlab_secrets_manager' - { + payload = { iss: Gitlab.config.gitlab.url, iat: now, nbf: now, @@ -26,19 +26,19 @@ def payload jti: SecureRandom.uuid, aud: aud, sub: sub, - user_id: sub, correlation_id: Labkit::Correlation::CorrelationId.current_id - }.merge(project_claims) + } + + payload.merge!(project_claims) if @project + payload[:user_id] = sub unless payload.key? :user_id + + payload end def project_claims - if project - return ::JSONWebToken::UserProjectTokenClaims + ::JSONWebToken::UserProjectTokenClaims .new(project: project, user: current_user) .generate - end - - {} end private diff --git a/ee/lib/tasks/gitlab/secrets_management/openbao.rake b/ee/lib/tasks/gitlab/secrets_management/openbao.rake index 8b899a8ce14880..75a5ade05f9b55 100644 --- a/ee/lib/tasks/gitlab/secrets_management/openbao.rake +++ b/ee/lib/tasks/gitlab/secrets_management/openbao.rake @@ -68,27 +68,19 @@ Usage: rake "gitlab:secrets_management:openbao:clone[/installation/dir]") smc = SecretsManagement::SecretsManagerClient.new(jwt: privileged_jwt) - recovery_already_exists = true - begin - result = smc.send(:make_request, :get, "/v1/sys/rotate/recovery/verify", {}, optional: true) - rescue - recovery_already_exists = false - end - - puts "recovery_already_exists: #{recovery_already_exists}" - if !recovery_already_exists - puts "init recovery:" - result = smc.send(:make_request, :post, "/v1/sys/rotate/recovery/init", { - secret_shares: 1, secret_threshold: 1}) + puts "init recovery:" + result = smc.send(:make_request, :post, "/v1/sys/rotate/recovery/init", + { secret_shares: 1, secret_threshold: 1 }) - p result - p "recovery" - p result["data"]["keys"][0] + if result["data"].key? "keys" + key = result["data"]["keys"][0] + puts "Key is: #{key}" else - puts "existing recovery:" - p result + puts "Cannot get key, key has already been retrieved." end + rescue SecretsManagement::SecretsManagerClient::ApiError => e + puts "Cannot get key, exception: #{e}" end end end -- GitLab From 6243c27361073723439d46fd3b284ca2f0e85345 Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Mon, 20 Oct 2025 11:06:50 +1300 Subject: [PATCH 06/17] Migrate code to SecretsManagerClient --- ee/lib/secrets_management/secrets_manager_client.rb | 11 +++++++++++ ee/lib/tasks/gitlab/secrets_management/openbao.rake | 8 ++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/ee/lib/secrets_management/secrets_manager_client.rb b/ee/lib/secrets_management/secrets_manager_client.rb index 66ac5a6d586090..7ee3ba93d4520d 100644 --- a/ee/lib/secrets_management/secrets_manager_client.rb +++ b/ee/lib/secrets_management/secrets_manager_client.rb @@ -12,6 +12,7 @@ class SecretsManagerClient OPENBAO_TOKEN_MAX_TTL = '15m' OPENBAO_EXPIRATION_LEEWAY = 150 OPENBAO_NOT_BEFORE_LEEWAY = 150 + OPENBAO_RECOVERY_SHARES_THRESHOLD = 1 OPENBAO_CLOCK_SKEW_LEEWAY = 60 OPENBAO_INLINE_AUTH_FAILED_HEADER = "X-Vault-Inline-Auth-Failed" OPENBAO_INLINE_AUTH_FAILED_VALUE = "true" @@ -258,6 +259,16 @@ def delete_policy(name) ) end + def rotate_recovery_init + recovery_values = { + secret_shares: OPENBAO_RECOVERY_SHARES_THRESHOLD, + secret_threshold: OPENBAO_RECOVERY_SHARES_THRESHOLD + } + + rotate_recovery_url = "/v1/sys/rotate/recovery/init" + result = make_request(:post, rotate_recovery_url, recovery_values) + end + def cel_login_jwt(mount_path:, role:, jwt:) url = "auth/#{mount_path}/cel/login" body = { role: role, jwt: jwt } diff --git a/ee/lib/tasks/gitlab/secrets_management/openbao.rake b/ee/lib/tasks/gitlab/secrets_management/openbao.rake index 75a5ade05f9b55..39177a2581c642 100644 --- a/ee/lib/tasks/gitlab/secrets_management/openbao.rake +++ b/ee/lib/tasks/gitlab/secrets_management/openbao.rake @@ -65,13 +65,9 @@ Usage: rake "gitlab:secrets_management:openbao:clone[/installation/dir]") desc 'GitLab | Secrets Management | Retrieve recovery keys from OpenBao' task :recovery_key_generate, [] => :gitlab_environment do privileged_jwt = SecretsManagement::SecretsManagerJwt.new.encoded + secrets_manager_client = SecretsManagement::SecretsManagerClient.new(jwt: privileged_jwt) - smc = SecretsManagement::SecretsManagerClient.new(jwt: privileged_jwt) - - puts "init recovery:" - result = smc.send(:make_request, :post, "/v1/sys/rotate/recovery/init", - { secret_shares: 1, secret_threshold: 1 }) - + result = secrets_manager_client.rotate_recovery_init if result["data"].key? "keys" key = result["data"]["keys"][0] puts "Key is: #{key}" -- GitLab From 6a6daa11e2c7f7f7f55321b14df05d36f166ccd4 Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Mon, 20 Oct 2025 11:16:45 +1300 Subject: [PATCH 07/17] Add exception tracking --- ee/lib/tasks/gitlab/secrets_management/openbao.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ee/lib/tasks/gitlab/secrets_management/openbao.rake b/ee/lib/tasks/gitlab/secrets_management/openbao.rake index 39177a2581c642..560a85d07a30c1 100644 --- a/ee/lib/tasks/gitlab/secrets_management/openbao.rake +++ b/ee/lib/tasks/gitlab/secrets_management/openbao.rake @@ -74,9 +74,9 @@ Usage: rake "gitlab:secrets_management:openbao:clone[/installation/dir]") else puts "Cannot get key, key has already been retrieved." end - rescue SecretsManagement::SecretsManagerClient::ApiError => e puts "Cannot get key, exception: #{e}" + Gitlab::ErrorTracking.track_and_raise_exception(e) end end end -- GitLab From 80e9e84d9f832d3135b3ea87ada2520194b352f3 Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Mon, 20 Oct 2025 11:18:02 +1300 Subject: [PATCH 08/17] Remove useless assignment --- ee/lib/secrets_management/secrets_manager_client.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ee/lib/secrets_management/secrets_manager_client.rb b/ee/lib/secrets_management/secrets_manager_client.rb index 7ee3ba93d4520d..80082bb7131448 100644 --- a/ee/lib/secrets_management/secrets_manager_client.rb +++ b/ee/lib/secrets_management/secrets_manager_client.rb @@ -266,7 +266,7 @@ def rotate_recovery_init } rotate_recovery_url = "/v1/sys/rotate/recovery/init" - result = make_request(:post, rotate_recovery_url, recovery_values) + make_request(:post, rotate_recovery_url, recovery_values) end def cel_login_jwt(mount_path:, role:, jwt:) -- GitLab From 809251a748588cc323393a93da97ca8d07a12a80 Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Mon, 20 Oct 2025 11:38:48 +1300 Subject: [PATCH 09/17] Add tests for SecretsManagerJwt, tidy up code --- ee/lib/secrets_management/secrets_manager_jwt.rb | 7 +++---- .../secrets_manager_jwt_spec.rb | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/ee/lib/secrets_management/secrets_manager_jwt.rb b/ee/lib/secrets_management/secrets_manager_jwt.rb index 8d85e39bb266d8..8b71af8c5f2d2f 100644 --- a/ee/lib/secrets_management/secrets_manager_jwt.rb +++ b/ee/lib/secrets_management/secrets_manager_jwt.rb @@ -3,6 +3,7 @@ module SecretsManagement class SecretsManagerJwt < Gitlab::Ci::JwtBase DEFAULT_TTL = 30.seconds + SYSTEM_UID = 'gitlab_secrets_manager' attr_reader :current_user, :project, :old_aud @@ -16,8 +17,6 @@ def initialize(current_user: nil, project: nil, old_aud: nil) def payload now = Time.now.to_i - sub = 'gitlab_secrets_manager' - payload = { iss: Gitlab.config.gitlab.url, iat: now, @@ -25,12 +24,12 @@ def payload exp: now + DEFAULT_TTL.to_i, jti: SecureRandom.uuid, aud: aud, - sub: sub, + sub: SYSTEM_UID, correlation_id: Labkit::Correlation::CorrelationId.current_id } payload.merge!(project_claims) if @project - payload[:user_id] = sub unless payload.key? :user_id + payload[:user_id] = SYSTEM_UID unless payload.key? :user_id payload end diff --git a/ee/spec/lib/secrets_management/secrets_manager_jwt_spec.rb b/ee/spec/lib/secrets_management/secrets_manager_jwt_spec.rb index 5c25278303f815..de6b88f0a95bc8 100644 --- a/ee/spec/lib/secrets_management/secrets_manager_jwt_spec.rb +++ b/ee/spec/lib/secrets_management/secrets_manager_jwt_spec.rb @@ -108,8 +108,20 @@ context 'when project is not present' do let(:current_project) { nil } - it 'raises an error due to the delegation to namespace' do - expect { payload }.to raise_error(NoMethodError) + it 'does not crash' do + expect { payload }.not_to raise_error + end + end + + context 'when both user and project are not present' do + let(:current_user) { nil } + let(:current_project) { nil } + + it 'includes project claims with nil user fields' do + expect(payload).to include( + user_id: described_class::SYSTEM_UID, + sub: described_class::SYSTEM_UID + ) end end end -- GitLab From 2db4d60a7e282a6324ae61912f8b9edb43aa45ac Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Mon, 20 Oct 2025 14:05:46 +1300 Subject: [PATCH 10/17] Fix failing tests --- ee/lib/secrets_management/secrets_manager_jwt.rb | 2 +- ee/spec/support/helpers/secrets_management/test_jwt.rb | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ee/lib/secrets_management/secrets_manager_jwt.rb b/ee/lib/secrets_management/secrets_manager_jwt.rb index e246cf21c0da3a..9557934613c9c6 100644 --- a/ee/lib/secrets_management/secrets_manager_jwt.rb +++ b/ee/lib/secrets_management/secrets_manager_jwt.rb @@ -29,7 +29,7 @@ def payload correlation_id: Labkit::Correlation::CorrelationId.current_id } - payload.merge!(project_claims) if @project + payload = payload.merge(project_claims) if project payload[:user_id] = SYSTEM_UID unless payload.key? :user_id payload diff --git a/ee/spec/support/helpers/secrets_management/test_jwt.rb b/ee/spec/support/helpers/secrets_management/test_jwt.rb index 5828baa8def861..246ccc01786aa5 100644 --- a/ee/spec/support/helpers/secrets_management/test_jwt.rb +++ b/ee/spec/support/helpers/secrets_management/test_jwt.rb @@ -24,6 +24,7 @@ def project_claims def payload claims = super + claims = claims.merge(project_claims) # super may not always call this method claims[:sub] = sub claims[:secrets_manager_scope] = secrets_manager_scope claims -- GitLab From 83ec4eed0ac695af07dd8ade653dadf4aea415ef Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Mon, 20 Oct 2025 14:24:02 +1300 Subject: [PATCH 11/17] Add test for #rotate_recovery_init --- .../secrets_manager_client_spec.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ee/spec/lib/secrets_management/secrets_manager_client_spec.rb b/ee/spec/lib/secrets_management/secrets_manager_client_spec.rb index 3dd58fea899ace..e518613f33aae7 100644 --- a/ee/spec/lib/secrets_management/secrets_manager_client_spec.rb +++ b/ee/spec/lib/secrets_management/secrets_manager_client_spec.rb @@ -1006,5 +1006,21 @@ expect(result).to contain_exactly(new_data: "DBPASS") end end + + describe '#rotate_recovery_init' do + subject(:rotate_recovery_init) { client.rotate_recovery_init } + + let(:response) { rotate_recovery_init } + let(:mocked_response) { '{"request_id"=>"id", "data"=>{"keys"=>["key"]}}' } + + before do + stub_request(:post, "#{described_class.configuration.host}/v1/sys/rotate/recovery/init") + .to_return(body: mocked_response) + end + + it 'configures the JWT auth method' do + expect(response).to eq(mocked_response) + end + end end end -- GitLab From c7244dbd8fad0a3118038559b0afe493fb42eea2 Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Tue, 21 Oct 2025 10:05:32 +1300 Subject: [PATCH 12/17] Add SecretsManagement::RecoveryKeys model In this commit, I've added the model for storing our recovery keys once we retrieve them from OpenBao. I've also added encryption for this model. --- db/docs/secrets_management_recovery_keys.yml | 11 +++++++++++ ...80958_create_secrets_management_recovery_keys.rb | 13 +++++++++++++ ee/app/models/secrets_management/recovery_key.rb | 11 +++++++++++ .../factories/secrets_management/recovery_keys.rb | 7 +++++++ .../models/secrets_management/recovery_key_spec.rb | 7 +++++++ 5 files changed, 49 insertions(+) create mode 100644 db/docs/secrets_management_recovery_keys.yml create mode 100644 db/migrate/20251020180958_create_secrets_management_recovery_keys.rb create mode 100644 ee/app/models/secrets_management/recovery_key.rb create mode 100644 ee/spec/factories/secrets_management/recovery_keys.rb create mode 100644 ee/spec/models/secrets_management/recovery_key_spec.rb diff --git a/db/docs/secrets_management_recovery_keys.yml b/db/docs/secrets_management_recovery_keys.yml new file mode 100644 index 00000000000000..dcdb348b9c0053 --- /dev/null +++ b/db/docs/secrets_management_recovery_keys.yml @@ -0,0 +1,11 @@ +--- +table_name: secrets_management_recovery_keys +classes: +- SecretsManagement::RecoveryKey +feature_categories: +- secrets_management +description: Contains the OpenBao instance-wide recovery key +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/208680 +milestone: '18.6' +gitlab_schema: gitlab_main_org +sharding_key: TODO diff --git a/db/migrate/20251020180958_create_secrets_management_recovery_keys.rb b/db/migrate/20251020180958_create_secrets_management_recovery_keys.rb new file mode 100644 index 00000000000000..2c7c6780e5471c --- /dev/null +++ b/db/migrate/20251020180958_create_secrets_management_recovery_keys.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class CreateSecretsManagementRecoveryKeys < Gitlab::Database::Migration[2.3] + milestone '18.6' + + def change + create_table :secrets_management_recovery_keys do |t| + t.text :key, limit: 10240 + + t.timestamps_with_timezone null: false + end + end +end diff --git a/ee/app/models/secrets_management/recovery_key.rb b/ee/app/models/secrets_management/recovery_key.rb new file mode 100644 index 00000000000000..ec32b27486f64e --- /dev/null +++ b/ee/app/models/secrets_management/recovery_key.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module SecretsManagement + class RecoveryKey < ApplicationRecord + self.table_name = 'secrets_management_recovery_keys' + + encrypts :key + + validates :key, presence: true + end +end diff --git a/ee/spec/factories/secrets_management/recovery_keys.rb b/ee/spec/factories/secrets_management/recovery_keys.rb new file mode 100644 index 00000000000000..1b2ae371b7579e --- /dev/null +++ b/ee/spec/factories/secrets_management/recovery_keys.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :secrets_management_recovery_keys, class: 'SecretsManagement::RecoveryKey' do + key { "secret_value" } + end +end diff --git a/ee/spec/models/secrets_management/recovery_key_spec.rb b/ee/spec/models/secrets_management/recovery_key_spec.rb new file mode 100644 index 00000000000000..5edf5301756197 --- /dev/null +++ b/ee/spec/models/secrets_management/recovery_key_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SecretsManagement::RecoveryKey, :gitlab_secrets_manager, feature_category: :secrets_management do + pending "add some examples to (or delete) #{__FILE__}" +end -- GitLab From 0ee39ec771e80ca6fd43b016af0109112c309d40 Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Tue, 21 Oct 2025 10:13:10 +1300 Subject: [PATCH 13/17] Add missing migration data --- db/schema_migrations/20251020180958 | 1 + db/structure.sql | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 db/schema_migrations/20251020180958 diff --git a/db/schema_migrations/20251020180958 b/db/schema_migrations/20251020180958 new file mode 100644 index 00000000000000..6008959991d2ea --- /dev/null +++ b/db/schema_migrations/20251020180958 @@ -0,0 +1 @@ +08c4a45b87a54562b06f9cade5029a5fae11e45bd2228886a7bcedaa4c0d1092 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index e8f7affb5a74b9..1d0c872b54a5bc 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -25633,6 +25633,23 @@ CREATE SEQUENCE secret_rotation_infos_id_seq ALTER SEQUENCE secret_rotation_infos_id_seq OWNED BY secret_rotation_infos.id; +CREATE TABLE secrets_management_recovery_keys ( + id bigint NOT NULL, + key text, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + CONSTRAINT check_e36c395dea CHECK ((char_length(key) <= 10240)) +); + +CREATE SEQUENCE secrets_management_recovery_keys_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE secrets_management_recovery_keys_id_seq OWNED BY secrets_management_recovery_keys.id; + CREATE TABLE security_attributes ( id bigint NOT NULL, namespace_id bigint NOT NULL, @@ -31614,6 +31631,8 @@ ALTER TABLE ONLY scim_oauth_access_tokens ALTER COLUMN id SET DEFAULT nextval('s ALTER TABLE ONLY secret_rotation_infos ALTER COLUMN id SET DEFAULT nextval('secret_rotation_infos_id_seq'::regclass); +ALTER TABLE ONLY secrets_management_recovery_keys ALTER COLUMN id SET DEFAULT nextval('secrets_management_recovery_keys_id_seq'::regclass); + ALTER TABLE ONLY security_attributes ALTER COLUMN id SET DEFAULT nextval('security_attributes_id_seq'::regclass); ALTER TABLE ONLY security_categories ALTER COLUMN id SET DEFAULT nextval('security_categories_id_seq'::regclass); @@ -35145,6 +35164,9 @@ ALTER TABLE ONLY secret_detection_token_statuses ALTER TABLE ONLY secret_rotation_infos ADD CONSTRAINT secret_rotation_infos_pkey PRIMARY KEY (id); +ALTER TABLE ONLY secrets_management_recovery_keys + ADD CONSTRAINT secrets_management_recovery_keys_pkey PRIMARY KEY (id); + ALTER TABLE ONLY security_attributes ADD CONSTRAINT security_attributes_pkey PRIMARY KEY (id); -- GitLab From 7aef6846cdbb49a4cd5514a9dbd231499020811a Mon Sep 17 00:00:00 2001 From: Sam Roque-Worcel Date: Tue, 21 Oct 2025 22:10:16 +0000 Subject: [PATCH 14/17] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Thong Kuah --- db/docs/secrets_management_recovery_keys.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/docs/secrets_management_recovery_keys.yml b/db/docs/secrets_management_recovery_keys.yml index dcdb348b9c0053..5f6e1595a96f2d 100644 --- a/db/docs/secrets_management_recovery_keys.yml +++ b/db/docs/secrets_management_recovery_keys.yml @@ -4,7 +4,7 @@ classes: - SecretsManagement::RecoveryKey feature_categories: - secrets_management -description: Contains the OpenBao instance-wide recovery key +description: Contains the OpenBao service recovery key introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/208680 milestone: '18.6' gitlab_schema: gitlab_main_org -- GitLab From 65e2bf8e09f0f085a0a6c5a067986cb358236d77 Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Wed, 22 Oct 2025 11:15:57 +1300 Subject: [PATCH 15/17] Choose correct schema, remove sharding_key --- db/docs/secrets_management_recovery_keys.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/db/docs/secrets_management_recovery_keys.yml b/db/docs/secrets_management_recovery_keys.yml index 5f6e1595a96f2d..a4cc1a32563fce 100644 --- a/db/docs/secrets_management_recovery_keys.yml +++ b/db/docs/secrets_management_recovery_keys.yml @@ -7,5 +7,4 @@ feature_categories: description: Contains the OpenBao service recovery key introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/208680 milestone: '18.6' -gitlab_schema: gitlab_main_org -sharding_key: TODO +gitlab_schema: gitlab_main_cell_local -- GitLab From d69805f11035d90e516283dacca52831e759666c Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Fri, 24 Oct 2025 10:20:30 +1300 Subject: [PATCH 16/17] Add active column to RecoveryKey model In this commit, I've added a new column named :active. This column will ensure we have only one recovery key at any given time, while making sure we don't accidentally delete any keys. --- ...58_create_secrets_management_recovery_keys.rb | 1 + db/structure.sql | 1 + ee/app/models/secrets_management/recovery_key.rb | 16 ++++++++++++++++ locale/gitlab.pot | 3 +++ 4 files changed, 21 insertions(+) diff --git a/db/migrate/20251020180958_create_secrets_management_recovery_keys.rb b/db/migrate/20251020180958_create_secrets_management_recovery_keys.rb index 2c7c6780e5471c..722379c8084151 100644 --- a/db/migrate/20251020180958_create_secrets_management_recovery_keys.rb +++ b/db/migrate/20251020180958_create_secrets_management_recovery_keys.rb @@ -6,6 +6,7 @@ class CreateSecretsManagementRecoveryKeys < Gitlab::Database::Migration[2.3] def change create_table :secrets_management_recovery_keys do |t| t.text :key, limit: 10240 + t.boolean :active, null: false, default: false t.timestamps_with_timezone null: false end diff --git a/db/structure.sql b/db/structure.sql index 50e85af637cccf..a6593e8f723b0d 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -25590,6 +25590,7 @@ ALTER SEQUENCE secret_rotation_infos_id_seq OWNED BY secret_rotation_infos.id; CREATE TABLE secrets_management_recovery_keys ( id bigint NOT NULL, key text, + active boolean DEFAULT false NOT NULL, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, CONSTRAINT check_e36c395dea CHECK ((char_length(key) <= 10240)) diff --git a/ee/app/models/secrets_management/recovery_key.rb b/ee/app/models/secrets_management/recovery_key.rb index ec32b27486f64e..b6165177c18021 100644 --- a/ee/app/models/secrets_management/recovery_key.rb +++ b/ee/app/models/secrets_management/recovery_key.rb @@ -7,5 +7,21 @@ class RecoveryKey < ApplicationRecord encrypts :key validates :key, presence: true + validates :active, inclusion: [true, false] + + validate :exactly_one_active + + def exactly_one_active + return unless active? + + active_count = SecretsManagement::RecoveryKey.where(active: true).count + + return if active_count == 0 + + errors.add( + :base, + _("Only one active RecoveryKey can exist at a time") + ) + end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 3db3438acc1bdb..ac9a5d3a74b248 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -45859,6 +45859,9 @@ msgstr "" msgid "Only members of this group can access the wiki." msgstr "" +msgid "Only one active RecoveryKey can exist at a time" +msgstr "" + msgid "Only one security policy bot is allowed per project" msgstr "" -- GitLab From 35383d744c79aad3b1edb856d1f303e4d973a190 Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Fri, 24 Oct 2025 11:31:02 +1300 Subject: [PATCH 17/17] Persist key to database In this commit, I've integrated the RecoveryKey model with the openbao.rake task. I've also modified the script to avoid leaving OpenBao in an inconsistent, "rotation started" state. --- .../models/secrets_management/recovery_key.rb | 5 +++-- .../secrets_manager_client.rb | 5 +++++ .../gitlab/secrets_management/openbao.rake | 20 ++++++++++++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/ee/app/models/secrets_management/recovery_key.rb b/ee/app/models/secrets_management/recovery_key.rb index b6165177c18021..ff4afa2c600e99 100644 --- a/ee/app/models/secrets_management/recovery_key.rb +++ b/ee/app/models/secrets_management/recovery_key.rb @@ -8,13 +8,14 @@ class RecoveryKey < ApplicationRecord validates :key, presence: true validates :active, inclusion: [true, false] - validate :exactly_one_active + scope :active, -> { where(active: true) } + def exactly_one_active return unless active? - active_count = SecretsManagement::RecoveryKey.where(active: true).count + active_count = SecretsManagement::RecoveryKey.active.count return if active_count == 0 diff --git a/ee/lib/secrets_management/secrets_manager_client.rb b/ee/lib/secrets_management/secrets_manager_client.rb index 80082bb7131448..171e5b5d55a283 100644 --- a/ee/lib/secrets_management/secrets_manager_client.rb +++ b/ee/lib/secrets_management/secrets_manager_client.rb @@ -269,6 +269,11 @@ def rotate_recovery_init make_request(:post, rotate_recovery_url, recovery_values) end + def rotate_recovery_cancel + rotate_recovery_url = "/v1/sys/rotate/recovery/init" + make_request(:delete, rotate_recovery_url, recovery_values) + end + def cel_login_jwt(mount_path:, role:, jwt:) url = "auth/#{mount_path}/cel/login" body = { role: role, jwt: jwt } diff --git a/ee/lib/tasks/gitlab/secrets_management/openbao.rake b/ee/lib/tasks/gitlab/secrets_management/openbao.rake index 560a85d07a30c1..b75336397e6123 100644 --- a/ee/lib/tasks/gitlab/secrets_management/openbao.rake +++ b/ee/lib/tasks/gitlab/secrets_management/openbao.rake @@ -70,9 +70,27 @@ Usage: rake "gitlab:secrets_management:openbao:clone[/installation/dir]") result = secrets_manager_client.rotate_recovery_init if result["data"].key? "keys" key = result["data"]["keys"][0] - puts "Key is: #{key}" + + old_key = SecretsManagement::RecoveryKey.active.take + if old_key + old_key.active = false + old_key.save! + puts "Marked old key as inactive." + end + + new_key = SecretsManagement::RecoveryKey.new do |nk| + nk.active = true + nk.key = key + end + + new_key.save! + + puts "Persisted key to the database" else puts "Cannot get key, key has already been retrieved." + + # Avoid leaving rotation in an inconsistent state. + secrets_manager_client.rotate_recovery_cancel end rescue SecretsManagement::SecretsManagerClient::ApiError => e puts "Cannot get key, exception: #{e}" -- GitLab