From 940366d6e5fceaff7bed294202d1305bdea5b524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Verstraeten?= Date: Tue, 2 Jun 2026 21:44:01 +0200 Subject: [PATCH] Add TLS support and CA handling for Mongo client Add TLS configuration options to MongoOptions (TLS, TLSCAFile, TLSInsecureSkipVerify) and corresponding builder setters. Implement applyTLSConfig to attach a tls.Config to the mongo client options, load and parse a PEM CA bundle (with error handling) and implicitly enable TLS when a CA file is provided. Wire TLS config into newMongoClientFromURI and newMongoClientFromComponents. Add unit tests for the new setters and behavior, and update README with an example and option table for TLS usage (e.g. AWS DocumentDB). --- README.md | 35 +++++++++++++++- pkg/database/mongodb.go | 79 ++++++++++++++++++++++++++++++++++++ pkg/database/mongodb_test.go | 43 ++++++++++++++++++++ 3 files changed, 156 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a8b7216..be70dc9 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,40 @@ func main() { } ``` -### Query Operations +### TLS / SSL Connections (e.g. AWS DocumentDB) + +MongoDB-compatible services such as AWS DocumentDB can enforce in-transit +encryption. Enable TLS through the builder and, when the service uses a private +certificate authority, point it at the CA bundle (for DocumentDB this is the +global RDS CA bundle, `global-bundle.pem`): + +```go +opts := database.NewMongoOptions(). + SetHost("docdb-cluster.cluster-xxxx.eu-west-1.docdb.amazonaws.com:27017"). + SetAuthSource("admin"). + SetUsername("user"). + SetPassword("password"). + SetTimeout(30). + SetRetryWrites(false). // DocumentDB does not support retryable writes + SetTLS(true). // enforce TLS + SetTLSCAFile("/etc/ssl/global-bundle.pem"). // trust the DocumentDB CA bundle (implicitly enables TLS) + Build() + +db, err := database.New(opts) +if err != nil { + log.Fatal(err) +} +``` + +TLS builder options: + +| Method | Description | +|--------|-------------| +| `SetTLS(bool)` | Enables/disables a TLS connection to the server. | +| `SetTLSCAFile(string)` | Path to a PEM file with the trusted certificate authorities. Setting a non-empty path implicitly enables TLS. | +| `SetTLSInsecureSkipVerify(bool)` | Disables certificate/hostname verification. Insecure — local testing only. | + + #### FindOne with Fluent API diff --git a/pkg/database/mongodb.go b/pkg/database/mongodb.go index 98e82cc..0600481 100644 --- a/pkg/database/mongodb.go +++ b/pkg/database/mongodb.go @@ -2,7 +2,10 @@ package database import ( "context" + "crypto/tls" + "crypto/x509" "fmt" + "os" "time" "github.com/go-playground/validator/v10" @@ -23,6 +26,18 @@ type MongoOptions struct { AuthMechanism string ReplicaSet string RetryWrites bool + + // TLS enables a TLS/SSL connection to the server. This is required for + // MongoDB-compatible services that enforce in-transit encryption, such as + // AWS DocumentDB with TLS turned on. + TLS bool + // TLSCAFile is the path to a PEM file containing one or more certificate + // authorities to trust when making a TLS connection. For AWS DocumentDB this + // is the global RDS CA bundle (e.g. global-bundle.pem). + TLSCAFile string + // TLSInsecureSkipVerify disables certificate and hostname verification. + // This is insecure and should only be used for local testing. + TLSInsecureSkipVerify bool } // Validate validates the MongoOptions configuration @@ -99,6 +114,32 @@ func (b *MongoOptionsBuilder) SetRetryWrites(retryWrites bool) *MongoOptionsBuil return b } +// SetTLS enables or disables a TLS/SSL connection to the server. +// Enable this for MongoDB-compatible services that enforce in-transit +// encryption, such as AWS DocumentDB with TLS turned on. +func (b *MongoOptionsBuilder) SetTLS(tls bool) *MongoOptionsBuilder { + b.options.TLS = tls + return b +} + +// SetTLSCAFile sets the path to a PEM file containing one or more certificate +// authorities to trust when establishing a TLS connection. Setting this also +// implicitly enables TLS. For AWS DocumentDB use the global RDS CA bundle. +func (b *MongoOptionsBuilder) SetTLSCAFile(caFile string) *MongoOptionsBuilder { + b.options.TLSCAFile = caFile + if caFile != "" { + b.options.TLS = true + } + return b +} + +// SetTLSInsecureSkipVerify disables certificate and hostname verification. +// This is insecure and should only be used for local testing. +func (b *MongoOptionsBuilder) SetTLSInsecureSkipVerify(skip bool) *MongoOptionsBuilder { + b.options.TLSInsecureSkipVerify = skip + return b +} + // Build builds the Mongo options func (b *MongoOptionsBuilder) Build() *MongoOptions { return b.options @@ -161,6 +202,10 @@ func newMongoClientFromURI(ctx context.Context, options *MongoOptions) (Database SetRetryWrites(options.RetryWrites). SetMonitor(otelmongo.NewMonitor(otelmongo.WithCommandAttributeDisabled(false))) + if err := applyTLSConfig(opts, options); err != nil { + return nil, err + } + client, err := mongo.Connect(ctx, opts) return &MongoClient{ Client: client, @@ -202,6 +247,10 @@ func newMongoClientFromComponents(ctx context.Context, options *MongoOptions) (D clientOpts.SetServerAPIOptions(serverAPI) } + if err := applyTLSConfig(clientOpts, options); err != nil { + return nil, err + } + client, err := mongo.Connect(ctx, clientOpts) return &MongoClient{ Client: client, @@ -209,6 +258,36 @@ func newMongoClientFromComponents(ctx context.Context, options *MongoOptions) (D }, err } +// applyTLSConfig configures TLS on the provided client options when TLS is +// enabled. It optionally loads a custom CA bundle (required by managed services +// such as AWS DocumentDB) and supports skipping verification for local testing. +func applyTLSConfig(clientOpts *moptions.ClientOptions, options *MongoOptions) error { + if !options.TLS && options.TLSCAFile == "" { + return nil + } + + tlsConfig := &tls.Config{ + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: options.TLSInsecureSkipVerify, + } + + if options.TLSCAFile != "" { + caBytes, err := os.ReadFile(options.TLSCAFile) + if err != nil { + return fmt.Errorf("failed to read TLS CA file %q: %w", options.TLSCAFile, err) + } + + caPool := x509.NewCertPool() + if !caPool.AppendCertsFromPEM(caBytes) { + return fmt.Errorf("failed to parse any certificates from TLS CA file %q", options.TLSCAFile) + } + tlsConfig.RootCAs = caPool + } + + clientOpts.SetTLSConfig(tlsConfig) + return nil +} + // Ping pings the MongoDB server to check connectivity func (m *MongoClient) Ping(ctx context.Context) error { err := m.Client.Ping(ctx, nil) diff --git a/pkg/database/mongodb_test.go b/pkg/database/mongodb_test.go index ec60455..39ede3f 100644 --- a/pkg/database/mongodb_test.go +++ b/pkg/database/mongodb_test.go @@ -216,6 +216,49 @@ func TestMongoOptionsBuilder(t *testing.T) { } }) + t.Run("TLSSetters", func(t *testing.T) { + opts := NewMongoOptions(). + SetUri("mongodb://localhost"). + SetTimeout(5000). + SetTLS(true). + SetTLSInsecureSkipVerify(true). + Build() + + if !opts.TLS { + t.Error("expected TLS to be true") + } + if !opts.TLSInsecureSkipVerify { + t.Error("expected TLSInsecureSkipVerify to be true") + } + }) + + t.Run("TLSCAFileImplicitlyEnablesTLS", func(t *testing.T) { + opts := NewMongoOptions(). + SetUri("mongodb://localhost"). + SetTimeout(5000). + SetTLSCAFile("/etc/ssl/global-bundle.pem"). + Build() + + if opts.TLSCAFile != "/etc/ssl/global-bundle.pem" { + t.Errorf("expected TLSCAFile to be '/etc/ssl/global-bundle.pem', got '%s'", opts.TLSCAFile) + } + if !opts.TLS { + t.Error("expected TLS to be implicitly enabled when a CA file is set") + } + }) + + t.Run("EmptyTLSCAFileDoesNotEnableTLS", func(t *testing.T) { + opts := NewMongoOptions(). + SetUri("mongodb://localhost"). + SetTimeout(5000). + SetTLSCAFile(""). + Build() + + if opts.TLS { + t.Error("expected TLS to remain disabled when CA file is empty") + } + }) + t.Run("PartialBuilder", func(t *testing.T) { opts := NewMongoOptions(). SetUri("mongodb://localhost").