Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ GO := go
GOPATH ?= $(firstword $(subst :, ,$(shell $(GO) env GOPATH)))
PROMU := $(GOPATH)/bin/promu
PROMU_VERSION := v0.17.0
YQ := $(GOPATH)/bin/yq
YQ_VERSION := v4.52.4

pkgs = $(shell $(GO) list ./... | grep -v /vendor/)

PREFIX ?= $(shell pwd)
Expand Down Expand Up @@ -53,6 +56,13 @@ build: promu
@echo ">> building binaries"
@$(PROMU) build --prefix $(PREFIX)

build-nomysql: yq promu
@echo ">> building binaries with -tag nomysql"
@echo ">> updating build tags to include 'nomysql'"
@$(YQ) eval '.build |= (.flags += ",nomysql")' .promu.yml > .promu_nomysql.yml
@$(PROMU) build --prefix $(PREFIX) --config .promu_nomysql.yml
@rm .promu_nomysql.yml

drivers-%:
@echo ">> generating drivers.go with selected drivers"
@$(GO) run drivers_gen.go -- $*
Expand Down Expand Up @@ -87,11 +97,19 @@ promu:
@set GOOS=windows
@set GOARCH=$(subst AMD64,amd64,$(patsubst i%86,386,$(shell echo %PROCESSOR_ARCHITECTURE%)))
@$(GO) install github.com/prometheus/promu@$(PROMU_VERSION)
yq:
@set GOOS=windows
@set GOARCH=$(subst AMD64,amd64,$(patsubst i%86,386,$(shell echo %PROCESSOR_ARCHITECTURE%)))
$(GO) install github.com/mikefarah/yq/v4@$(YQ_VERSION)
else
promu:
@GOOS=$(shell uname -s | tr A-Z a-z) \
GOARCH=$(subst x86_64,amd64,$(patsubst i%86,386,$(shell uname -m))) \
$(GO) install github.com/prometheus/promu@$(PROMU_VERSION)
yq:
@GOOS=$(shell uname -s | tr A-Z a-z) \
GOARCH=$(subst x86_64,amd64,$(patsubst i%86,386,$(shell uname -m))) \
$(GO) install github.com/mikefarah/yq/v4@$(YQ_VERSION)
endif

.PHONY: all style format build test vet tarball docker promu
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,35 @@ The format of the file is described in the
[exporter-toolkit](https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md) repository.
</details>

<details>
<summary>MySQL Custom TLS Certificate Support</summary>

Since v0.20.0 SQL Exporter supports custom TLS certificates for MySQL connections. This is useful when you have a
self-signed certificate, a certificate from a private CA, or want to use mTLS for MySQL connections.

To use custom TLS certificates for MySQL connections, you need to add the following parameters to the DSN:

1. `tls=custom` to indicate that you want to use a custom TLS configuration (required to enable custom TLS support);
2. `tls-ca=<PATH_TO_CA_CERT>` to specify the path to the CA certificate file (if your certificate is self-signed or
from a private CA);
3. `tls-cert=<PATH_TO_CLIENT_CERT>` and `tls-key=<PATH_TO_CLIENT_KEY>` to specify the paths to the client certificate
and key files if you want to use mTLS (if your MySQL server requires client authentication).

The DSN would look like this:
```
mysql://user:password@hostname:port/dbname?tls=custom&tls-ca=/path/to/ca.pem
mysql://user:password@hostname:port/dbname?tls=custom&tls-cert=/path/to/client-cert.pem&tls-key=/path/to/client-key.pem
```

This configuration is only applied to MySQL as there is no way to provide the configuration natively. For other
databases, you may want to consult their documentation on how to set up TLS connections and apply the necessary
parameters to the DSN if it's supported by the driver.

TLS Configuration is bound to the hostname+port combinations, so if there are connections using the same hostname+port
combination, they will share and re-use the same TLS configuration.
</details>

## Support

