From bcf131e9e036bd26ae7cde1864e8867ef1833800 Mon Sep 17 00:00:00 2001 From: Dmitry Rybakov Date: Thu, 9 Jul 2026 14:36:22 +0200 Subject: [PATCH 1/3] RUBY-3800 Add test variant for the Atlas Secure Frontend Processor (SFP) Add SFP (monguard) connectivity tests that run against a preconfigured Atlas cluster proxied by a Secure Frontend Processor. The tests cover unauthenticated, SCRAM-SHA-256, and X.509 access, each authenticated case under baseline, compression, and Server API v1 variations, per the atlas-sfp-testing specification. Add a dedicated Evergreen "sfp" build variant with a test-sfp task that fetches credentials from the drivers/sfp secrets vault and runs the new spec/sfp suite. --- .evergreen/config.yml | 35 ++++++++ .evergreen/config/common.yml.erb | 26 ++++++ .evergreen/config/standard.yml.erb | 9 ++ .evergreen/run-tests-sfp.sh | 37 ++++++++ spec/sfp/sfp_spec.rb | 130 +++++++++++++++++++++++++++++ 5 files changed, 237 insertions(+) create mode 100755 .evergreen/run-tests-sfp.sh create mode 100644 spec/sfp/sfp_spec.rb diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 9e3a53ba15..e7a8e93ae0 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -349,6 +349,28 @@ functions: AUTH=${AUTH} SSL=${SSL} TOPOLOGY=${TOPOLOGY} RVM_RUBY="${RVM_RUBY}" \ .evergreen/run-tests-atlas.sh + "export SFP credentials": + - command: subprocess.exec + type: test + params: + binary: bash + working_dir: "src" + include_expansions_in_env: [AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, DRIVERS_TOOLS] + args: + - "${DRIVERS_TOOLS}/.evergreen/secrets_handling/setup-secrets.sh" + - "drivers/sfp" + + "run SFP tests": + - command: shell.exec + type: test + params: + shell: bash + working_dir: "src" + script: | + ${PREPARE_SHELL} + RVM_RUBY="${RVM_RUBY}" \ + .evergreen/run-tests-sfp.sh + pre: - func: assume-test-secrets-ec2-role - func: "fetch source" @@ -550,6 +572,10 @@ tasks: commands: - func: "export Atlas credentials" - func: "run Atlas tests" + - name: "test-sfp" + commands: + - func: "export SFP credentials" + - func: "run SFP tests" - name: "test" commands: - func: "run tests" @@ -1726,6 +1752,15 @@ buildvariants: tasks: - name: test-atlas + - matrix_name: "sfp" + matrix_spec: + ruby: ["ruby-4.0", "ruby-3.4", "ruby-3.3", "ruby-3.2", "ruby-3.1"] + os: ubuntu2204 + display_name: "Atlas SFP tests ${ruby}" + tags: ["pr"] + tasks: + - name: test-sfp + # https://jira.mongodb.org/browse/RUBY-3311 # - matrix_name: "aws-lambda" # matrix_spec: diff --git a/.evergreen/config/common.yml.erb b/.evergreen/config/common.yml.erb index cdb72e6803..9d440736d8 100644 --- a/.evergreen/config/common.yml.erb +++ b/.evergreen/config/common.yml.erb @@ -346,6 +346,28 @@ functions: AUTH=${AUTH} SSL=${SSL} TOPOLOGY=${TOPOLOGY} RVM_RUBY="${RVM_RUBY}" \ .evergreen/run-tests-atlas.sh + "export SFP credentials": + - command: subprocess.exec + type: test + params: + binary: bash + working_dir: "src" + include_expansions_in_env: [AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, DRIVERS_TOOLS] + args: + - "${DRIVERS_TOOLS}/.evergreen/secrets_handling/setup-secrets.sh" + - "drivers/sfp" + + "run SFP tests": + - command: shell.exec + type: test + params: + shell: bash + working_dir: "src" + script: | + ${PREPARE_SHELL} + RVM_RUBY="${RVM_RUBY}" \ + .evergreen/run-tests-sfp.sh + pre: - func: assume-test-secrets-ec2-role - func: "fetch source" @@ -547,6 +569,10 @@ tasks: commands: - func: "export Atlas credentials" - func: "run Atlas tests" + - name: "test-sfp" + commands: + - func: "export SFP credentials" + - func: "run SFP tests" - name: "test" commands: - func: "run tests" diff --git a/.evergreen/config/standard.yml.erb b/.evergreen/config/standard.yml.erb index 984a05cce2..ea1c3d019c 100644 --- a/.evergreen/config/standard.yml.erb +++ b/.evergreen/config/standard.yml.erb @@ -540,6 +540,15 @@ buildvariants: tasks: - name: test-atlas + - matrix_name: "sfp" + matrix_spec: + ruby: <%= supported_mri_rubies_3_ubuntu %> + os: ubuntu2204 + display_name: "Atlas SFP tests ${ruby}" + tags: ["pr"] + tasks: + - name: test-sfp + # https://jira.mongodb.org/browse/RUBY-3311 # - matrix_name: "aws-lambda" # matrix_spec: diff --git a/.evergreen/run-tests-sfp.sh b/.evergreen/run-tests-sfp.sh new file mode 100755 index 0000000000..f4b0808465 --- /dev/null +++ b/.evergreen/run-tests-sfp.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +set -ex + +. `dirname "$0"`/../spec/shared/shlib/distro.sh +. `dirname "$0"`/../spec/shared/shlib/set_env.sh +. `dirname "$0"`/functions.sh + +set_env_vars +set_env_python + +# Install rbenv and download the requested ruby version +rm -rf ~/.rbenv +git clone https://github.com/rbenv/rbenv.git ~/.rbenv +rm -rf ~/.rbenv/versions/ +curl --retry 3 -fL http://boxes.10gen.com/build/toolchain-drivers/mongo-ruby-toolchain/library/`host_distro`/$RVM_RUBY.tar.xz |tar -xC $HOME/.rbenv/ -Jf - +export PATH="$HOME/.rbenv/bin:$PATH" +eval "$(rbenv init - bash)" +export FULL_RUBY_VERSION=$(ls ~/.rbenv/versions | head -n1) +rbenv global $FULL_RUBY_VERSION + +export JAVA_HOME=/opt/java/jdk21 +export JAVACMD=$JAVA_HOME/bin/java + +bundle_install + +echo "Running specs" + +export SFP_TESTING=1 + +if test -f secrets-export.sh; then + # shellcheck disable=SC1091 + . ./secrets-export.sh +fi + +bundle exec rspec spec/sfp \ + --format Rfc::Riff --format RspecJunitFormatter --out tmp/rspec.xml diff --git a/spec/sfp/sfp_spec.rb b/spec/sfp/sfp_spec.rb new file mode 100644 index 0000000000..0631707082 --- /dev/null +++ b/spec/sfp/sfp_spec.rb @@ -0,0 +1,130 @@ +# frozen_string_literal: true + +require 'lite_spec_helper' +require 'base64' +require 'tempfile' +require 'uri' + +# Connectivity tests for an Atlas Secure Frontend Processor (SFP), a proxy that +# sits in front of Atlas clusters to provide additional security capabilities. +# See the atlas-sfp-testing specification for the full list of required tests. +# +# These tests run against a preconfigured Atlas cluster and are driven entirely +# by environment variables. They are skipped unless SFP_TESTING is set. +describe 'Atlas SFP connectivity' do + before do + skip 'Set SFP_TESTING to run tests against a live Atlas SFP cluster' unless ENV['SFP_TESTING'] + end + + # Each authenticated test runs under three variations: a baseline, one with a + # compressor enabled, and one with Server API version 1. + variations = { + 'baseline' => {}, + 'with compression' => { compressors: %w[ zlib ] }, + 'with server API version 1' => { server_api: { version: '1' } }, + }.freeze + + after do + client.close + rescue StandardError + # no-op + end + + shared_examples 'connects successfully' do + it 'succeeds at ping' do + result = client.use(:admin).database.command(ping: 1).documents.first + expect(result['ok']).to eq(1) + end + + it 'reports the expected authentication state' do + info = client.use(:admin).database.command(connectionStatus: 1).documents.first + expect(info['ok']).to eq(1) + authenticated_users = info[:authInfo][:authenticatedUsers] + if authenticated + expect(authenticated_users).not_to be_empty + else + expect(authenticated_users).to be_empty + end + end + end + + shared_examples 'performs CRUD operations' do + # Drivers MUST use a unique collection name for each test run. + let(:collection) { client.use('db')["sfp_test_#{BSON::ObjectId.new}"] } + + # Drivers MUST drop the test collection after the test completes, + # regardless of success or failure. + after do + collection.drop + rescue StandardError + # no-op + end + + it 'inserts and reads back a document' do + collection.insert_one(_id: 0) + expect(collection.find(_id: 0).to_a).to eq([ { '_id' => 0 } ]) + end + end + + context 'when unauthenticated' do + let(:authenticated) { false } + let(:client) { Mongo::Client.new(ENV.fetch('SFP_ATLAS_URI')) } + + include_examples 'connects successfully' + end + + context 'when using SCRAM-SHA-256 authentication' do + let(:authenticated) { true } + + variations.each do |description, options| + context description do + let(:client) do + Mongo::Client.new( + ENV.fetch('SFP_ATLAS_URI'), + { + user: ENV.fetch('SFP_ATLAS_USER'), + password: ENV.fetch('SFP_ATLAS_PASSWORD'), + auth_mech: :scram256, + }.merge(options) + ) + end + + include_examples 'connects successfully' + include_examples 'performs CRUD operations' + end + end + end + + context 'when using X.509 authentication' do + let(:authenticated) { true } + + let(:client_certificate) do + decoded = Base64.strict_decode64(ENV.fetch('SFP_ATLAS_X509_BASE64')) + file = Tempfile.new([ 'sfp-x509-cert', '.pem' ]) + file.write(decoded) + File.chmod(0o600, file.path) + file.close + file + end + + let(:uri) do + "#{ENV.fetch('SFP_ATLAS_X509_URI')}&tlsCertificateKeyFile=" \ + "#{URI::DEFAULT_PARSER.escape(client_certificate.path)}" + end + + after do + client_certificate.unlink + rescue StandardError + # no-op + end + + variations.each do |description, options| + context description do + let(:client) { Mongo::Client.new(uri, options) } + + include_examples 'connects successfully' + include_examples 'performs CRUD operations' + end + end + end +end From 124b1be6a38d6393064f8e401dc6f0d02a2f1b96 Mon Sep 17 00:00:00 2001 From: Dmitry Rybakov Date: Thu, 9 Jul 2026 14:40:37 +0200 Subject: [PATCH 2/3] Bumpd det --- .mod/drivers-evergreen-tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mod/drivers-evergreen-tools b/.mod/drivers-evergreen-tools index 9c21ebe36b..b0aded0e55 160000 --- a/.mod/drivers-evergreen-tools +++ b/.mod/drivers-evergreen-tools @@ -1 +1 @@ -Subproject commit 9c21ebe36bf099988360cb83288e4820aa3bf27c +Subproject commit b0aded0e55f747fafa942e2368e77bd128fc5755 From 4bd6a09a9c3077a65dcf999c2cda0e49af826aa4 Mon Sep 17 00:00:00 2001 From: Dmitry Rybakov Date: Thu, 9 Jul 2026 16:32:03 +0200 Subject: [PATCH 3/3] RUBY-3800 DIAGNOSTIC present X.509 cert on all SFP connections The SFP proxy closes certless TLS handshakes on SFP_ATLAS_URI with "SSL_read: unexpected eof", while X.509 connections (which present a client certificate) succeed. Present the certificate at the TLS layer for the unauthenticated and SCRAM connections too, with the auth mechanism unchanged, to confirm whether the proxy requires mutual TLS for all connections. This is a diagnostic, not a final fix. --- spec/sfp/sfp_spec.rb | 52 +++++++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/spec/sfp/sfp_spec.rb b/spec/sfp/sfp_spec.rb index 0631707082..db028649d4 100644 --- a/spec/sfp/sfp_spec.rb +++ b/spec/sfp/sfp_spec.rb @@ -24,10 +24,37 @@ 'with server API version 1' => { server_api: { version: '1' } }, }.freeze + # DIAGNOSTIC (RUBY-3800): the SFP proxy closes certless TLS handshakes on + # SFP_ATLAS_URI ("SSL_read: unexpected eof"), while connections presenting the + # X.509 client certificate succeed. Present the certificate at the TLS layer + # for every connection (auth mechanism unchanged) to test whether the proxy + # requires mutual TLS universally. + let(:client_certificate) do + decoded = Base64.strict_decode64(ENV.fetch('SFP_ATLAS_X509_BASE64')) + file = Tempfile.new([ 'sfp-x509-cert', '.pem' ]) + file.write(decoded) + File.chmod(0o600, file.path) + file.close + file + end + + # tlsCertificateKeyFile maps to both :ssl_cert and :ssl_key in the driver. + let(:tls_certificate_options) do + { ssl_cert: client_certificate.path, ssl_key: client_certificate.path } + end + after do - client.close - rescue StandardError - # no-op + begin + client.close + rescue StandardError + # no-op + end + + begin + client_certificate.unlink + rescue StandardError + # no-op + end end shared_examples 'connects successfully' do @@ -68,7 +95,7 @@ context 'when unauthenticated' do let(:authenticated) { false } - let(:client) { Mongo::Client.new(ENV.fetch('SFP_ATLAS_URI')) } + let(:client) { Mongo::Client.new(ENV.fetch('SFP_ATLAS_URI'), tls_certificate_options) } include_examples 'connects successfully' end @@ -85,7 +112,7 @@ user: ENV.fetch('SFP_ATLAS_USER'), password: ENV.fetch('SFP_ATLAS_PASSWORD'), auth_mech: :scram256, - }.merge(options) + }.merge(tls_certificate_options).merge(options) ) end @@ -98,26 +125,11 @@ context 'when using X.509 authentication' do let(:authenticated) { true } - let(:client_certificate) do - decoded = Base64.strict_decode64(ENV.fetch('SFP_ATLAS_X509_BASE64')) - file = Tempfile.new([ 'sfp-x509-cert', '.pem' ]) - file.write(decoded) - File.chmod(0o600, file.path) - file.close - file - end - let(:uri) do "#{ENV.fetch('SFP_ATLAS_X509_URI')}&tlsCertificateKeyFile=" \ "#{URI::DEFAULT_PARSER.escape(client_certificate.path)}" end - after do - client_certificate.unlink - rescue StandardError - # no-op - end - variations.each do |description, options| context description do let(:client) { Mongo::Client.new(uri, options) }