Draft: Resolve "[Rake] OpenBao Recovery Key Generation"
What does this MR do and why?
References
Testing
gdk stop openbao && yes | gdk reset-openbao-data && gdk start && sleep 3 && bundle exec rake gitlab:secrets_management:openbao:recovery_key_generate
ok: down: /Users/samroque-worcel/code/gdk/services/openbao: 1s
[...]
Persisted key to database
~/code/gdk/gitlab % bundle exec rails c
--------------------------------------------------------------------------------
Ruby: ruby 3.3.9 (2025-07-24 revision f5c772fc7c) [arm64-darwin24]
GitLab: 18.6.0-pre (d69805f1103) EE
GitLab Shell: 14.45.3
PostgreSQL: 16.10
--------------------------------------------------------------------------------
Loading development environment (Rails 7.1.5.2)
[1] pry(main)> SecretsManagement::RecoveryKey.active.take.key
SecretsManagement::RecoveryKey Load (0.3ms) SELECT "secrets_management_recovery_keys".* FROM "secrets_management_recovery_keys" WHERE "secrets_management_recovery_keys"."active" = TRUE LIMIT 1 /*application:console,db_config_database:gitlabhq_development,db_config_name:main,console_hostname:sroque-worcel--20250519-QHYJX,console_username:samroque-worcel,line:(pry):1:in `__pry__'*/
=> "5ed5fe015df1ae9a9148a21e89d73562203aa2ad92fe2b1d3965fa9b7a4c83d3"
Database overview:
For secrets manager, we use cell-local OpenBao instances. Access to these is pretty restricted and in certain cases we need to interact with these instances using the recovery key. There is one recovery key per instance. Because the data is sensitive, we've opted to encrypt the data using encrypts:
r = SecretsManagement::RecoveryKey.new { |m| m.key = "test" }.save
TRANSACTION (0.1ms) BEGIN
SecretsManagement::RecoveryKey Create (1.5ms) INSERT INTO "secrets_management_recovery_keys" ("key", "created_at", "updated_at") VALUES ('{"p":"OM5P/Q==","h":{"iv":"MeBM2a5ro9cMQHgZ","at":"soFE2mjzBvqsS99gbn9Bjw==","i":"MWU2MQ=="}}', '2025-10-20 20:34:18.636187', '2025-10-20 20:34:18.636187') RETURNING "id" /*application:console,db_config_database:gitlabhq_development,db_config_name:main,console_hostname:sroque-worcel--20250519-QHYJX,console_username:samroque-worcel,line:(pry):1:in `__pry__'*/
TRANSACTION (0.1ms) COMMIT
=> true
OpenBao provides more documentation, but the summary of it is that the recovery key can be retrieved once. Once retrieved, the endpoint returns an empty array. To my mind, this enforces two requirements:
- We should enforce that there is only one recovery key in the database.
- We should avoid losing the recovery key.
Nevertheless, from a Rails perspective, it is possible that the rake command may be called two times, and the API returns two distinct recovery keys. This could happen, for example, if the OpenBao instance is reset. To make the script resilient, we should handle this scenario.
I considered two approaches:
- Enforce a restriction at the model/database level that enforces that only one row exists in the table. If a new recovery key is retrieved, we can upsert that value. This is the simplest but risks data loss.
- Add a boolean attribute on the Model, i.e.
active. When a new row is inserted, we mark all existing rows asinactive. This prevents data loss.
Note on indexes
An index on "active" is not required because there are less than 1000 rows, according to the docs.
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.
Related to #571318