Add support for instance-level SSH certificates via configuration

Summary

Add support for instance-level SSH certificates in gitlab-sshd by introducing a trusted_user_ca_keys configuration option. This allows self-managed GitLab administrators to configure trusted Certificate Authorities (CAs) directly in config.yml, enabling SSH certificate authentication without requiring Rails/database changes.

This is the gitlab-sshd equivalent of OpenSSH's TrustedUserCAKeys directive, providing a native alternative to the existing OpenSSH-based SSH certificate setup.

Background

GitLab currently supports two SSH certificate flows:

  1. OpenSSH-based (instance-level): Configured via sshd_config with TrustedUserCAKeys and AuthorizedPrincipalsCommand. Requires manual OpenSSH configuration.
  2. Group-level SSH certificates: Managed via API/database, supported by gitlab-sshd. CAs are registered per top-level group and scoped to that namespace.

For self-managed instances using gitlab-sshd, there's currently no way to configure instance-level SSH certificates without switching to OpenSSH. This feature bridges that gap.

Proposal

Add a new configuration option trusted_user_ca_keys to the sshd section of config.yml:

sshd:
  # Trusted user CA public keys for instance-level SSH certificate authentication.
  # Certificates signed by these CAs will be trusted for authentication.
  # The certificate's KeyId must contain the GitLab username.
  trusted_user_ca_keys:
    - /etc/gitlab/ssh_user_ca.pub

How it works:

  1. Admin generates a CA key pair and configures the public key path in config.yml
  2. Admin signs user certificates with the CA, setting KeyId to the GitLab username:
    ssh-keygen -s /path/to/ca -I <gitlab-username> -V +1d user-key.pub
  3. When a user connects with a certificate signed by the configured CA:
    • gitlab-sshd validates the certificate signature and expiry
    • The KeyId is used as the GitLab username
    • Standard GitLab access checks proceed (user existence, project permissions via /allowed endpoint)

Key characteristics:

  • No namespace restriction (instance-wide access, same as OpenSSH flow)
  • No Rails/database changes required
  • No feature flag required - enabled when trusted_user_ca_keys is configured
  • Supports multiple CA files for rotation and multi-CA setups
  • Falls back to group-level certificate check (via Rails API) if CA is not locally trusted

Implementation Details

Affected files:

  • internal/config/config.go - Add TrustedUserCAKeys []string to ServerConfig
  • internal/sshd/server_config.go - Add CA loading, local trust verification, and instance-level auth path
  • internal/sshd/server_config_test.go - Add tests
  • config.yml.example - Document new option

Changes to handleUserCertificate:

  1. Check if certificate's signing CA matches any locally configured trusted CA
  2. If matched: return permissions with username from cert.KeyId (no namespace)
  3. If not matched: fall back to existing group-level flow via /authorized_certs API

Acceptance Criteria

  • trusted_user_ca_keys configuration option accepts a list of file paths
  • CA public keys are loaded and parsed at startup (invalid files logged and skipped)
  • Certificates signed by configured CAs are trusted for authentication
  • Certificate KeyId is used as the GitLab username
  • No namespace restriction applied (instance-wide access)
  • Expired certificates are rejected
  • Wrong certificate type (host cert) is rejected
  • Certificates signed by unknown CAs fall back to group-level flow
  • Multiple CA files supported
  • Configuration documented in config.yml.example
  • Unit tests cover all scenarios

Security Considerations

  • Trust model: Instance admins already have full control, so instance-level CA trust introduces no new privilege escalation (unlike group-level on GitLab.com)
  • No Enterprise User requirement: Unlike group-level certificates, this doesn't need the Enterprise User check since all users are already under admin authority on self-managed
  • User validation: Invalid usernames in KeyId will fail at the /allowed endpoint with appropriate errors

Limitations

  • CA management requires filesystem access and service restart (no API/UI)
  • No audit events for CA configuration changes
  • No certificate revocation beyond CA removal
  • config.yml must be synchronized across all gitlab-sshd nodes in clustered deployments