mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-04-17 15:01:18 +00:00
Merge pull request #1952 from vmware-tanzu/jtc/issue-1605-limit-tls-ciphers-for-tls1.2-v2
Allow admin user to further limit TLS ciphers used for TLS1.2 client requests and server ports (not including CLI)
This commit is contained in:
@@ -100,6 +100,9 @@ data:
|
||||
log:
|
||||
level: (@= getAndValidateLogLevel() @)
|
||||
(@ end @)
|
||||
tls:
|
||||
onedottwo:
|
||||
allowedCiphers: (@= str(data.values.allowed_ciphers_for_tls_onedottwo) @)
|
||||
---
|
||||
#@ if data.values.image_pull_dockerconfigjson and data.values.image_pull_dockerconfigjson != "":
|
||||
apiVersion: v1
|
||||
|
||||
@@ -214,3 +214,20 @@ https_proxy: ""
|
||||
#@ localhost endpoints, and the known instance metadata IP address for public cloud providers."
|
||||
#@schema/desc no_proxy_desc
|
||||
no_proxy: "$(KUBERNETES_SERVICE_HOST),169.254.169.254,127.0.0.1,localhost,.svc,.cluster.local"
|
||||
|
||||
#@schema/title "Allowed Ciphers for TLS 1.2"
|
||||
#@ allowed_ciphers_for_tls_onedottwo_desc = "When specified, only the ciphers listed will be used for TLS 1.2. \
|
||||
#@ This includes both server-side and client-side TLS connections. \
|
||||
#@ This list must only include cipher suites that Pinniped is configured to accept \
|
||||
#@ (see internal/crypto/ptls/profiles.go and internal/crypto/ptls/profiles_fips_strict.go). \
|
||||
#@ Allowing too few ciphers may cause critical parts of Pinniped to be unable to function. For example, \
|
||||
#@ Kubernetes pod readiness checks, Pinniped pods acting as a client to the Kubernetes API server, \
|
||||
#@ Pinniped pods acting as a client to external identity providers, or Pinniped pods acting as an APIService server \
|
||||
#@ all need to be able to function with the allowed TLS cipher suites. \
|
||||
#@ An empty array means accept Pinniped's defaults."
|
||||
#@schema/desc allowed_ciphers_for_tls_onedottwo_desc
|
||||
#@schema/examples ("Example with a few secure ciphers", ["TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256"])
|
||||
#! No type, default, or validation is required here.
|
||||
#! An empty array is perfectly valid, as is any array of strings.
|
||||
allowed_ciphers_for_tls_onedottwo:
|
||||
- ""
|
||||
|
||||
@@ -53,6 +53,11 @@ _: #@ template.replace(data.values.custom_labels)
|
||||
#@ "apiService": defaultResourceNameWithSuffix("api"),
|
||||
#@ },
|
||||
#@ "labels": labels(),
|
||||
#@ "tls": {
|
||||
#@ "onedottwo": {
|
||||
#@ "allowedCiphers": data.values.allowed_ciphers_for_tls_onedottwo
|
||||
#@ }
|
||||
#@ }
|
||||
#@ }
|
||||
#@ if data.values.log_level:
|
||||
#@ config["log"] = {}
|
||||
|
||||
@@ -203,3 +203,20 @@ no_proxy: "$(KUBERNETES_SERVICE_HOST),169.254.169.254,127.0.0.1,localhost,.svc,.
|
||||
#@schema/nullable
|
||||
#@schema/validation ("a map with keys 'http' and 'https', whose values are either the string 'disabled' or a map having keys 'network' and 'address', and the value of 'network' must be one of the allowed values", validate_endpoints)
|
||||
endpoints: { }
|
||||
|
||||
#@schema/title "Allowed Ciphers for TLS 1.2"
|
||||
#@ allowed_ciphers_for_tls_onedottwo_desc = "When specified, only the ciphers listed will be used for TLS 1.2. \
|
||||
#@ This includes both server-side and client-side TLS connections. \
|
||||
#@ This list must only include cipher suites that Pinniped is configured to accept \
|
||||
#@ (see internal/crypto/ptls/profiles.go and internal/crypto/ptls/profiles_fips_strict.go). \
|
||||
#@ Allowing too few ciphers may cause critical parts of Pinniped to be unable to function. For example, \
|
||||
#@ Kubernetes pod readiness checks, Pinniped pods acting as a client to the Kubernetes API server, \
|
||||
#@ Pinniped pods acting as a client to external identity providers, or Pinniped pods acting as an APIService server \
|
||||
#@ all need to be able to function with the allowed TLS cipher suites. \
|
||||
#@ An empty array means accept Pinniped's defaults."
|
||||
#@schema/desc allowed_ciphers_for_tls_onedottwo_desc
|
||||
#@schema/examples ("Example with a few secure ciphers", ["TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256"])
|
||||
#! No type, default, or validation is required here.
|
||||
#! An empty array is perfectly valid, as is any array of strings.
|
||||
allowed_ciphers_for_tls_onedottwo:
|
||||
- ""
|
||||
|
||||
@@ -112,11 +112,14 @@ func (a *App) runServer(ctx context.Context) error {
|
||||
featuregates.DisableKubeFeatureGate(features.UnauthenticatedHTTP2DOSMitigation)
|
||||
|
||||
// Read the server config file.
|
||||
cfg, err := concierge.FromPath(ctx, a.configPath)
|
||||
cfg, err := concierge.FromPath(ctx, a.configPath, ptls.SetUserConfiguredAllowedCipherSuitesForTLSOneDotTwo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not load config: %w", err)
|
||||
}
|
||||
|
||||
// The above server config should have set the allowed ciphers global, so now log the ciphers for all profiles.
|
||||
ptls.LogAllProfiles(plog.New())
|
||||
|
||||
// Discover in which namespace we are installed.
|
||||
podInfo, err := downward.Load(a.downwardAPIPath)
|
||||
if err != nil {
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"go.pinniped.dev/internal/constable"
|
||||
"go.pinniped.dev/internal/crypto/ptls"
|
||||
"go.pinniped.dev/internal/groupsuffix"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
)
|
||||
@@ -42,7 +43,7 @@ const (
|
||||
// Note! The Config file should contain base64-encoded WebhookCABundle data.
|
||||
// This function will decode that base64-encoded data to PEM bytes to be stored
|
||||
// in the Config.
|
||||
func FromPath(ctx context.Context, path string) (*Config, error) {
|
||||
func FromPath(ctx context.Context, path string, setAllowedCiphers ptls.SetAllowedCiphersFunc) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read file: %w", err)
|
||||
@@ -83,6 +84,10 @@ func FromPath(ctx context.Context, path string) (*Config, error) {
|
||||
return nil, fmt.Errorf("validate log level: %w", err)
|
||||
}
|
||||
|
||||
if err := setAllowedCiphers(config.TLS.OneDotTwo.AllowedCiphers); err != nil {
|
||||
return nil, fmt.Errorf("validate tls: %w", err)
|
||||
}
|
||||
|
||||
if config.Labels == nil {
|
||||
config.Labels = make(map[string]string)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package concierge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
@@ -17,10 +18,11 @@ import (
|
||||
|
||||
func TestFromPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
yaml string
|
||||
wantConfig *Config
|
||||
wantError string
|
||||
name string
|
||||
yaml string
|
||||
allowedCiphersError error
|
||||
wantConfig *Config
|
||||
wantError string
|
||||
}{
|
||||
{
|
||||
name: "Fully filled out",
|
||||
@@ -59,6 +61,12 @@ func TestFromPath(t *testing.T) {
|
||||
imagePullSecrets: [kube-cert-agent-image-pull-secret]
|
||||
log:
|
||||
level: debug
|
||||
tls:
|
||||
onedottwo:
|
||||
allowedCiphers:
|
||||
- foo
|
||||
- bar
|
||||
- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
|
||||
`),
|
||||
wantConfig: &Config{
|
||||
DiscoveryInfo: DiscoveryInfoSpec{
|
||||
@@ -98,6 +106,15 @@ func TestFromPath(t *testing.T) {
|
||||
Log: plog.LogSpec{
|
||||
Level: plog.LevelDebug,
|
||||
},
|
||||
TLS: TLSSpec{
|
||||
OneDotTwo: TLSProtocolSpec{
|
||||
AllowedCiphers: []string{
|
||||
"foo",
|
||||
"bar",
|
||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -587,6 +604,31 @@ func TestFromPath(t *testing.T) {
|
||||
`),
|
||||
wantError: "validate apiGroupSuffix: a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')",
|
||||
},
|
||||
{
|
||||
name: "returns setAllowedCiphers errors",
|
||||
yaml: here.Doc(`
|
||||
---
|
||||
names:
|
||||
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
|
||||
credentialIssuer: pinniped-config
|
||||
apiService: pinniped-api
|
||||
impersonationLoadBalancerService: impersonationLoadBalancerService-value
|
||||
impersonationClusterIPService: impersonationClusterIPService-value
|
||||
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
|
||||
impersonationCACertificateSecret: impersonationCACertificateSecret-value
|
||||
impersonationSignerSecret: impersonationSignerSecret-value
|
||||
impersonationSignerSecret: impersonationSignerSecret-value
|
||||
agentServiceAccount: agentServiceAccount-value
|
||||
impersonationProxyServiceAccount: impersonationProxyServiceAccount-value
|
||||
impersonationProxyLegacySecret: impersonationProxyLegacySecret-value
|
||||
tls:
|
||||
onedottwo:
|
||||
allowedCiphers:
|
||||
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
|
||||
`),
|
||||
allowedCiphersError: fmt.Errorf("some error from setAllowedCiphers"),
|
||||
wantError: "validate tls: some error from setAllowedCiphers",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
@@ -595,10 +637,10 @@ func TestFromPath(t *testing.T) {
|
||||
// Write yaml to temp file
|
||||
f, err := os.CreateTemp("", "pinniped-test-config-yaml-*")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
t.Cleanup(func() {
|
||||
err := os.Remove(f.Name())
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
})
|
||||
_, err = f.WriteString(test.yaml)
|
||||
require.NoError(t, err)
|
||||
err = f.Close()
|
||||
@@ -607,14 +649,23 @@ func TestFromPath(t *testing.T) {
|
||||
// Test FromPath()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
config, err := FromPath(ctx, f.Name())
|
||||
|
||||
var actualCiphers []string
|
||||
setAllowedCiphers := func(ciphers []string) error {
|
||||
actualCiphers = ciphers
|
||||
return test.allowedCiphersError
|
||||
}
|
||||
|
||||
config, err := FromPath(ctx, f.Name(), setAllowedCiphers)
|
||||
|
||||
if test.wantError != "" {
|
||||
require.EqualError(t, err, test.wantError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.wantConfig, config)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.wantConfig, config)
|
||||
require.Equal(t, test.wantConfig.TLS.OneDotTwo.AllowedCiphers, actualCiphers)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,18 @@ type Config struct {
|
||||
KubeCertAgentConfig KubeCertAgentSpec `json:"kubeCertAgent"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
Log plog.LogSpec `json:"log"`
|
||||
TLS TLSSpec `json:"tls"`
|
||||
}
|
||||
|
||||
type TLSSpec struct {
|
||||
OneDotTwo TLSProtocolSpec `json:"onedottwo"`
|
||||
}
|
||||
|
||||
type TLSProtocolSpec struct {
|
||||
// AllowedCiphers will permit Pinniped to use only the listed ciphers.
|
||||
// This affects Pinniped both when it acts as a client and as a server.
|
||||
// If empty, Pinniped will use a built-in list of ciphers.
|
||||
AllowedCiphers []string `json:"allowedCiphers"`
|
||||
}
|
||||
|
||||
// DiscoveryInfoSpec contains configuration knobs specific to
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"go.pinniped.dev/internal/constable"
|
||||
"go.pinniped.dev/internal/crypto/ptls"
|
||||
"go.pinniped.dev/internal/groupsuffix"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
)
|
||||
@@ -35,7 +36,7 @@ const (
|
||||
// FromPath loads an Config from a provided local file path, inserts any
|
||||
// defaults (from the Config documentation), and verifies that the config is
|
||||
// valid (Config documentation).
|
||||
func FromPath(ctx context.Context, path string) (*Config, error) {
|
||||
func FromPath(ctx context.Context, path string, setAllowedCiphers ptls.SetAllowedCiphersFunc) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read file: %w", err)
|
||||
@@ -95,6 +96,9 @@ func FromPath(ctx context.Context, path string) (*Config, error) {
|
||||
if err := validateAtLeastOneEnabledEndpoint(*config.Endpoints.HTTPS, *config.Endpoints.HTTP); err != nil {
|
||||
return nil, fmt.Errorf("validate endpoints: %w", err)
|
||||
}
|
||||
if err := setAllowedCiphers(config.TLS.OneDotTwo.AllowedCiphers); err != nil {
|
||||
return nil, fmt.Errorf("validate tls: %w", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
@@ -18,10 +18,11 @@ import (
|
||||
|
||||
func TestFromPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
yaml string
|
||||
wantConfig *Config
|
||||
wantError string
|
||||
name string
|
||||
yaml string
|
||||
allowedCiphersError error
|
||||
wantConfig *Config
|
||||
wantError string
|
||||
}{
|
||||
{
|
||||
name: "Happy",
|
||||
@@ -45,6 +46,12 @@ func TestFromPath(t *testing.T) {
|
||||
level: info
|
||||
format: json
|
||||
aggregatedAPIServerPort: 12345
|
||||
tls:
|
||||
onedottwo:
|
||||
allowedCiphers:
|
||||
- foo
|
||||
- bar
|
||||
- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
|
||||
`),
|
||||
wantConfig: &Config{
|
||||
APIGroupSuffix: ptr.To("some.suffix.com"),
|
||||
@@ -70,6 +77,15 @@ func TestFromPath(t *testing.T) {
|
||||
Format: plog.FormatJSON,
|
||||
},
|
||||
AggregatedAPIServerPort: ptr.To[int64](12345),
|
||||
TLS: TLSSpec{
|
||||
OneDotTwo: TLSProtocolSpec{
|
||||
AllowedCiphers: []string{
|
||||
"foo",
|
||||
"bar",
|
||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -251,6 +267,20 @@ func TestFromPath(t *testing.T) {
|
||||
`),
|
||||
wantError: "validate aggregatedAPIServerPort: must be within range 1024 to 65535",
|
||||
},
|
||||
{
|
||||
name: "returns setAllowedCiphers errors",
|
||||
yaml: here.Doc(`
|
||||
---
|
||||
names:
|
||||
defaultTLSCertificateSecret: my-secret-name
|
||||
tls:
|
||||
onedottwo:
|
||||
allowedCiphers:
|
||||
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
|
||||
`),
|
||||
allowedCiphersError: fmt.Errorf("some error from setAllowedCiphers"),
|
||||
wantError: "validate tls: some error from setAllowedCiphers",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
@@ -259,10 +289,10 @@ func TestFromPath(t *testing.T) {
|
||||
// Write yaml to temp file
|
||||
f, err := os.CreateTemp("", "pinniped-test-config-yaml-*")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
t.Cleanup(func() {
|
||||
err := os.Remove(f.Name())
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
})
|
||||
_, err = f.WriteString(test.yaml)
|
||||
require.NoError(t, err)
|
||||
err = f.Close()
|
||||
@@ -271,14 +301,23 @@ func TestFromPath(t *testing.T) {
|
||||
// Test FromPath()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
config, err := FromPath(ctx, f.Name())
|
||||
|
||||
var actualCiphers []string
|
||||
setAllowedCiphers := func(ciphers []string) error {
|
||||
actualCiphers = ciphers
|
||||
return test.allowedCiphersError
|
||||
}
|
||||
|
||||
config, err := FromPath(ctx, f.Name(), setAllowedCiphers)
|
||||
|
||||
if test.wantError != "" {
|
||||
require.EqualError(t, err, test.wantError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.wantConfig, config)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.wantConfig, config)
|
||||
require.Equal(t, test.wantConfig.TLS.OneDotTwo.AllowedCiphers, actualCiphers)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,18 @@ type Config struct {
|
||||
Log plog.LogSpec `json:"log"`
|
||||
Endpoints *Endpoints `json:"endpoints"`
|
||||
AggregatedAPIServerPort *int64 `json:"aggregatedAPIServerPort"`
|
||||
TLS TLSSpec `json:"tls"`
|
||||
}
|
||||
|
||||
type TLSSpec struct {
|
||||
OneDotTwo TLSProtocolSpec `json:"onedottwo"`
|
||||
}
|
||||
|
||||
type TLSProtocolSpec struct {
|
||||
// AllowedCiphers will permit Pinniped to use only the listed ciphers.
|
||||
// This affects Pinniped both when it acts as a client and as a server.
|
||||
// If empty, Pinniped will use a built-in list of ciphers.
|
||||
AllowedCiphers []string `json:"allowedCiphers"`
|
||||
}
|
||||
|
||||
// NamesConfigSpec configures the names of some Kubernetes resources for the Supervisor.
|
||||
|
||||
226
internal/crypto/ptls/common.go
Normal file
226
internal/crypto/ptls/common.go
Normal file
@@ -0,0 +1,226 @@
|
||||
// Copyright 2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ptls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
|
||||
"go.pinniped.dev/internal/plog"
|
||||
)
|
||||
|
||||
// validatedUserConfiguredAllowedCipherSuitesForTLSOneDotTwo is the validated configuration of allowed cipher suites
|
||||
// provided by the user, as set by SetUserConfiguredAllowedCipherSuitesForTLSOneDotTwo().
|
||||
// This global variable is atomic so that it can not be set and read at the same time.
|
||||
//
|
||||
//nolint:gochecknoglobals // this needs to be global because it will be set at application startup from configuration values
|
||||
var validatedUserConfiguredAllowedCipherSuitesForTLSOneDotTwo atomic.Value
|
||||
|
||||
type SetAllowedCiphersFunc func([]string) error
|
||||
|
||||
// SetUserConfiguredAllowedCipherSuitesForTLSOneDotTwo allows configuration/setup components to constrain the
|
||||
// allowed TLS ciphers for TLS1.2. It implements SetAllowedCiphersFunc.
|
||||
func SetUserConfiguredAllowedCipherSuitesForTLSOneDotTwo(userConfiguredAllowedCipherSuitesForTLSOneDotTwo []string) error {
|
||||
plog.Info("setting user-configured allowed ciphers for TLS 1.2", "userConfiguredAllowedCipherSuites", userConfiguredAllowedCipherSuitesForTLSOneDotTwo)
|
||||
|
||||
validatedSuites, err := validateAllowedCiphers(
|
||||
allHardcodedAllowedCipherSuites(),
|
||||
userConfiguredAllowedCipherSuitesForTLSOneDotTwo,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
validatedUserConfiguredAllowedCipherSuitesForTLSOneDotTwo.Store(validatedSuites)
|
||||
return nil
|
||||
}
|
||||
|
||||
// getUserConfiguredAllowedCipherSuitesForTLSOneDotTwo returns the user-configured list of allowed ciphers for TLS1.2.
|
||||
// It is not exported so that it is only available to this package.
|
||||
func getUserConfiguredAllowedCipherSuitesForTLSOneDotTwo() []*tls.CipherSuite {
|
||||
userConfiguredAllowedCipherSuites, ok := (validatedUserConfiguredAllowedCipherSuitesForTLSOneDotTwo.Load()).([]*tls.CipherSuite)
|
||||
if ok {
|
||||
return userConfiguredAllowedCipherSuites
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// constrainCipherSuites returns the intersection of its parameters, as a list of cipher suite IDs.
|
||||
// If userConfiguredAllowedCipherSuites is empty, it will return the list from cipherSuites as IDs.
|
||||
func constrainCipherSuites(
|
||||
cipherSuites []*tls.CipherSuite,
|
||||
userConfiguredAllowedCipherSuites []*tls.CipherSuite,
|
||||
) []uint16 {
|
||||
// If the user did not configure any allowed ciphers suites, then return the IDs
|
||||
// for the ciphers in cipherSuites in the same sort order as quickly as possible.
|
||||
if len(userConfiguredAllowedCipherSuites) == 0 {
|
||||
cipherSuiteIDs := make([]uint16, len(cipherSuites))
|
||||
for i := range cipherSuites {
|
||||
cipherSuiteIDs[i] = cipherSuites[i].ID
|
||||
}
|
||||
return cipherSuiteIDs
|
||||
}
|
||||
|
||||
// Make two sets so we can intersect them below.
|
||||
cipherSuiteIDsSet := sets.New[uint16]()
|
||||
for _, s := range cipherSuites {
|
||||
cipherSuiteIDsSet.Insert(s.ID)
|
||||
}
|
||||
userConfiguredAllowedCipherSuiteIDsSet := sets.New[uint16]()
|
||||
for _, s := range userConfiguredAllowedCipherSuites {
|
||||
userConfiguredAllowedCipherSuiteIDsSet.Insert(s.ID)
|
||||
}
|
||||
|
||||
// Calculate the intersection of sets.
|
||||
intersection := cipherSuiteIDsSet.Intersection(userConfiguredAllowedCipherSuiteIDsSet)
|
||||
|
||||
// If the user did not provide any valid allowed cipher suites, use cipherSuiteIDsSet.
|
||||
// Note that the user-configured allowed cipher suites are validated elsewhere, so
|
||||
// this should only happen when the user chose not to specify any allowed cipher suites.
|
||||
if len(intersection) == 0 {
|
||||
intersection = cipherSuiteIDsSet
|
||||
}
|
||||
|
||||
result := intersection.UnsortedList()
|
||||
// Preserve the original order as shown in the cipherSuites parameter.
|
||||
slices.SortFunc(result, func(a, b uint16) int {
|
||||
return slices.IndexFunc(cipherSuites, func(cipher *tls.CipherSuite) bool { return cipher.ID == a }) -
|
||||
slices.IndexFunc(cipherSuites, func(cipher *tls.CipherSuite) bool { return cipher.ID == b })
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func translateIDIntoSecureCipherSuites(ids []uint16) []*tls.CipherSuite {
|
||||
golangSecureCipherSuites := tls.CipherSuites()
|
||||
result := make([]*tls.CipherSuite, 0)
|
||||
|
||||
for _, golangSecureCipherSuite := range golangSecureCipherSuites {
|
||||
// As of golang 1.22, all cipher suites from tls.CipherSuites are secure, so this is just future-proofing.
|
||||
if golangSecureCipherSuite.Insecure { // untested
|
||||
continue
|
||||
}
|
||||
|
||||
if !slices.Contains(golangSecureCipherSuite.SupportedVersions, tls.VersionTLS12) {
|
||||
continue
|
||||
}
|
||||
|
||||
if slices.Contains(ids, golangSecureCipherSuite.ID) {
|
||||
result = append(result, golangSecureCipherSuite)
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve the order as shown in ids
|
||||
slices.SortFunc(result, func(a, b *tls.CipherSuite) int {
|
||||
return slices.Index(ids, a.ID) - slices.Index(ids, b.ID)
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// buildTLSConfig will return a tls.Config with CipherSuites from the intersection of cipherSuites and userConfiguredAllowedCipherSuites.
|
||||
func buildTLSConfig(
|
||||
rootCAs *x509.CertPool,
|
||||
cipherSuites []*tls.CipherSuite,
|
||||
userConfiguredAllowedCipherSuites []*tls.CipherSuite,
|
||||
) *tls.Config {
|
||||
return &tls.Config{
|
||||
// Can't use SSLv3 because of POODLE and BEAST
|
||||
// Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher
|
||||
// Can't use TLSv1.1 because of RC4 cipher usage
|
||||
//
|
||||
// The Kubernetes API Server must use TLS 1.2, at a minimum,
|
||||
// to protect the confidentiality of sensitive data during electronic dissemination.
|
||||
// https://stigviewer.com/stig/kubernetes/2021-06-17/finding/V-242378
|
||||
MinVersion: tls.VersionTLS12,
|
||||
|
||||
CipherSuites: constrainCipherSuites(cipherSuites, userConfiguredAllowedCipherSuites),
|
||||
|
||||
// enable HTTP2 for go's 1.7 HTTP Server
|
||||
// setting this explicitly is only required in very specific circumstances
|
||||
// it is simpler to just set it here than to try and determine if we need to
|
||||
NextProtos: []string{"h2", "http/1.1"},
|
||||
|
||||
// optional root CAs, nil means use the host's root CA set
|
||||
RootCAs: rootCAs,
|
||||
}
|
||||
}
|
||||
|
||||
// validateAllowedCiphers will take in the user-configured allowed cipher names and validate them against a list of
|
||||
// ciphers. If any userConfiguredAllowedCipherSuites names are not in the cipherSuites, return a descriptive error.
|
||||
// An empty list of userConfiguredAllowedCipherSuites means that the user wants the all default ciphers from cipherSuites.
|
||||
// Returns the tls.CipherSuite representation when all userConfiguredAllowedCipherSuites are valid.
|
||||
func validateAllowedCiphers(
|
||||
cipherSuites []*tls.CipherSuite,
|
||||
userConfiguredAllowedCipherSuites []string,
|
||||
) ([]*tls.CipherSuite, error) {
|
||||
if len(userConfiguredAllowedCipherSuites) < 1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cipherSuiteNames := make([]string, len(cipherSuites))
|
||||
for i, cipher := range cipherSuites {
|
||||
cipherSuiteNames[i] = cipher.Name
|
||||
}
|
||||
|
||||
// Allow some loosening of the names for legacy reasons.
|
||||
for i := range userConfiguredAllowedCipherSuites {
|
||||
switch userConfiguredAllowedCipherSuites[i] {
|
||||
case "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305":
|
||||
userConfiguredAllowedCipherSuites[i] = "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256"
|
||||
case "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305":
|
||||
userConfiguredAllowedCipherSuites[i] = "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure that all allowedCipherNames are actually configured within Pinniped.
|
||||
var invalidCipherNames []string
|
||||
for _, allowedCipherName := range userConfiguredAllowedCipherSuites {
|
||||
if !slices.Contains(cipherSuiteNames, allowedCipherName) {
|
||||
invalidCipherNames = append(invalidCipherNames, allowedCipherName)
|
||||
}
|
||||
}
|
||||
|
||||
if len(invalidCipherNames) > 0 {
|
||||
return nil, fmt.Errorf("unrecognized ciphers [%s], ciphers must be from list [%s]",
|
||||
strings.Join(invalidCipherNames, ", "),
|
||||
strings.Join(cipherSuiteNames, ", "))
|
||||
}
|
||||
|
||||
// Now translate the allowedCipherNames into their *tls.CipherSuite representation.
|
||||
var validCiphers []*tls.CipherSuite
|
||||
for _, cipher := range cipherSuites {
|
||||
if slices.Contains(userConfiguredAllowedCipherSuites, cipher.Name) {
|
||||
validCiphers = append(validCiphers, cipher)
|
||||
}
|
||||
}
|
||||
|
||||
return validCiphers, nil
|
||||
}
|
||||
|
||||
// allHardcodedAllowedCipherSuites returns the full list of all hardcoded ciphers that are allowed for any profile.
|
||||
// Note that it will return different values depending on if the code was compiled in FIPS or non-FIPS mode.
|
||||
func allHardcodedAllowedCipherSuites() []*tls.CipherSuite {
|
||||
// First append all secure and LDAP cipher suites.
|
||||
result := translateIDIntoSecureCipherSuites(append(secureCipherSuiteIDs, additionalSecureCipherSuiteIDsOnlyForLDAPClients...))
|
||||
|
||||
// Then append any insecure cipher suites that might be allowed.
|
||||
// insecureCipherSuiteIDs is empty except when compiled in FIPS mode.
|
||||
for _, golangInsecureCipherSuite := range tls.InsecureCipherSuites() {
|
||||
if !slices.Contains(golangInsecureCipherSuite.SupportedVersions, tls.VersionTLS12) {
|
||||
continue
|
||||
}
|
||||
|
||||
if slices.Contains(insecureCipherSuiteIDs, golangInsecureCipherSuite.ID) {
|
||||
result = append(result, golangInsecureCipherSuite)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
468
internal/crypto/ptls/common_test.go
Normal file
468
internal/crypto/ptls/common_test.go
Normal file
@@ -0,0 +1,468 @@
|
||||
// Copyright 2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ptls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSetAllowedCiphersForTLSOneDotTwo(t *testing.T) {
|
||||
t.Run("with valid ciphers, mutates the global state", func(t *testing.T) {
|
||||
require.Empty(t, getUserConfiguredAllowedCipherSuitesForTLSOneDotTwo())
|
||||
// With no user-configured allowed ciphers, expect all the hardcoded ciphers
|
||||
require.Equal(t, []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
}, Default(nil).CipherSuites)
|
||||
require.Equal(t, []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
}, DefaultLDAP(nil).CipherSuites)
|
||||
require.Empty(t, Secure(nil).CipherSuites)
|
||||
|
||||
t.Cleanup(func() {
|
||||
err := SetUserConfiguredAllowedCipherSuitesForTLSOneDotTwo(nil)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, getUserConfiguredAllowedCipherSuitesForTLSOneDotTwo())
|
||||
|
||||
require.Equal(t, []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
}, Default(nil).CipherSuites)
|
||||
require.Equal(t, []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
}, DefaultLDAP(nil).CipherSuites)
|
||||
require.Empty(t, Secure(nil).CipherSuites)
|
||||
})
|
||||
|
||||
userConfiguredAllowedCipherSuites := []string{
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", // this is an LDAP-only cipher
|
||||
}
|
||||
err := SetUserConfiguredAllowedCipherSuitesForTLSOneDotTwo(userConfiguredAllowedCipherSuites)
|
||||
require.NoError(t, err)
|
||||
stored := getUserConfiguredAllowedCipherSuitesForTLSOneDotTwo()
|
||||
var storedNames []string
|
||||
for _, suite := range stored {
|
||||
storedNames = append(storedNames, suite.Name)
|
||||
}
|
||||
require.Equal(t, userConfiguredAllowedCipherSuites, storedNames)
|
||||
|
||||
require.Equal(t, []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
}, Default(nil).CipherSuites)
|
||||
require.Equal(t, []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
}, DefaultLDAP(nil).CipherSuites)
|
||||
require.Empty(t, Secure(nil).CipherSuites)
|
||||
})
|
||||
|
||||
t.Run("SetUserConfiguredAllowedCipherSuitesForTLSOneDotTwo calls validateAllowedCiphers and returns error", func(t *testing.T) {
|
||||
err := SetUserConfiguredAllowedCipherSuitesForTLSOneDotTwo([]string{"foo"})
|
||||
require.Regexp(t, regexp.QuoteMeta("unrecognized ciphers [foo], ciphers must be from list [TLS")+".*"+regexp.QuoteMeta("]"), err.Error())
|
||||
})
|
||||
}
|
||||
|
||||
func TestConstrainCipherSuites(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cipherSuites []*tls.CipherSuite
|
||||
userConfiguredAllowedCipherSuites []*tls.CipherSuite
|
||||
wantCipherSuites []uint16
|
||||
}{
|
||||
{
|
||||
name: "with empty inputs, returns empty output",
|
||||
wantCipherSuites: make([]uint16, 0),
|
||||
},
|
||||
{
|
||||
name: "with empty userConfiguredAllowedCipherSuites, returns cipherSuites",
|
||||
cipherSuites: []*tls.CipherSuite{
|
||||
{ID: 0},
|
||||
{ID: 1},
|
||||
{ID: 2},
|
||||
},
|
||||
wantCipherSuites: []uint16{0, 1, 2},
|
||||
},
|
||||
{
|
||||
name: "with userConfiguredAllowedCipherSuites, returns only ciphers found in both inputs",
|
||||
cipherSuites: []*tls.CipherSuite{
|
||||
{ID: 0},
|
||||
{ID: 1},
|
||||
{ID: 2},
|
||||
{ID: 3},
|
||||
{ID: 4},
|
||||
},
|
||||
userConfiguredAllowedCipherSuites: []*tls.CipherSuite{
|
||||
{ID: 1},
|
||||
{ID: 3},
|
||||
{ID: 999},
|
||||
},
|
||||
wantCipherSuites: []uint16{1, 3},
|
||||
},
|
||||
{
|
||||
name: "with all invalid userConfiguredAllowedCipherSuites, returns cipherSuites",
|
||||
cipherSuites: []*tls.CipherSuite{
|
||||
{ID: 0},
|
||||
{ID: 1},
|
||||
{ID: 2},
|
||||
{ID: 3},
|
||||
{ID: 4},
|
||||
},
|
||||
userConfiguredAllowedCipherSuites: []*tls.CipherSuite{
|
||||
{ID: 1000},
|
||||
{ID: 2000},
|
||||
{ID: 3000},
|
||||
},
|
||||
wantCipherSuites: []uint16{0, 1, 2, 3, 4},
|
||||
},
|
||||
{
|
||||
name: "preserves order from cipherSuites",
|
||||
cipherSuites: []*tls.CipherSuite{
|
||||
{ID: 0},
|
||||
{ID: 1},
|
||||
{ID: 2},
|
||||
{ID: 3},
|
||||
{ID: 4},
|
||||
},
|
||||
userConfiguredAllowedCipherSuites: []*tls.CipherSuite{
|
||||
{ID: 5},
|
||||
{ID: 4},
|
||||
{ID: 3},
|
||||
{ID: 2},
|
||||
},
|
||||
wantCipherSuites: []uint16{2, 3, 4},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
actual := constrainCipherSuites(test.cipherSuites, test.userConfiguredAllowedCipherSuites)
|
||||
require.Equal(t, test.wantCipherSuites, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTLSSecureCipherSuites checks whether golang has changed the list of secure ciphers.
|
||||
// This was written against golang 1.22.2.
|
||||
// Pinniped has chosen to use only secure ciphers returned by tls.CipherSuites, so in the future we want to be aware of
|
||||
// changes to this list of ciphers (additions or removals).
|
||||
//
|
||||
// If golang adds ciphers, we should consider adding them to the list of possible ciphers for Pinniped's profiles.
|
||||
// If golang removes ciphers, we should consider removing them from the list of possible ciphers for Pinniped's profiles.
|
||||
// Any cipher modifications should be added to release notes so that Pinniped admins can choose to modify their
|
||||
// allowedCiphers accordingly.
|
||||
func TestTLSSecureCipherSuites(t *testing.T) {
|
||||
expectedCipherSuites := []uint16{
|
||||
tls.TLS_AES_128_GCM_SHA256,
|
||||
tls.TLS_AES_256_GCM_SHA384,
|
||||
tls.TLS_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
}
|
||||
|
||||
tlsSecureCipherSuites := tls.CipherSuites()
|
||||
require.Equal(t, len(expectedCipherSuites), len(tlsSecureCipherSuites))
|
||||
for _, suite := range tlsSecureCipherSuites {
|
||||
require.False(t, suite.Insecure)
|
||||
require.Contains(t, expectedCipherSuites, suite.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildTLSConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
aCertPool := x509.NewCertPool()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
rootCAs *x509.CertPool
|
||||
cipherSuites []*tls.CipherSuite
|
||||
userConfiguredAllowedCipherSuiteIDs []uint16
|
||||
wantConfig *tls.Config
|
||||
}{
|
||||
{
|
||||
name: "happy path",
|
||||
rootCAs: aCertPool,
|
||||
cipherSuites: tls.CipherSuites(),
|
||||
wantConfig: &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_AES_128_GCM_SHA256,
|
||||
tls.TLS_AES_256_GCM_SHA384,
|
||||
tls.TLS_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, //nolint:gosec // this is for testing purposes
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
},
|
||||
NextProtos: []string{"h2", "http/1.1"},
|
||||
RootCAs: aCertPool,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with no userConfiguredAllowedCipherSuites, returns cipherSuites",
|
||||
cipherSuites: func() []*tls.CipherSuite {
|
||||
result := tls.CipherSuites()
|
||||
return result[:2]
|
||||
}(),
|
||||
wantConfig: &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_AES_128_GCM_SHA256,
|
||||
tls.TLS_AES_256_GCM_SHA384,
|
||||
},
|
||||
NextProtos: []string{"h2", "http/1.1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with allowed Ciphers, restricts CipherSuites to just those ciphers",
|
||||
rootCAs: aCertPool,
|
||||
cipherSuites: tls.CipherSuites(),
|
||||
userConfiguredAllowedCipherSuiteIDs: []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
},
|
||||
wantConfig: &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, //nolint:gosec // this is a test
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
},
|
||||
NextProtos: []string{"h2", "http/1.1"},
|
||||
RootCAs: aCertPool,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with allowed ciphers in random order, returns ciphers in the order from cipherSuites",
|
||||
cipherSuites: tls.CipherSuites(),
|
||||
userConfiguredAllowedCipherSuiteIDs: []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
},
|
||||
wantConfig: &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, //nolint:gosec // this is for testing purposes
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
},
|
||||
NextProtos: []string{"h2", "http/1.1"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
userConfiguredAllowedCipherSuites := make([]*tls.CipherSuite, 0)
|
||||
for _, allowedCipher := range test.userConfiguredAllowedCipherSuiteIDs {
|
||||
for _, cipherSuite := range tls.CipherSuites() {
|
||||
if allowedCipher == cipherSuite.ID {
|
||||
userConfiguredAllowedCipherSuites = append(userConfiguredAllowedCipherSuites, cipherSuite)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
actualConfig := buildTLSConfig(test.rootCAs, test.cipherSuites, userConfiguredAllowedCipherSuites)
|
||||
require.Equal(t, test.wantConfig, actualConfig)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAllowedCiphers(t *testing.T) {
|
||||
cipherSuites := tls.CipherSuites()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cipherSuites []*tls.CipherSuite
|
||||
userConfiguredAllowedCipherSuites []string
|
||||
wantCipherSuites []*tls.CipherSuite
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "empty inputs result in empty outputs",
|
||||
},
|
||||
{
|
||||
name: "with all valid inputs, returns the ciphers",
|
||||
cipherSuites: cipherSuites,
|
||||
userConfiguredAllowedCipherSuites: []string{
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
|
||||
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
|
||||
},
|
||||
wantCipherSuites: func() []*tls.CipherSuite {
|
||||
var result []*tls.CipherSuite
|
||||
for _, suite := range cipherSuites {
|
||||
switch suite.Name {
|
||||
case "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
|
||||
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA":
|
||||
result = append(result, suite)
|
||||
default:
|
||||
}
|
||||
}
|
||||
return result
|
||||
}(),
|
||||
},
|
||||
{
|
||||
name: "with all valid inputs, allows some legacy cipher names and returns the ciphers",
|
||||
cipherSuites: cipherSuites,
|
||||
userConfiguredAllowedCipherSuites: []string{
|
||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
|
||||
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
|
||||
},
|
||||
wantCipherSuites: func() []*tls.CipherSuite {
|
||||
var result []*tls.CipherSuite
|
||||
for _, suite := range cipherSuites {
|
||||
switch suite.Name {
|
||||
case "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
|
||||
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256":
|
||||
result = append(result, suite)
|
||||
default:
|
||||
}
|
||||
}
|
||||
return result
|
||||
}(),
|
||||
},
|
||||
{
|
||||
name: "with invalid input, return an error with all known ciphers",
|
||||
cipherSuites: cipherSuites[:2],
|
||||
userConfiguredAllowedCipherSuites: []string{"foo"},
|
||||
wantErr: "unrecognized ciphers [foo], ciphers must be from list [TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384]",
|
||||
},
|
||||
{
|
||||
name: "with some valid and some invalid input, return an error with all known ciphers",
|
||||
cipherSuites: cipherSuites[6:9],
|
||||
userConfiguredAllowedCipherSuites: []string{
|
||||
"foo",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||
"bar",
|
||||
},
|
||||
wantErr: "unrecognized ciphers [foo, bar], ciphers must be from list [TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384]",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
actual, err := validateAllowedCiphers(test.cipherSuites, test.userConfiguredAllowedCipherSuites)
|
||||
if len(test.wantErr) > 0 {
|
||||
require.ErrorContains(t, err, test.wantErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, test.wantCipherSuites, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslateIDIntoSecureCipherSuites(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
inputs []uint16
|
||||
wantOutputs []uint16
|
||||
}{
|
||||
{
|
||||
name: "returns ciphers found in tls.CipherSuites",
|
||||
inputs: []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
},
|
||||
wantOutputs: []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "returns ciphers in the input order",
|
||||
inputs: []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
},
|
||||
wantOutputs: []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ignores cipher suites not returned by tls.CipherSuites",
|
||||
inputs: []uint16{
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ignores cipher suites that only support TLS1.3",
|
||||
inputs: []uint16{
|
||||
tls.TLS_AES_128_GCM_SHA256,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
actual := translateIDIntoSecureCipherSuites(test.inputs)
|
||||
|
||||
require.Len(t, actual, len(test.wantOutputs))
|
||||
for i, suite := range actual {
|
||||
require.Equal(t, test.wantOutputs[i], suite.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
41
internal/crypto/ptls/log_profiles.go
Normal file
41
internal/crypto/ptls/log_profiles.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright 2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ptls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
|
||||
"go.pinniped.dev/internal/plog"
|
||||
)
|
||||
|
||||
func cipherSuiteNamesForCipherSuites(ciphers []uint16) []string {
|
||||
names := make([]string, len(ciphers))
|
||||
for i, suite := range ciphers {
|
||||
names[i] = tls.CipherSuiteName(suite)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func tlsVersionName(tlsVersion uint16) string {
|
||||
if tlsVersion == 0 {
|
||||
return "NONE"
|
||||
}
|
||||
return tls.VersionName(tlsVersion)
|
||||
}
|
||||
|
||||
func logProfile(name string, log plog.Logger, profile *tls.Config) {
|
||||
log.Info("tls configuration",
|
||||
"profile name", name,
|
||||
"MinVersion", tlsVersionName(profile.MinVersion),
|
||||
"MaxVersion", tlsVersionName(profile.MaxVersion),
|
||||
"CipherSuites", cipherSuiteNamesForCipherSuites(profile.CipherSuites),
|
||||
"NextProtos", profile.NextProtos,
|
||||
)
|
||||
}
|
||||
|
||||
func LogAllProfiles(log plog.Logger) {
|
||||
logProfile("Default", log, Default(nil))
|
||||
logProfile("DefaultLDAP", log, DefaultLDAP(nil))
|
||||
logProfile("Secure", log, Secure(nil))
|
||||
}
|
||||
30
internal/crypto/ptls/log_profiles_test.go
Normal file
30
internal/crypto/ptls/log_profiles_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright 2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ptls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.pinniped.dev/internal/plog"
|
||||
)
|
||||
|
||||
func TestLogAllProfiles(t *testing.T) {
|
||||
var log bytes.Buffer
|
||||
logger := plog.TestLogger(t, &log)
|
||||
|
||||
LogAllProfiles(logger)
|
||||
|
||||
expectedLines := []string{
|
||||
`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"ptls/log_profiles.go:<line>$ptls.logProfile","message":"tls configuration","profile name":"Default","MinVersion":"TLS 1.2","MaxVersion":"NONE","CipherSuites":["TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256","TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"],"NextProtos":["h2","http/1.1"]}`,
|
||||
`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"ptls/log_profiles.go:<line>$ptls.logProfile","message":"tls configuration","profile name":"DefaultLDAP","MinVersion":"TLS 1.2","MaxVersion":"NONE","CipherSuites":["TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256","TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256","TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA","TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA","TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA","TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA"],"NextProtos":["h2","http/1.1"]}`,
|
||||
`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"ptls/log_profiles.go:<line>$ptls.logProfile","message":"tls configuration","profile name":"Secure","MinVersion":"TLS 1.3","MaxVersion":"NONE","CipherSuites":[],"NextProtos":["h2","http/1.1"]}`,
|
||||
}
|
||||
expectedOutput := strings.Join(expectedLines, "\n") + "\n"
|
||||
|
||||
require.Equal(t, expectedOutput, log.String())
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Note that everything in this file is overridden by profiles_fips_strict.go when Pinniped is built in FIPS-only mode.
|
||||
//go:build !fips_strict
|
||||
|
||||
package ptls
|
||||
@@ -17,6 +18,62 @@ import (
|
||||
"go.pinniped.dev/internal/plog"
|
||||
)
|
||||
|
||||
var (
|
||||
// secureCipherSuiteIDs is the list of TLS ciphers to use for both clients and servers when using TLS 1.2.
|
||||
//
|
||||
// The order does not matter in go 1.17+ https://go.dev/blog/tls-cipher-suites.
|
||||
// We match crypto/tls.cipherSuitesPreferenceOrder because it makes unit tests easier to write.
|
||||
//
|
||||
// as of 2021-10-19, Mozilla Guideline v5.6, Go 1.17.2, intermediate configuration, supports:
|
||||
// - Firefox 27
|
||||
// - Android 4.4.2
|
||||
// - Chrome 31
|
||||
// - Edge
|
||||
// - IE 11 on Windows 7
|
||||
// - Java 8u31
|
||||
// - OpenSSL 1.0.1
|
||||
// - Opera 20
|
||||
// - Safari 9
|
||||
// https://ssl-config.mozilla.org/#server=go&version=1.17.2&config=intermediate&guideline=5.6
|
||||
//
|
||||
// The Kubernetes API server must use approved cipher suites.
|
||||
// https://stigviewer.com/stig/kubernetes/2021-06-17/finding/V-242418
|
||||
//
|
||||
// These are all AEADs with ECDHE, some use ChaCha20Poly1305 while others use AES-GCM,
|
||||
// which provides forward secrecy, confidentiality and authenticity of data.
|
||||
secureCipherSuiteIDs = []uint16{ //nolint:gochecknoglobals // please treat this as a const
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
}
|
||||
|
||||
// insecureCipherSuiteIDs is a list of additional ciphers that should be allowed for both clients
|
||||
// and servers when using TLS 1.2.
|
||||
//
|
||||
// This list is empty when compiled in non-FIPS mode, so we will not use any insecure ciphers in non-FIPS mode.
|
||||
insecureCipherSuiteIDs []uint16 //nolint:gochecknoglobals // please treat this as a const
|
||||
|
||||
// additionalSecureCipherSuiteIDsOnlyForLDAPClients are additional ciphers to use only for LDAP clients
|
||||
// when using TLS 1.2. These can be used when the Pinniped Supervisor is making calls to an LDAP server
|
||||
// configured by an LDAPIdentityProvider or ActiveDirectoryIdentityProvider.
|
||||
//
|
||||
// Adds less secure ciphers to support the default AWS Active Directory config.
|
||||
//
|
||||
// These are all CBC with ECDHE. Golang considers these to be secure. However,
|
||||
// these provide forward secrecy and confidentiality of data, but not authenticity.
|
||||
// MAC-then-Encrypt CBC ciphers are susceptible to padding oracle attacks.
|
||||
// See https://crypto.stackexchange.com/a/205 and https://crypto.stackexchange.com/a/224
|
||||
additionalSecureCipherSuiteIDsOnlyForLDAPClients = []uint16{ //nolint:gochecknoglobals // please treat this as a const
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
}
|
||||
)
|
||||
|
||||
// init prints a log message to tell the operator how Pinniped was compiled. This makes it obvious
|
||||
// that they are using Pinniped in FIPS-mode or not, which is otherwise hard to observe.
|
||||
func init() { //nolint:gochecknoinits
|
||||
@@ -39,73 +96,19 @@ const SecureTLSConfigMinTLSVersion = tls.VersionTLS13
|
||||
// A. servers whose clients are outside our control and who may reasonably wish to use TLS 1.2, and
|
||||
// B. clients who need to interact with servers that might not support TLS 1.3.
|
||||
// Note that this will behave differently when compiled in FIPS mode (see profiles_fips_strict.go).
|
||||
// Default returns a tls.Config with a minimum of TLS1.2+ and a few ciphers that can be further constrained by configuration.
|
||||
func Default(rootCAs *x509.CertPool) *tls.Config {
|
||||
return &tls.Config{
|
||||
// Can't use SSLv3 because of POODLE and BEAST
|
||||
// Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher
|
||||
// Can't use TLSv1.1 because of RC4 cipher usage
|
||||
//
|
||||
// The Kubernetes API Server must use TLS 1.2, at a minimum,
|
||||
// to protect the confidentiality of sensitive data during electronic dissemination.
|
||||
// https://stigviewer.com/stig/kubernetes/2021-06-17/finding/V-242378
|
||||
MinVersion: tls.VersionTLS12,
|
||||
|
||||
// the order does not matter in go 1.17+ https://go.dev/blog/tls-cipher-suites
|
||||
// we match crypto/tls.cipherSuitesPreferenceOrder because it makes unit tests easier to write
|
||||
// this list is ignored when TLS 1.3 is used
|
||||
//
|
||||
// as of 2021-10-19, Mozilla Guideline v5.6, Go 1.17.2, intermediate configuration, supports:
|
||||
// - Firefox 27
|
||||
// - Android 4.4.2
|
||||
// - Chrome 31
|
||||
// - Edge
|
||||
// - IE 11 on Windows 7
|
||||
// - Java 8u31
|
||||
// - OpenSSL 1.0.1
|
||||
// - Opera 20
|
||||
// - Safari 9
|
||||
// https://ssl-config.mozilla.org/#server=go&version=1.17.2&config=intermediate&guideline=5.6
|
||||
//
|
||||
// The Kubernetes API server must use approved cipher suites.
|
||||
// https://stigviewer.com/stig/kubernetes/2021-06-17/finding/V-242418
|
||||
CipherSuites: []uint16{
|
||||
// these are all AEADs with ECDHE, some use ChaCha20Poly1305 while others use AES-GCM
|
||||
// this provides forward secrecy, confidentiality and authenticity of data
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
},
|
||||
|
||||
// enable HTTP2 for go's 1.7 HTTP Server
|
||||
// setting this explicitly is only required in very specific circumstances
|
||||
// it is simpler to just set it here than to try and determine if we need to
|
||||
NextProtos: []string{"h2", "http/1.1"},
|
||||
|
||||
// optional root CAs, nil means use the host's root CA set
|
||||
RootCAs: rootCAs,
|
||||
}
|
||||
ciphers := translateIDIntoSecureCipherSuites(secureCipherSuiteIDs)
|
||||
return buildTLSConfig(rootCAs, ciphers, getUserConfiguredAllowedCipherSuitesForTLSOneDotTwo())
|
||||
}
|
||||
|
||||
// DefaultLDAP TLS profile should be used by clients who need to interact with potentially old LDAP servers
|
||||
// that might not support TLS 1.3 and that might use older ciphers.
|
||||
// Note that this will behave differently when compiled in FIPS mode (see profiles_fips_strict.go).
|
||||
func DefaultLDAP(rootCAs *x509.CertPool) *tls.Config {
|
||||
c := Default(rootCAs)
|
||||
// add less secure ciphers to support the default AWS Active Directory config
|
||||
c.CipherSuites = append(c.CipherSuites,
|
||||
// CBC with ECDHE
|
||||
// this provides forward secrecy and confidentiality of data but not authenticity
|
||||
// MAC-then-Encrypt CBC ciphers are susceptible to padding oracle attacks
|
||||
// See https://crypto.stackexchange.com/a/205 and https://crypto.stackexchange.com/a/224
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
)
|
||||
return c
|
||||
ciphers := translateIDIntoSecureCipherSuites(secureCipherSuiteIDs)
|
||||
ciphers = append(ciphers, translateIDIntoSecureCipherSuites(additionalSecureCipherSuiteIDsOnlyForLDAPClients)...)
|
||||
return buildTLSConfig(rootCAs, ciphers, getUserConfiguredAllowedCipherSuitesForTLSOneDotTwo())
|
||||
}
|
||||
|
||||
// Secure TLS profile should be used by:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// The configurations here override the usual configs when Pinniped is built in fips-only mode.
|
||||
// This file overrides profiles.go when Pinniped is built in FIPS-only mode.
|
||||
//go:build fips_strict
|
||||
|
||||
package ptls
|
||||
@@ -20,6 +20,37 @@ import (
|
||||
"go.pinniped.dev/internal/plog"
|
||||
)
|
||||
|
||||
// The union of these three variables is all the FIPS-approved TLS 1.2 ciphers.
|
||||
// If this list does not match the boring crypto compiler's list then the TestFIPSCipherSuites integration
|
||||
// test should fail, which indicates that this list needs to be updated.
|
||||
var (
|
||||
// secureCipherSuiteIDs is the list of TLS ciphers to use for both clients and servers when using TLS 1.2.
|
||||
//
|
||||
// FIPS allows the use of these ciphers which golang considers secure.
|
||||
secureCipherSuiteIDs = []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
}
|
||||
|
||||
// insecureCipherSuiteIDs is a list of additional ciphers that should be allowed for both clients
|
||||
// and servers when using TLS 1.2.
|
||||
//
|
||||
// FIPS allows the use of these specific ciphers that golang considers insecure.
|
||||
insecureCipherSuiteIDs = []uint16{
|
||||
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
||||
}
|
||||
|
||||
// additionalSecureCipherSuiteIDsOnlyForLDAPClients are additional ciphers to use only for LDAP clients
|
||||
// when using TLS 1.2. These can be used when the Pinniped Supervisor is making calls to an LDAP server
|
||||
// configured by an LDAPIdentityProvider or ActiveDirectoryIdentityProvider.
|
||||
//
|
||||
// When compiled in FIPS mode, there are no extras for LDAP clients.
|
||||
additionalSecureCipherSuiteIDsOnlyForLDAPClients []uint16
|
||||
)
|
||||
|
||||
// init: see comment in profiles.go.
|
||||
func init() {
|
||||
switch filepath.Base(os.Args[0]) {
|
||||
@@ -40,37 +71,18 @@ const SecureTLSConfigMinTLSVersion = tls.VersionTLS12
|
||||
|
||||
// Default: see comment in profiles.go.
|
||||
// This chooses different cipher suites and/or TLS versions compared to non-FIPS mode.
|
||||
// In FIPS mode, this will use the union of the secureCipherSuiteIDs, additionalSecureCipherSuiteIDsOnlyForLDAPClients,
|
||||
// and insecureCipherSuiteIDs values defined above.
|
||||
func Default(rootCAs *x509.CertPool) *tls.Config {
|
||||
return &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
// Until goboring supports TLS 1.3, make the max version 1.2.
|
||||
MaxVersion: tls.VersionTLS12,
|
||||
|
||||
// This is all the fips-approved TLS 1.2 ciphers.
|
||||
// The list is hard-coded for convenience of testing.
|
||||
// If this list does not match the boring crypto compiler's list then the TestFIPSCipherSuites integration
|
||||
// test should fail, which indicates that this list needs to be updated.
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
||||
},
|
||||
|
||||
// enable HTTP2 for go's 1.7 HTTP Server
|
||||
// setting this explicitly is only required in very specific circumstances
|
||||
// it is simpler to just set it here than to try and determine if we need to
|
||||
NextProtos: []string{"h2", "http/1.1"},
|
||||
|
||||
// optional root CAs, nil means use the host's root CA set
|
||||
RootCAs: rootCAs,
|
||||
}
|
||||
config := buildTLSConfig(rootCAs, allHardcodedAllowedCipherSuites(), getUserConfiguredAllowedCipherSuitesForTLSOneDotTwo())
|
||||
// Until goboring supports TLS 1.3, make the max version 1.2.
|
||||
config.MaxVersion = tls.VersionTLS12
|
||||
return config
|
||||
}
|
||||
|
||||
// DefaultLDAP: see comment in profiles.go.
|
||||
// This chooses different cipher suites and/or TLS versions compared to non-FIPS mode.
|
||||
// In FIPS mode, this is not any different from the Default profile.
|
||||
func DefaultLDAP(rootCAs *x509.CertPool) *tls.Config {
|
||||
return Default(rootCAs)
|
||||
}
|
||||
@@ -78,6 +90,7 @@ func DefaultLDAP(rootCAs *x509.CertPool) *tls.Config {
|
||||
// Secure: see comment in profiles.go.
|
||||
// This chooses different cipher suites and/or TLS versions compared to non-FIPS mode.
|
||||
// Until goboring supports TLS 1.3, make the Secure profile the same as the Default profile in FIPS mode.
|
||||
// Until then, this is not any different from the Default profile in FIPS mode.
|
||||
func Secure(rootCAs *x509.CertPool) *tls.Config {
|
||||
return Default(rootCAs)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apiserver/pkg/server/options"
|
||||
)
|
||||
|
||||
@@ -17,7 +18,6 @@ func TestDefault(t *testing.T) {
|
||||
|
||||
aCertPool := x509.NewCertPool()
|
||||
|
||||
actual := Default(aCertPool)
|
||||
expected := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
CipherSuites: []uint16{
|
||||
@@ -32,7 +32,7 @@ func TestDefault(t *testing.T) {
|
||||
RootCAs: aCertPool,
|
||||
}
|
||||
|
||||
require.Equal(t, expected, actual)
|
||||
require.Equal(t, expected, Default(aCertPool))
|
||||
}
|
||||
|
||||
func TestDefaultLDAP(t *testing.T) {
|
||||
@@ -40,7 +40,6 @@ func TestDefaultLDAP(t *testing.T) {
|
||||
|
||||
aCertPool := x509.NewCertPool()
|
||||
|
||||
actual := DefaultLDAP(aCertPool)
|
||||
expected := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
CipherSuites: []uint16{
|
||||
@@ -59,7 +58,7 @@ func TestDefaultLDAP(t *testing.T) {
|
||||
RootCAs: aCertPool,
|
||||
}
|
||||
|
||||
require.Equal(t, expected, actual)
|
||||
require.Equal(t, expected, DefaultLDAP(aCertPool))
|
||||
}
|
||||
|
||||
func TestSecure(t *testing.T) {
|
||||
@@ -67,7 +66,6 @@ func TestSecure(t *testing.T) {
|
||||
|
||||
aCertPool := x509.NewCertPool()
|
||||
|
||||
actual := Secure(aCertPool)
|
||||
expected := &tls.Config{
|
||||
MinVersion: tls.VersionTLS13,
|
||||
CipherSuites: nil, // TLS 1.3 ciphers are not configurable
|
||||
@@ -75,17 +73,76 @@ func TestSecure(t *testing.T) {
|
||||
RootCAs: aCertPool,
|
||||
}
|
||||
|
||||
require.Equal(t, expected, actual)
|
||||
require.Equal(t, expected, Secure(aCertPool))
|
||||
}
|
||||
|
||||
func TestSecureServing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
opts := &options.SecureServingOptionsWithLoopback{SecureServingOptions: &options.SecureServingOptions{}}
|
||||
SecureServing(opts)
|
||||
require.Equal(t, options.SecureServingOptionsWithLoopback{
|
||||
|
||||
expected := options.SecureServingOptionsWithLoopback{
|
||||
SecureServingOptions: &options.SecureServingOptions{
|
||||
MinTLSVersion: "VersionTLS13",
|
||||
},
|
||||
}, *opts)
|
||||
}
|
||||
|
||||
SecureServing(opts)
|
||||
require.Equal(t, expected, *opts)
|
||||
}
|
||||
|
||||
func TestCipherSuitesForDefault(t *testing.T) {
|
||||
t.Run("contains exactly and only the expected values", func(t *testing.T) {
|
||||
expected := []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
}
|
||||
|
||||
require.Equal(t, expected, Default(nil).CipherSuites)
|
||||
})
|
||||
|
||||
t.Run("is a subset of TestCipherSuitesForDefaultLDAP", func(t *testing.T) {
|
||||
defaultSuiteIDs := Default(nil).CipherSuites
|
||||
ldapSuiteIDs := DefaultLDAP(nil).CipherSuites
|
||||
|
||||
require.Greater(t, len(defaultSuiteIDs), 0)
|
||||
require.GreaterOrEqual(t, len(ldapSuiteIDs), len(defaultSuiteIDs))
|
||||
|
||||
require.Equal(t, 0, sets.New[uint16](defaultSuiteIDs...).Difference(sets.New[uint16](ldapSuiteIDs...)).Len())
|
||||
})
|
||||
}
|
||||
|
||||
func TestCipherSuitesForDefaultLDAP(t *testing.T) {
|
||||
t.Run("contains exactly and only the expected values", func(t *testing.T) {
|
||||
expected := []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
|
||||
// Add these for LDAP
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
}
|
||||
|
||||
require.Equal(t, expected, DefaultLDAP(nil).CipherSuites)
|
||||
})
|
||||
|
||||
t.Run("is a superset of TestCipherSuitesForDefault", func(t *testing.T) {
|
||||
defaultSuiteIDs := Default(nil).CipherSuites
|
||||
ldapSuiteIDs := DefaultLDAP(nil).CipherSuites
|
||||
|
||||
require.Greater(t, len(defaultSuiteIDs), 0)
|
||||
require.GreaterOrEqual(t, len(ldapSuiteIDs), len(defaultSuiteIDs))
|
||||
|
||||
require.True(t, sets.New[uint16](ldapSuiteIDs...).IsSuperset(sets.New[uint16](defaultSuiteIDs...)))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,8 +19,6 @@ import (
|
||||
"k8s.io/client-go/transport"
|
||||
)
|
||||
|
||||
// TODO decide if we need to expose the four TLS levels (secure, default, default-ldap, legacy) as config.
|
||||
|
||||
// defaultServingOptionsMinTLSVersion is the minimum tls version in the format
|
||||
// expected by SecureServingOptions.MinTLSVersion from
|
||||
// k8s.io/apiserver/pkg/server/options.
|
||||
@@ -28,21 +26,6 @@ const defaultServingOptionsMinTLSVersion = "VersionTLS12"
|
||||
|
||||
type ConfigFunc func(*x509.CertPool) *tls.Config
|
||||
|
||||
func Legacy(rootCAs *x509.CertPool) *tls.Config {
|
||||
c := Default(rootCAs)
|
||||
// add all the ciphers (even the crappy ones) except the ones that Go considers to be outright broken like 3DES
|
||||
c.CipherSuites = suitesToIDs(tls.CipherSuites())
|
||||
return c
|
||||
}
|
||||
|
||||
func suitesToIDs(suites []*tls.CipherSuite) []uint16 {
|
||||
out := make([]uint16, 0, len(suites))
|
||||
for _, suite := range suites {
|
||||
out = append(out, suite.ID)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func Merge(tlsConfigFunc ConfigFunc, tlsConfig *tls.Config) {
|
||||
secureTLSConfig := tlsConfigFunc(nil)
|
||||
|
||||
|
||||
@@ -5,11 +5,8 @@ package ptls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/go-semver/semver"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apiserver/pkg/server/options"
|
||||
)
|
||||
@@ -37,13 +34,6 @@ func TestDefaultServing(t *testing.T) {
|
||||
func TestMerge(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
runtimeVersion := runtime.Version()
|
||||
if strings.HasPrefix(runtimeVersion, "go") {
|
||||
runtimeVersion, _ = strings.CutPrefix(runtimeVersion, "go")
|
||||
}
|
||||
runtimeVersionSemver, err := semver.NewVersion(runtimeVersion)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
tlsConfigFunc ConfigFunc
|
||||
@@ -167,33 +157,6 @@ func TestMerge(t *testing.T) {
|
||||
NextProtos: []string{"panda"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "legacy without NextProtos",
|
||||
tlsConfigFunc: Legacy,
|
||||
tlsConfig: &tls.Config{
|
||||
ServerName: "something-to-check-passthrough",
|
||||
},
|
||||
want: &tls.Config{
|
||||
ServerName: "something-to-check-passthrough",
|
||||
MinVersion: tls.VersionTLS12,
|
||||
CipherSuites: wantLegacyCipherSuites(runtimeVersionSemver),
|
||||
NextProtos: []string{"h2", "http/1.1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "legacy with NextProtos",
|
||||
tlsConfigFunc: Legacy,
|
||||
tlsConfig: &tls.Config{ //nolint:gosec // not concerned with TLS MinVersion here
|
||||
ServerName: "a different thing for passthrough",
|
||||
NextProtos: []string{"panda"},
|
||||
},
|
||||
want: &tls.Config{
|
||||
ServerName: "a different thing for passthrough",
|
||||
MinVersion: tls.VersionTLS12,
|
||||
CipherSuites: wantLegacyCipherSuites(runtimeVersionSemver),
|
||||
NextProtos: []string{"panda"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -204,31 +167,3 @@ func TestMerge(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func wantLegacyCipherSuites(runtime *semver.Version) []uint16 {
|
||||
var ciphers []uint16
|
||||
if runtime.Major == 1 && runtime.Minor < 22 {
|
||||
ciphers = append(ciphers, []uint16{
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
||||
}...)
|
||||
}
|
||||
ciphers = append(ciphers, []uint16{
|
||||
tls.TLS_AES_128_GCM_SHA256,
|
||||
tls.TLS_AES_256_GCM_SHA384,
|
||||
tls.TLS_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
}...)
|
||||
return ciphers
|
||||
}
|
||||
|
||||
@@ -742,11 +742,14 @@ func main() error { // return an error instead of plog.Fatal to allow defer stat
|
||||
ctx := signalCtx()
|
||||
|
||||
// Read the server config file.
|
||||
cfg, err := supervisor.FromPath(ctx, os.Args[2])
|
||||
cfg, err := supervisor.FromPath(ctx, os.Args[2], ptls.SetUserConfiguredAllowedCipherSuitesForTLSOneDotTwo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not load config: %w", err)
|
||||
}
|
||||
|
||||
// The above server config should have set the allowed ciphers global, so now log the ciphers for all profiles.
|
||||
ptls.LogAllProfiles(plog.New())
|
||||
|
||||
return runSupervisor(ctx, podInfo, cfg)
|
||||
}
|
||||
|
||||
|
||||
@@ -1523,7 +1523,7 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
|
||||
stdout, stderr := testlib.RunNmapSSLEnum(t, "127.0.0.1", 10445)
|
||||
|
||||
require.Empty(t, stderr)
|
||||
require.Contains(t, stdout, testlib.GetExpectedCiphers(ptls.Default(nil)), "stdout:\n%s", stdout)
|
||||
require.Contains(t, stdout, testlib.GetExpectedCiphers(ptls.Default(nil), testlib.DefaultCipherSuitePreference), "stdout:\n%s", stdout)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
48
test/integration/limited_ciphers_fips_test.go
Normal file
48
test/integration/limited_ciphers_fips_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright 2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build fips_strict
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestLimitedCiphersFIPS_Disruptive will confirm that the Pinniped Supervisor and Concierge expose only those
|
||||
// ciphers listed in configuration, when compiled in FIPS mode.
|
||||
// This does not test the CLI, since it does not have a feature to limit cipher suites.
|
||||
func TestLimitedCiphersFIPS_Disruptive(t *testing.T) {
|
||||
performLimitedCiphersTest(t,
|
||||
// The user-configured ciphers for both the Supervisor and Concierge.
|
||||
// This is a subset of the hardcoded ciphers from profiles_fips_strict.go.
|
||||
[]string{
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
|
||||
"TLS_RSA_WITH_AES_256_GCM_SHA384", // this is an insecure cipher but allowed for FIPS
|
||||
},
|
||||
// Expected server configuration for the Supervisor's OIDC endpoints.
|
||||
&tls.Config{
|
||||
MinVersion: tls.VersionTLS12, // Supervisor OIDC always allows TLS 1.2 clients to connect
|
||||
MaxVersion: tls.VersionTLS12, // boringcrypto does not use TLS 1.3 yet
|
||||
CipherSuites: []uint16{
|
||||
// Supervisor OIDC endpoints configured with EC certs use only EC ciphers.
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
},
|
||||
},
|
||||
// Expected server configuration for the Supervisor and Concierge aggregated API endpoints.
|
||||
&tls.Config{
|
||||
MinVersion: tls.VersionTLS12, // boringcrypto does not use TLS 1.3 yet
|
||||
MaxVersion: tls.VersionTLS12, // boringcrypto does not use TLS 1.3 yet
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
43
test/integration/limited_ciphers_test.go
Normal file
43
test/integration/limited_ciphers_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !fips_strict
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestLimitedCiphersNotFIPS_Disruptive will confirm that the Pinniped Supervisor and Concierge expose only those
|
||||
// ciphers listed in configuration, when compiled in non-FIPS mode.
|
||||
// This does not test the CLI, since it does not have a feature to limit cipher suites.
|
||||
func TestLimitedCiphersNotFIPS_Disruptive(t *testing.T) {
|
||||
performLimitedCiphersTest(t,
|
||||
// The user-configured ciphers for both the Supervisor and Concierge.
|
||||
// This is a subset of the hardcoded ciphers from profiles.go.
|
||||
[]string{
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
|
||||
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
|
||||
},
|
||||
// Expected server configuration for the Supervisor's OIDC endpoints.
|
||||
&tls.Config{
|
||||
MinVersion: tls.VersionTLS12, // Supervisor OIDC always allows TLS 1.2 clients to connect
|
||||
MaxVersion: tls.VersionTLS13,
|
||||
CipherSuites: []uint16{
|
||||
// Supervisor OIDC endpoints configured with EC certs use only EC ciphers.
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
},
|
||||
},
|
||||
// Expected server configuration for the Supervisor and Concierge aggregated API endpoints.
|
||||
&tls.Config{
|
||||
MinVersion: tls.VersionTLS13, // do not allow TLS 1.2 clients to connect
|
||||
MaxVersion: tls.VersionTLS13,
|
||||
CipherSuites: nil, // TLS 1.3 ciphers are not configurable
|
||||
},
|
||||
)
|
||||
}
|
||||
211
test/integration/limited_ciphers_utils_test.go
Normal file
211
test/integration/limited_ciphers_utils_test.go
Normal file
@@ -0,0 +1,211 @@
|
||||
// Copyright 2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"go.pinniped.dev/internal/config/concierge"
|
||||
"go.pinniped.dev/internal/config/supervisor"
|
||||
"go.pinniped.dev/test/testlib"
|
||||
)
|
||||
|
||||
type stringEditorFunc func(t *testing.T, in string) string
|
||||
|
||||
func performLimitedCiphersTest(
|
||||
t *testing.T,
|
||||
allowedCiphersConfig []string,
|
||||
expectedConfigForSupervisorOIDCEndpoints *tls.Config,
|
||||
expectedConfigForAggregatedAPIEndpoints *tls.Config,
|
||||
) {
|
||||
env := testEnvForPodShutdownTests(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
editSupervisorAllowedCiphersConfig := func(t *testing.T, configMapData string) string {
|
||||
t.Helper()
|
||||
|
||||
var config supervisor.Config
|
||||
err := yaml.Unmarshal([]byte(configMapData), &config)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Empty(t, config.TLS.OneDotTwo.AllowedCiphers) // precondition
|
||||
|
||||
config.TLS.OneDotTwo.AllowedCiphers = allowedCiphersConfig
|
||||
|
||||
updatedConfig, err := yaml.Marshal(config)
|
||||
require.NoError(t, err)
|
||||
return string(updatedConfig)
|
||||
}
|
||||
|
||||
editConciergeAllowedCiphersConfig := func(t *testing.T, configMapData string) string {
|
||||
t.Helper()
|
||||
|
||||
var config concierge.Config
|
||||
err := yaml.Unmarshal([]byte(configMapData), &config)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Empty(t, config.TLS.OneDotTwo.AllowedCiphers) // precondition
|
||||
|
||||
config.TLS.OneDotTwo.AllowedCiphers = allowedCiphersConfig
|
||||
|
||||
updatedConfig, err := yaml.Marshal(config)
|
||||
require.NoError(t, err)
|
||||
return string(updatedConfig)
|
||||
}
|
||||
|
||||
// Update Supervisor's allowed ciphers in its static configmap and restart pods.
|
||||
updateStaticConfigMapAndRestartApp(t,
|
||||
ctx,
|
||||
env.SupervisorNamespace,
|
||||
env.SupervisorAppName+"-static-config",
|
||||
env.SupervisorAppName,
|
||||
false,
|
||||
editSupervisorAllowedCiphersConfig,
|
||||
)
|
||||
|
||||
// Update Concierge's allowed ciphers in its static configmap and restart pods.
|
||||
updateStaticConfigMapAndRestartApp(t,
|
||||
ctx,
|
||||
env.ConciergeNamespace,
|
||||
env.ConciergeAppName+"-config",
|
||||
env.ConciergeAppName,
|
||||
true,
|
||||
editConciergeAllowedCiphersConfig,
|
||||
)
|
||||
|
||||
// Probe TLS config of Supervisor's OIDC endpoints.
|
||||
expectTLSConfigForServicePort(t, ctx,
|
||||
env.SupervisorAppName+"-nodeport", env.SupervisorNamespace, "10509",
|
||||
expectedConfigForSupervisorOIDCEndpoints,
|
||||
)
|
||||
|
||||
// Probe TLS config of Supervisor's aggregated endpoints.
|
||||
expectTLSConfigForServicePort(t, ctx,
|
||||
env.SupervisorAppName+"-api", env.SupervisorNamespace, "10510",
|
||||
expectedConfigForAggregatedAPIEndpoints,
|
||||
)
|
||||
|
||||
// Probe TLS config of Concierge's aggregated endpoints.
|
||||
expectTLSConfigForServicePort(t, ctx,
|
||||
env.ConciergeAppName+"-api", env.ConciergeNamespace, "10511",
|
||||
expectedConfigForAggregatedAPIEndpoints,
|
||||
)
|
||||
}
|
||||
|
||||
func expectTLSConfigForServicePort(
|
||||
t *testing.T,
|
||||
ctx context.Context,
|
||||
serviceName string,
|
||||
serviceNamespace string,
|
||||
localPortAsStr string,
|
||||
expectedConfig *tls.Config,
|
||||
) {
|
||||
portAsInt, err := strconv.Atoi(localPortAsStr)
|
||||
require.NoError(t, err)
|
||||
portAsUint := uint16(portAsInt) // okay to cast because it will only be legal port numbers
|
||||
|
||||
startKubectlPortForward(ctx, t, localPortAsStr, "443", serviceName, serviceNamespace)
|
||||
|
||||
stdout, stderr := testlib.RunNmapSSLEnum(t, "127.0.0.1", portAsUint)
|
||||
require.Empty(t, stderr)
|
||||
|
||||
expectedNMapOutput := testlib.GetExpectedCiphers(expectedConfig, "server")
|
||||
assert.Contains(t,
|
||||
stdout,
|
||||
expectedNMapOutput,
|
||||
"actual nmap output:\n%s", stdout,
|
||||
"but was expected to contain:\n%s", expectedNMapOutput,
|
||||
)
|
||||
}
|
||||
|
||||
func updateStaticConfigMapAndRestartApp(
|
||||
t *testing.T,
|
||||
ctx context.Context,
|
||||
namespace string,
|
||||
staticConfigMapName string,
|
||||
appName string,
|
||||
isConcierge bool,
|
||||
editConfigMapFunc stringEditorFunc,
|
||||
) {
|
||||
configMapClient := testlib.NewKubernetesClientset(t).CoreV1().ConfigMaps(namespace)
|
||||
|
||||
staticConfigMap, err := configMapClient.Get(ctx, staticConfigMapName, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
originalConfig := staticConfigMap.Data["pinniped.yaml"]
|
||||
require.NotEmpty(t, originalConfig)
|
||||
|
||||
t.Cleanup(func() {
|
||||
cleanupCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
staticConfigMapForCleanup, err := configMapClient.Get(cleanupCtx, staticConfigMapName, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
staticConfigMapForCleanup.Data = make(map[string]string)
|
||||
staticConfigMapForCleanup.Data["pinniped.yaml"] = originalConfig
|
||||
|
||||
_, err = configMapClient.Update(cleanupCtx, staticConfigMapForCleanup, metav1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
restartAllPodsOfApp(t, namespace, appName, isConcierge)
|
||||
})
|
||||
|
||||
staticConfigMap.Data = make(map[string]string)
|
||||
staticConfigMap.Data["pinniped.yaml"] = editConfigMapFunc(t, originalConfig)
|
||||
|
||||
_, err = configMapClient.Update(ctx, staticConfigMap, metav1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
restartAllPodsOfApp(t, namespace, appName, isConcierge)
|
||||
}
|
||||
|
||||
// restartAllPodsOfApp will immediately scale to 0 and then scale back.
|
||||
// There are no uses of t.Cleanup since these actions need to happen immediately.
|
||||
func restartAllPodsOfApp(
|
||||
t *testing.T,
|
||||
namespace string,
|
||||
appName string,
|
||||
isConcierge bool,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
ignorePodsWithNameSubstring := ""
|
||||
if isConcierge {
|
||||
ignorePodsWithNameSubstring = "-kube-cert-agent-"
|
||||
}
|
||||
|
||||
// Precondition: the app should have some pods running initially.
|
||||
initialPods := getRunningPodsByNamePrefix(t, namespace, appName+"-", ignorePodsWithNameSubstring)
|
||||
require.Greater(t, len(initialPods), 0)
|
||||
|
||||
// Scale down the deployment's number of replicas to 0, which will shut down all the pods.
|
||||
originalScale := updateDeploymentScale(t, namespace, appName, 0)
|
||||
require.Greater(t, originalScale, 0)
|
||||
|
||||
testlib.RequireEventually(t, func(requireEventually *require.Assertions) {
|
||||
newPods := getRunningPodsByNamePrefix(t, namespace, appName+"-", ignorePodsWithNameSubstring)
|
||||
requireEventually.Len(newPods, 0, "wanted zero pods")
|
||||
}, 2*time.Minute, 200*time.Millisecond)
|
||||
|
||||
// Reset the application to its original scale.
|
||||
updateDeploymentScale(t, namespace, appName, originalScale)
|
||||
|
||||
testlib.RequireEventually(t, func(requireEventually *require.Assertions) {
|
||||
newPods := getRunningPodsByNamePrefix(t, namespace, appName+"-", ignorePodsWithNameSubstring)
|
||||
requireEventually.Len(newPods, originalScale, "wanted %d pods", originalScale)
|
||||
requireEventually.True(allPodsReady(newPods), "wanted all new pods to be ready")
|
||||
}, 2*time.Minute, 200*time.Millisecond)
|
||||
}
|
||||
@@ -24,18 +24,22 @@ import (
|
||||
// before they die.
|
||||
// Never run this test in parallel since deleting the pods is disruptive, see main_test.go.
|
||||
func TestPodShutdown_Disruptive(t *testing.T) {
|
||||
// Only run this test in CI on Kind clusters, because something about restarting the pods
|
||||
// in this test breaks the "kubectl port-forward" commands that we are using in CI for
|
||||
// AKS, EKS, and GKE clusters. The Go code that we wrote for graceful pod shutdown should
|
||||
// not be sensitive to which distribution it runs on, so running this test only on Kind
|
||||
// should give us sufficient coverage for what we are trying to test here.
|
||||
env := testlib.IntegrationEnv(t, testlib.SkipPodRestartAssertions()).
|
||||
WithKubeDistribution(testlib.KindDistro)
|
||||
env := testEnvForPodShutdownTests(t)
|
||||
|
||||
shutdownAllPodsOfApp(t, env, env.ConciergeNamespace, env.ConciergeAppName, true)
|
||||
shutdownAllPodsOfApp(t, env, env.SupervisorNamespace, env.SupervisorAppName, false)
|
||||
}
|
||||
|
||||
// testEnvForPodShutdownTests builds an env with the following description:
|
||||
// Only run this test in CI on Kind clusters, because something about restarting the pods
|
||||
// in this test breaks the "kubectl port-forward" commands that we are using in CI for
|
||||
// AKS, EKS, and GKE clusters. The Go code that we wrote for graceful pod shutdown should
|
||||
// not be sensitive to which distribution it runs on, so running this test only on Kind
|
||||
// should give us sufficient coverage for what we are trying to test here.
|
||||
func testEnvForPodShutdownTests(t *testing.T) *testlib.TestEnv {
|
||||
return testlib.IntegrationEnv(t, testlib.SkipPodRestartAssertions()).WithKubeDistribution(testlib.KindDistro)
|
||||
}
|
||||
|
||||
func shutdownAllPodsOfApp(
|
||||
t *testing.T,
|
||||
env *testlib.TestEnv,
|
||||
@@ -89,11 +93,12 @@ func shutdownAllPodsOfApp(
|
||||
t.Cleanup(func() {
|
||||
updateDeploymentScale(t, namespace, appName, originalScale)
|
||||
|
||||
// Wait for all the new pods to be running.
|
||||
// Wait for all the new pods to be running and ready.
|
||||
var newPods []corev1.Pod
|
||||
testlib.RequireEventually(t, func(requireEventually *require.Assertions) {
|
||||
newPods = getRunningPodsByNamePrefix(t, namespace, appName+"-", ignorePodsWithNameSubstring)
|
||||
requireEventually.Len(newPods, originalScale, "wanted pods to return to original scale")
|
||||
requireEventually.True(allPodsReady(newPods), "wanted all new pods to be ready")
|
||||
}, 2*time.Minute, 200*time.Millisecond)
|
||||
|
||||
// After a short time, leader election should have finished and the lease should contain the name of
|
||||
@@ -181,6 +186,24 @@ func getRunningPodsByNamePrefix(
|
||||
return foundPods
|
||||
}
|
||||
|
||||
func allPodsReady(pods []corev1.Pod) bool {
|
||||
for _, pod := range pods {
|
||||
if !isPodReady(pod) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isPodReady(pod corev1.Pod) bool {
|
||||
for _, cond := range pod.Status.Conditions {
|
||||
if cond.Type == corev1.PodReady {
|
||||
return cond.Status == corev1.ConditionTrue
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func updateDeploymentScale(t *testing.T, namespace string, deploymentName string, newScale int) int {
|
||||
t.Helper()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
|
||||
|
||||
@@ -94,7 +94,7 @@ func TestSecureTLSConciergeAggregatedAPI_Parallel(t *testing.T) {
|
||||
stdout, stderr := testlib.RunNmapSSLEnum(t, "127.0.0.1", 10446)
|
||||
|
||||
require.Empty(t, stderr)
|
||||
require.Contains(t, stdout, testlib.GetExpectedCiphers(ptls.Secure(nil)), "stdout:\n%s", stdout)
|
||||
require.Contains(t, stdout, testlib.GetExpectedCiphers(ptls.Secure(nil), testlib.DefaultCipherSuitePreference), "stdout:\n%s", stdout)
|
||||
}
|
||||
|
||||
// TLS checks safe to run in parallel with serial tests, see main_test.go.
|
||||
@@ -109,7 +109,7 @@ func TestSecureTLSSupervisorAggregatedAPI_Parallel(t *testing.T) {
|
||||
stdout, stderr := testlib.RunNmapSSLEnum(t, "127.0.0.1", 10447)
|
||||
|
||||
require.Empty(t, stderr)
|
||||
require.Contains(t, stdout, testlib.GetExpectedCiphers(ptls.Secure(nil)), "stdout:\n%s", stdout)
|
||||
require.Contains(t, stdout, testlib.GetExpectedCiphers(ptls.Secure(nil), testlib.DefaultCipherSuitePreference), "stdout:\n%s", stdout)
|
||||
}
|
||||
|
||||
func TestSecureTLSSupervisor(t *testing.T) {
|
||||
@@ -136,7 +136,7 @@ func TestSecureTLSSupervisor(t *testing.T) {
|
||||
defaultECDSAOnly.CipherSuites = ciphers
|
||||
|
||||
require.Empty(t, stderr)
|
||||
require.Contains(t, stdout, testlib.GetExpectedCiphers(defaultECDSAOnly), "stdout:\n%s", stdout)
|
||||
require.Contains(t, stdout, testlib.GetExpectedCiphers(defaultECDSAOnly, testlib.DefaultCipherSuitePreference), "stdout:\n%s", stdout)
|
||||
}
|
||||
|
||||
type fakeT struct {
|
||||
|
||||
@@ -38,19 +38,22 @@ func RunNmapSSLEnum(t *testing.T, host string, port uint16) (string, string) {
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
//nolint:gosec // we are not performing malicious argument injection against ourselves
|
||||
cmd := exec.CommandContext(ctx, "nmap", "--script", "ssl-enum-ciphers",
|
||||
cmd := exec.CommandContext(ctx,
|
||||
"nmap",
|
||||
"-Pn",
|
||||
"--script", "+ssl-enum-ciphers",
|
||||
"-p", strconv.FormatUint(uint64(port), 10),
|
||||
host,
|
||||
)
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
t.Log("Running cmd: " + strings.Join(cmd.Args, " "))
|
||||
require.NoErrorf(t, cmd.Run(), "stderr:\n%s\n\nstdout:\n%s\n\n", stderr.String(), stdout.String())
|
||||
|
||||
return stdout.String(), stderr.String()
|
||||
}
|
||||
|
||||
func GetExpectedCiphers(config *tls.Config) string {
|
||||
func GetExpectedCiphers(config *tls.Config, preference string) string {
|
||||
skip12 := config.MinVersion == tls.VersionTLS13
|
||||
skip13 := config.MaxVersion == tls.VersionTLS12
|
||||
|
||||
@@ -86,7 +89,7 @@ func GetExpectedCiphers(config *tls.Config) string {
|
||||
}
|
||||
s.WriteString("\n")
|
||||
}
|
||||
tls12Bit = fmt.Sprintf(tls12Base, s.String(), cipherSuitePreference)
|
||||
tls12Bit = fmt.Sprintf(tls12Base, s.String(), preference)
|
||||
}
|
||||
|
||||
if !skip13 {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build fips_strict
|
||||
@@ -9,4 +9,4 @@ package testlib
|
||||
// incorrectly shown as 'client' in some cases.
|
||||
// in fips-only mode, it correctly shows the cipher preference
|
||||
// as 'server', while in non-fips mode it shows as 'client'.
|
||||
const cipherSuitePreference = "server"
|
||||
const DefaultCipherSuitePreference = "server"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !fips_strict
|
||||
@@ -9,4 +9,4 @@ package testlib
|
||||
// incorrectly shown as 'client' in some cases.
|
||||
// in fips-only mode, it correctly shows the cipher preference
|
||||
// as 'server', while in non-fips mode it shows as 'client'.
|
||||
const cipherSuitePreference = "client"
|
||||
const DefaultCipherSuitePreference = "client"
|
||||
|
||||
Reference in New Issue
Block a user