Geo SSH proxy fails with SSH certificate authentication
Summary
Currently, Geo's SSH proxying is incompatible with SSH certificate authentication. It has been so since the time when both SSH proxying and SSH certificate authentication existed, so technically it was an unimplemented feature addition, and now it's a bug.
- https://docs.gitlab.com/administration/operations/ssh_certificates/
- https://docs.gitlab.com/user/group/ssh_certificates/
More details according to Duo
When using SSH certificates for authentication (via AuthorizedPrincipalsCommand), Geo SSH proxy operations fail with "Geo push user is invalid" error. This occurs because Geo::PushUser only supports the key-{ID} format for gl_id, not the username-{KEY_ID} format used by SSH certificates.
Click to expand the flow in the codebase that leads to the error
- Secondary's
GeoGitAccess#geo_custom_ssh_actioncreates payload withgl_idinusername-{KEY_ID}format (from certificate) - Primary's
GitHttpController#ssh_upload_packhandles the request -
access_actormethod callsGeo::PushUser.new(geo_gl_id) -
Geo::PushUser#usercallsidentify_using_ssh_key(gl_id) -
Gitlab::Identifier#identify_using_ssh_keyexpectskey-{ID}format and queriesUser.find_by_ssh_key_id(key_id) - This fails for SSH certificate users because there's no record in the
keystable - Returns
nilandaccess_actorraises'Geo push user is invalid.'
Click to expand details on `gl_id` formats
The Different gl_id Formats and Their Origins
The gl_id format depends on how the user authenticates via SSH:
1. Traditional SSH Keys (key-{ID}):
When a user authenticates with an SSH key stored in GitLab's database, the AuthorizedKeysCommand returns:
command="gitlab-shell upload-pack key_id=1"
GitLab Shell then uses key-{ID} as the gl_id, and Gitlab::Identifier#identify_using_ssh_key looks up the user via User.find_by_ssh_key_id(key_id).
2. SSH Certificates (username-{USERNAME}):
When using SSH certificate authentication via AuthorizedPrincipalsCommand, the command outputs:
command="/opt/gitlab/.../gitlab-shell username-{KEY_ID}",... {PRINCIPAL}
Here, {KEY_ID} is the username extracted from the certificate (not a database ID). There's no database lookup needed because the username comes directly from the cryptographically-signed certificate.
3. HTTP Push (user-{ID}):
For HTTP Git operations, the user is already authenticated via the web session/API, so gl_id uses user-{ID} with the numeric user ID.
Why Gitlab::Identifier#identify Only Handles user-* and key-*
The identify method was designed for the internal API flow where:
-
user-{ID}comes from HTTP authentication (user already identified) -
key-{ID}comes from traditional SSH key lookup (key exists in database)
The username-{USERNAME} format was added later for SSH certificates and bypasses the need for database lookup during initial authentication. The username is trusted because it's embedded in a certificate signed by a trusted CA.
The Bug
Geo::PushUser directly calls identify_using_ssh_key(gl_id), which assumes key-{ID} format. When Geo proxies an SSH request from a certificate-authenticated user, the gl_id is username-{USERNAME}, which fails because there's no SSH key record to look up.
Steps to reproduce
- Configure GitLab with SSH certificate authentication using
AuthorizedPrincipalsCommand - Disable traditional SSH key uploads (common security policy)
- Set up Geo with a secondary site configured as read-only (
GEO_SECONDARY_PROXY=0) - Attempt to clone/pull a repository via SSH from the secondary when the repository is out-of-date
What is the current bug behavior?
When the secondary proxies an SSH request to the primary for an out-of-date repository:
Error message:
remote: Geo push user is invalid.
fatal: Could not read from remote repository.
What is the expected correct behavior?
Geo::PushUser should support the username-{KEY_ID} format used by SSH certificate authentication, allowing users to be identified by username lookup.
Potential solutions
Click to expand an idea from Duo
This is Duo's idea which I have not vetted as of 13 Jan 2026. I added the security items.
- Update
Geo::PushUser#userto use the genericidentify()method - Extend
Gitlab::Identifier#identifyto handle theusername-{KEY_ID}pattern - Add an
identify_using_usernamemethod to look up users by username - Consider security implications
- Get app sec review
Relevant code
ee/app/models/geo/push_user.rb:
def user
@user ||= identify_using_ssh_key(gl_id)
end
lib/gitlab/identifier.rb already has a generic identify() method that handles multiple formats, but it does not include username-*:
def identify(identifier)
case identifier
when /\Auser-\d+\Z/
identify_using_user(identifier)
when /\Akey-\d+\Z/
identify_using_ssh_key(identifier)
end
end
Possible Workarounds
- If you do not use CNH/CNG, you might try enabling
Feature.enable(:geo_proxy_fetch_ssh_to_primary) && Feature.enable(:geo_proxy_push_ssh_to_primary)since I believe the unreleased change in flow does not run the offending code. This possible workaround has not been verified empirically, as of 13 Jan 2026. References [Feature flag] Rollout of `geo_proxy_fetch_ssh_... (#466045), [Feature flag] Rollout of `geo_proxy_push_ssh_t... (#466057). (This is not working for CNH/CNG yet because you cannot disable proxy request buffering by path in the NGINX ingress. You could enable the FFs if you disable proxy request buffering completely. This is low risk if your env is not on the public internet.)
Related issues
Customer request for help: https://gitlab.com/gitlab-com/request-for-help/-/issues/4040