diff --git a/test/integration/concierge_impersonation_proxy_test.go b/test/integration/concierge_impersonation_proxy_test.go index 0fc6b718d..968f322ba 100644 --- a/test/integration/concierge_impersonation_proxy_test.go +++ b/test/integration/concierge_impersonation_proxy_test.go @@ -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) }) }) diff --git a/test/integration/limited_ciphers_fips_test.go b/test/integration/limited_ciphers_fips_test.go new file mode 100644 index 000000000..20697e266 --- /dev/null +++ b/test/integration/limited_ciphers_fips_test.go @@ -0,0 +1,29 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +//go:build fips_strict + +package integration + +import ( + "crypto/tls" + "testing" +) + +// TestLimitedCiphers_Disruptive will confirm that the Pinniped Supervisor exposes only those ciphers listed in +// configuration. +// This does not test the Concierge (which has the same feature) since the Concierge does not have exposed API +// endpoints with the Default profile. +// 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, + []string{ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384", // this is an insecure cipher but allowed for FIPS + }, + []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + }) +} diff --git a/test/integration/limited_ciphers_test.go b/test/integration/limited_ciphers_test.go new file mode 100644 index 000000000..bbc831221 --- /dev/null +++ b/test/integration/limited_ciphers_test.go @@ -0,0 +1,30 @@ +// 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 exposes only those ciphers listed in +// configuration. +// This does not test the Concierge (which has the same feature) since the Concierge does not have exposed API +// endpoints with the Default profile. +// 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, + []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", + }, + []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + }) +} diff --git a/test/integration/limited_ciphers_utils_test.go b/test/integration/limited_ciphers_utils_test.go new file mode 100644 index 000000000..7e54eba5e --- /dev/null +++ b/test/integration/limited_ciphers_utils_test.go @@ -0,0 +1,156 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package integration + +import ( + "context" + "crypto/tls" + "testing" + "time" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" + + "go.pinniped.dev/internal/config/supervisor" + "go.pinniped.dev/test/testlib" +) + +func performLimitedCiphersTest(t *testing.T, allowedCiphers []string, expectedCiphers []uint16) { + env := testOnKindWithPodShutdown(t) + + client := testlib.NewKubernetesClientset(t) + configMapClient := client.CoreV1().ConfigMaps(env.SupervisorNamespace) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + t.Cleanup(cancel) + + staticConfigMapName := env.SupervisorAppName + "-static-config" + supervisorStaticConfigMap, err := configMapClient.Get(ctx, staticConfigMapName, metav1.GetOptions{}) + require.NoError(t, err) + + originalSupervisorConfig := supervisorStaticConfigMap.Data["pinniped.yaml"] + require.NotEmpty(t, originalSupervisorConfig) + + t.Cleanup(func() { + supervisorStaticConfigMapCleanup, err := configMapClient.Get(ctx, staticConfigMapName, metav1.GetOptions{}) + require.NoError(t, err) + + supervisorStaticConfigMapCleanup.Data = make(map[string]string) + supervisorStaticConfigMapCleanup.Data["pinniped.yaml"] = originalSupervisorConfig + + _, err = configMapClient.Update(ctx, supervisorStaticConfigMapCleanup, metav1.UpdateOptions{}) + require.NoError(t, err) + + // this will cycle all the pods + restartAllPodsOfApp(t, env.SupervisorNamespace, env.SupervisorAppName, false) + }) + + var config supervisor.Config + err = yaml.Unmarshal([]byte(originalSupervisorConfig), &config) + require.NoError(t, err) + + // As a precondition of this test, ensure that the list of allowedCiphers is empty + require.Empty(t, config.TLS.OneDotTwo.AllowedCiphers) + + config.TLS.OneDotTwo.AllowedCiphers = allowedCiphers + + updatedSupervisorConfig, err := yaml.Marshal(config) + require.NoError(t, err) + + supervisorStaticConfigMap.Data = make(map[string]string) + supervisorStaticConfigMap.Data["pinniped.yaml"] = string(updatedSupervisorConfig) + + _, err = configMapClient.Update(ctx, supervisorStaticConfigMap, metav1.UpdateOptions{}) + require.NoError(t, err) + + // this will cycle all the pods + restartAllPodsOfApp(t, env.SupervisorNamespace, env.SupervisorAppName, false) + + startKubectlPortForward(ctx, t, "10509", "443", env.SupervisorAppName+"-nodeport", env.SupervisorNamespace) + stdout, stderr := testlib.RunNmapSSLEnum(t, "127.0.0.1", 10509) + require.Empty(t, stderr) + + expectedCiphersConfig := &tls.Config{ + MinVersion: tls.VersionTLS12, + MaxVersion: testlib.MaxTLSVersion, + CipherSuites: expectedCiphers, + } + + require.Contains(t, stdout, testlib.GetExpectedCiphers(expectedCiphersConfig, "server"), "stdout:\n%s", stdout) + +} + +// 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) + + 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) + }, 2*time.Minute, 200*time.Millisecond) +} + +// TestRemoveAllowedCiphersFromStaticConfig_Disruptive updates the Supervisor's static configuration to make sure that the allowed ciphers list is empty. +// It will restart the Supervisor pods. Skipped because it's only here for local testing purposes. +func TestRemoveAllowedCiphersFromStaticConfig_Disruptive(t *testing.T) { + t.Skip() + + env := testOnKindWithPodShutdown(t) + + client := testlib.NewKubernetesClientset(t) + configMapClient := client.CoreV1().ConfigMaps(env.SupervisorNamespace) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + staticConfigMapName := env.SupervisorAppName + "-static-config" + supervisorStaticConfigMap, err := configMapClient.Get(ctx, staticConfigMapName, metav1.GetOptions{}) + require.NoError(t, err) + + originalSupervisorConfig := supervisorStaticConfigMap.Data["pinniped.yaml"] + require.NotEmpty(t, originalSupervisorConfig) + + var config supervisor.Config + err = yaml.Unmarshal([]byte(originalSupervisorConfig), &config) + require.NoError(t, err) + + config.TLS.OneDotTwo.AllowedCiphers = nil + + updatedConfigBytes, err := yaml.Marshal(config) + require.NoError(t, err) + + supervisorStaticConfigMap.Data = make(map[string]string) + supervisorStaticConfigMap.Data["pinniped.yaml"] = string(updatedConfigBytes) + + _, err = configMapClient.Update(ctx, supervisorStaticConfigMap, metav1.UpdateOptions{}) + require.NoError(t, err) + + // this will cycle all the pods + restartAllPodsOfApp(t, env.SupervisorNamespace, env.SupervisorAppName, false) +} diff --git a/test/integration/securetls_test.go b/test/integration/securetls_test.go index a6bc9c499..9c86385ba 100644 --- a/test/integration/securetls_test.go +++ b/test/integration/securetls_test.go @@ -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 { diff --git a/test/testlib/securetls.go b/test/testlib/securetls.go index 3111c39fc..84c542ed4 100644 --- a/test/testlib/securetls.go +++ b/test/testlib/securetls.go @@ -50,7 +50,7 @@ func RunNmapSSLEnum(t *testing.T, host string, port uint16) (string, 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 +86,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 { diff --git a/test/testlib/securetls_preference_fips.go b/test/testlib/securetls_preference_fips.go index 3d76932e6..fa34b702d 100644 --- a/test/testlib/securetls_preference_fips.go +++ b/test/testlib/securetls_preference_fips.go @@ -1,12 +1,16 @@ -// 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 package testlib +import "crypto/tls" + // Because of a bug in nmap, the cipher suite preference is // 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" + +const MaxTLSVersion = tls.VersionTLS12 diff --git a/test/testlib/securetls_preference_nonfips.go b/test/testlib/securetls_preference_nonfips.go index 3306a9c1b..dd2ffd508 100644 --- a/test/testlib/securetls_preference_nonfips.go +++ b/test/testlib/securetls_preference_nonfips.go @@ -1,12 +1,16 @@ -// 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 package testlib +import "crypto/tls" + // Because of a bug in nmap, the cipher suite preference is // 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" + +const MaxTLSVersion = tls.VersionTLS13