Skip to content

Commit c807e9f

Browse files
committed
Enable TLS automatically for Amazon RDS endpoints
A connection to an *.rds.amazonaws.com address now verifies against Amazon's RDS root bundle, embedded here, unless the DSN asked for something else. Three Block repositories had already written this: strata's pkg/mysqlrds, vitess's go/vt/topo/mysqltopo, and spirit's pkg/dbconn each carry a copy of the bundle, a hostname regexp, and a RegisterTLSConfig call. They agree on what should happen and disagree on the details — two of the three regexps require the leading dot in `.rds.amazonaws.com` and one does not, one pins a TLS minimum version and two take the Go default, one checks the result of AppendCertsFromPEM and two discard it, and strata's bundle is three ca-west-1 roots newer than the other two. Every consumer of the driver has to get all of that right independently, and getting it wrong by omission produces an unencrypted connection rather than an error. The driver is where this belongs: it is the only layer that sees every connection, and the address is all the input it needs. Design: - The hook is one line in Config.normalize, so it covers both entry points (ParseDSN and NewConnector) and everything downstream of them. All the logic is in rds.go, a file upstream does not have. - It fires only when neither cfg.TLS nor cfg.TLSConfig is set, so anything the DSN specifies wins — including tls=false, which is the documented opt-out. normalize then fills in ServerName as it does for any other config, making this identity verification and not just encryption. - The regexp requires the leading dot. Without it `notrds.amazonaws.com` matches; that fails safely (verification against RDS roots fails rather than trusting the wrong CA) but a confusing handshake error is still worse than not matching. - MinVersion is TLS 1.2 rather than the Go default, so a future change to that default cannot quietly weaken an RDS connection. - The bundle is the newest of the three (strata's, which has ca-west-1), and covers the aws partition only — which is why the regexp does not match the China or GovCloud endpoint forms. RDSTLSConfig() is exported for anything that needs the trust store under a different name. Tests cover the endpoint patterns including the near-misses, the precedence of each way a DSN can specify TLS, the independence of the per-connection configs (normalize writes ServerName into them), and the bundle itself — a truncated PEM would otherwise parse into an empty pool and fail every RDS connection at handshake time, a long way from the mistake. Full suite passes against MySQL 8.0.44, race enabled.
1 parent a3178f8 commit c807e9f

5 files changed

Lines changed: 3377 additions & 4 deletions

