Skip to content

Commit

Permalink
server: minimum TLS version configuration, default to 1.2 (open-polic…
Browse files Browse the repository at this point in the history
…y-agent#3517)

* Support for minimum TLS version

OPA server now supports min TLS version, TLS versions supported are 1.0, 1.1, 1.2, 1.3.

Since TLS 1.0 and 1.1 are deprecated, default min TLS version for OPA is TLS 1.2 but
if someone wants to restrict OPA to use a specific minimum TLS version, they can
specify it using cmd parameter `--min-tls-version`.

Fixes open-policy-agent#3226.

Signed-off-by: Amruta Kale <amruta.kale@styra.com>
  • Loading branch information
kale-amruta authored and juliafriedman8 committed Jul 13, 2021
1 parent f968a2a commit 3512f7f
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 6 deletions.
11 changes: 11 additions & 0 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type runCmdParams struct {
skipVersionCheck bool
authentication *util.EnumFlag
authorization *util.EnumFlag
minTLSVersion *util.EnumFlag
logLevel *util.EnumFlag
logFormat *util.EnumFlag
algorithm string
Expand All @@ -50,6 +51,7 @@ func newRunParams() runCmdParams {
rt: runtime.NewParams(),
authentication: util.NewEnumFlag("off", []string{"token", "tls", "off"}),
authorization: util.NewEnumFlag("off", []string{"basic", "off"}),
minTLSVersion: util.NewEnumFlag("1.2", []string{"1.0", "1.1", "1.2", "1.3"}),
logLevel: util.NewEnumFlag("info", []string{"debug", "info", "error"}),
logFormat: util.NewEnumFlag("json", []string{"text", "json", "json-pretty"}),
}
Expand Down Expand Up @@ -178,6 +180,7 @@ To skip bundle verification, use the --skip-verify flag.
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().VarP(cmdParams.logLevel, "log-level", "l", "set log level")
runCommand.Flags().VarP(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")
Expand Down Expand Up @@ -220,6 +223,13 @@ func initRuntime(ctx context.Context, params runCmdParams, args []string) (*runt
"off": server.AuthorizationOff,
}

minTLSVersions := map[string]uint16{
"1.0": tls.VersionTLS10,
"1.1": tls.VersionTLS11,
"1.2": tls.VersionTLS12,
"1.3": tls.VersionTLS13,
}

cert, err := loadCertificate(params.tlsCertFile, params.tlsPrivateKeyFile)
if err != nil {
return nil, err
Expand All @@ -235,6 +245,7 @@ func initRuntime(ctx context.Context, params runCmdParams, args []string) (*runt

params.rt.Authentication = authenticationSchemes[params.authentication.String()]
params.rt.Authorization = authorizationScheme[params.authorization.String()]
params.rt.MinTLSVersion = minTLSVersions[params.minTLSVersion.String()]
params.rt.Certificate = cert
params.rt.Logging = runtime.LoggingConfig{
Level: params.logLevel.String(),
Expand Down
7 changes: 6 additions & 1 deletion runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ type Params struct {
// CertPool holds the CA certs trusted by the OPA server.
CertPool *x509.CertPool

// MinVersion contains the minimum TLS version that is acceptable.
// If zero, TLS 1.2 is currently taken as the minimum.
MinTLSVersion uint16

// HistoryPath is the filename to store the interactive shell user
// input history.
HistoryPath string
Expand Down Expand Up @@ -372,7 +376,8 @@ func (rt *Runtime) Serve(ctx context.Context) error {
WithDecisionIDFactory(rt.decisionIDFactory).
WithDecisionLoggerWithErr(rt.decisionLogger).
WithRuntime(rt.Manager.Info).
WithMetrics(rt.metrics)
WithMetrics(rt.metrics).
WithMinTLSVersion(rt.Params.MinTLSVersion)

if rt.Params.DiagnosticAddrs != nil {
rt.server = rt.server.WithDiagnosticAddresses(*rt.Params.DiagnosticAddrs)
Expand Down
29 changes: 29 additions & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ const (
AuthenticationTLS
)

var supportedTLSVersions = []uint16{tls.VersionTLS10, tls.VersionTLS11, tls.VersionTLS12, tls.VersionTLS13}

// AuthorizationScheme enumerates the supported authorization schemes. The authorization
// scheme determines how access to OPA is controlled.
type AuthorizationScheme int
Expand All @@ -69,6 +71,8 @@ const (
AuthorizationBasic
)

const defaultMinTLSVersion = tls.VersionTLS12

// Set of handlers for use in the "handler" dimension of the duration metric.
const (
PromHandlerV0Data = "v0/data"
Expand Down Expand Up @@ -100,6 +104,7 @@ type Server struct {
authorization AuthorizationScheme
cert *tls.Certificate
certPool *x509.CertPool
minTLSVersion uint16
mtx sync.RWMutex
partials map[string]rego.PartialResult
preparedEvalQueries *cache
Expand Down Expand Up @@ -302,6 +307,15 @@ func (s *Server) WithRouter(router *mux.Router) *Server {
return s
}

func (s *Server) WithMinTLSVersion(minTLSVersion uint16) *Server {
if isMinTLSVersionSupported(minTLSVersion) {
s.minTLSVersion = minTLSVersion
} else {
s.minTLSVersion = defaultMinTLSVersion
}
return s
}

// Listeners returns functions that listen and serve connections.
func (s *Server) Listeners() ([]Loop, error) {
loops := []Loop{}
Expand Down Expand Up @@ -463,6 +477,15 @@ func (b *baseHTTPListener) Type() httpListenerType {
return b.t
}

func isMinTLSVersionSupported(TLSVersion uint16) bool {
for _, version := range supportedTLSVersions {
if TLSVersion == version {
return true
}
}
return false
}

func (s *Server) getListener(addr string, h http.Handler, t httpListenerType) (Loop, httpListener, error) {
parsedURL, err := parseURL(addr, s.cert != nil)
if err != nil {
Expand Down Expand Up @@ -517,6 +540,12 @@ func (s *Server) getListenerForHTTPSServer(u *url.URL, h http.Handler, t httpLis
httpsServer.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert
}

if s.minTLSVersion != 0 {
httpsServer.TLSConfig.MinVersion = s.minTLSVersion
} else {
httpsServer.TLSConfig.MinVersion = defaultMinTLSVersion
}

l := newHTTPListener(&httpsServer, t)

httpsLoop := func() error { return l.ListenAndServeTLS("", "") }
Expand Down
84 changes: 79 additions & 5 deletions test/e2e/tls/tls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,22 @@ import (
var testRuntime *e2e.TestRuntime
var pool *x509.CertPool

var minTLSVersions = map[string]uint16{
"1.0": tls.VersionTLS10,
"1.1": tls.VersionTLS11,
"1.2": tls.VersionTLS12,
"1.3": tls.VersionTLS13,
}

// print error to stderr, exit 1
func fatal(err interface{}) {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(1)
}

func TestMain(m *testing.M) {
minTLSVersion := flag.String("--min-tls-version", "1.2", "minimum TLS Version")
TLSVersion := minTLSVersions[*minTLSVersion]
flag.Parse()

caCertPEM, err := ioutil.ReadFile("testdata/ca.pem")
Expand Down Expand Up @@ -69,6 +78,9 @@ allow {
testServerParams.Authentication = server.AuthenticationTLS
testServerParams.Authorization = server.AuthorizationBasic
testServerParams.Paths = []string{"system.authz:" + tmpfile.Name()}
if TLSVersion != 0 {
testServerParams.MinTLSVersion = TLSVersion
}

testRuntime, err = e2e.NewTestRuntime(testServerParams)
if err != nil {
Expand All @@ -77,19 +89,78 @@ 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(pool, "testdata/client-cert.pem", "testdata/client-key.pem")
testRuntime.Client = newClient(0, pool, "testdata/client-cert.pem", "testdata/client-key.pem")

os.Exit(testRuntime.RunTests(m))
}

func TestMinTLSVersion(t *testing.T) {
endpoint := testRuntime.URL()
t.Run("TLS version not suported by server", func(t *testing.T) {

c := newClient(tls.VersionTLS10, pool, "testdata/client-cert.pem", "testdata/client-key.pem")
_, err := c.Get(endpoint)

if err == nil {
t.Error("expected err - protocol version not supported, got nil")
}

})
t.Run("TLS Version supported by server", func(t *testing.T) {

c := newClient(tls.VersionTLS12, pool, "testdata/client-cert.pem", "testdata/client-key.pem")
resp, err := c.Get(endpoint)
if err != nil {
t.Fatalf("GET: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("expected status 200, got %s", resp.Status)
}
})

}

func TestNotDefaultTLSVersion(t *testing.T) {

oldArgs := os.Args
defer func() { os.Args = oldArgs }()
os.Args = []string{"cmd", "--min-tls-version", "1.3"}
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")
_, err := c.Get(endpoint)

if err == nil {
t.Error("expected err - protocol version not supported, got nil")
}

})

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")
resp, err := c.Get(endpoint)
if err != nil {
t.Fatalf("GET: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("expected status 200, got %s", resp.Status)
}
})

}

func TestAuthenticationTLS(t *testing.T) {
endpoint := testRuntime.URL() + "/v1/data/foo"

// Note: This test is redundant. When the testRuntime starts the server, it
// already queries the health endpoint using a properly authenticated, and
// authorized, http client.
t.Run("happy path", func(t *testing.T) {
c := newClient(pool, "testdata/client-cert.pem", "testdata/client-key.pem")
c := newClient(0, pool, "testdata/client-cert.pem", "testdata/client-key.pem")
resp, err := c.Get(endpoint)
if err != nil {
t.Fatalf("GET: %v", err)
Expand All @@ -101,7 +172,7 @@ func TestAuthenticationTLS(t *testing.T) {
})

t.Run("authn successful, authz failed", func(t *testing.T) {
c := newClient(pool, "testdata/client-cert-2.pem", "testdata/client-key-2.pem")
c := newClient(0, pool, "testdata/client-cert-2.pem", "testdata/client-key-2.pem")
resp, err := c.Get(endpoint)
if err != nil {
t.Fatalf("GET: %v", err)
Expand All @@ -113,15 +184,15 @@ func TestAuthenticationTLS(t *testing.T) {
})

t.Run("client trusts server, but doesn't provide client cert", func(t *testing.T) {
c := newClient(pool)
c := newClient(0, pool)
_, err := c.Get(endpoint)
if _, ok := err.(*url.Error); !ok {
t.Errorf("expected *url.Error, got %T: %v", err, err)
}
})
}

func newClient(pool *x509.CertPool, clientKeyPair ...string) *http.Client {
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
Expand All @@ -139,6 +210,9 @@ func newClient(pool *x509.CertPool, clientKeyPair ...string) *http.Client {
}
tr.TLSClientConfig.Certificates = []tls.Certificate{clientCert}
}
if maxTLSVersion != 0 {
tr.TLSClientConfig.MaxVersion = maxTLSVersion
}
c.Transport = tr
return &c
}

0 comments on commit 3512f7f

Please sign in to comment.