Skip to content

Commit 5834c5d

Browse files
authored
Add TLS support to Prometheus metrics server (#3322)
- Add WithTLSConfig() and WithTLSCertFiles() server options - Support METRICS_TLS_CERT and METRICS_TLS_KEY env vars
1 parent b3fe2e5 commit 5834c5d

3 files changed

Lines changed: 409 additions & 10 deletions

File tree

observability/metrics/prometheus/server.go

Lines changed: 117 additions & 8 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 (
3236
defaultPrometheusPort = "9090"
33-
defaultPrometheusReportingPeriod = 5
3437
maxPrometheusPort = 65535
3538
minPrometheusPort = 1024
3639
defaultPrometheusHost = "" // IPv4 and IPv6
3740
prometheusPortEnvName = "METRICS_PROMETHEUS_PORT"
3841
prometheusHostEnvName = "METRICS_PROMETHEUS_HOST"
42+
prometheusTLSCertEnvName = "METRICS_PROMETHEUS_TLS_CERT"
43+
prometheusTLSKeyEnvName = "METRICS_PROMETHEUS_TLS_KEY"
44+
prometheusTLSClientAuthEnvName = "METRICS_PROMETHEUS_TLS_CLIENT_AUTH"
45+
prometheusTLSClientCAFileEnvName = "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,37 +67,75 @@ 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, prometheusTLSClientCAFileEnvName)
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

6794
addr := net.JoinHostPort(o.host, o.port)
6895

6996
return &Server{
7097
http: &http.Server{
71-
Addr: addr,
72-
Handler: mux,
98+
Addr: addr,
99+
Handler: mux,
100+
TLSConfig: tlsConfig,
73101
// https://medium.com/a-journey-with-go/go-understand-and-mitigate-slowloris-attack-711c1b1403f6
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+
func (s *Server) ListenAndServeTLS(certFile, keyFile string) error {
116+
return s.http.ListenAndServeTLS(certFile, keyFile)
117+
}
118+
119+
// Serve starts the metrics server, choosing TLS or plain HTTP based on the server configuration.
120+
// If both METRICS_PROMETHEUS_TLS_CERT and METRICS_PROMETHEUS_TLS_KEY are set, it calls ListenAndServeTLS
121+
func (s *Server) Serve() error {
122+
if s.certFile != "" && s.keyFile != "" {
123+
return s.http.ListenAndServeTLS(s.certFile, s.keyFile)
124+
}
125+
return s.http.ListenAndServe()
81126
}
82127

83128
func (s *Server) Shutdown(ctx context.Context) error {
84129
return s.http.Shutdown(ctx)
85130
}
86131

87132
type options struct {
88-
host string
89-
port string
133+
host string
134+
port string
135+
certFile string
136+
keyFile string
137+
clientAuth string
138+
clientCAFile string
90139
}
91140

92141
func WithHost(host string) ServerOption {
@@ -113,6 +162,33 @@ func validate(o *options) error {
113162
port, minPrometheusPort, maxPrometheusPort)
114163
}
115164

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

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

0 commit comments

Comments
 (0)