@@ -18,30 +18,41 @@ package prometheus
1818
1919import (
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
3135const (
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
4150type ServerOption func (* options )
4251
4352type Server struct {
44- http * http.Server
53+ http * http.Server
54+ certFile string
55+ keyFile string
4556}
4657
4758func 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
83128func (s * Server ) Shutdown (ctx context.Context ) error {
84129 return s .http .Shutdown (ctx )
85130}
86131
87132type 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
92141func 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