@@ -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 (
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
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,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
83130func (s * Server ) Shutdown (ctx context.Context ) error {
84131 return s .http .Shutdown (ctx )
85132}
86133
87134type 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
92143func 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