Skip to content

Commit 9367cfd

Browse files
feat: add TLS config for MySQL driver (#907)
* feat: add TLS config for MySQL driver * fix: add explicitly minimal TLS version * fix: strip tls params after tls config init * chore: cleanup * fix: register TLSConfig once * fix: minor fixes * refactor: change tag to nomysql * build: add build-nomysql task * refactor: naming, improve logs, add comments * fix: support multiple tls configs for jobs mode * fix: update func comments, formatting * docs: add MySQL Custom TLS section
1 parent 73ef8a1 commit 9367cfd

File tree

5 files changed

+185
-0
lines changed

5 files changed

+185
-0
lines changed

Makefile

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ GO := go
2323
GOPATH ?= $(firstword $(subst :, ,$(shell $(GO) env GOPATH)))
2424
PROMU := $(GOPATH)/bin/promu
2525
PROMU_VERSION := v0.17.0
26+
YQ := $(GOPATH)/bin/yq
27+
YQ_VERSION := v4.52.4
28+
2629
pkgs = $(shell $(GO) list ./... | grep -v /vendor/)
2730

2831
PREFIX ?= $(shell pwd)
@@ -53,6 +56,13 @@ build: promu
5356
@echo ">> building binaries"
5457
@$(PROMU) build --prefix $(PREFIX)
5558

59+
build-nomysql: yq promu
60+
@echo ">> building binaries with -tag nomysql"
61+
@echo ">> updating build tags to include 'nomysql'"
62+
@$(YQ) eval '.build |= (.flags += ",nomysql")' .promu.yml > .promu_nomysql.yml
63+
@$(PROMU) build --prefix $(PREFIX) --config .promu_nomysql.yml
64+
@rm .promu_nomysql.yml
65+
5666
drivers-%:
5767
@echo ">> generating drivers.go with selected drivers"
5868
@$(GO) run drivers_gen.go -- $*
@@ -87,11 +97,19 @@ promu:
8797
@set GOOS=windows
8898
@set GOARCH=$(subst AMD64,amd64,$(patsubst i%86,386,$(shell echo %PROCESSOR_ARCHITECTURE%)))
8999
@$(GO) install github.com/prometheus/promu@$(PROMU_VERSION)
100+
yq:
101+
@set GOOS=windows
102+
@set GOARCH=$(subst AMD64,amd64,$(patsubst i%86,386,$(shell echo %PROCESSOR_ARCHITECTURE%)))
103+
$(GO) install github.com/mikefarah/yq/v4@$(YQ_VERSION)
90104
else
91105
promu:
92106
@GOOS=$(shell uname -s | tr A-Z a-z) \
93107
GOARCH=$(subst x86_64,amd64,$(patsubst i%86,386,$(shell uname -m))) \
94108
$(GO) install github.com/prometheus/promu@$(PROMU_VERSION)
109+
yq:
110+
@GOOS=$(shell uname -s | tr A-Z a-z) \
111+
GOARCH=$(subst x86_64,amd64,$(patsubst i%86,386,$(shell uname -m))) \
112+
$(GO) install github.com/mikefarah/yq/v4@$(YQ_VERSION)
95113
endif
96114

97115
.PHONY: all style format build test vet tarball docker promu

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,35 @@ The format of the file is described in the
432432
[exporter-toolkit](https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md) repository.
433433
</details>
434434

435+
<details>
436+
<summary>MySQL Custom TLS Certificate Support</summary>
437+
438+
Since v0.20.0 SQL Exporter supports custom TLS certificates for MySQL connections. This is useful when you have a
439+
self-signed certificate, a certificate from a private CA, or want to use mTLS for MySQL connections.
440+
441+
To use custom TLS certificates for MySQL connections, you need to add the following parameters to the DSN:
442+
443+
1. `tls=custom` to indicate that you want to use a custom TLS configuration (required to enable custom TLS support);
444+
2. `tls-ca=<PATH_TO_CA_CERT>` to specify the path to the CA certificate file (if your certificate is self-signed or
445+
from a private CA);
446+
3. `tls-cert=<PATH_TO_CLIENT_CERT>` and `tls-key=<PATH_TO_CLIENT_KEY>` to specify the paths to the client certificate
447+
and key files if you want to use mTLS (if your MySQL server requires client authentication).
448+
449+
The DSN would look like this:
450+
```
451+
mysql://user:password@hostname:port/dbname?tls=custom&tls-ca=/path/to/ca.pem
452+
mysql://user:password@hostname:port/dbname?tls=custom&tls-cert=/path/to/client-cert.pem&tls-key=/path/to/client-key.pem
453+
```
454+
455+
This configuration is only applied to MySQL as there is no way to provide the configuration natively. For other
456+
databases, you may want to consult their documentation on how to set up TLS connections and apply the necessary
457+
parameters to the DSN if it's supported by the driver.
458+
459+
TLS Configuration is bound to the hostname+port combinations, so if there are connections using the same hostname+port
460+
combination, they will share and re-use the same TLS configuration.
461+
</details>
462+
463+
## Support
435464

436465
If you have an issue using sql_exporter, please check [Discussions](https://github.com/burningalchemist/sql_exporter/discussions) or
437466
closed [Issues](https://github.com/burningalchemist/sql_exporter/issues?q=is%3Aissue+is%3Aclosed) first. Chances are

sql.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package sql_exporter
33
import (
44
"context"
55
"database/sql"
6+
"encoding/base64"
67
"errors"
78
"fmt"
89
"log/slog"
@@ -33,6 +34,38 @@ func OpenConnection(ctx context.Context, logContext, dsn string, maxConns, maxId
3334
driver = url.GoDriver
3435
}
3536

37+
// Register custom TLS config for MySQL if needed
38+
if driver == "mysql" && url.Query().Get("tls") == "custom" {
39+
40+
// Encode the hostname and port to create a unique name for the TLS configuration. This ensures that different
41+
// DSNs with the same TLS parameters will reuse the same TLS configuration.
42+
configName := "custom-" + base64.URLEncoding.WithPadding(base64.NoPadding).
43+
EncodeToString([]byte(url.Hostname()+url.Port()))
44+
45+
if err := handleMySQLTLSConfig(configName, url.Query()); err != nil {
46+
return nil,
47+
fmt.Errorf("failed to register MySQL TLS config: %w", err)
48+
}
49+
50+
// Strip TLS parameters from the URL as they are interpreted as system variables by the MySQL driver which
51+
// causes connection failure. The TLS configuration is already registered globally.
52+
q := url.Query()
53+
for _, param := range mysqlTLSParams {
54+
q.Del(param)
55+
}
56+
57+
// Set the "tls" parameter to the unique name of the registered TLS configuration.
58+
q.Set("tls", configName)
59+
url.RawQuery = q.Encode()
60+
61+
// Regenerate the DSN without TLS parameters for logging and connection purposes
62+
tlsStripped, _, err := dburl.GenMysql(url)
63+
if err != nil {
64+
return nil, fmt.Errorf("failed to generate MySQL DSN: %w", err)
65+
}
66+
url.DSN = tlsStripped
67+
}
68+
3669
// Open the DB handle in a separate goroutine so we can terminate early if the context closes.
3770
go func() {
3871
conn, err = sql.Open(driver, url.DSN)

tls_mysql.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//go:build !nomysql
2+
3+
package sql_exporter
4+
5+
import (
6+
"crypto/tls"
7+
"crypto/x509"
8+
"errors"
9+
"fmt"
10+
"log/slog"
11+
"net/url"
12+
"os"
13+
"sync"
14+
15+
"github.com/go-sql-driver/mysql"
16+
)
17+
18+
const (
19+
mysqlTLSParamCACert = "tls-ca"
20+
mysqlTLSParamClientCert = "tls-cert"
21+
mysqlTLSParamClientKey = "tls-key"
22+
)
23+
24+
// mysqlTLSParams is a list of TLS parameters that can be used in MySQL DSNs. It is used to identify and strip TLS
25+
// parameters from the DSN after registering the TLS configuration, as these parameters are not recognized by the MySQL
26+
// driver and would cause connection failure if left in the DSN.
27+
var (
28+
mysqlTLSParams = []string{mysqlTLSParamCACert, mysqlTLSParamClientCert, mysqlTLSParamClientKey}
29+
30+
onceMap sync.Map
31+
)
32+
33+
// handleMySQLTLSConfig wraps the registration of a MySQL TLS configuration in a thread-safe manner. It uses a
34+
// sync.Once to ensure that the TLS configuration for a given config name is registered only once, even if multiple
35+
// goroutines attempt to register it concurrently.
36+
func handleMySQLTLSConfig(configName string, params url.Values) error {
37+
onceConn, _ := onceMap.LoadOrStore(configName, &sync.Once{})
38+
once := onceConn.(*sync.Once)
39+
var err error
40+
once.Do(func() {
41+
err = registerMySQLTLSConfig(configName, params)
42+
if err != nil {
43+
slog.Error("Failed to register MySQL TLS config", "error", err)
44+
}
45+
})
46+
return err
47+
}
48+
49+
// registerMySQLTLSConfig registers a custom TLS configuration for MySQL with the given config name and parameters.
50+
func registerMySQLTLSConfig(configName string, params url.Values) error {
51+
caCert := params.Get(mysqlTLSParamCACert)
52+
clientCert := params.Get(mysqlTLSParamClientCert)
53+
clientKey := params.Get(mysqlTLSParamClientKey)
54+
55+
slog.Debug("MySQL TLS config", "configName", configName, mysqlTLSParamCACert, caCert,
56+
mysqlTLSParamClientCert, clientCert, mysqlTLSParamClientKey, clientKey)
57+
58+
var rootCertPool *x509.CertPool
59+
if caCert != "" {
60+
rootCertPool = x509.NewCertPool()
61+
pem, err := os.ReadFile(caCert)
62+
if err != nil {
63+
return fmt.Errorf("failed to read CA certificate: %w", err)
64+
}
65+
if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
66+
return errors.New("failed to append PEM")
67+
}
68+
}
69+
70+
var certs []tls.Certificate
71+
if clientCert != "" || clientKey != "" {
72+
if clientCert == "" || clientKey == "" {
73+
return errors.New("both tls-cert and tls-key must be provided for client authentication")
74+
}
75+
cert, err := tls.LoadX509KeyPair(clientCert, clientKey)
76+
if err != nil {
77+
return fmt.Errorf("failed to load client certificate and key: %w", err)
78+
}
79+
certs = append(certs, cert)
80+
}
81+
82+
tlsConfig := &tls.Config{
83+
RootCAs: rootCertPool,
84+
Certificates: certs,
85+
MinVersion: tls.VersionTLS12,
86+
}
87+
88+
return mysql.RegisterTLSConfig(configName, tlsConfig)
89+
}

tls_nomysql.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//go:build nomysql
2+
3+
package sql_exporter
4+
5+
import (
6+
"errors"
7+
"net/url"
8+
)
9+
10+
// There are no TLS parameters to strip when MySQL support is disabled, but we need to define the variable to avoid compilation errors in sql.go.
11+
var mysqlTLSParams = []string{}
12+
13+
// registerMySQLTLSConfig is a stub function that returns an error indicating that MySQL TLS support is disabled when the "nomysql" build tag is used.
14+
func handleMySQLTLSConfig(_ url.Values) error {
15+
return errors.New("MySQL TLS support disabled (built with -tags nomysql)")
16+
}

0 commit comments

Comments
 (0)