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:
Ashutosh Narkar
2024-01-22 15:10:51 -08:00
parent 33e1ce9353
commit 737b213ba6
5 changed files with 189 additions and 35 deletions
+53
View File
@@ -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.01.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.01.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 {
+48
View File
@@ -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{
+4
View File
@@ -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.01.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
View File
@@ -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.01.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
View File
@@ -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
}