Send SSH client IP to /authorized_certs
What does this MR do and why?
Addresses "Gap 2" in https://gitlab.com/gitlab-org/gitlab/-/work_items/611349#gap-2-failed-authentications-are-not-audited
gitlab-sshd now sends the SSH client's IP address when it resolves an SSH certificate through GitLab. GitLab records an audit event when certificate authentication fails, and that event needs the client's address, not GitLab Shell's.
authorizedcerts.Client.GetByKeyaccepts a rawremoteAddrargument, strips the port, and sends a valid IP ascheck_ipto/api/v4/internal/authorized_certs.handleUserCertificateandresolveCertificateViaAPIpass the SSH connection's remote address through to the client, where parsing and validation happen.- When PROXY protocol is enabled, the address is the client address from the PROXY header, the same address used for
check_ipon/allowed. check_ipis left out when the address is unknown or invalid, including Unix socket paths. GitLab versions that don't know the parameter ignore it.- Client tests cover IPv4 and IPv6 with and without ports, zoned IPv6, an empty address, and a Unix socket path. Callback tests verify that IPv4 and IPv6 client IPs are sent when
/authorized_certsrejects the certificate. A real-server PROXY protocol test assertingcheck_ipis deferred to a follow-up.
References
- https://gitlab.com/gitlab-org/gitlab/-/work_items/611349
- Companion GitLab Rails MR: gitlab!259775 (merged)
How to set up and validate locally
- Attempt SSH authentication with a valid certificate signed by an unregistered CA.
- Verify that the resulting audit event records the SSH client's IP.
Authentication is expected to fail: the successful outcome of this test is an audit event with the correct client address.
Prerequisites
- A working Linux GDK running GitLab EE with an Ultimate development license.
- GDK's
gitlab-sshdconfigured to listen on127.0.0.1:2222, without PROXY protocol. - The ability to bind the SSH client to
127.0.0.2. Using a distinct loopback address lets this test distinguish the SSH client from GitLab Shell's Rails-request address. - Clean working trees in GDK's active
gitlab/andgitlab-shell/checkouts.
1. Check out and build both MRs
Run from your GDK root directory. Record both repositories' current branches for cleanup and keep this terminal open for subsequent commands:
export GDK_ROOT="$PWD"
mkdir -p "$GDK_ROOT/tmp"
export CERT_DIR="$(mktemp -d "$GDK_ROOT/tmp/cert-client-ip.XXXXXX")"
cd "$GDK_ROOT/gitlab"
git fetch https://gitlab.com/gitlab-org/gitlab.git merge-requests/259775/head
git switch --detach FETCH_HEAD
bundle install
cd "$GDK_ROOT/gitlab-shell"
git fetch https://gitlab.com/gitlab-org/gitlab-shell.git merge-requests/1578/head
git switch --detach FETCH_HEAD
make compile
cd "$GDK_ROOT"
gdk restart rails-web sidekiq sshdThese must be the checkouts used by GDK's services. make compile builds both GitLab Shell and gitlab-sshd. Neither MR requires a frontend build or database migration.
2. Create an SSH certificate
In the same terminal:
export CERT_ID="cert-ip-check-$(date -u +%Y%m%d%H%M%S)"
ssh-keygen -t ed25519 -N '' -f "$CERT_DIR/ca"
ssh-keygen -t ed25519 -N '' -f "$CERT_DIR/client"
ssh-keygen \
-s "$CERT_DIR/ca" \
-I "$CERT_ID" \
-n git \
-V '-5m:+1h' \
"$CERT_DIR/client.pub"
printf 'Certificate identity: %s\n' "$CERT_ID"Record the printed identity. Do not register this CA in GitLab or add it to sshd.trusted_user_ca_keys, and do not register the client public key as a GitLab SSH key. The valid certificate must reach /authorized_certs and be rejected because its CA is unknown.
3. Attempt authentication
ssh -F /dev/null \
-b 127.0.0.2 \
-p 2222 \
-i "$CERT_DIR/client" \
-o CertificateFile="$CERT_DIR/client-cert.pub" \
-o IdentitiesOnly=yes \
-o IdentityAgent=none \
-o PreferredAuthentications=publickey \
-o BatchMode=yes \
-o UserKnownHostsFile="$CERT_DIR/known_hosts" \
-o StrictHostKeyChecking=accept-new \
-T git@127.0.0.1Expect Permission denied (publickey) and a nonzero exit status. This is intentional. The command sends the test certificate from 127.0.0.2 and avoids reusing keys from your SSH agent.
4. Verify the audit event
From your GDK root, open a Rails console:
gdk rails consoleRun the following, replacing the identity with the value printed in step 2:
identity = 'paste-the-certificate-identity-from-step-2'
event = AuditEvents::InstanceAuditEvent
.where(event_name: 'ssh_certificate_authentication_failed', author_name: identity)
.order(:id).last
raise 'Audit event not found' unless event
pp event.attributes
pp event.details
raise 'Unexpected client IP' unless event.ip_address.to_s == '127.0.0.2'
raise 'Unexpected failure reason' unless event.details.with_indifferent_access[:reason] == 'certificate_not_found'Expect an ssh_certificate_authentication_failed event for the test identity, with reason certificate_not_found and ip_address equal to 127.0.0.2, not 127.0.0.1. The event is recorded synchronously, so no audit streaming receiver is needed.
- A certificate authentication attempt rejected by
/authorized_certsproduces an audit event containing the SSH client's IP address.
Cleanup
- Exit the Rails console.
- Return both repositories to their previous branches and run
make compileingitlab-shell. - From your GDK root, run
gdk restart rails-web sidekiq sshd. - Remove the temporary certificate directory after checking its path with
echo "$CERT_DIR".
Either merge order is safe: older Rails versions ignore check_ip, and the companion Rails change falls back to the request IP when it is missing.
