From 1f1b6c884ee7eea76afb653ce78b12d8814368cd Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 27 Oct 2020 14:57:25 -0700 Subject: [PATCH] Add integration test: supervisor TLS termination and SNI virtual hosting - Also reduce the minimum allowed TLS version to v1.2, because v1.3 is not yet supported by some common clients, e.g. the default MacOS curl command --- cmd/pinniped-supervisor/main.go | 7 +- hack/prepare-for-integration-tests.sh | 1 + test/integration/supervisor_discovery_test.go | 242 +++++++++++++++--- test/integration/supervisor_keys_test.go | 2 +- test/library/client.go | 5 +- test/library/env.go | 92 ++++--- 6 files changed, 261 insertions(+), 88 deletions(-) diff --git a/cmd/pinniped-supervisor/main.go b/cmd/pinniped-supervisor/main.go index 7b296fa9..94a924bd 100644 --- a/cmd/pinniped-supervisor/main.go +++ b/cmd/pinniped-supervisor/main.go @@ -206,10 +206,11 @@ func run(serverInstallationNamespace string, cfg *supervisor.Config) error { //nolint: gosec // Intentionally binding to all network interfaces. httpsListener, err := tls.Listen("tcp", ":443", &tls.Config{ - MinVersion: tls.VersionTLS13, + MinVersion: tls.VersionTLS12, // Allow v1.2 because clients like the default `curl` on MacOS don't support 1.3 yet. GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { - klog.InfoS("GetCertificate called", "info.ServerName", info.ServerName) - return dynamicTLSCertProvider.GetTLSCert(strings.ToLower(info.ServerName)), nil + cert := dynamicTLSCertProvider.GetTLSCert(strings.ToLower(info.ServerName)) + klog.InfoS("GetCertificate called for port 443", "info.ServerName", info.ServerName, "foundCert", cert != nil) + return cert, nil }, }) if err != nil { diff --git a/hack/prepare-for-integration-tests.sh b/hack/prepare-for-integration-tests.sh index 0e06fe1f..c7b5aa10 100755 --- a/hack/prepare-for-integration-tests.sh +++ b/hack/prepare-for-integration-tests.sh @@ -282,6 +282,7 @@ export PINNIPED_TEST_SUPERVISOR_NAMESPACE=${supervisor_namespace} export PINNIPED_TEST_SUPERVISOR_APP_NAME=${supervisor_app_name} export PINNIPED_TEST_SUPERVISOR_CUSTOM_LABELS='${supervisor_custom_labels}' export PINNIPED_TEST_SUPERVISOR_HTTP_ADDRESS="127.0.0.1:12345" +export PINNIPED_TEST_SUPERVISOR_HTTPS_ADDRESS="localhost:12344" export PINNIPED_TEST_CLI_OIDC_ISSUER=http://127.0.0.1:12346/dex export PINNIPED_TEST_CLI_OIDC_CLIENT_ID=pinniped-cli export PINNIPED_TEST_CLI_OIDC_LOCALHOST_PORT=48095 diff --git a/test/integration/supervisor_discovery_test.go b/test/integration/supervisor_discovery_test.go index b536429c..ebed769a 100644 --- a/test/integration/supervisor_discovery_test.go +++ b/test/integration/supervisor_discovery_test.go @@ -7,9 +7,11 @@ import ( "context" "crypto/tls" "crypto/x509" + "crypto/x509/pkix" "encoding/json" "fmt" "io/ioutil" + "net" "net/http" "net/url" "strings" @@ -18,42 +20,93 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" "go.pinniped.dev/generated/1.19/apis/config/v1alpha1" pinnipedclientset "go.pinniped.dev/generated/1.19/client/clientset/versioned" + "go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/here" "go.pinniped.dev/test/library" ) +func TestSupervisorTLSTerminationWithSNI(t *testing.T) { + env := library.IntegrationEnv(t) + pinnipedClient := library.NewPinnipedClientset(t) + kubeClient := library.NewClientset(t) + + ns := env.SupervisorNamespace + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + temporarilyRemoveAllOIDCProviderConfigs(ctx, t, ns, pinnipedClient) + + scheme := "https" + address := env.SupervisorHTTPSAddress // hostname and port for direct access to the supervisor's port 443 + + hostname1 := strings.Split(address, ":")[0] + issuer1 := fmt.Sprintf("%s://%s/issuer1", scheme, address) + sniCertificateSecretName1 := "integration-test-sni-cert-1" + + // Create an OIDCProviderConfig with an sniCertificateSecretName. + oidcProviderConfig1 := library.CreateTestOIDCProvider(ctx, t, issuer1, sniCertificateSecretName1) + requireStatus(t, pinnipedClient, oidcProviderConfig1.Namespace, oidcProviderConfig1.Name, v1alpha1.SuccessOIDCProviderStatus) + + // The sniCertificateSecretName Secret does not exist, so the endpoints should fail with TLS errors. + requireEndpointHasTLSErrorBecauseCertificatesAreNotReady(t, issuer1) + + // Create the Secret. + ca1 := createSNICertificateSecret(ctx, t, ns, hostname1, sniCertificateSecretName1, kubeClient) + + // Now that the Secret exists, we should be able to access the endpoints by hostname using the CA. + _ = requireDiscoveryEndpointsAreWorking(t, scheme, address, string(ca1.Bundle()), issuer1, nil) + + // Update the config to take away the sniCertificateSecretName. + sniCertificateSecretName1update := "integration-test-sni-cert-1-update" + oidcProviderConfig1LatestVersion, err := pinnipedClient.ConfigV1alpha1().OIDCProviderConfigs(ns).Get(ctx, oidcProviderConfig1.Name, metav1.GetOptions{}) + require.NoError(t, err) + oidcProviderConfig1LatestVersion.Spec.SNICertificateSecretName = sniCertificateSecretName1update + _, err = pinnipedClient.ConfigV1alpha1().OIDCProviderConfigs(ns).Update(ctx, oidcProviderConfig1LatestVersion, metav1.UpdateOptions{}) + require.NoError(t, err) + + // The the endpoints should fail with TLS errors again. + requireEndpointHasTLSErrorBecauseCertificatesAreNotReady(t, issuer1) + + // Create a Secret at the updated name. + ca1update := createSNICertificateSecret(ctx, t, ns, hostname1, sniCertificateSecretName1update, kubeClient) + + // Now that the Secret exists at the new name, we should be able to access the endpoints by hostname using the CA. + _ = requireDiscoveryEndpointsAreWorking(t, scheme, address, string(ca1update.Bundle()), issuer1, nil) + + // To test SNI virtual hosting, send requests to discovery endpoints when the public address is different from the issuer name. + hostname2 := "some-issuer-host-and-port-that-doesnt-match-public-supervisor-address.com" + hostnamePort2 := "2684" + issuer2 := fmt.Sprintf("%s://%s:%s/issuer2", scheme, hostname2, hostnamePort2) + sniCertificateSecretName2 := "integration-test-sni-cert-2" + + // Create an OIDCProviderConfig with an sniCertificateSecretName. + oidcProviderConfig2 := library.CreateTestOIDCProvider(ctx, t, issuer2, sniCertificateSecretName2) + requireStatus(t, pinnipedClient, oidcProviderConfig2.Namespace, oidcProviderConfig2.Name, v1alpha1.SuccessOIDCProviderStatus) + + // Create the Secret. + ca2 := createSNICertificateSecret(ctx, t, ns, hostname2, sniCertificateSecretName2, kubeClient) + + // Now that the Secret exists, we should be able to access the endpoints by hostname using the CA. + _ = requireDiscoveryEndpointsAreWorking(t, scheme, hostname2+":"+hostnamePort2, string(ca2.Bundle()), issuer2, map[string]string{ + hostname2 + ":" + hostnamePort2: address, + }) +} + func TestSupervisorOIDCDiscovery(t *testing.T) { env := library.IntegrationEnv(t) client := library.NewPinnipedClientset(t) ns := env.SupervisorNamespace - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() - // Temporarily remove any existing OIDCProviderConfigs from the cluster so we can test from a clean slate. - originalConfigList, err := client.ConfigV1alpha1().OIDCProviderConfigs(ns).List(ctx, metav1.ListOptions{}) - require.NoError(t, err) - for _, config := range originalConfigList.Items { - err := client.ConfigV1alpha1().OIDCProviderConfigs(ns).Delete(ctx, config.Name, metav1.DeleteOptions{}) - require.NoError(t, err) - } - - // When this test has finished, recreate any OIDCProviderConfigs that had existed on the cluster before this test. - t.Cleanup(func() { - cleanupCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) - defer cancel() - - for _, config := range originalConfigList.Items { - thisConfig := config - thisConfig.ResourceVersion = "" // Get rid of resource version since we can't create an object with one. - _, err := client.ConfigV1alpha1().OIDCProviderConfigs(ns).Create(cleanupCtx, &thisConfig, metav1.CreateOptions{}) - require.NoError(t, err) - } - }) + temporarilyRemoveAllOIDCProviderConfigs(ctx, t, ns, client) tests := []struct { Scheme string @@ -61,7 +114,7 @@ func TestSupervisorOIDCDiscovery(t *testing.T) { CABundle string }{ {Scheme: "http", Address: env.SupervisorHTTPAddress}, - {Scheme: "https", Address: env.SupervisorHTTPSAddress, CABundle: env.SupervisorHTTPSCABundle}, + {Scheme: "https", Address: env.SupervisorHTTPSIngressAddress, CABundle: env.SupervisorHTTPSIngressCABundle}, } for _, test := range tests { @@ -98,7 +151,7 @@ func TestSupervisorOIDCDiscovery(t *testing.T) { // When multiple OIDCProviderConfigs exist at the same time they each serve a unique discovery endpoint. config3, jwks3 := requireCreatingOIDCProviderConfigCausesDiscoveryEndpointsToAppear(ctx, t, scheme, addr, caBundle, issuer3, client) config4, jwks4 := requireCreatingOIDCProviderConfigCausesDiscoveryEndpointsToAppear(ctx, t, scheme, addr, caBundle, issuer4, client) - requireDiscoveryEndpointsAreWorking(t, scheme, addr, caBundle, issuer3) // discovery for issuer3 is still working after issuer4 started working + requireDiscoveryEndpointsAreWorking(t, scheme, addr, caBundle, issuer3, nil) // discovery for issuer3 is still working after issuer4 started working // The auto-created JWK's were different from each other. require.NotEqual(t, jwks3.Keys[0]["x"], jwks4.Keys[0]["x"]) require.NotEqual(t, jwks3.Keys[0]["y"], jwks4.Keys[0]["y"]) @@ -106,7 +159,7 @@ func TestSupervisorOIDCDiscovery(t *testing.T) { // Editing a provider to change the issuer name updates the endpoints that are being served. updatedConfig4 := editOIDCProviderConfigIssuerName(t, config4, client, ns, issuer5) requireDiscoveryEndpointsAreNotFound(t, scheme, addr, caBundle, issuer4) - jwks5 := requireDiscoveryEndpointsAreWorking(t, scheme, addr, caBundle, issuer5) + jwks5 := requireDiscoveryEndpointsAreWorking(t, scheme, addr, caBundle, issuer5, nil) // The JWK did not change when the issuer name was updated. require.Equal(t, jwks4.Keys[0], jwks5.Keys[0]) @@ -116,14 +169,14 @@ func TestSupervisorOIDCDiscovery(t *testing.T) { // When the same issuer is added twice, both issuers are marked as duplicates, and neither provider is serving. config6Duplicate1, _ := requireCreatingOIDCProviderConfigCausesDiscoveryEndpointsToAppear(ctx, t, scheme, addr, caBundle, issuer6, client) - config6Duplicate2 := library.CreateTestOIDCProvider(ctx, t, issuer6) + config6Duplicate2 := library.CreateTestOIDCProvider(ctx, t, issuer6, "") requireStatus(t, client, ns, config6Duplicate1.Name, v1alpha1.DuplicateOIDCProviderStatus) requireStatus(t, client, ns, config6Duplicate2.Name, v1alpha1.DuplicateOIDCProviderStatus) requireDiscoveryEndpointsAreNotFound(t, scheme, addr, caBundle, issuer6) // If we delete the first duplicate issuer, the second duplicate issuer starts serving. requireDelete(t, client, ns, config6Duplicate1.Name) - requireWellKnownEndpointIsWorking(t, scheme, addr, caBundle, issuer6) + requireWellKnownEndpointIsWorking(t, scheme, addr, caBundle, issuer6, nil) requireStatus(t, client, ns, config6Duplicate2.Name, v1alpha1.SuccessOIDCProviderStatus) // When we finally delete all issuers, the endpoint should be down. @@ -135,13 +188,74 @@ func TestSupervisorOIDCDiscovery(t *testing.T) { requireDeletingOIDCProviderConfigCausesDiscoveryEndpointsToDisappear(t, config7, client, ns, scheme, addr, caBundle, issuer7) // When we create a provider with an invalid issuer, the status is set to invalid. - badConfig := library.CreateTestOIDCProvider(ctx, t, badIssuer) + badConfig := library.CreateTestOIDCProvider(ctx, t, badIssuer, "") requireStatus(t, client, ns, badConfig.Name, v1alpha1.InvalidOIDCProviderStatus) requireDiscoveryEndpointsAreNotFound(t, scheme, addr, caBundle, badIssuer) requireDeletingOIDCProviderConfigCausesDiscoveryEndpointsToDisappear(t, badConfig, client, ns, scheme, addr, caBundle, badIssuer) } } +func createSNICertificateSecret(ctx context.Context, t *testing.T, ns string, hostname string, sniCertificateSecretName string, kubeClient kubernetes.Interface) *certauthority.CA { + // Create a CA. + ca, err := certauthority.New(pkix.Name{CommonName: "Acme Corp"}, 1000*time.Hour) + require.NoError(t, err) + + // Using the CA, create a TLS server cert. + tlsCert, err := ca.Issue(pkix.Name{CommonName: hostname}, []string{hostname}, 1000*time.Hour) + require.NoError(t, err) + + // Write the serving cert to the SNI secret. + tlsCertChainPEM, tlsPrivateKeyPEM, err := certauthority.ToPEM(tlsCert) + require.NoError(t, err) + secret := corev1.Secret{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: sniCertificateSecretName, + Namespace: ns, + }, + StringData: map[string]string{ + "tls.crt": string(tlsCertChainPEM), + "tls.key": string(tlsPrivateKeyPEM), + }, + } + _, err = kubeClient.CoreV1().Secrets(ns).Create(ctx, &secret, metav1.CreateOptions{}) + require.NoError(t, err) + + // Delete the Secret when the test ends. + t.Cleanup(func() { + t.Helper() + deleteCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + err := kubeClient.CoreV1().Secrets(ns).Delete(deleteCtx, sniCertificateSecretName, metav1.DeleteOptions{}) + require.NoError(t, err) + }) + + return ca +} + +func temporarilyRemoveAllOIDCProviderConfigs(ctx context.Context, t *testing.T, ns string, client pinnipedclientset.Interface) { + // Temporarily remove any existing OIDCProviderConfigs from the cluster so we can test from a clean slate. + originalConfigList, err := client.ConfigV1alpha1().OIDCProviderConfigs(ns).List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + for _, config := range originalConfigList.Items { + err := client.ConfigV1alpha1().OIDCProviderConfigs(ns).Delete(ctx, config.Name, metav1.DeleteOptions{}) + require.NoError(t, err) + } + + // When this test has finished, recreate any OIDCProviderConfigs that had existed on the cluster before this test. + t.Cleanup(func() { + cleanupCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + for _, config := range originalConfigList.Items { + thisConfig := config + thisConfig.ResourceVersion = "" // Get rid of resource version since we can't create an object with one. + _, err := client.ConfigV1alpha1().OIDCProviderConfigs(ns).Create(cleanupCtx, &thisConfig, metav1.CreateOptions{}) + require.NoError(t, err) + } + }) +} + func jwksURLForIssuer(scheme, host, path string) string { return fmt.Sprintf("%s://%s/%s/jwks.json", scheme, host, strings.TrimPrefix(path, "/")) } @@ -160,14 +274,14 @@ func requireDiscoveryEndpointsAreNotFound(t *testing.T, supervisorScheme, superv func requireEndpointNotFound(t *testing.T, url, host, caBundle string) { t.Helper() - httpClient := newHTTPClient(caBundle) + httpClient := newHTTPClient(t, caBundle, nil) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() requestNonExistentPath, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) require.NoError(t, err) - requestNonExistentPath.Header.Add("Host", host) + requestNonExistentPath.Host = host var response *http.Response assert.Eventually(t, func() bool { @@ -180,6 +294,23 @@ func requireEndpointNotFound(t *testing.T, url, host, caBundle string) { require.NoError(t, err) } +func requireEndpointHasTLSErrorBecauseCertificatesAreNotReady(t *testing.T, url string) { + t.Helper() + httpClient := newHTTPClient(t, "", nil) + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + require.NoError(t, err) + + assert.Eventually(t, func() bool { + _, err = httpClient.Do(request) //nolint:bodyclose + return err != nil && strings.Contains(err.Error(), "tls: unrecognized name") + }, 10*time.Second, 200*time.Millisecond) + require.Error(t, err) + require.EqualError(t, err, `Get "https://localhost:12344/issuer1": remote error: tls: unrecognized name`) +} + func requireCreatingOIDCProviderConfigCausesDiscoveryEndpointsToAppear( ctx context.Context, t *testing.T, @@ -188,15 +319,15 @@ func requireCreatingOIDCProviderConfigCausesDiscoveryEndpointsToAppear( client pinnipedclientset.Interface, ) (*v1alpha1.OIDCProviderConfig, *ExpectedJWKSResponseFormat) { t.Helper() - newOIDCProviderConfig := library.CreateTestOIDCProvider(ctx, t, issuerName) - jwksResult := requireDiscoveryEndpointsAreWorking(t, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName) + newOIDCProviderConfig := library.CreateTestOIDCProvider(ctx, t, issuerName, "") + jwksResult := requireDiscoveryEndpointsAreWorking(t, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName, nil) requireStatus(t, client, newOIDCProviderConfig.Namespace, newOIDCProviderConfig.Name, v1alpha1.SuccessOIDCProviderStatus) return newOIDCProviderConfig, jwksResult } -func requireDiscoveryEndpointsAreWorking(t *testing.T, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName string) *ExpectedJWKSResponseFormat { - requireWellKnownEndpointIsWorking(t, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName) - jwksResult := requireJWKSEndpointIsWorking(t, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName) +func requireDiscoveryEndpointsAreWorking(t *testing.T, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName string, dnsOverrides map[string]string) *ExpectedJWKSResponseFormat { + requireWellKnownEndpointIsWorking(t, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName, dnsOverrides) + jwksResult := requireJWKSEndpointIsWorking(t, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName, dnsOverrides) return jwksResult } @@ -220,11 +351,11 @@ func requireDeletingOIDCProviderConfigCausesDiscoveryEndpointsToDisappear( requireDiscoveryEndpointsAreNotFound(t, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName) } -func requireWellKnownEndpointIsWorking(t *testing.T, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName string) { +func requireWellKnownEndpointIsWorking(t *testing.T, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName string, dnsOverrides map[string]string) { t.Helper() issuerURL, err := url.Parse(issuerName) require.NoError(t, err) - response, responseBody := requireSuccessEndpointResponse(t, wellKnownURLForIssuer(supervisorScheme, supervisorAddress, issuerURL.Path), issuerName, supervisorCABundle) //nolint:bodyclose + response, responseBody := requireSuccessEndpointResponse(t, wellKnownURLForIssuer(supervisorScheme, supervisorAddress, issuerURL.Path), issuerName, supervisorCABundle, dnsOverrides) //nolint:bodyclose // Check that the response matches our expectations. expectedResultTemplate := here.Doc(`{ @@ -250,12 +381,17 @@ type ExpectedJWKSResponseFormat struct { Keys []map[string]string } -func requireJWKSEndpointIsWorking(t *testing.T, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName string) *ExpectedJWKSResponseFormat { +func requireJWKSEndpointIsWorking(t *testing.T, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName string, dnsOverrides map[string]string) *ExpectedJWKSResponseFormat { t.Helper() issuerURL, err := url.Parse(issuerName) require.NoError(t, err) - response, responseBody := requireSuccessEndpointResponse(t, jwksURLForIssuer(supervisorScheme, supervisorAddress, issuerURL.Path), issuerName, supervisorCABundle) //nolint:bodyclose + response, responseBody := requireSuccessEndpointResponse(t, //nolint:bodyclose + jwksURLForIssuer(supervisorScheme, supervisorAddress, issuerURL.Path), + issuerName, + supervisorCABundle, + dnsOverrides, + ) var result ExpectedJWKSResponseFormat err = json.Unmarshal([]byte(responseBody), &result) @@ -277,9 +413,10 @@ func requireJWKSEndpointIsWorking(t *testing.T, supervisorScheme, supervisorAddr return &result } -func requireSuccessEndpointResponse(t *testing.T, endpointURL, issuer, caBundle string) (*http.Response, string) { +func requireSuccessEndpointResponse(t *testing.T, endpointURL, issuer, caBundle string, dnsOverrides map[string]string) (*http.Response, string) { t.Helper() - httpClient := newHTTPClient(caBundle) + httpClient := newHTTPClient(t, caBundle, dnsOverrides) + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() @@ -360,12 +497,33 @@ func requireStatus(t *testing.T, client pinnipedclientset.Interface, ns, name st require.Equalf(t, status, opc.Status.Status, "unexpected status (message = '%s')", opc.Status.Message) } -func newHTTPClient(caBundle string) *http.Client { +func newHTTPClient(t *testing.T, caBundle string, dnsOverrides map[string]string) *http.Client { c := &http.Client{} + + realDialer := &net.Dialer{} + overrideDialContext := func(ctx context.Context, network, addr string) (net.Conn, error) { + replacementAddr, hasKey := dnsOverrides[addr] + if hasKey { + t.Logf("DialContext replacing addr %s with %s", addr, replacementAddr) + addr = replacementAddr + } else if dnsOverrides != nil { + t.Fatal("dnsOverrides was provided but not used, which was probably a mistake") + } + return realDialer.DialContext(ctx, network, addr) + } + if caBundle != "" { // CA bundle is optional caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM([]byte(caBundle)) - c.Transport = &http.Transport{TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS13, RootCAs: caCertPool}} + c.Transport = &http.Transport{ + DialContext: overrideDialContext, + TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS13, RootCAs: caCertPool}, + } + } else { + c.Transport = &http.Transport{ + DialContext: overrideDialContext, + } } + return c } diff --git a/test/integration/supervisor_keys_test.go b/test/integration/supervisor_keys_test.go index 52f5ba09..19e1978d 100644 --- a/test/integration/supervisor_keys_test.go +++ b/test/integration/supervisor_keys_test.go @@ -27,7 +27,7 @@ func TestSupervisorOIDCKeys(t *testing.T) { defer cancel() // Create our OPC under test. - opc := library.CreateTestOIDCProvider(ctx, t, "") + opc := library.CreateTestOIDCProvider(ctx, t, "", "") // Ensure a secret is created with the OPC's JWKS. var updatedOPC *configv1alpha1.OIDCProviderConfig diff --git a/test/library/client.go b/test/library/client.go index f12219da..95f798b4 100644 --- a/test/library/client.go +++ b/test/library/client.go @@ -165,7 +165,7 @@ func CreateTestWebhookIDP(ctx context.Context, t *testing.T) corev1.TypedLocalOb // // If the provided issuer is not the empty string, then it will be used for the // OIDCProviderConfig.Spec.Issuer field. Else, a random issuer will be generated. -func CreateTestOIDCProvider(ctx context.Context, t *testing.T, issuer string) *configv1alpha1.OIDCProviderConfig { +func CreateTestOIDCProvider(ctx context.Context, t *testing.T, issuer, sniCertificateSecretName string) *configv1alpha1.OIDCProviderConfig { t.Helper() testEnv := IntegrationEnv(t) @@ -186,7 +186,8 @@ func CreateTestOIDCProvider(ctx context.Context, t *testing.T, issuer string) *c Annotations: map[string]string{"pinniped.dev/testName": t.Name()}, }, Spec: configv1alpha1.OIDCProviderConfigSpec{ - Issuer: issuer, + Issuer: issuer, + SNICertificateSecretName: sniCertificateSecretName, }, }, metav1.CreateOptions{}) require.NoError(t, err, "could not create test OIDCProviderConfig") diff --git a/test/library/env.go b/test/library/env.go index ed39a193..22b8d666 100644 --- a/test/library/env.go +++ b/test/library/env.go @@ -26,17 +26,18 @@ const ( type TestEnv struct { t *testing.T - ConciergeNamespace string `json:"conciergeNamespace"` - SupervisorNamespace string `json:"supervisorNamespace"` - ConciergeAppName string `json:"conciergeAppName"` - SupervisorAppName string `json:"supervisorAppName"` - SupervisorCustomLabels map[string]string `json:"supervisorCustomLabels"` - ConciergeCustomLabels map[string]string `json:"conciergeCustomLabels"` - Capabilities map[Capability]bool `json:"capabilities"` - TestWebhook idpv1alpha1.WebhookIdentityProviderSpec `json:"testWebhook"` - SupervisorHTTPAddress string `json:"supervisorHttpAddress"` - SupervisorHTTPSAddress string `json:"supervisorHttpsAddress"` - SupervisorHTTPSCABundle string `json:"supervisorHttpsCABundle"` + ConciergeNamespace string `json:"conciergeNamespace"` + SupervisorNamespace string `json:"supervisorNamespace"` + ConciergeAppName string `json:"conciergeAppName"` + SupervisorAppName string `json:"supervisorAppName"` + SupervisorCustomLabels map[string]string `json:"supervisorCustomLabels"` + ConciergeCustomLabels map[string]string `json:"conciergeCustomLabels"` + Capabilities map[Capability]bool `json:"capabilities"` + TestWebhook idpv1alpha1.WebhookIdentityProviderSpec `json:"testWebhook"` + SupervisorHTTPAddress string `json:"supervisorHttpAddress"` + SupervisorHTTPSAddress string `json:"supervisorHttpsAddress"` + SupervisorHTTPSIngressAddress string `json:"supervisorHttpsIngressAddress"` + SupervisorHTTPSIngressCABundle string `json:"supervisorHttpsIngressCABundle"` TestUser struct { Token string `json:"token"` @@ -75,51 +76,62 @@ func IntegrationEnv(t *testing.T) *TestEnv { err := yaml.Unmarshal([]byte(capabilitiesDescriptionYAML), &result) require.NoErrorf(t, err, "capabilities specification was invalid YAML") - needEnv := func(key string) string { - t.Helper() - value := os.Getenv(key) - require.NotEmptyf(t, value, "must specify %s env var for integration tests", key) - return value - } + loadEnvVars(t, &result) - result.ConciergeNamespace = needEnv("PINNIPED_TEST_CONCIERGE_NAMESPACE") - result.ConciergeAppName = needEnv("PINNIPED_TEST_CONCIERGE_APP_NAME") - result.TestUser.ExpectedUsername = needEnv("PINNIPED_TEST_USER_USERNAME") - result.TestUser.ExpectedGroups = strings.Split(strings.ReplaceAll(needEnv("PINNIPED_TEST_USER_GROUPS"), " ", ""), ",") - result.TestUser.Token = needEnv("PINNIPED_TEST_USER_TOKEN") - result.TestWebhook.Endpoint = needEnv("PINNIPED_TEST_WEBHOOK_ENDPOINT") - result.SupervisorNamespace = needEnv("PINNIPED_TEST_SUPERVISOR_NAMESPACE") - result.SupervisorAppName = needEnv("PINNIPED_TEST_SUPERVISOR_APP_NAME") - result.TestWebhook.TLS = &idpv1alpha1.TLSSpec{CertificateAuthorityData: needEnv("PINNIPED_TEST_WEBHOOK_CA_BUNDLE")} + result.t = t + return &result +} + +func needEnv(t *testing.T, key string) string { + t.Helper() + value := os.Getenv(key) + require.NotEmptyf(t, value, "must specify %s env var for integration tests", key) + return value +} + +func loadEnvVars(t *testing.T, result *TestEnv) { + t.Helper() + + result.ConciergeNamespace = needEnv(t, "PINNIPED_TEST_CONCIERGE_NAMESPACE") + result.ConciergeAppName = needEnv(t, "PINNIPED_TEST_CONCIERGE_APP_NAME") + result.TestUser.ExpectedUsername = needEnv(t, "PINNIPED_TEST_USER_USERNAME") + result.TestUser.ExpectedGroups = strings.Split(strings.ReplaceAll(needEnv(t, "PINNIPED_TEST_USER_GROUPS"), " ", ""), ",") + result.TestUser.Token = needEnv(t, "PINNIPED_TEST_USER_TOKEN") + result.TestWebhook.Endpoint = needEnv(t, "PINNIPED_TEST_WEBHOOK_ENDPOINT") + result.SupervisorNamespace = needEnv(t, "PINNIPED_TEST_SUPERVISOR_NAMESPACE") + result.SupervisorAppName = needEnv(t, "PINNIPED_TEST_SUPERVISOR_APP_NAME") + result.TestWebhook.TLS = &idpv1alpha1.TLSSpec{CertificateAuthorityData: needEnv(t, "PINNIPED_TEST_WEBHOOK_CA_BUNDLE")} result.SupervisorHTTPAddress = os.Getenv("PINNIPED_TEST_SUPERVISOR_HTTP_ADDRESS") - result.SupervisorHTTPSAddress = os.Getenv("PINNIPED_TEST_SUPERVISOR_HTTPS_ADDRESS") + result.SupervisorHTTPSIngressAddress = os.Getenv("PINNIPED_TEST_SUPERVISOR_HTTPS_INGRESS_ADDRESS") + result.SupervisorHTTPSIngressCABundle = os.Getenv("PINNIPED_TEST_SUPERVISOR_HTTPS_INGRESS_CA_BUNDLE") // optional require.NotEmptyf(t, - result.SupervisorHTTPAddress+result.SupervisorHTTPSAddress, - "must specify either PINNIPED_TEST_SUPERVISOR_HTTP_ADDRESS or PINNIPED_TEST_SUPERVISOR_HTTPS_ADDRESS env var (or both) for integration tests", + result.SupervisorHTTPAddress+result.SupervisorHTTPSIngressAddress, + "must specify either PINNIPED_TEST_SUPERVISOR_HTTP_ADDRESS or PINNIPED_TEST_SUPERVISOR_HTTPS_INGRESS_ADDRESS env var (or both) for integration tests", + ) + result.SupervisorHTTPSAddress = needEnv(t, "PINNIPED_TEST_SUPERVISOR_HTTPS_ADDRESS") + require.NotRegexp(t, "^[0-9]", result.SupervisorHTTPSAddress, + "PINNIPED_TEST_SUPERVISOR_HTTPS_ADDRESS must be a hostname with an optional port and cannot be an IP address", ) - result.SupervisorHTTPSCABundle = os.Getenv("PINNIPED_TEST_SUPERVISOR_HTTPS_CA_BUNDLE") // optional - conciergeCustomLabelsYAML := needEnv("PINNIPED_TEST_CONCIERGE_CUSTOM_LABELS") + conciergeCustomLabelsYAML := needEnv(t, "PINNIPED_TEST_CONCIERGE_CUSTOM_LABELS") var conciergeCustomLabels map[string]string - err = yaml.Unmarshal([]byte(conciergeCustomLabelsYAML), &conciergeCustomLabels) + err := yaml.Unmarshal([]byte(conciergeCustomLabelsYAML), &conciergeCustomLabels) require.NoErrorf(t, err, "PINNIPED_TEST_CONCIERGE_CUSTOM_LABELS must be a YAML map of string to string") result.ConciergeCustomLabels = conciergeCustomLabels require.NotEmpty(t, result.ConciergeCustomLabels, "PINNIPED_TEST_CONCIERGE_CUSTOM_LABELS cannot be empty") - supervisorCustomLabelsYAML := needEnv("PINNIPED_TEST_SUPERVISOR_CUSTOM_LABELS") + supervisorCustomLabelsYAML := needEnv(t, "PINNIPED_TEST_SUPERVISOR_CUSTOM_LABELS") var supervisorCustomLabels map[string]string err = yaml.Unmarshal([]byte(supervisorCustomLabelsYAML), &supervisorCustomLabels) require.NoErrorf(t, err, "PINNIPED_TEST_SUPERVISOR_CUSTOM_LABELS must be a YAML map of string to string") result.SupervisorCustomLabels = supervisorCustomLabels require.NotEmpty(t, result.SupervisorCustomLabels, "PINNIPED_TEST_SUPERVISOR_CUSTOM_LABELS cannot be empty") - result.OIDCUpstream.Issuer = needEnv("PINNIPED_TEST_CLI_OIDC_ISSUER") - result.OIDCUpstream.ClientID = needEnv("PINNIPED_TEST_CLI_OIDC_CLIENT_ID") - result.OIDCUpstream.LocalhostPort, _ = strconv.Atoi(needEnv("PINNIPED_TEST_CLI_OIDC_LOCALHOST_PORT")) - result.OIDCUpstream.Username = needEnv("PINNIPED_TEST_CLI_OIDC_USERNAME") - result.OIDCUpstream.Password = needEnv("PINNIPED_TEST_CLI_OIDC_PASSWORD") - result.t = t - return &result + result.OIDCUpstream.Issuer = needEnv(t, "PINNIPED_TEST_CLI_OIDC_ISSUER") + result.OIDCUpstream.ClientID = needEnv(t, "PINNIPED_TEST_CLI_OIDC_CLIENT_ID") + result.OIDCUpstream.LocalhostPort, _ = strconv.Atoi(needEnv(t, "PINNIPED_TEST_CLI_OIDC_LOCALHOST_PORT")) + result.OIDCUpstream.Username = needEnv(t, "PINNIPED_TEST_CLI_OIDC_USERNAME") + result.OIDCUpstream.Password = needEnv(t, "PINNIPED_TEST_CLI_OIDC_PASSWORD") } func (e *TestEnv) HasCapability(cap Capability) bool {