11package net
22
33import (
4+ "bytes"
45 "context"
56 "crypto/tls"
7+ "crypto/x509"
68 "fmt"
79 "io"
810 "net/http"
911 "net/http/httptrace"
1012 "net/url"
1113 "strings"
1214 "sync"
15+ "sync/atomic"
1316 "time"
1417 "unicode/utf8"
1518
@@ -42,6 +45,109 @@ type clientTraceTimeT int
4245
4346const clientTraceTime clientTraceTimeT = 1
4447
48+ type CertReloader struct {
49+ cert atomic.Pointer [tls.Certificate ]
50+ sp * secrets.SecretPaths
51+ certFile string
52+ keyFile string
53+ lastCertPEM []byte
54+ lastKeyPEM []byte
55+ quit chan struct {}
56+ once sync.Once
57+ log logging.Logger
58+ }
59+
60+ // NewCertReloader reads cert and key from provided files and starts
61+ // an update loop that every interval reads the files. If cert or
62+ // key changed and are valid it will swap the data, such that
63+ // GetClientCertificate returns the new rotated *tls.Certificate.
64+ // You have to use Close() in order to not leak a goroutine.
65+ func NewCertReloader (certFile , keyFile string , interval time.Duration , log logging.Logger ) (* CertReloader , error ) {
66+ sp := secrets .NewSecretPaths (interval )
67+ if err := sp .Add (certFile ); err != nil {
68+ sp .Close ()
69+ return nil , fmt .Errorf ("cert file: %w" , err )
70+ }
71+ if err := sp .Add (keyFile ); err != nil {
72+ sp .Close ()
73+ return nil , fmt .Errorf ("key file: %w" , err )
74+ }
75+ certPEM , ok := sp .GetSecret (certFile )
76+ if ! ok {
77+ sp .Close ()
78+ return nil , fmt .Errorf ("cert file not found: %s" , certFile )
79+ }
80+ keyPEM , ok := sp .GetSecret (keyFile )
81+ if ! ok {
82+ sp .Close ()
83+ return nil , fmt .Errorf ("key file not found: %s" , keyFile )
84+ }
85+ cert , err := tls .X509KeyPair (certPEM , keyPEM )
86+ if err != nil {
87+ sp .Close ()
88+ return nil , fmt .Errorf ("initial key pair: %w" , err )
89+ }
90+ cr := & CertReloader {
91+ sp : sp ,
92+ certFile : certFile ,
93+ keyFile : keyFile ,
94+ lastCertPEM : certPEM ,
95+ lastKeyPEM : keyPEM ,
96+ quit : make (chan struct {}),
97+ log : log ,
98+ }
99+ cr .cert .Store (& cert )
100+ go cr .refreshLoop (interval )
101+ return cr , nil
102+ }
103+
104+ func (cr * CertReloader ) refreshLoop (interval time.Duration ) {
105+ ticker := time .NewTicker (interval )
106+ defer ticker .Stop ()
107+ for {
108+ select {
109+ case <- ticker .C :
110+ cr .reload ()
111+ case <- cr .quit :
112+ return
113+ }
114+ }
115+ }
116+
117+ func (cr * CertReloader ) reload () {
118+ certPEM , certOK := cr .sp .GetSecret (cr .certFile )
119+ keyPEM , keyOK := cr .sp .GetSecret (cr .keyFile )
120+ if ! certOK || ! keyOK {
121+ return
122+ }
123+ if bytes .Equal (certPEM , cr .lastCertPEM ) && bytes .Equal (keyPEM , cr .lastKeyPEM ) {
124+ return
125+ }
126+ cert , err := tls .X509KeyPair (certPEM , keyPEM )
127+ if err != nil {
128+ cr .log .Errorf ("certReloader: Failed to parse key pair: %v" , err )
129+ return
130+ }
131+ cr .lastCertPEM = certPEM
132+ cr .lastKeyPEM = keyPEM
133+ cr .log .Info ("certReloader: updated cert" )
134+ cr .cert .Store (& cert )
135+ }
136+
137+ // GetClientCertificate returns the tls.Certificate and can be used as
138+ // tls.Config.GetClientCertificate to get rotated client certs.
139+ func (cr * CertReloader ) GetClientCertificate (* tls.CertificateRequestInfo ) (* tls.Certificate , error ) {
140+ return cr .cert .Load (), nil
141+ }
142+
143+ // Close stops all background goroutines.
144+ func (cr * CertReloader ) Close () {
145+ cr .once .Do (func () {
146+ close (cr .quit )
147+ cr .sp .Close ()
148+ })
149+ }
150+
45151// Client adds additional features like Bearer token injection, and
46152// opentracing to the wrapped http.Client with the same interface as
47153// http.Client from the stdlib.
@@ -50,6 +156,7 @@ type Client struct {
50156 client http.Client
51157 tr * Transport
52158 sr secrets.SecretsReader
159+ cr * CertReloader
53160}
54161
55162// NewClient creates a wrapped http.Client and uses Transport to
@@ -74,11 +181,32 @@ func NewClient(o Options) *Client {
74181 }
75182 sp := secrets .NewSecretPaths (o .BearerTokenRefreshInterval )
76183 if err := sp .Add (o .BearerTokenFile ); err != nil {
77- o .Log .Errorf ("failed to read secret: %v" , err )
184+ o .Log .Errorf ("Failed to read secret: %v" , err )
78185 }
79186 sr = secrets .NewStaticDelegateSecret (sp , o .BearerTokenFile )
80187 }
81188
189+ var cr * CertReloader
190+ if o .CertFile != "" && o .KeyFile != "" && o .Transport == nil {
191+ if o .CertRefreshInterval == 0 {
192+ if o .BearerTokenRefreshInterval != 0 {
193+ o .CertRefreshInterval = o .BearerTokenRefreshInterval
194+ } else {
195+ o .CertRefreshInterval = defaultRefreshInterval
196+ }
197+ }
198+ var err error
199+ cr , err = NewCertReloader (o .CertFile , o .KeyFile , o .CertRefreshInterval , o .Log )
200+ if err != nil {
201+ o .Log .Errorf ("Failed to initialize cert reloader: %v" , err )
202+ } else {
203+ tr .tr .TLSClientConfig = & tls.Config {
204+ RootCAs : o .RootCAs ,
205+ GetClientCertificate : cr .GetClientCertificate ,
206+ }
207+ }
208+ }
209+
82210 c := & Client {
83211 once : sync.Once {},
84212 client : http.Client {
@@ -88,6 +216,7 @@ func NewClient(o Options) *Client {
88216 },
89217 tr : tr ,
90218 sr : sr ,
219+ cr : cr ,
91220 }
92221
93222 return c
@@ -99,6 +228,9 @@ func (c *Client) Close() {
99228 if c .sr != nil {
100229 c .sr .Close ()
101230 }
231+ if c .cr != nil {
232+ c .cr .Close ()
233+ }
102234 })
103235}
104236
@@ -229,6 +361,18 @@ type Options struct {
229361 // SecretsReader is used to read and refresh bearer tokens
230362 SecretsReader secrets.SecretsReader
231363
364+ // CertFile is the path to a PEM-encoded client certificate for mTLS.
365+ // Must be set together with KeyFile. Ignored when Transport is non-nil.
366+ CertFile string
367+ // KeyFile is the path to a PEM-encoded private key for mTLS.
368+ // Must be set together with CertFile. Ignored when Transport is non-nil.
369+ KeyFile string
370+ // CertRefreshInterval is how often CertFile/KeyFile are re-read.
371+ // Defaults to BearerTokenRefreshInterval, or 5 minutes if both are zero.
372+ CertRefreshInterval time.Duration
373+ // RootCAs to pass as part of the TLSClientConfig to the underlying http.Transport
374+ RootCAs * x509.CertPool
375+
232376 // Log is used for error logging
233377 Log logging.Logger
234378
0 commit comments