Add instance_ssh_certificates table, model, and feature flag

Why

Lay the foundation for instance-level SSH certificate authorities. GitLab Dedicated admins currently must edit config.yml on the filesystem to trust an SSH CA; this table lets that configuration move into the database instead.

What

Add instance_ssh_certificates table, model, and feature flag

Reviewer note

One deliberate deviations from the related issue recommendation:

  • No sharding key exemption entry. The table uses gitlab_schema: gitlab_main_cell_setting, which already sets require_sharding_key: false. Adding an exemption would fail spec/lib/gitlab/organizations/sharding_key_spec.rb:406, and that same spec forbids a sharding key on a cell-local table in the first place.

Changelog: added

Database migration

Click to expand
VERSION=20260819124706 bundle exec rails db:migrate:down:main
main: == [advisory_lock_connection] object_id: 162860, pg_backend_pid: 50508
main: == 20260819124706 CreateInstanceSshCertificates: reverting ====================
main: -- drop_table(:instance_ssh_certificates)
main:    -> 0.0803s
main: == 20260819124706 CreateInstanceSshCertificates: reverted (0.0903s) ===========

main: == [advisory_lock_connection] object_id: 162860, pg_backend_pid: 50508

Pro tip: try out scripts/database/migrate.rb for better migration management.

VERSION=20260819124706 bundle exec rails db:migrate:main
main: == [advisory_lock_connection] object_id: 163280, pg_backend_pid: 51236
main: == 20260819124706 CreateInstanceSshCertificates: migrating ====================
main: -- create_table(:instance_ssh_certificates)
main: -- quote_column_name(:title)
main:    -> 0.0000s
main: -- quote_column_name(:key)
main:    -> 0.0000s
main:    -> 0.0538s
main: == 20260819124706 CreateInstanceSshCertificates: migrated (0.0611s) ===========

main: == [advisory_lock_connection] object_id: 163280, pg_backend_pid: 51236

Pro tip: try out scripts/database/migrate.rb for better migration management.

Database review

InstanceSshCertificate.for_fingerprint scope

scope :for_fingerprint, ->(fingerprint) { where(fingerprint: fingerprint) }

fingerprint is a sha256_attribute, so the supplied value is encoded to bytea before the query. There is no pagination or additional chaining; fingerprint carries a unique index, so the scope returns at most one row.

Full query (from InstanceSshCertificate.for_fingerprint(...).to_sql):

SELECT "instance_ssh_certificates".*
FROM "instance_ssh_certificates"
WHERE "instance_ssh_certificates"."fingerprint" = $1;

Execution plan (EXPLAIN (ANALYZE, BUFFERS), backed by the unique index index_instance_ssh_certificates_on_fingerprint):

Index Scan using index_instance_ssh_certificates_on_fingerprint on instance_ssh_certificates  (cost=0.15..2.17 rows=1 width=120) (actual time=0.001..0.001 rows=0 loops=1)
  Index Cond: (fingerprint = '\xe3b0c...'::bytea)
  Buffers: shared hit=2
Planning:
  Buffers: shared hit=95
Planning Time: 0.345 ms
Execution Time: 0.010 ms

The query is a single-row index lookup on the unique fingerprint index (estimated rows=1), so it needs no pagination or limit. The table is empty locally, hence actual rows=0, but the plan is representative: the unique constraint guarantees at most one match regardless of table size.

References

Screenshots or screen recordings

Before After

How to set up and validate locally

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 #611272 (closed)

Edited by Hunter Stewart

Merge request reports

Loading
Loading