Verify TLS certificates in the Duo verifier WebSocket check
What does this MR do and why?
The WebSocket connectivity check in gitlab:duo:verify_self_hosted_setup opens TLS with an OpenSSL::SSL::SSLContext that has no certificate store, so it cannot verify any certificate, and it retries without verification after any SSLError. For every HTTPS instance it connects to, the task therefore prints "TLS certificate verification was skipped (self-signed or untrusted certificate)". The check also never compares the certificate with the hostname, so adding a store alone would report a certificate issued for another host as verified.
TransportConnectivityCheck, in the same task, already verifies against the system and GitLab CA bundles and checks the hostname. This MR moves its TLS code unchanged into a TlsSocketHelpers module that both checks include. The tcp_socket method both checks defined moves there too, reading each check's own CONNECT_TIMEOUT. The WebSocket check now calls post_connection_check on a verified certificate. The unverified retry stays, for certificate verification failures only. A hostname mismatch now ends in the error branch, which prints the existing Could not reach the WebSocket endpoint text and the SSL error naming the mismatch. A protocol error already ended there, and no longer gets a second, unverified attempt. The shared code also closes the TCP socket of a failed handshake, which the WebSocket check left open.
flowchart TD
A["TLS handshake, verified against the system and GitLab CA bundles"] -->|certificate verifies| B{"Hostname matches?"}
B -->|yes| C["Upgrade request: verified, no note"]
B -->|no| E["Error result, no note"]
A -->|certificate verify failed| D["Retry without verification"]
D --> F["Upgrade request: unverified, with the note"]
A -->|any other SSL error| EAgainst https://gitlab.com, the certificate was reported unverified before this change and verified after it; against https://wrong.host.badssl.com, unverified before and a hostname mismatch error after. RuboCop is clean on the changed files. The five new TLS examples fail against the current check and pass with this change. One more example in each check's spec covers that check's connect timeout, and the verifier spec passes unchanged.
Claude Code wrote this change, and we ran the checks above in a simulated environment: a GitLab source checkout on a test VM.
References
Related to #628161 (closed), where @niklasjanz reported the unguarded retry and the missing CA store.
Introduced in !243425 (merged)
!254328 (merged) changes the request headers in the same file; the two merge cleanly in either order.
Screenshots or screen recordings
Not applicable: a rake task with no UI.
How to set up and validate locally
- Configure Duo self-hosted with a reachable AI Gateway, on an instance whose HTTPS
external_urlhas a publicly trusted certificate. - Run
bundle exec rake gitlab:duo:verify_self_hosted_setup. Before: the WebSocket section ends with the "verification was skipped" note. After: no note. bundle exec rspec ee/spec/lib/gitlab/duo/administration/agent_platform_websocket_check_spec.rb ee/spec/lib/gitlab/duo/administration/transport_connectivity_check_spec.rb
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.