diff --git a/cmd/run.go b/cmd/run.go index c3290d59cb..006d64ff27 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -12,6 +12,7 @@ import ( "io/ioutil" "os" "path" + "time" "github.com/spf13/cobra" @@ -30,6 +31,7 @@ type runCmdParams struct { tlsCertFile string tlsPrivateKeyFile string tlsCACertFile string + tlsCertRefresh time.Duration ignore []string serverMode bool skipVersionCheck bool @@ -170,19 +172,20 @@ To skip bundle verification, use the --skip-verify flag. runCommand.Flags().StringVarP(&cmdParams.rt.HistoryPath, "history", "H", historyPath(), "set path of history file") cmdParams.rt.Addrs = runCommand.Flags().StringSliceP("addr", "a", []string{defaultAddr}, "set listening address of the server (e.g., [ip]: for TCP, unix:// for UNIX domain socket)") cmdParams.rt.DiagnosticAddrs = runCommand.Flags().StringSlice("diagnostic-addr", []string{}, "set read-only diagnostic listening address of the server for /health and /metric APIs (e.g., [ip]: for TCP, unix:// for UNIX domain socket)") - runCommand.Flags().BoolVarP(&cmdParams.rt.H2CEnabled, "h2c", "", false, "enable H2C for HTTP listeners") + runCommand.Flags().BoolVar(&cmdParams.rt.H2CEnabled, "h2c", false, "enable H2C for HTTP listeners") runCommand.Flags().StringVarP(&cmdParams.rt.OutputFormat, "format", "f", "pretty", "set shell output format, i.e, pretty, json") runCommand.Flags().BoolVarP(&cmdParams.rt.Watch, "watch", "w", false, "watch command line files for changes") addMaxErrorsFlag(runCommand.Flags(), &cmdParams.rt.ErrorLimit) - runCommand.Flags().BoolVarP(&cmdParams.rt.PprofEnabled, "pprof", "", false, "enables pprof endpoints") - runCommand.Flags().StringVarP(&cmdParams.tlsCertFile, "tls-cert-file", "", "", "set path of TLS certificate file") - runCommand.Flags().StringVarP(&cmdParams.tlsPrivateKeyFile, "tls-private-key-file", "", "", "set path of TLS private key file") - runCommand.Flags().StringVarP(&cmdParams.tlsCACertFile, "tls-ca-cert-file", "", "", "set path of TLS CA cert file") - runCommand.Flags().VarP(cmdParams.authentication, "authentication", "", "set authentication scheme") - runCommand.Flags().VarP(cmdParams.authorization, "authorization", "", "set authorization scheme") - runCommand.Flags().VarP(cmdParams.minTLSVersion, "min-tls-version", "", "set minimum TLS version to be used by OPA's server, default is 1.2") + runCommand.Flags().BoolVar(&cmdParams.rt.PprofEnabled, "pprof", false, "enables pprof endpoints") + runCommand.Flags().StringVar(&cmdParams.tlsCertFile, "tls-cert-file", "", "set path of TLS certificate file") + runCommand.Flags().StringVar(&cmdParams.tlsPrivateKeyFile, "tls-private-key-file", "", "set path of TLS private key file") + runCommand.Flags().StringVar(&cmdParams.tlsCACertFile, "tls-ca-cert-file", "", "set path of TLS CA cert file") + runCommand.Flags().DurationVar(&cmdParams.tlsCertRefresh, "tls-cert-refresh-period", 0, "set certificate refresh period") + runCommand.Flags().Var(cmdParams.authentication, "authentication", "set authentication scheme") + runCommand.Flags().Var(cmdParams.authorization, "authorization", "set authorization scheme") + runCommand.Flags().Var(cmdParams.minTLSVersion, "min-tls-version", "set minimum TLS version to be used by OPA's server") runCommand.Flags().VarP(cmdParams.logLevel, "log-level", "l", "set log level") - runCommand.Flags().VarP(cmdParams.logFormat, "log-format", "", "set log format") + runCommand.Flags().Var(cmdParams.logFormat, "log-format", "set log format") runCommand.Flags().IntVar(&cmdParams.rt.GracefulShutdownPeriod, "shutdown-grace-period", 10, "set the time (in seconds) that the server will wait to gracefully shut down") runCommand.Flags().IntVar(&cmdParams.rt.ShutdownWaitPeriod, "shutdown-wait-period", 0, "set the time (in seconds) that the server will wait before initiating shutdown") addConfigOverrides(runCommand.Flags(), &cmdParams.rt.ConfigOverrides) @@ -235,6 +238,10 @@ func initRuntime(ctx context.Context, params runCmdParams, args []string) (*runt return nil, err } + params.rt.CertificateFile = params.tlsCertFile + params.rt.CertificateKeyFile = params.tlsPrivateKeyFile + params.rt.CertificateRefresh = params.tlsCertRefresh + if params.tlsCACertFile != "" { pool, err := loadCertPool(params.tlsCACertFile) if err != nil { @@ -270,12 +277,7 @@ func initRuntime(ctx context.Context, params runCmdParams, args []string) (*runt return nil, fmt.Errorf("enable bundle mode (ie. --bundle) to verify bundle files or directories") } - rt, err := runtime.NewRuntime(ctx, params.rt) - if err != nil { - return nil, err - } - - return rt, nil + return runtime.NewRuntime(ctx, params.rt) } func startRuntime(ctx context.Context, rt *runtime.Runtime, serverMode bool) { diff --git a/docs/content/security.md b/docs/content/security.md index 857df07cf0..331ce434ff 100644 --- a/docs/content/security.md +++ b/docs/content/security.md @@ -27,6 +27,12 @@ startup: OPA will exit immediately with a non-zero status code if only one of these flags is specified. +The server can track the certificate and key files' contents, and reload them if necessary: + +- ``--tls-cert-refresh=`` specifies how often OPA should check the TLS certificate and + private key file for changes (defaults to 0s, disabling periodic refresh). This argument accepts + any duration, such as "30s", "5m" or "24h". + Note that for using TLS-based authentication, a CA cert file can be provided: - ``--tls-ca-cert-file=`` specifies the path of the file containing the CA cert. @@ -78,8 +84,9 @@ curl http://localhost:8181/v1/data curl -k https://localhost:8181/v1/data ``` -> We have to use cURL's `-k/--insecure` flag because we are using a -> self-signed certificate. +{{< info >}} +We have to use cURL's `-k/--insecure` flag because we are using a self-signed certificate. +{{< /info >}} ## Authentication and Authorization diff --git a/runtime/runtime.go b/runtime/runtime.go index 4a2f427197..a7f13f0c47 100644 --- a/runtime/runtime.go +++ b/runtime/runtime.go @@ -96,6 +96,14 @@ type Params struct { // is nil, the server will NOT use TLS. Certificate *tls.Certificate + // CertificateFile and CertificateKeyFile are the paths to the cert and its + // keyfile. It'll be used to periodically reload the files from disk if they + // have changed. The server will attempt to refresh every 5 minutes, unless + // a different CertificateRefresh time.Duration is provided + CertificateFile string + CertificateKeyFile string + CertificateRefresh time.Duration + // CertPool holds the CA certs trusted by the OPA server. CertPool *x509.CertPool @@ -403,6 +411,7 @@ func (rt *Runtime) Serve(ctx context.Context) error { WithAddresses(*rt.Params.Addrs). WithH2CEnabled(rt.Params.H2CEnabled). WithCertificate(rt.Params.Certificate). + WithCertificatePaths(rt.Params.CertificateFile, rt.Params.CertificateKeyFile, rt.Params.CertificateRefresh). WithCertPool(rt.Params.CertPool). WithAuthentication(rt.Params.Authentication). WithAuthorization(rt.Params.Authorization). diff --git a/server/certs.go b/server/certs.go new file mode 100644 index 0000000000..41f0922858 --- /dev/null +++ b/server/certs.go @@ -0,0 +1,76 @@ +// Copyright 2021 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package server + +import ( + "bytes" + "crypto/sha256" + "crypto/tls" + "io" + "os" + "time" + + "github.com/open-policy-agent/opa/logging" +) + +func (s *Server) getCertificate(h *tls.ClientHelloInfo) (*tls.Certificate, error) { + s.certMtx.RLock() + defer s.certMtx.RUnlock() + return s.cert, nil +} + +func (s *Server) certLoop(logger logging.Logger) Loop { + return func() error { + for range time.NewTicker(s.certRefresh).C { + certHash, err := hash(s.certFile) + if err != nil { + logger.Info("Failed to refresh server certificate: %s.", err.Error()) + continue + } + certKeyHash, err := hash(s.certKeyFile) + if err != nil { + logger.Info("Failed to refresh server certificate: %s.", err.Error()) + continue + } + + s.certMtx.Lock() + + different := !bytes.Equal(s.certFileHash, certHash) || + !bytes.Equal(s.certKeyFileHash, certKeyHash) + + if different { // load and store + newCert, err := tls.LoadX509KeyPair(s.certFile, s.certKeyFile) + if err != nil { + logger.Info("Failed to refresh server certificate: %s.", err.Error()) + s.certMtx.Unlock() + continue + } + s.cert = &newCert + s.certFileHash = certHash + s.certKeyFileHash = certKeyHash + logger.Debug("Refreshed server certificate.") + } + + s.certMtx.Unlock() + } + + return nil + } +} + +func hash(file string) ([]byte, error) { + f, err := os.Open(file) + if err != nil { + return nil, err + } + defer f.Close() + + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return nil, err + } + + return h.Sum(nil), nil +} diff --git a/server/server.go b/server/server.go index 244d6b01a1..327d8dc2f2 100644 --- a/server/server.go +++ b/server/server.go @@ -105,6 +105,12 @@ type Server struct { authentication AuthenticationScheme authorization AuthorizationScheme cert *tls.Certificate + certMtx sync.RWMutex + certFile string + certFileHash []byte + certKeyFile string + certKeyFileHash []byte + certRefresh time.Duration certPool *x509.CertPool minTLSVersion uint16 mtx sync.RWMutex @@ -231,6 +237,15 @@ func (s *Server) WithCertificate(cert *tls.Certificate) *Server { return s } +// WithCertificatePaths sets the server-side certificate and keyfile paths +// that the server will periodically check for changes, and reload if necessary. +func (s *Server) WithCertificatePaths(certFile, keyFile string, refresh time.Duration) *Server { + s.certFile = certFile + s.certKeyFile = keyFile + s.certRefresh = refresh + return s +} + // WithCertPool sets the server-side cert pool that the server will use. func (s *Server) WithCertPool(pool *x509.CertPool) *Server { s.certPool = pool @@ -332,12 +347,12 @@ func (s *Server) Listeners() ([]Loop, error) { for t, binding := range handlerBindings { for _, addr := range binding.addrs { - loop, listener, err := s.getListener(addr, binding.handler, t) + l, listener, err := s.getListener(addr, binding.handler, t) if err != nil { return nil, err } s.httpListeners = append(s.httpListeners, listener) - loops = append(loops, loop) + loops = append(loops, l...) } } @@ -399,7 +414,7 @@ type httpListener interface { Addr() string ListenAndServe() error ListenAndServeTLS(certFile, keyFile string) error - Shutdown(ctx context.Context) error + Shutdown(context.Context) error Type() httpListenerType } @@ -488,26 +503,39 @@ func isMinTLSVersionSupported(TLSVersion uint16) bool { return false } -func (s *Server) getListener(addr string, h http.Handler, t httpListenerType) (Loop, httpListener, error) { +func (s *Server) getListener(addr string, h http.Handler, t httpListenerType) ([]Loop, httpListener, error) { parsedURL, err := parseURL(addr, s.cert != nil) if err != nil { return nil, nil, err } + var loops []Loop var loop Loop var listener httpListener switch parsedURL.Scheme { case "unix": loop, listener, err = s.getListenerForUNIXSocket(parsedURL, h, t) + loops = []Loop{loop} case "http": loop, listener, err = s.getListenerForHTTPServer(parsedURL, h, t) + loops = []Loop{loop} case "https": loop, listener, err = s.getListenerForHTTPSServer(parsedURL, h, t) + logger := s.manager.Logger().WithFields(map[string]interface{}{ + "cert-file": s.certFile, + "cert-key-file": s.certKeyFile, + }) + if s.certRefresh > 0 { + certLoop := s.certLoop(logger) + loops = []Loop{loop, certLoop} + } else { + loops = []Loop{loop} + } default: err = fmt.Errorf("invalid url scheme %q", parsedURL.Scheme) } - return loop, listener, err + return loops, listener, err } func (s *Server) getListenerForHTTPServer(u *url.URL, h http.Handler, t httpListenerType) (Loop, httpListener, error) { @@ -521,6 +549,7 @@ func (s *Server) getListenerForHTTPServer(u *url.URL, h http.Handler, t httpList } l := newHTTPListener(&h1s, t) + return l.ListenAndServe, l, nil } @@ -534,8 +563,8 @@ func (s *Server) getListenerForHTTPSServer(u *url.URL, h http.Handler, t httpLis Addr: u.Host, Handler: h, TLSConfig: &tls.Config{ - Certificates: []tls.Certificate{*s.cert}, - ClientCAs: s.certPool, + GetCertificate: s.getCertificate, + ClientCAs: s.certPool, }, } if s.authentication == AuthenticationTLS { diff --git a/test/e2e/certrefresh/certrefresh_test.go b/test/e2e/certrefresh/certrefresh_test.go new file mode 100644 index 0000000000..70fa800706 --- /dev/null +++ b/test/e2e/certrefresh/certrefresh_test.go @@ -0,0 +1,186 @@ +// Copyright 2021 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package certrefresh + +import ( + "crypto/tls" + "crypto/x509" + "flag" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "path/filepath" + "testing" + "time" + + "github.com/open-policy-agent/opa/test/e2e" +) + +var testRuntime *e2e.TestRuntime +var pool *x509.CertPool + +// print error to stderr, exit 1 +func fatal(err interface{}) { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) +} + +const ( + certFile0 = "testdata/server-cert.pem" + certKeyFile0 = "testdata/server-key.pem" + serial0 = "481849676048721749484276160748693385016044597443" + certFile1 = "testdata/server-cert-new.pem" + certKeyFile1 = "testdata/server-key-new.pem" + serial1 = "481849676048721749484276160748693385016044597444" +) + +var certFile, certKeyFile string + +func TestMain(m *testing.M) { + flag.Parse() + caCertPEM, err := ioutil.ReadFile("testdata/ca.pem") + if err != nil { + fatal(err) + } + pool = x509.NewCertPool() + if ok := pool.AppendCertsFromPEM(caCertPEM); !ok { + fatal("failed to parse CA cert") + } + + tmp, err := os.MkdirTemp("", "e2e_certrefresh") + if err != nil { + fatal(err) + } + defer os.RemoveAll(tmp) + + certFile = filepath.Join(tmp, "server-cert.pem") + if err := copy(certFile0, certFile); err != nil { + fatal(err) + } + + certKeyFile = filepath.Join(tmp, "server-key.pem") + if err := copy(certKeyFile0, certKeyFile); err != nil { + fatal(err) + } + + cert, err := tls.LoadX509KeyPair(certFile, certKeyFile) + if err != nil { + fatal(err) + } + + testServerParams := e2e.NewAPIServerTestParams() + testServerParams.Addrs = &[]string{"https://127.0.0.1:0"} + testServerParams.CertPool = pool + testServerParams.Certificate = &cert + testServerParams.CertificateFile = certFile + testServerParams.CertificateKeyFile = certKeyFile + testServerParams.CertificateRefresh = time.Millisecond + + testRuntime, err = e2e.NewTestRuntime(testServerParams) + if err != nil { + fatal(err) + } + + // We need a client with proper TLS setup, otherwise the health check + // that loops to determine if the server is ready will fail. + testRuntime.Client = newClient() + + os.Exit(testRuntime.RunTests(m)) +} + +func TestCertificateRotation(t *testing.T) { + + // before rotation + cert := getCert(t) + if exp, act := serial0, string(cert.SerialNumber.String()); exp != act { + t.Fatalf("expected signature %s, got %s", exp, act) + } + + // replace file on disk + replaceCerts(t, certFile1, certKeyFile1) + time.Sleep(10 * time.Millisecond) // file reload happens every millisecond + + // after rotation + cert = getCert(t) + if exp, act := serial1, string(cert.SerialNumber.String()); exp != act { + t.Fatalf("expected signature %s, got %s", exp, act) + } + + // replace file with nothing + replaceCerts(t, os.DevNull, os.DevNull) + time.Sleep(10 * time.Millisecond) + + // second cert still used + cert = getCert(t) + if exp, act := serial1, string(cert.SerialNumber.String()); exp != act { + t.Fatalf("expected signature %s, got %s", exp, act) + } + + // go back to first cert + replaceCerts(t, certFile0, certKeyFile0) + time.Sleep(10 * time.Millisecond) + cert = getCert(t) + if exp, act := serial0, string(cert.SerialNumber.String()); exp != act { + t.Fatalf("expected signature %s, got %s", exp, act) + } +} + +func newClient() *http.Client { + c := *http.DefaultClient + tr := http.DefaultTransport.(*http.Transport).Clone() + tr.TLSClientConfig = &tls.Config{ + RootCAs: pool, + } + c.Transport = tr + return &c +} + +func copy(from, to string) error { + src, err := os.Open(from) + if err != nil { + return err + } + defer src.Close() + + dst, err := os.Create(to) + if err != nil { + return err + } + defer dst.Close() + + _, err = io.Copy(dst, src) + return err +} + +func getCert(t *testing.T) *x509.Certificate { + t.Helper() + u, err := url.Parse(testRuntime.URL()) + if err != nil { + t.Fatal(err) + } + c := newClient() + cfg := c.Transport.(*http.Transport).TLSClientConfig + conn, err := tls.Dial("tcp", u.Host, cfg) + if err != nil { + t.Fatalf("dial: %v", err) + } + defer conn.Close() + + return conn.ConnectionState().PeerCertificates[0] +} + +func replaceCerts(t *testing.T, cert, key string) { + t.Helper() + + if err := copy(cert, certFile); err != nil { + t.Fatal(err) + } + if err := copy(key, certKeyFile); err != nil { + t.Fatal(err) + } +} diff --git a/test/e2e/certrefresh/testdata/.gitignore b/test/e2e/certrefresh/testdata/.gitignore new file mode 100644 index 0000000000..e7fe15b29b --- /dev/null +++ b/test/e2e/certrefresh/testdata/.gitignore @@ -0,0 +1,4 @@ +*.srl +*.cnf +csr.pem +ca-key.pem diff --git a/test/e2e/certrefresh/testdata/ca.pem b/test/e2e/certrefresh/testdata/ca.pem new file mode 100644 index 0000000000..d5ded3bbe0 --- /dev/null +++ b/test/e2e/certrefresh/testdata/ca.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDATCCAemgAwIBAgIUA4ZQzwvqo24ke4p0O9+aZm/dlbswDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAwwFbXktY2EwHhcNMjExMjA3MTIzMzA4WhcNMzExMjA1MTIz +MzA4WjAQMQ4wDAYDVQQDDAVteS1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAKio/u2Lxj1qxjepg5WAkNtRb229tyV4gUviRFqpnAg8Q95k+/eUizeJ +c2D/u3/1XEwebCRsmkLHSKL+0e1pLViCqGhv6FTZZaBwzlEIdwe01h6oLC2BKv6v +wIwyyoKCW5RbMTZxkNnyvpMQ5Vg5x19iEguPvhaeuJkQ0lucAZjZ6ds9fq6XipRh +iMhNOzPX2WgYC83HQf0IcMQq1U3WQhPFn0sjfGpSShrCCTR8ZX8r5HgCyWZ/aH3d +7sFxnwwKjnKm+zRuPk6H57wuXnBcwb7f3CY2hu3PGNbqwD3fQjOqOnqPllddVU0f +eD7F0uVI1iPmw71qOXLrFk33gWKtkbECAwEAAaNTMFEwHQYDVR0OBBYEFE2v+4GQ +pdiEeW/FwxczFjYOdw05MB8GA1UdIwQYMBaAFE2v+4GQpdiEeW/FwxczFjYOdw05 +MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAEykMcbiGUfHdKZg +n8y8z7Tafspe1YuzJi7307Y8bHE3k8X/KC/UysJ+2nzx+/4f3s0Nq+w1fwZYXXiq +MBmSi7+esK23XFHxlPTIlP+/EoPq1vKOYFgcRam4a8pPFCKPxru5zmEhfU2fudv8 +8Ke2rM4bEwzsebGhE1K8JseptI5fhyG7nQ7PfKQJYafJlICnOWrQpQfQDVaiWx5e +qst5pEyaRqQmsp7GPlTklsIyt0G8QFo5DvZM+cegznkCr1X3FH3GDeWwcsNiZdS/ +2wY9cRymkEkJZqdD5aw4rIEHg6Ei1lEZp34bK9PJIyzYBB/E4oCbolXJxscrB1o+ +HdbitFY= +-----END CERTIFICATE----- diff --git a/test/e2e/certrefresh/testdata/gencerts.sh b/test/e2e/certrefresh/testdata/gencerts.sh new file mode 100755 index 0000000000..c6df4a8690 --- /dev/null +++ b/test/e2e/certrefresh/testdata/gencerts.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# adapted from +# https://github.com/dexidp/dex/blob/2d1ac74ec0ca12ae4d36072525d976c1a596820a/examples/k8s/gencert.sh#L22 + +cat <req.cnf +[req] +req_extensions = v3_req +distinguished_name = req_distinguished_name + +[req_distinguished_name] + +[v3_req] +basicConstraints = CA:FALSE +keyUsage = nonRepudiation, digitalSignature, keyEncipherment +subjectAltName = @alt_names + +[alt_names] +DNS.1 = opa.example.com +IP.1 = 127.0.0.1 +EOF + +openssl genrsa -out ca-key.pem 2048 +openssl req -x509 -new -nodes -key ca-key.pem -days 3650 -out ca.pem -subj "/CN=my-ca" + +openssl genrsa -out server-key.pem 2048 +openssl req -new -key server-key.pem -out csr.pem -subj "/CN=my-server" -config req.cnf +openssl x509 -req -in csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -days 3650 -extensions v3_req -extfile req.cnf + +openssl genrsa -out server-key-new.pem 2048 +openssl req -new -key server-key-new.pem -out csr.pem -subj "/CN=my-server" -config req.cnf +openssl x509 -req -in csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out server-cert-new.pem -days 3650 -extensions v3_req -extfile req.cnf diff --git a/test/e2e/certrefresh/testdata/server-cert-new.pem b/test/e2e/certrefresh/testdata/server-cert-new.pem new file mode 100644 index 0000000000..0af8fff69c --- /dev/null +++ b/test/e2e/certrefresh/testdata/server-cert-new.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC7jCCAdagAwIBAgIUVGbjAlogOqO2AFW1KYi4d0YasMQwDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAwwFbXktY2EwHhcNMjExMjA3MTIzMzA4WhcNMzExMjA1MTIz +MzA4WjAUMRIwEAYDVQQDDAlteS1zZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCmzS3eYtA5xT9QH0dwWSueCsJI8+Rlsf69k1q65k/JfzKxYZCW +wlOzOwY8IK5EFS9bxde5IOnXW1pyTWocIVVaI0oVTplmZtS3zv8+nWnR8ckeWUZX +22HD/s4YTvel/0Ji1bE4aErHiw31JgZ/dPoLXhCT6MoVmnGHpt2MroqruXtOSykz +6YzsQb8rIVQdo6YkTqv6/FEku6RcHhJPXt4FwLQgl5MMSU+aHRdCFmnwOxSxSYdl +ayWkO9V9dl7eSt0/EOOD7YWfUhZpK3cYhAvmZ41W5cGdztdVbPwsJG8I03xRJokf +qq6r26T/mpDvGrmqSJMgOsI3Z0DV2AJOmhDhAgMBAAGjPDA6MAkGA1UdEwQCMAAw +CwYDVR0PBAQDAgXgMCAGA1UdEQQZMBeCD29wYS5leGFtcGxlLmNvbYcEfwAAATAN +BgkqhkiG9w0BAQsFAAOCAQEATyfGgOSBS6TDQVq3kZ7HrjvYrH7M5pZ4w+Ofjdi3 +t6EaMutFo4aMFqZmBEM23zIxzc1bysaEtrQbQnzkkKd2VvQS0FKsPQmBontFfSQW +nhhwfCOiZhY7T44L9pBKX7rQVciaFajCR2uXiCEUh2xLToeH00uuouXqPbREHwUX +RJgBvvck+eMvNKKsADyrovIs3J+lHkgcPvrb5UmugTh3AiMIGuaB7dDkxAyqLWMG +ygGBzgry7fwAe+CJ31fT1jlAS3YqR99ZQUFmP+7nBicvxrl/YBn1na/xgGePlqfo +U4cLNJLhwNhde+cddR6hNzaFriggz2dQPCzmLVJnlGCI2A== +-----END CERTIFICATE----- diff --git a/test/e2e/certrefresh/testdata/server-cert.pem b/test/e2e/certrefresh/testdata/server-cert.pem new file mode 100644 index 0000000000..2ebfd2f3cd --- /dev/null +++ b/test/e2e/certrefresh/testdata/server-cert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC7jCCAdagAwIBAgIUVGbjAlogOqO2AFW1KYi4d0YasMMwDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAwwFbXktY2EwHhcNMjExMjA3MTIzMzA4WhcNMzExMjA1MTIz +MzA4WjAUMRIwEAYDVQQDDAlteS1zZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQD1czYcLY58r+xWnxpveghrTIN6sFvWXgCo0/12Cg0pkz/BfOhr +yxJg7WBnzg3jKaXokXV3xMRCqWoaa+enI7MyviwQypBoJ5vDlXosYm1pzuWkp8Ye +gnbB5tATAn+Czl1vfqQjp2o7xeqdm0l0N5H27zMrlDiMnGD5swguOiZqtIuM8+z1 +sgB/cXU821qXsN4jmopaWXUPaF9VM9VUc3sb0DAfSXwZgv0HDA0RNET7GTXBcX6k +F+jB9RgCFFNc+sT3Q2arNxBYzOApJxTGl/5Pojtnvg79r2RLTto1a9NHZ0tAltMQ +d4q26zJR5i/lSiUxYv2xTrLySNo7OShs2c8FAgMBAAGjPDA6MAkGA1UdEwQCMAAw +CwYDVR0PBAQDAgXgMCAGA1UdEQQZMBeCD29wYS5leGFtcGxlLmNvbYcEfwAAATAN +BgkqhkiG9w0BAQsFAAOCAQEAmH6iPWtUIWIN++io7D2Ei2YOg9zgfibULPOdQflD +M0s1kE8MQZRJxxYe3aTSEp8XU8EVAYa2OieJSB1tQzLM91ObBnfFd8ZVPsHGbfX2 +xxkzPnppD6mI1c+4/iVdq3iGrniUfE48cmyrAPcGMnCMIzNQ21Uqv6Mn0/9ZCNmA +KKkfTL/7UjMp8xeVbf/Au4zFVnE2Gr9rY9roq3p5t6rf0HeFIcy/AIhM5Ygrds3O +ynznSMIR9E9rTjSFKUhy9rqOgwXqMw1bCO/8obZrYBrBZK/EcKbt/VdGEMfVXSKh +RiW+Sp0p6mENb3KwVnWAs1Fj3YHsha0Lu78odMb5RK42eg== +-----END CERTIFICATE----- diff --git a/test/e2e/certrefresh/testdata/server-key-new.pem b/test/e2e/certrefresh/testdata/server-key-new.pem new file mode 100644 index 0000000000..d33cf08b37 --- /dev/null +++ b/test/e2e/certrefresh/testdata/server-key-new.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAps0t3mLQOcU/UB9HcFkrngrCSPPkZbH+vZNauuZPyX8ysWGQ +lsJTszsGPCCuRBUvW8XXuSDp11tack1qHCFVWiNKFU6ZZmbUt87/Pp1p0fHJHllG +V9thw/7OGE73pf9CYtWxOGhKx4sN9SYGf3T6C14Qk+jKFZpxh6bdjK6Kq7l7Tksp +M+mM7EG/KyFUHaOmJE6r+vxRJLukXB4ST17eBcC0IJeTDElPmh0XQhZp8DsUsUmH +ZWslpDvVfXZe3krdPxDjg+2Fn1IWaSt3GIQL5meNVuXBnc7XVWz8LCRvCNN8USaJ +H6quq9uk/5qQ7xq5qkiTIDrCN2dA1dgCTpoQ4QIDAQABAoIBAHbZKx2RepwvFvWX +0+cRIirxr40belmbgc7B95vEDoWbxBrvUX6Z59mE7ORaxNBt59iUFykpcnSn+sIG +ttxkQ9R94INeBZ8ZFegB7YxHzOZySML/CUgAYKCuJVrcqUf1oO+bIzL13JJhWgia +l3apeqAu3dEFxTevW8Uz+BgNJXFFCRktHSGs3NrWlU80SufLBPxJMgccVJDNAhwC +nguKf5uJ9L9Gx5XuTS87DyWmRh7g8INjbNy8jYgr6mwa9jwVTlvIhkBM3/SGtnD9 +rFvdkQx1ctWkIlWKeZzsWL0t+blZhd/BlgSZC9vRu87Pa0V/bN9jfRHJgMMghwQI +bbOQBCECgYEA1c09IhjQIONpGvrDCxdR68q8fllKMLGMDtInlnXfyRExezclD0Uc +RVcuqNIpCdDPcsETwMmbjG9crdG+GEf05If7zs1uA6z7AUY5ux/1etFSP3jWKm00 +lyuVksvuzYjlRXJdzMB6HAbZt6GUj/ND7BVV/8YumeV+46gWMha35cMCgYEAx7ko +vyQqwQiFOOyOCQNkYqMsyZbqa6N3DpTx/727MbFyq0mFbIHkQyiz7vtHWgrEtjiN +7eeXDYlKavL1tUpapqaNz6z2tm/p0M6gWRsxvGcxo/Gr0D14cM4waaFvANndo+UD +0zAilF9JSVW3cUcKf7I+KZ/r3SpiaNHr0egpcIsCgYBWxikEuLtoTcQv7gzRaJKY +N72PLmA9KSJmNYdZuter/K1vi+8fpnYV8o9+d2WulTBNK+3/dhQKyHv+FD2qDzJm +uoZJ5fi7xy5M0xrFRvBT+7b9Cecqaw5IOKlJXjm688/SAtvtKUWmMGWW8R6h2iL8 +I6C24dGyJoH8lhEEHVJgDwKBgQCrLW1Y9byXGaBlO4o4+2lMiSJX3Tsp6j6ehtYr +JQiN/NKVMDxk1ac4UGh3iXKMH/KdYzdyEi4K8gKQS5CAQywS7WlZ95q0npK93nrc +JEyqd5+6LeXeYvEZbf9caXpkNlaapCx1EypwFIMRkZ/aPNMowzI4JtLXCf6ybEk7 +7UmnJQKBgQDLClpvxpLn6EwtueVju13glS7naeGFxhT2cjfARP4LNY8FS1aZK5YW +w2ronxp5ktRz5UOLneQ9D6YAmy/i33SZBV+7q+/MiJGtuFj0PwzT01vmp98S0AUU +fUq8zZUqiUtASZpbjzgb4tfIBlHj7vUbBPLbR4Liyhg6eFZwwgQQfQ== +-----END RSA PRIVATE KEY----- diff --git a/test/e2e/certrefresh/testdata/server-key.pem b/test/e2e/certrefresh/testdata/server-key.pem new file mode 100644 index 0000000000..7ef728d0c4 --- /dev/null +++ b/test/e2e/certrefresh/testdata/server-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA9XM2HC2OfK/sVp8ab3oIa0yDerBb1l4AqNP9dgoNKZM/wXzo +a8sSYO1gZ84N4yml6JF1d8TEQqlqGmvnpyOzMr4sEMqQaCebw5V6LGJtac7lpKfG +HoJ2webQEwJ/gs5db36kI6dqO8XqnZtJdDeR9u8zK5Q4jJxg+bMILjomarSLjPPs +9bIAf3F1PNtal7DeI5qKWll1D2hfVTPVVHN7G9AwH0l8GYL9BwwNETRE+xk1wXF+ +pBfowfUYAhRTXPrE90NmqzcQWMzgKScUxpf+T6I7Z74O/a9kS07aNWvTR2dLQJbT +EHeKtusyUeYv5UolMWL9sU6y8kjaOzkobNnPBQIDAQABAoIBAAK2Jx7gkfZmqyG5 +2DzrCDTHP5yXXixcFX3H+cDYE5Ul/0pP6vFl6OoRNUNwT073ItIS6U1Nay2hWX65 +OnHqPwyMdUgqNLYx2dKrUBI1dCf7FSZghBvKLS2vMxVCrc3wIbAdogqSyuWmJhVf +pcwW4RHtSo9sr8M95wRbKff4xHvhRS9trfuNtOBu8ODZ/xynoaQ8rPfeLeCTaaV+ +LKxNBcj+3+xWSAtAB9+xaERSEJGJIX4kQ/lonGPA7kkxrhTsc0p5jQKl6rOhK4iG +ZRfsUTasMAek9LMF1+AFB0jvzuzJIYVntiC/VX3IRSsPUmmWz7FWLAHoHFtvXgZd +U76KnYECgYEA/e3ZlbDvHIpQOQFTGiHbRti/Z/C7oSohk4cW7QGYZA1KNfbY9Pp5 +9NRsV5GsBDOdvaKRtFTFPcfbJKwAyWSspacIJ4wE/3M02Cam/3JDA5PZH0rfcsID +Bq/zW96m08ZCklGDuEToCdb4fB7zmbsmc+JqZCbvVFEkgPGXRXKYakkCgYEA93Oo +sVoWL5OaaCa28FGBVN0hbS6DwIjjATsVEeEbacpmiUma0vST8xR4Hvbtxd7s9kGc +kSSvYU0U/HxGtk8ZtwfNG7FBJZDHAFauL2H6DuROhNXNhDJJv/QklEYeJuWLJ8Wf +SrioK55Ds5tv8cazgSrz4aU0I2MB40hyd4hFnt0CgYEAsEs90QtyNuJgJ/Ofenke +/+Tjnoon+hCCFyam6A0/e9cuOqESp6JuoWgJgBKG1rPvRAVmG0jvV6E1qBQyx5+5 +rZh1tN8laSTW/2p2bsspc4ZmK6+TytyftTjbQGEoeccf2O33ASv13T7+bU4f2g9w +9uuu6bGOX3+mVE9msrSI1OECgYEAhPtvKQCU87SLQnWr0rK6onTERfy9aXcnJ74s +sJMdPFk9iYI45i3yZKwXceyaE8Cd8CmKjqX8anoWUSoohkk0NJzIqZ00uY94osHy +khxBWkdvuwt7ixPLdpEqJ1UXVyf9BL67wFhEaEyBbcCXBIQYa849ioJR5sKKfS6t +9XcSkzECgYAuk8zQzZFyemDI7dfPI8vuja59ay1mA9olqTw6EyIcU/HwYt0Y7gKW +f0agM8vU11s3QmDgY1oj1tMhXWP0caKBN2c5EvuvNgsJe0P4jZT20fqZtqeBXZfx +xllcbFWrM9hfxjd3JjOR4YVdLvfu5yvEB+3JvEaWG5o49TBz667yiw== +-----END RSA PRIVATE KEY----- diff --git a/test/e2e/tls/tls_test.go b/test/e2e/tls/tls_test.go index 78dd380877..37386089cd 100644 --- a/test/e2e/tls/tls_test.go +++ b/test/e2e/tls/tls_test.go @@ -11,6 +11,7 @@ import ( "net/url" "os" "testing" + "time" "github.com/open-policy-agent/opa/server" "github.com/open-policy-agent/opa/test/e2e" @@ -45,7 +46,9 @@ func TestMain(m *testing.M) { if ok := pool.AppendCertsFromPEM(caCertPEM); !ok { fatal("failed to parse CA cert") } - cert, err := tls.LoadX509KeyPair("testdata/server-cert.pem", "testdata/server-key.pem") + certFile := "testdata/server-cert.pem" + certKeyFile := "testdata/server-key.pem" + cert, err := tls.LoadX509KeyPair(certFile, certKeyFile) if err != nil { fatal(err) } @@ -76,6 +79,9 @@ allow { testServerParams.Addrs = &[]string{"https://127.0.0.1:0"} testServerParams.CertPool = pool testServerParams.Certificate = &cert + testServerParams.CertificateFile = certFile + testServerParams.CertificateKeyFile = certKeyFile + testServerParams.CertificateRefresh = time.Millisecond testServerParams.Authentication = server.AuthenticationTLS testServerParams.Authorization = server.AuthorizationBasic testServerParams.Paths = []string{"system.authz:" + tmpfile.Name()} @@ -119,7 +125,6 @@ func TestMinTLSVersion(t *testing.T) { t.Errorf("expected status 200, got %s", resp.Status) } }) - } func TestNotDefaultTLSVersion(t *testing.T) { @@ -198,15 +203,11 @@ func TestAuthenticationTLS(t *testing.T) { func newClient(maxTLSVersion uint16, pool *x509.CertPool, clientKeyPair ...string) *http.Client { c := *http.DefaultClient - // Note: zero-values in http.Transport are bad settings -- they let the client - // leak connections -- but it's good enough for these tests. Don't instantiate - // http.Transport without providing non-zero values in non-test code, please. - // See https://github.com/golang/go/issues/19620 for details. - tr := &http.Transport{ - TLSClientConfig: &tls.Config{ - RootCAs: pool, - }, + tr := http.DefaultTransport.(*http.Transport).Clone() + tr.TLSClientConfig = &tls.Config{ + RootCAs: pool, } + if len(clientKeyPair) == 2 { clientCert, err := tls.LoadX509KeyPair(clientKeyPair[0], clientKeyPair[1]) if err != nil {