From cc4816ededaaa8232712cdc639a0e74a20e742a3 Mon Sep 17 00:00:00 2001 From: Stephan Renatus Date: Thu, 9 Dec 2021 10:13:14 +0100 Subject: [PATCH] server+runtime: add TLS cert refreshing (#4107) This adds a new flag to `opa run`, intended for server usage with HTTPS listeners: `--tls-cert-refresh-period`. If used with a positive duration, such as "5m" (5 minutes), "24h", etc, the server will track the certificate and key files' contents. When their content changes, the certificates will be reloaded. On an error in reloading, it will log (info) the error and try again in the next round. Fixes #2500. Signed-off-by: Stephan Renatus --- cmd/run.go | 32 +-- docs/content/security.md | 11 +- runtime/runtime.go | 9 + server/certs.go | 76 +++++++ server/server.go | 43 +++- test/e2e/certrefresh/certrefresh_test.go | 186 ++++++++++++++++++ test/e2e/certrefresh/testdata/.gitignore | 4 + test/e2e/certrefresh/testdata/ca.pem | 19 ++ test/e2e/certrefresh/testdata/gencerts.sh | 31 +++ .../certrefresh/testdata/server-cert-new.pem | 18 ++ test/e2e/certrefresh/testdata/server-cert.pem | 18 ++ .../certrefresh/testdata/server-key-new.pem | 27 +++ test/e2e/certrefresh/testdata/server-key.pem | 27 +++ test/e2e/tls/tls_test.go | 21 +- 14 files changed, 488 insertions(+), 34 deletions(-) create mode 100644 server/certs.go create mode 100644 test/e2e/certrefresh/certrefresh_test.go create mode 100644 test/e2e/certrefresh/testdata/.gitignore create mode 100644 test/e2e/certrefresh/testdata/ca.pem create mode 100755 test/e2e/certrefresh/testdata/gencerts.sh create mode 100644 test/e2e/certrefresh/testdata/server-cert-new.pem create mode 100644 test/e2e/certrefresh/testdata/server-cert.pem create mode 100644 test/e2e/certrefresh/testdata/server-key-new.pem create mode 100644 test/e2e/certrefresh/testdata/server-key.pem 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 {