File tree

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,19 @@ import path or driver name.
1616
| --- | --- | --- |
1717
| [`QueryResultContext`](unified.go) | Executes arbitrary SQL and returns the response in the shape the server chose — exactly one of `driver.Rows` or `driver.Result`. Callers handling SQL they did not write (a proxy, a REPL) otherwise have to classify statements up front to pick between `QueryContext` and `ExecContext`, and a misclassification either discards a resultset or loses the OK-packet metadata. | Raised upstream as [go-sql-driver/mysql#1793](https://github.com/go-sql-driver/mysql/issues/1793), still open. Merged here as [#1](https://github.com/block/mysql/pull/1). |
1818
| [`Warnings()`](warnings.go) | Exposes the warning count from the OK/EOF packet that terminated the last statement — the same number MySQL reports as `@@warning_count`. Warnings themselves live in per-connection state that only `SHOW WARNINGS` can read, so the count is what makes surfacing them affordable: it says whether that round trip would return anything. | Not yet raised upstream. Merged here as [#2](https://github.com/block/mysql/pull/2). |
19+
| [RDS auto-TLS](rds.go) | A connection to an `*.rds.amazonaws.com` endpoint verifies against Amazon's RDS root bundle, embedded here, unless the DSN asked for something else. Without it every deployment ships its own copy of the bundle and its own `tls=` wiring, and an unencrypted RDS connection is a silent omission rather than an error. | Not yet raised upstream. |
1920

20-
Both are reached through `(*sql.Conn).Raw` and a structural interface
21+
The first two are reached through `(*sql.Conn).Raw` and a structural interface
2122
assertion, so a consumer can depend on the *capability* without a compile-time
2223
dependency on this module. See the doc comments in `unified.go` and
2324
`warnings.go` for the exact contracts.
2425

26+
RDS auto-TLS needs no API at all: it applies to any connection whose address
27+
looks like an RDS or Aurora endpoint. Anything the DSN specifies still wins,
28+
including `tls=false`, and `mysql.RDSTLSConfig()` returns the same
29+
configuration for an RDS instance reached under a name that doesn't look like
30+
one (a CNAME, or a proxy).
31+
2532
## What this fork changes
2633

2734
Two things, both for packaging reasons only. Neither alters protocol behaviour.
@@ -76,9 +83,10 @@ git fetch upstream
7683
git merge upstream/master
7784
```
7885

79-
Edits to upstream files are confined to two things: the module path and driver
80-
name (`go.mod`, `driver.go`, plus doc comments and test call sites that spell
81-
either one out), and the CI matrix (see below). The capabilities above live in
86+
Edits to upstream files are confined to three things: the module path and
87+
driver name (`go.mod`, `driver.go`, plus doc comments and test call sites that
88+
spell either one out), the CI matrix (see below), and a one-line call in
89+
`Config.normalize` that hands off to `rds.go`. The capabilities above live in
8290
files upstream does not have, which is what keeps merges near-mechanical.
8391
Additions are cheapest when they follow the same shape: new files, or new
8492
methods on existing types, in preference to reworking an upstream code path.

dsn.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,11 @@ func (cfg *Config) normalize() error {
231231
cfg.Addr = ensureHavePort(cfg.Addr)
232232
}
233233

234+
// Fork addition: an RDS endpoint with no TLS asked for in the DSN gets the
235+
// embedded RDS trust store. Runs before the switch below so that anything
236+
// the DSN did specify still wins. See rds.go.
237+
cfg.applyRDSAutoTLS()
238+
234239
if cfg.TLS == nil {
235240
switch cfg.TLSConfig {
236241
case "false", "":

rds.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2+
//
3+
// Copyright 2026 The Go-MySQL-Driver Authors. All rights reserved.
4+
//
5+
// This Source Code Form is subject to the terms of the Mozilla Public
6+
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7+
// You can obtain one at http://mozilla.org/MPL/2.0/.
8+
9+
package mysql
10+
11+
import (
12+
"crypto/tls"
13+
"crypto/x509"
14+
_ "embed"
15+
"regexp"
16+
"sync"
17+
)
18+
19+
// rdsGlobalBundle is Amazon's global RDS certificate bundle, containing the
20+
// root CAs for every RDS region in the aws partition.
21+
//
22+
// Refresh it from the canonical location, which is a concatenation of the
23+
// per-region bundles and is what the AWS documentation tells operators to
24+
// download:
25+
//
26+
// curl -o rdsGlobalBundle.pem https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem
27+
//
28+
// Adding a root is backwards compatible, so refreshing early costs nothing.
29+
// The bundle covers the `aws` partition only; the China (`amazonaws.com.cn`)
30+
// and GovCloud partitions publish separate trust stores, which is also why
31+
// rdsAddr does not match their endpoint forms — see RDSTLSConfig for how to
32+
// use a different bundle.
33+
//
34+
//go:embed rdsGlobalBundle.pem
35+
var rdsGlobalBundle []byte
36+
37+
// rdsAddr matches an Amazon RDS or Aurora endpoint, with an optional port.
38+
//
39+
// The leading dot is load-bearing: without it the pattern also accepts
40+
// `notrds.amazonaws.com`, so a host outside RDS could pull a connection onto
41+
// the RDS trust store. That misfires safely — verification against RDS roots
42+
// fails, rather than trusting the wrong CA — but a confusing handshake error
43+
// is still worse than not matching.
44+
var rdsAddr = regexp.MustCompile(`\.rds\.amazonaws\.com(:\d+)?$`)
45+
46+
// IsRDSAddr reports whether addr is an Amazon RDS or Aurora endpoint, with or
47+
// without a port. Connections to such an address are given TLS automatically;
48+
// see RDSTLSConfig.
49+
func IsRDSAddr(addr string) bool {
50+
return rdsAddr.MatchString(addr)
51+
}
52+
53+
// rdsRootCAs parses the embedded bundle once. A CertPool is safe for
54+
// concurrent use once built, and every TLS config below shares this one.
55+
var rdsRootCAs = sync.OnceValue(func() *x509.CertPool {
56+
pool := x509.NewCertPool()
57+
// A parse failure here would mean the embedded bundle is malformed, which
58+
// is a build-time mistake rather than a runtime condition: the result is an
59+
// empty pool, and every RDS connection then fails to verify. There is
60+
// nothing useful to do about it at this point in the call path, and
61+
// TestRDSGlobalBundleParses catches it before it can ship.
62+
pool.AppendCertsFromPEM(rdsGlobalBundle)
63+
return pool
64+
})
65+
66+
// RDSTLSConfig returns a TLS configuration that verifies an Amazon RDS or
67+
// Aurora server against the embedded RDS root bundle.
68+
//
69+
// Connections to an address IsRDSAddr recognizes get this automatically, so
70+
// most callers never need it. Use it for an RDS instance reached under a name
71+
// that does not look like one — a CNAME, or a proxy — by registering it and
72+
// naming it in the DSN:
73+
//
74+
// mysql.RegisterTLSConfig("rds", mysql.RDSTLSConfig())
75+
// db, _ := sql.Open("block-mysql", "user:pass@tcp(db.internal:3306)/schema?tls=rds")
76+
//
77+
// Each call returns a new config, so it can be modified freely — to trust a
78+
// different partition's bundle, for instance. The returned config verifies the
79+
// server name, so a proxy must present a certificate for the name dialed.
80+
func RDSTLSConfig() *tls.Config {
81+
return &tls.Config{
82+
RootCAs: rdsRootCAs(),
83+
// RDS has supported TLS 1.2 everywhere for years, and 1.0/1.1 are
84+
// deprecated. Upstream leaves this to the Go default (currently 1.2 for
85+
// clients); pinning it means a future default change cannot quietly
86+
// weaken an RDS connection.
87+
MinVersion: tls.VersionTLS12,
88+
}
89+
}
90+
91+
// applyRDSAutoTLS gives a connection to an RDS endpoint a verified TLS
92+
// configuration when the DSN did not ask for one.
93+
//
94+
// This is a fork addition (see README.md). Upstream leaves TLS entirely to the
95+
// DSN, which in an RDS deployment means every caller has to carry the bundle
96+
// and wire up `tls=` itself — Block had three separate copies of exactly that
97+
// before this existed. Doing it in the driver makes the safe thing the default
98+
// while leaving it fully overridable: any explicit `tls=` in the DSN, including
99+
// `tls=false`, is honoured, because this only fires when nothing else set one.
100+
func (cfg *Config) applyRDSAutoTLS() {
101+
if cfg.TLS != nil || cfg.TLSConfig != "" {
102+
return
103+
}
104+
if !IsRDSAddr(cfg.Addr) {
105+
return
106+
}
107+
cfg.TLS = RDSTLSConfig()
108+
}

0 commit comments

Comments
 (0)