Skip to content

Commit 6906ce7

Browse files
committed
Add TLS support to Prometheus metrics server
- Add WithTLSConfig() and WithTLSCertFiles() server options - Support METRICS_TLS_CERT and METRICS_TLS_KEY env vars
1 parent 5504026 commit 6906ce7

File tree

3 files changed

+383
-15
lines changed

3 files changed

+383
-15
lines changed

observability/metrics/prometheus/server.go

Lines changed: 118 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,41 @@ package prometheus
1818

1919
import (
2020
"context"
21+
"crypto/tls"
22+
"crypto/x509"
2123
"fmt"
2224
"net"
2325
"net/http"
2426
"os"
2527
"strconv"
28+
"strings"
2629
"time"
2730

2831
"github.com/prometheus/client_golang/prometheus/promhttp"
32+
knativetls "knative.dev/pkg/network/tls"
2933
)
3034

3135
const (
32-
defaultPrometheusPort = "9090"
33-
defaultPrometheusReportingPeriod = 5
34-
maxPrometheusPort = 65535
35-
minPrometheusPort = 1024
36-
defaultPrometheusHost = "" // IPv4 and IPv6
37-
prometheusPortEnvName = "METRICS_PROMETHEUS_PORT"
38-
prometheusHostEnvName = "METRICS_PROMETHEUS_HOST"
36+
defaultPrometheusPort = "9090"
37+
maxPrometheusPort = 65535
38+
minPrometheusPort = 1024
39+
defaultPrometheusHost = "" // IPv4 and IPv6
40+
prometheusPortEnvName = "METRICS_PROMETHEUS_PORT"
41+
prometheusHostEnvName = "METRICS_PROMETHEUS_HOST"
42+
prometheusTLSCertEnvName = "METRICS_PROMETHEUS_TLS_CERT"
43+
prometheusTLSKeyEnvName = "METRICS_PROMETHEUS_TLS_KEY"
44+
prometheusTLSClientAuthEnvName = "METRICS_PROMETHEUS_TLS_CLIENT_AUTH"
45+
prometheusTLSClientCAFileEnv = "METRICS_PROMETHEUS_TLS_CLIENT_CA_FILE"
46+
// used with network/tls.DefaultConfigFromEnv. E.g. METRICS_PROMETHEUS_TLS_MIN_VERSION.
47+
prometheusTLSEnvPrefix = "METRICS_PROMETHEUS_"
3948
)
4049

4150
type ServerOption func(*options)
4251

4352
type Server struct {
44-
http *http.Server
53+
http *http.Server
54+
certFile string
55+
keyFile string
4556
}
4657

4758
func NewServer(opts ...ServerOption) (*Server, error) {
@@ -56,11 +67,27 @@ func NewServer(opts ...ServerOption) (*Server, error) {
5667

5768
envOverride(&o.host, prometheusHostEnvName)
5869
envOverride(&o.port, prometheusPortEnvName)
70+
envOverride(&o.certFile, prometheusTLSCertEnvName)
71+
envOverride(&o.keyFile, prometheusTLSKeyEnvName)
72+
envOverride(&o.clientAuth, prometheusTLSClientAuthEnvName)
73+
envOverride(&o.clientCAFile, prometheusTLSClientCAFileEnv)
5974

6075
if err := validate(&o); err != nil {
6176
return nil, err
6277
}
6378

79+
var tlsConfig *tls.Config
80+
if o.certFile != "" && o.keyFile != "" {
81+
cfg, err := knativetls.DefaultConfigFromEnv(prometheusTLSEnvPrefix)
82+
if err != nil {
83+
return nil, err
84+
}
85+
if err := applyPrometheusClientAuth(cfg, &o); err != nil {
86+
return nil, err
87+
}
88+
tlsConfig = cfg
89+
}
90+
6491
mux := http.NewServeMux()
6592
mux.Handle("GET /metrics", promhttp.Handler())
6693

@@ -71,22 +98,46 @@ func NewServer(opts ...ServerOption) (*Server, error) {
7198
Addr: addr,
7299
Handler: mux,
73100
// https://medium.com/a-journey-with-go/go-understand-and-mitigate-slowloris-attack-711c1b1403f6
101+
TLSConfig: tlsConfig,
74102
ReadHeaderTimeout: 5 * time.Second,
75103
},
104+
certFile: o.certFile,
105+
keyFile: o.keyFile,
76106
}, nil
77107
}
78108