If you have an issue using sql_exporter, please check [Discussions](https://github.com/burningalchemist/sql_exporter/discussions) or
closed [Issues](https://github.com/burningalchemist/sql_exporter/issues?q=is%3Aissue+is%3Aclosed) first. Chances are
Expand Down
33 changes: 33 additions & 0 deletions sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package sql_exporter
import (
"context"
"database/sql"
"encoding/base64"
"errors"
"fmt"
"log/slog"
Expand Down Expand Up @@ -33,6 +34,38 @@ func OpenConnection(ctx context.Context, logContext, dsn string, maxConns, maxId
driver = url.GoDriver
}

// Register custom TLS config for MySQL if needed
if driver == "mysql" && url.Query().Get("tls") == "custom" {

// Encode the hostname and port to create a unique name for the TLS configuration. This ensures that different
// DSNs with the same TLS parameters will reuse the same TLS configuration.
configName := "custom-" + base64.URLEncoding.WithPadding(base64.NoPadding).
EncodeToString([]byte(url.Hostname()+url.Port()))

if err := handleMySQLTLSConfig(configName, url.Query()); err != nil {
return nil,
fmt.Errorf("failed to register MySQL TLS config: %w", err)
}

// Strip TLS parameters from the URL as they are interpreted as system variables by the MySQL driver which
// causes connection failure. The TLS configuration is already registered globally.
q := url.Query()
for _, param := range mysqlTLSParams {
q.Del(param)
}

// Set the "tls" parameter to the unique name of the registered TLS configuration.
q.Set("tls", configName)
url.RawQuery = q.Encode()

// Regenerate the DSN without TLS parameters for logging and connection purposes
tlsStripped, _, err := dburl.GenMysql(url)
if err != nil {
return nil, fmt.Errorf("failed to generate MySQL DSN: %w", err)
}
url.DSN = tlsStripped
}

// Open the DB handle in a separate goroutine so we can terminate early if the context closes.
go func() {
conn, err = sql.Open(driver, url.DSN)
Expand Down
89 changes: 89 additions & 0 deletions tls_mysql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//go:build !nomysql

package sql_exporter

import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"log/slog"
"net/url"
"os"
"sync"

"github.com/go-sql-driver/mysql"
)

const (
mysqlTLSParamCACert = "tls-ca"
mysqlTLSParamClientCert = "tls-cert"
mysqlTLSParamClientKey = "tls-key"
)

// mysqlTLSParams is a list of TLS parameters that can be used in MySQL DSNs. It is used to identify and strip TLS
// parameters from the DSN after registering the TLS configuration, as these parameters are not recognized by the MySQL
// driver and would cause connection failure if left in the DSN.
var (
mysqlTLSParams = []string{mysqlTLSParamCACert, mysqlTLSParamClientCert, mysqlTLSParamClientKey}

onceMap sync.Map
)

// handleMySQLTLSConfig wraps the registration of a MySQL TLS configuration in a thread-safe manner. It uses a
// sync.Once to ensure that the TLS configuration for a given config name is registered only once, even if multiple
// goroutines attempt to register it concurrently.
func handleMySQLTLSConfig(configName string, params url.Values) error {
onceConn, _ := onceMap.LoadOrStore(configName, &sync.Once{})
once := onceConn.(*sync.Once)
var err error
once.Do(func() {
err = registerMySQLTLSConfig(configName, params)
if err != nil {
slog.Error("Failed to register MySQL TLS config", "error", err)
}
})
return err
}

// registerMySQLTLSConfig registers a custom TLS configuration for MySQL with the given config name and parameters.
func registerMySQLTLSConfig(configName string, params url.Values) error {
caCert := params.Get(mysqlTLSParamCACert)
clientCert := params.Get(mysqlTLSParamClientCert)
clientKey := params.Get(mysqlTLSParamClientKey)

slog.Debug("MySQL TLS config", "configName", configName, mysqlTLSParamCACert, caCert,
mysqlTLSParamClientCert, clientCert, mysqlTLSParamClientKey, clientKey)

var rootCertPool *x509.CertPool
if caCert != "" {
rootCertPool = x509.NewCertPool()
pem, err := os.ReadFile(caCert)
if err != nil {
return fmt.Errorf("failed to read CA certificate: %w", err)
}
if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
return errors.New("failed to append PEM")
}
}

var certs []tls.Certificate
if clientCert != "" || clientKey != "" {
if clientCert == "" || clientKey == "" {
return errors.New("both tls-cert and tls-key must be provided for client authentication")
}
cert, err := tls.LoadX509KeyPair(clientCert, clientKey)
if err != nil {
return fmt.Errorf("failed to load client certificate and key: %w", err)
}
certs = append(certs, cert)
}

tlsConfig := &tls.Config{
RootCAs: rootCertPool,
Certificates: certs,
MinVersion: tls.VersionTLS12,
}

return mysql.RegisterTLSConfig(configName, tlsConfig)
}
16 changes: 16 additions & 0 deletions tls_nomysql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//go:build nomysql

package sql_exporter

import (
"errors"
"net/url"
)

// 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.
var mysqlTLSParams = []string{}

// registerMySQLTLSConfig is a stub function that returns an error indicating that MySQL TLS support is disabled when the "nomysql" build tag is used.
func handleMySQLTLSConfig(_ url.Values) error {
return errors.New("MySQL TLS support disabled (built with -tags nomysql)")
}
Loading