mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
Allow TLS cipher suites to be set for the OPA server
This change adds a new flag to `opa run` to allow users to specify a list of enabled TLS 1.0–1.2 cipher suites. This allows users to control the cipher suites the OPA server supports during a TLS handshake. Signed-off-by: Ashutosh Narkar <anarkar4387@gmail.com>
This commit is contained in:
+53
@@ -49,6 +49,7 @@ type runCmdParams struct {
|
||||
skipBundleVerify bool
|
||||
skipKnownSchemaCheck bool
|
||||
excludeVerifyFiles []string
|
||||
cipherSuites []string
|
||||
}
|
||||
|
||||
func newRunParams() runCmdParams {
|
||||
@@ -181,6 +182,17 @@ be expanded in the future. To disable this, use the --skip-known-schema-check fl
|
||||
The --v1-compatible flag can be used to opt-in to OPA features and behaviors that will be enabled by default in a future OPA v1.0 release.
|
||||
Current behaviors enabled by this flag include:
|
||||
- setting OPA's listening address to "localhost:8181" by default.
|
||||
|
||||
The --tls-cipher-suites flag can be used to specify the list of enabled TLS 1.0–1.2 cipher suites. Note that TLS 1.3
|
||||
cipher suites are not configurable. Following are the supported TLS 1.0 - 1.2 cipher suites (IANA):
|
||||
TLS_RSA_WITH_RC4_128_SHA, TLS_RSA_WITH_3DES_EDE_CBC_SHA, TLS_RSA_WITH_AES_128_CBC_SHA, TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
TLS_RSA_WITH_AES_128_CBC_SHA256, TLS_RSA_WITH_AES_128_GCM_SHA256, TLS_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
|
||||
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, TLS_ECDHE_RSA_WITH_RC4_128_SHA, TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
|
||||
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
|
||||
|
||||
See https://godoc.org/crypto/tls#pkg-constants for more information.
|
||||
`,
|
||||
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
@@ -221,6 +233,7 @@ Current behaviors enabled by this flag include:
|
||||
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")
|
||||
runCommand.Flags().BoolVar(&cmdParams.skipKnownSchemaCheck, "skip-known-schema-check", false, "disables type checking on known input schemas")
|
||||
runCommand.Flags().StringSliceVar(&cmdParams.cipherSuites, "tls-cipher-suites", []string{}, "set list of enabled TLS 1.0–1.2 cipher suites (IANA)")
|
||||
addConfigOverrides(runCommand.Flags(), &cmdParams.rt.ConfigOverrides)
|
||||
addConfigOverrideFiles(runCommand.Flags(), &cmdParams.rt.ConfigOverrideFiles)
|
||||
addBundleModeFlag(runCommand.Flags(), &cmdParams.rt.BundleMode, false)
|
||||
@@ -332,6 +345,15 @@ func initRuntime(ctx context.Context, params runCmdParams, args []string, addrSe
|
||||
|
||||
params.rt.SkipKnownSchemaCheck = params.skipKnownSchemaCheck
|
||||
|
||||
if len(params.cipherSuites) > 0 {
|
||||
cipherSuites, err := verifyCipherSuites(params.cipherSuites)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
params.rt.CipherSuites = cipherSuites
|
||||
}
|
||||
|
||||
rt, err := runtime.NewRuntime(ctx, params.rt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -355,6 +377,37 @@ func startRuntime(ctx context.Context, rt *runtime.Runtime, serverMode bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func verifyCipherSuites(cipherSuites []string) (*[]uint16, error) {
|
||||
cipherSuitesMap := map[string]*tls.CipherSuite{}
|
||||
|
||||
for _, c := range tls.CipherSuites() {
|
||||
cipherSuitesMap[c.Name] = c
|
||||
}
|
||||
|
||||
for _, c := range tls.InsecureCipherSuites() {
|
||||
cipherSuitesMap[c.Name] = c
|
||||
}
|
||||
|
||||
cipherSuitesIds := []uint16{}
|
||||
for _, c := range cipherSuites {
|
||||
val, ok := cipherSuitesMap[c]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid cipher suite %v", c)
|
||||
}
|
||||
|
||||
// verify no TLS 1.3 cipher suites as they are not configurable
|
||||
for _, ver := range val.SupportedVersions {
|
||||
if ver == tls.VersionTLS13 {
|
||||
return nil, fmt.Errorf("TLS 1.3 cipher suite \"%v\" is not configurable", c)
|
||||
}
|
||||
}
|
||||
|
||||
cipherSuitesIds = append(cipherSuitesIds, val.ID)
|
||||
}
|
||||
|
||||
return &cipherSuitesIds, nil
|
||||
}
|
||||
|
||||
func historyPath() string {
|
||||
home := os.Getenv("HOME")
|
||||
if len(home) == 0 {
|
||||
|
||||
@@ -7,8 +7,11 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -149,6 +152,51 @@ func TestInitRuntimeVerifyNonBundle(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitRuntimeCipherSuites(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
cipherSuites []string
|
||||
expErr bool
|
||||
expCipherSuites []uint16
|
||||
}{
|
||||
{"no cipher suites", []string{}, false, []uint16{}},
|
||||
{"secure and insecure cipher suites", []string{"TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_RC4_128_SHA"}, false, []uint16{tls.TLS_RSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, tls.TLS_RSA_WITH_RC4_128_SHA}},
|
||||
{"invalid cipher suites", []string{"foo"}, true, []uint16{}},
|
||||
{"tls 1.3 cipher suite", []string{"TLS_AES_128_GCM_SHA256"}, true, []uint16{}},
|
||||
{"tls 1.2-1.3 cipher suite", []string{"TLS_RSA_WITH_AES_128_GCM_SHA256", "TLS_AES_128_GCM_SHA256"}, true, []uint16{}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
params := newTestRunParams()
|
||||
|
||||
if len(tc.cipherSuites) != 0 {
|
||||
params.cipherSuites = tc.cipherSuites
|
||||
}
|
||||
|
||||
rt, err := initRuntime(context.Background(), params, nil, false)
|
||||
fmt.Println(err)
|
||||
|
||||
if !tc.expErr && err != nil {
|
||||
t.Fatal("Unexpected error occurred:", err)
|
||||
} else if tc.expErr && err == nil {
|
||||
t.Fatal("Expected error but got nil")
|
||||
} else if err == nil {
|
||||
if len(tc.expCipherSuites) > 0 {
|
||||
if !reflect.DeepEqual(*rt.Params.CipherSuites, tc.expCipherSuites) {
|
||||
t.Fatalf("expected cipher suites %v but got %v", tc.expCipherSuites, *rt.Params.CipherSuites)
|
||||
}
|
||||
} else {
|
||||
if rt.Params.CipherSuites != nil {
|
||||
t.Fatal("expected no value defined for cipher suites")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitRuntimeSkipKnownSchemaCheck(t *testing.T) {
|
||||
|
||||
fs := map[string]string{
|
||||
|
||||
@@ -231,6 +231,9 @@ type Params struct {
|
||||
// This flag allows users to opt-in to the new behavior and helps transition to the future release upon which
|
||||
// the new behavior will be enabled by default.
|
||||
V1Compatible bool
|
||||
|
||||
// CipherSuites specifies the list of enabled TLS 1.0–1.2 cipher suites
|
||||
CipherSuites *[]uint16
|
||||
}
|
||||
|
||||
// LoggingConfig stores the configuration for OPA's logging behaviour.
|
||||
@@ -550,6 +553,7 @@ func (rt *Runtime) Serve(ctx context.Context) error {
|
||||
WithRuntime(rt.Manager.Info).
|
||||
WithMetrics(rt.metrics).
|
||||
WithMinTLSVersion(rt.Params.MinTLSVersion).
|
||||
WithCipherSuites(rt.Params.CipherSuites).
|
||||
WithDistributedTracingOpts(rt.Params.DistributedTracingOpts)
|
||||
|
||||
// If decision_logging plugin enabled, check to see if we opted in to the ND builtins cache.
|
||||
|
||||
+38
-25
@@ -146,6 +146,7 @@ type Server struct {
|
||||
distributedTracingOpts tracing.Options
|
||||
ndbCacheEnabled bool
|
||||
unixSocketPerm *string
|
||||
cipherSuites *[]uint16
|
||||
}
|
||||
|
||||
// Metrics defines the interface that the server requires for recording HTTP
|
||||
@@ -400,6 +401,12 @@ func (s *Server) WithNDBCacheEnabled(ndbCacheEnabled bool) *Server {
|
||||
return s
|
||||
}
|
||||
|
||||
// WithCipherSuites sets the list of enabled TLS 1.0–1.2 cipher suites.
|
||||
func (s *Server) WithCipherSuites(cipherSuites *[]uint16) *Server {
|
||||
s.cipherSuites = cipherSuites
|
||||
return s
|
||||
}
|
||||
|
||||
// WithUnixSocketPermission sets the permission for the Unix domain socket if used to listen for
|
||||
// incoming connections. Applies to the sockets the server is listening on including diagnostic API's.
|
||||
func (s *Server) WithUnixSocketPermission(unixSocketPerm *string) *Server {
|
||||
@@ -635,38 +642,44 @@ func (s *Server) getListenerForHTTPSServer(u *url.URL, h http.Handler, t httpLis
|
||||
return nil, nil, fmt.Errorf("TLS certificate required but not supplied")
|
||||
}
|
||||
|
||||
httpsServer := http.Server{
|
||||
Addr: u.Host,
|
||||
Handler: h,
|
||||
TLSConfig: &tls.Config{
|
||||
GetCertificate: s.getCertificate,
|
||||
// GetConfigForClient is used to ensure that a fresh config is provided containing the latest cert pool.
|
||||
// This is not required, but appears to be how connect time updates config should be done:
|
||||
// https://github.com/golang/go/issues/16066#issuecomment-250606132
|
||||
GetConfigForClient: func(info *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
s.tlsConfigMtx.Lock()
|
||||
defer s.tlsConfigMtx.Unlock()
|
||||
tlsConfig := tls.Config{
|
||||
GetCertificate: s.getCertificate,
|
||||
// GetConfigForClient is used to ensure that a fresh config is provided containing the latest cert pool.
|
||||
// This is not required, but appears to be how connect time updates config should be done:
|
||||
// https://github.com/golang/go/issues/16066#issuecomment-250606132
|
||||
GetConfigForClient: func(info *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
s.tlsConfigMtx.Lock()
|
||||
defer s.tlsConfigMtx.Unlock()
|
||||
|
||||
cfg := &tls.Config{
|
||||
GetCertificate: s.getCertificate,
|
||||
ClientCAs: s.certPool,
|
||||
}
|
||||
cfg := &tls.Config{
|
||||
GetCertificate: s.getCertificate,
|
||||
ClientCAs: s.certPool,
|
||||
}
|
||||
|
||||
if s.authentication == AuthenticationTLS {
|
||||
cfg.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
}
|
||||
if s.authentication == AuthenticationTLS {
|
||||
cfg.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
}
|
||||
|
||||
if s.minTLSVersion != 0 {
|
||||
cfg.MinVersion = s.minTLSVersion
|
||||
} else {
|
||||
cfg.MinVersion = defaultMinTLSVersion
|
||||
}
|
||||
if s.minTLSVersion != 0 {
|
||||
cfg.MinVersion = s.minTLSVersion
|
||||
} else {
|
||||
cfg.MinVersion = defaultMinTLSVersion
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
},
|
||||
if s.cipherSuites != nil {
|
||||
cfg.CipherSuites = *s.cipherSuites
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
},
|
||||
}
|
||||
|
||||
httpsServer := http.Server{
|
||||
Addr: u.Host,
|
||||
Handler: h,
|
||||
TLSConfig: &tlsConfig,
|
||||
}
|
||||
|
||||
l := newHTTPListener(&httpsServer, t)
|
||||
|
||||
httpsLoop := func() error { return l.ListenAndServeTLS("", "") }
|
||||
|
||||
+46
-10
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -88,6 +89,9 @@ allow {
|
||||
testServerParams.MinTLSVersion = TLSVersion
|
||||
}
|
||||
|
||||
// RSA cipher suite given server's key is RSA
|
||||
testServerParams.CipherSuites = &[]uint16{tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA}
|
||||
|
||||
testRuntime, err = e2e.NewTestRuntime(testServerParams)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
@@ -95,16 +99,43 @@ allow {
|
||||
|
||||
// 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(0, pool, "testdata/client-cert.pem", "testdata/client-key.pem")
|
||||
testRuntime.Client = newClient(0, pool, nil, "testdata/client-cert.pem", "testdata/client-key.pem")
|
||||
|
||||
os.Exit(testRuntime.RunTests(m))
|
||||
}
|
||||
|
||||
func TestCipherSuites(t *testing.T) {
|
||||
endpoint := testRuntime.URL()
|
||||
t.Run("Cipher suite supported by both client and server", func(t *testing.T) {
|
||||
|
||||
c := newClient(tls.VersionTLS12, pool, &[]uint16{tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA}, "testdata/client-cert.pem", "testdata/client-key.pem")
|
||||
_, err := c.Get(endpoint)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("No cipher suite supported by both client and server", func(t *testing.T) {
|
||||
|
||||
// Since server's key is RSA, client specifying an ECDSA cipher suite should result in an error
|
||||
c := newClient(tls.VersionTLS12, pool, &[]uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA}, "testdata/client-cert.pem", "testdata/client-key.pem")
|
||||
_, err := c.Get(endpoint)
|
||||
if err == nil {
|
||||
t.Error("expected err - no cipher suite supported by both client and server, got nil")
|
||||
}
|
||||
|
||||
expErr := "tls: handshake failure"
|
||||
if !strings.Contains(err.Error(), expErr) {
|
||||
t.Fatalf("unexpected error message %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMinTLSVersion(t *testing.T) {
|
||||
endpoint := testRuntime.URL()
|
||||
t.Run("TLS version not suported by server", func(t *testing.T) {
|
||||
t.Run("TLS version not supported by server", func(t *testing.T) {
|
||||
|
||||
c := newClient(tls.VersionTLS10, pool, "testdata/client-cert.pem", "testdata/client-key.pem")
|
||||
c := newClient(tls.VersionTLS10, pool, nil, "testdata/client-cert.pem", "testdata/client-key.pem")
|
||||
_, err := c.Get(endpoint)
|
||||
|
||||
if err == nil {
|
||||
@@ -114,7 +145,7 @@ func TestMinTLSVersion(t *testing.T) {
|
||||
})
|
||||
t.Run("TLS Version supported by server", func(t *testing.T) {
|
||||
|
||||
c := newClient(tls.VersionTLS12, pool, "testdata/client-cert.pem", "testdata/client-key.pem")
|
||||
c := newClient(tls.VersionTLS12, pool, nil, "testdata/client-cert.pem", "testdata/client-key.pem")
|
||||
resp, err := c.Get(endpoint)
|
||||
if err != nil {
|
||||
t.Fatalf("GET: %v", err)
|
||||
@@ -134,7 +165,7 @@ func TestNotDefaultTLSVersion(t *testing.T) {
|
||||
endpoint := testRuntime.URL()
|
||||
t.Run("server started with min TLS Version 1.3, client connecting with not supported TLS version", func(t *testing.T) {
|
||||
|
||||
c := newClient(tls.VersionTLS10, pool, "testdata/client-cert.pem", "testdata/client-key.pem")
|
||||
c := newClient(tls.VersionTLS10, pool, nil, "testdata/client-cert.pem", "testdata/client-key.pem")
|
||||
_, err := c.Get(endpoint)
|
||||
|
||||
if err == nil {
|
||||
@@ -148,7 +179,7 @@ func TestNotDefaultTLSVersion(t *testing.T) {
|
||||
|
||||
t.Run("server started with min TLS Version 1.3, client connecting supported TLS version", func(t *testing.T) {
|
||||
|
||||
c := newClient(tls.VersionTLS13, pool, "testdata/client-cert.pem", "testdata/client-key.pem")
|
||||
c := newClient(tls.VersionTLS13, pool, nil, "testdata/client-cert.pem", "testdata/client-key.pem")
|
||||
resp, err := c.Get(endpoint)
|
||||
if err != nil {
|
||||
t.Fatalf("GET: %v", err)
|
||||
@@ -168,7 +199,7 @@ func TestAuthenticationTLS(t *testing.T) {
|
||||
// already queries the health endpoint using a properly authenticated, and
|
||||
// authorized, http client.
|
||||
t.Run("happy path", func(t *testing.T) {
|
||||
c := newClient(0, pool, "testdata/client-cert.pem", "testdata/client-key.pem")
|
||||
c := newClient(0, pool, nil, "testdata/client-cert.pem", "testdata/client-key.pem")
|
||||
resp, err := c.Get(endpoint)
|
||||
if err != nil {
|
||||
t.Fatalf("GET: %v", err)
|
||||
@@ -180,7 +211,7 @@ func TestAuthenticationTLS(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("authn successful, authz failed", func(t *testing.T) {
|
||||
c := newClient(0, pool, "testdata/client-cert-2.pem", "testdata/client-key-2.pem")
|
||||
c := newClient(0, pool, nil, "testdata/client-cert-2.pem", "testdata/client-key-2.pem")
|
||||
resp, err := c.Get(endpoint)
|
||||
if err != nil {
|
||||
t.Fatalf("GET: %v", err)
|
||||
@@ -192,7 +223,7 @@ func TestAuthenticationTLS(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("client trusts server, but doesn't provide client cert", func(t *testing.T) {
|
||||
c := newClient(0, pool)
|
||||
c := newClient(0, pool, nil)
|
||||
_, err := c.Get(endpoint)
|
||||
if _, ok := err.(*url.Error); !ok {
|
||||
t.Errorf("expected *url.Error, got %T: %v", err, err)
|
||||
@@ -200,7 +231,7 @@ func TestAuthenticationTLS(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func newClient(maxTLSVersion uint16, pool *x509.CertPool, clientKeyPair ...string) *http.Client {
|
||||
func newClient(maxTLSVersion uint16, pool *x509.CertPool, cipherSuites *[]uint16, clientKeyPair ...string) *http.Client {
|
||||
c := *http.DefaultClient
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.TLSClientConfig = &tls.Config{
|
||||
@@ -217,6 +248,11 @@ func newClient(maxTLSVersion uint16, pool *x509.CertPool, clientKeyPair ...strin
|
||||
if maxTLSVersion != 0 {
|
||||
tr.TLSClientConfig.MaxVersion = maxTLSVersion
|
||||
}
|
||||
|
||||
if cipherSuites != nil {
|
||||
tr.TLSClientConfig.CipherSuites = *cipherSuites
|
||||
}
|
||||
|
||||
c.Transport = tr
|
||||
return &c
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user