79-
func (s *Server) ListenAndServe() {
80-
s.http.ListenAndServe()
109+
// ListenAndServe starts the metrics server on plain HTTP.
110+
func (s *Server) ListenAndServe() error {
111+
return s.http.ListenAndServe()
112+
}
113+
114+
// ListenAndServeTLS starts the metrics server on TLS (HTTPS) using the given certificate and key files.
115+
// When TLS is enabled via cert/key env vars, TLSConfig is built from METRICS_PROMETHEUS_TLS_* (see network/tls).
116+
func (s *Server) ListenAndServeTLS(certFile, keyFile string) error {
117+
return s.http.ListenAndServeTLS(certFile, keyFile)
118+
}
119+
120+
// Serve starts the metrics server, choosing TLS or plain HTTP based on the server configuration.
121+
// If both METRICS_PROMETHEUS_TLS_CERT and METRICS_PROMETHEUS_TLS_KEY are set, it calls ListenAndServeTLS
122+
// (TLS settings from METRICS_PROMETHEUS_TLS_* via network/tls). Otherwise it calls ListenAndServe.
123+
func (s *Server) Serve() error {
124+
if s.certFile != "" && s.keyFile != "" {
125+
return s.http.ListenAndServeTLS(s.certFile, s.keyFile)
126+
}
127+
return s.http.ListenAndServe()
81128
}
82129

83130
func (s *Server) Shutdown(ctx context.Context) error {
84131
return s.http.Shutdown(ctx)
85132
}
86133

87134
type options struct {
88-
host string
89-
port string
135+
host string
136+
port string
137+
certFile string
138+
keyFile string
139+
clientAuth string
140+
clientCAFile string
90141
}
91142

92143
func WithHost(host string) ServerOption {
@@ -113,6 +164,28 @@ func validate(o *options) error {
113164
port, minPrometheusPort, maxPrometheusPort)
114165
}
115166

167+
if (o.certFile != "" && o.keyFile == "") || (o.certFile == "" && o.keyFile != "") {
168+
return fmt.Errorf("both %s and %s must be set or neither", prometheusTLSCertEnvName, prometheusTLSKeyEnvName)
169+
}
170+
171+
tlsEnabled := o.certFile != "" && o.keyFile != ""
172+
auth := strings.TrimSpace(strings.ToLower(o.clientAuth))
173+
174+
if auth != "" && auth != "none" && auth != "optional" && auth != "require" {
175+
return fmt.Errorf("invalid %s %q: must be %q, %q, or %q",
176+
prometheusTLSClientAuthEnvName, o.clientAuth, "none", "optional", "require")
177+
}
178+
179+
if !tlsEnabled && ((auth != "" && auth != "none") || o.clientCAFile != "") {
180+
return fmt.Errorf("%s and %s require TLS to be enabled (%s and %s must be set)",
181+
prometheusTLSClientAuthEnvName, prometheusTLSClientCAFileEnv, prometheusTLSCertEnvName, prometheusTLSKeyEnvName)
182+
}
183+
184+
if tlsEnabled && (auth == "optional" || auth == "require") && strings.TrimSpace(o.clientCAFile) == "" {
185+
return fmt.Errorf("%s must be set when %s is %q (client certs cannot be validated without a CA)",
186+
prometheusTLSClientCAFileEnv, prometheusTLSClientAuthEnvName, auth)
187+
}
188+
116189
return nil
117190
}
118191

@@ -122,3 +195,36 @@ func envOverride(target *string, envName string) {
122195
*target = val
123196
}
124197
}
198+
199+
// applyPrometheusClientAuth configures mTLS (client certificate verification) on cfg.
200+
// o.clientAuth and o.clientCAFile are populated from env vars; validate() has already checked them.
201+
func applyPrometheusClientAuth(cfg *tls.Config, o *options) error {
202+
v := strings.TrimSpace(strings.ToLower(o.clientAuth))
203+
if v == "" || v == "none" {
204+
return nil
205+
}
206+
207+
var clientAuth tls.ClientAuthType
208+
switch v {
209+
case "optional":
210+
clientAuth = tls.VerifyClientCertIfGiven
211+
case "require":
212+
clientAuth = tls.RequireAndVerifyClientCert
213+
}
214+
215+
caFile := strings.TrimSpace(o.clientCAFile)
216+
if caFile != "" {
217+
pem, err := os.ReadFile(caFile)
218+
if err != nil {
219+
return fmt.Errorf("reading %s: %w", prometheusTLSClientCAFileEnv, err)
220+
}
221+
pool := x509.NewCertPool()
222+
if !pool.AppendCertsFromPEM(pem) {
223+
return fmt.Errorf("no valid CA certificates found in %s", prometheusTLSClientCAFileEnv)
224+
}
225+
cfg.ClientCAs = pool
226+
}
227+
228+
cfg.ClientAuth = clientAuth
229+
return nil
230+
}

0 commit comments

Comments
 (0)