diff --git a/cmd/pinniped/cmd/flag_types.go b/cmd/pinniped/cmd/flag_types.go index 73c8cfd1..c0e3624d 100644 --- a/cmd/pinniped/cmd/flag_types.go +++ b/cmd/pinniped/cmd/flag_types.go @@ -7,6 +7,8 @@ import ( "flag" "fmt" "strings" + + configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1" ) // conciergeMode represents the method by which we should connect to the Concierge on a cluster during login. @@ -25,7 +27,7 @@ func (c *conciergeMode) String() string { switch *c { case modeImpersonationProxy: return "ImpersonationProxy" - case modeTokenCredentialRequestAPI, modeUnknown: + case modeTokenCredentialRequestAPI: return "TokenCredentialRequestAPI" default: return "TokenCredentialRequestAPI" @@ -51,3 +53,15 @@ func (c *conciergeMode) Set(s string) error { func (c *conciergeMode) Type() string { return "mode" } + +// MatchesFrontend returns true iff the flag matches the type of the provided frontend. +func (c *conciergeMode) MatchesFrontend(frontend *configv1alpha1.CredentialIssuerFrontend) bool { + switch *c { + case modeImpersonationProxy: + return frontend.Type == configv1alpha1.ImpersonationProxyFrontendType + case modeTokenCredentialRequestAPI: + return frontend.Type == configv1alpha1.TokenCredentialRequestAPIFrontendType + default: + return true + } +} diff --git a/cmd/pinniped/cmd/flag_types_test.go b/cmd/pinniped/cmd/flag_types_test.go index 26f107b7..8c0faeda 100644 --- a/cmd/pinniped/cmd/flag_types_test.go +++ b/cmd/pinniped/cmd/flag_types_test.go @@ -7,17 +7,25 @@ import ( "testing" "github.com/stretchr/testify/require" + + configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1" ) func TestConciergeModeFlag(t *testing.T) { var m conciergeMode require.Equal(t, "mode", m.Type()) require.Equal(t, modeUnknown, m) + require.NoError(t, m.Set("")) + require.Equal(t, modeUnknown, m) require.EqualError(t, m.Set("foo"), `invalid mode "foo", valid modes are TokenCredentialRequestAPI and ImpersonationProxy`) + require.True(t, m.MatchesFrontend(&configv1alpha1.CredentialIssuerFrontend{Type: configv1alpha1.TokenCredentialRequestAPIFrontendType})) + require.True(t, m.MatchesFrontend(&configv1alpha1.CredentialIssuerFrontend{Type: configv1alpha1.ImpersonationProxyFrontendType})) require.NoError(t, m.Set("TokenCredentialRequestAPI")) require.Equal(t, modeTokenCredentialRequestAPI, m) require.Equal(t, "TokenCredentialRequestAPI", m.String()) + require.True(t, m.MatchesFrontend(&configv1alpha1.CredentialIssuerFrontend{Type: configv1alpha1.TokenCredentialRequestAPIFrontendType})) + require.False(t, m.MatchesFrontend(&configv1alpha1.CredentialIssuerFrontend{Type: configv1alpha1.ImpersonationProxyFrontendType})) require.NoError(t, m.Set("tokencredentialrequestapi")) require.Equal(t, modeTokenCredentialRequestAPI, m) @@ -26,6 +34,8 @@ func TestConciergeModeFlag(t *testing.T) { require.NoError(t, m.Set("ImpersonationProxy")) require.Equal(t, modeImpersonationProxy, m) require.Equal(t, "ImpersonationProxy", m.String()) + require.False(t, m.MatchesFrontend(&configv1alpha1.CredentialIssuerFrontend{Type: configv1alpha1.TokenCredentialRequestAPIFrontendType})) + require.True(t, m.MatchesFrontend(&configv1alpha1.CredentialIssuerFrontend{Type: configv1alpha1.ImpersonationProxyFrontendType})) require.NoError(t, m.Set("impersonationproxy")) require.Equal(t, modeImpersonationProxy, m) diff --git a/cmd/pinniped/cmd/kubeconfig.go b/cmd/pinniped/cmd/kubeconfig.go index d0025840..968e15d7 100644 --- a/cmd/pinniped/cmd/kubeconfig.go +++ b/cmd/pinniped/cmd/kubeconfig.go @@ -283,43 +283,56 @@ func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, f } func configureConcierge(credentialIssuer *configv1alpha1.CredentialIssuer, authenticator metav1.Object, flags *getKubeconfigParams, v1Cluster *clientcmdapi.Cluster, oidcCABundle *string, execConfig *clientcmdapi.ExecConfig, log logr.Logger) error { - var conciergeCABundleData []byte - // Autodiscover the --concierge-mode. - if flags.concierge.mode == modeUnknown { //nolint:nestif - strategyLoop: - for _, strategy := range credentialIssuer.Status.Strategies { - if strategy.Status != configv1alpha1.SuccessStrategyStatus || strategy.Frontend == nil { - continue - } - switch strategy.Frontend.Type { - case configv1alpha1.TokenCredentialRequestAPIFrontendType: - log.Info("detected Concierge in TokenCredentialRequest API mode") - flags.concierge.mode = modeTokenCredentialRequestAPI - break strategyLoop - case configv1alpha1.ImpersonationProxyFrontendType: + frontend, err := getConciergeFrontend(credentialIssuer, flags.concierge.mode) + if err != nil { + return err + } - flags.concierge.mode = modeImpersonationProxy - flags.concierge.endpoint = strategy.Frontend.ImpersonationProxyInfo.Endpoint - var err error - conciergeCABundleData, err = base64.StdEncoding.DecodeString(strategy.Frontend.ImpersonationProxyInfo.CertificateAuthorityData) - if err != nil { - return fmt.Errorf("autodiscovered Concierge CA bundle is invalid: %w", err) - } - log.Info("detected Concierge in impersonation proxy mode", "endpoint", strategy.Frontend.ImpersonationProxyInfo.Endpoint) - break strategyLoop - default: - // Skip any unknown frontend types. - } - } - if flags.concierge.mode == modeUnknown { - // Fall back to deprecated field for backwards compatibility. - if credentialIssuer.Status.KubeConfigInfo != nil { - flags.concierge.mode = modeTokenCredentialRequestAPI - } else { - return fmt.Errorf("could not autodiscover --concierge-mode and none was provided") + // Auto-set --concierge-mode if it wasn't explicitly set. + if flags.concierge.mode == modeUnknown { + switch frontend.Type { + case configv1alpha1.TokenCredentialRequestAPIFrontendType: + log.Info("discovered Concierge operating in TokenCredentialRequest API mode") + flags.concierge.mode = modeTokenCredentialRequestAPI + case configv1alpha1.ImpersonationProxyFrontendType: + log.Info("discovered Concierge operating in impersonation proxy mode") + flags.concierge.mode = modeImpersonationProxy + } + } + + // Auto-set --concierge-endpoint if it wasn't explicitly set. + if flags.concierge.endpoint == "" { + switch frontend.Type { + case configv1alpha1.TokenCredentialRequestAPIFrontendType: + flags.concierge.endpoint = v1Cluster.Server + case configv1alpha1.ImpersonationProxyFrontendType: + flags.concierge.endpoint = frontend.ImpersonationProxyInfo.Endpoint + } + log.Info("discovered Concierge endpoint", "endpoint", flags.concierge.endpoint) + } + + // Load specified --concierge-ca-bundle or autodiscover a value. + var conciergeCABundleData []byte + if flags.concierge.caBundlePath != "" { + caBundleString, err := loadCABundlePaths([]string{flags.concierge.caBundlePath}) + if err != nil { + return fmt.Errorf("could not read --concierge-ca-bundle: %w", err) + } + conciergeCABundleData = []byte(caBundleString) + log.Info("loaded Concierge certificate authority bundle", "roots", countCACerts(conciergeCABundleData)) + } else { + switch frontend.Type { + case configv1alpha1.TokenCredentialRequestAPIFrontendType: + conciergeCABundleData = v1Cluster.CertificateAuthorityData + case configv1alpha1.ImpersonationProxyFrontendType: + var err error + conciergeCABundleData, err = base64.StdEncoding.DecodeString(frontend.ImpersonationProxyInfo.CertificateAuthorityData) + if err != nil { + return fmt.Errorf("autodiscovered Concierge CA bundle is invalid: %w", err) } } + log.Info("discovered Concierge certificate authority bundle", "roots", countCACerts(conciergeCABundleData)) } switch auth := authenticator.(type) { @@ -342,13 +355,13 @@ func configureConcierge(credentialIssuer *configv1alpha1.CredentialIssuer, authe // If the --oidc-issuer flag was not set explicitly, default it to the spec.issuer field of the JWTAuthenticator. if flags.oidc.issuer == "" { - log.Info("detected OIDC issuer", "issuer", auth.Spec.Issuer) + log.Info("discovered OIDC issuer", "issuer", auth.Spec.Issuer) flags.oidc.issuer = auth.Spec.Issuer } // If the --oidc-request-audience flag was not set explicitly, default it to the spec.audience field of the JWTAuthenticator. if flags.oidc.requestAudience == "" { - log.Info("detected OIDC audience", "audience", auth.Spec.Audience) + log.Info("discovered OIDC audience", "audience", auth.Spec.Audience) flags.oidc.requestAudience = auth.Spec.Audience } @@ -359,29 +372,11 @@ func configureConcierge(credentialIssuer *configv1alpha1.CredentialIssuer, authe if err != nil { return fmt.Errorf("tried to autodiscover --oidc-ca-bundle, but JWTAuthenticator %s has invalid spec.tls.certificateAuthorityData: %w", auth.Name, err) } - log.Info("detected OIDC CA bundle", "length", len(decoded)) + log.Info("discovered OIDC CA bundle", "roots", countCACerts(decoded)) *oidcCABundle = string(decoded) } } - if flags.concierge.endpoint == "" { - log.Info("detected concierge endpoint", "endpoint", v1Cluster.Server) - flags.concierge.endpoint = v1Cluster.Server - } - - if conciergeCABundleData == nil { - if flags.concierge.caBundlePath == "" { - log.Info("detected concierge CA bundle", "length", len(v1Cluster.CertificateAuthorityData)) - conciergeCABundleData = v1Cluster.CertificateAuthorityData - } else { - caBundleString, err := loadCABundlePaths([]string{flags.concierge.caBundlePath}) - if err != nil { - return fmt.Errorf("could not read --concierge-ca-bundle: %w", err) - } - conciergeCABundleData = []byte(caBundleString) - } - } - // Append the flags to configure the Concierge credential exchange at runtime. execConfig.Args = append(execConfig.Args, "--enable-concierge", @@ -393,14 +388,53 @@ func configureConcierge(credentialIssuer *configv1alpha1.CredentialIssuer, authe "--concierge-mode="+flags.concierge.mode.String(), ) - // If we're in impersonation proxy mode, the main server endpoint for the kubeconfig also needs to point to the proxy - if flags.concierge.mode == modeImpersonationProxy { - log.Info("switching kubeconfig cluster to point at impersonation proxy endpoint", "endpoint", flags.concierge.endpoint) - v1Cluster.CertificateAuthorityData = conciergeCABundleData - v1Cluster.Server = flags.concierge.endpoint + // Point kubectl at the concierge endpoint. + v1Cluster.Server = flags.concierge.endpoint + v1Cluster.CertificateAuthorityData = conciergeCABundleData + return nil +} + +func getConciergeFrontend(credentialIssuer *configv1alpha1.CredentialIssuer, mode conciergeMode) (*configv1alpha1.CredentialIssuerFrontend, error) { + for _, strategy := range credentialIssuer.Status.Strategies { + // Skip unhealthy strategies. + if strategy.Status != configv1alpha1.SuccessStrategyStatus { + continue + } + + // Backfill the .status.strategies[].frontend field from .status.kubeConfigInfo for backwards compatibility. + if strategy.Type == configv1alpha1.KubeClusterSigningCertificateStrategyType && strategy.Frontend == nil && credentialIssuer.Status.KubeConfigInfo != nil { + strategy = *strategy.DeepCopy() + strategy.Frontend = &configv1alpha1.CredentialIssuerFrontend{ + Type: configv1alpha1.TokenCredentialRequestAPIFrontendType, + TokenCredentialRequestAPIInfo: &configv1alpha1.TokenCredentialRequestAPIInfo{ + Server: credentialIssuer.Status.KubeConfigInfo.Server, + CertificateAuthorityData: credentialIssuer.Status.KubeConfigInfo.CertificateAuthorityData, + }, + } + } + + // If the strategy frontend is still nil, skip. + if strategy.Frontend == nil { + continue + } + + // Skip any unknown frontend types. + switch strategy.Frontend.Type { + case configv1alpha1.TokenCredentialRequestAPIFrontendType, configv1alpha1.ImpersonationProxyFrontendType: + default: + continue + } + // Skip strategies that don't match --concierge-mode. + if !mode.MatchesFrontend(strategy.Frontend) { + continue + } + return strategy.Frontend, nil } - return nil + if mode == modeUnknown { + return nil, fmt.Errorf("could not autodiscover --concierge-mode") + } + return nil, fmt.Errorf("could not find successful Concierge strategy matching --concierge-mode=%s", mode.String()) } func loadCABundlePaths(paths []string) (string, error) { @@ -614,3 +648,9 @@ func validateKubeconfig(ctx context.Context, flags getKubeconfigParams, kubeconf } } } + +func countCACerts(pemData []byte) int { + pool := x509.NewCertPool() + pool.AppendCertsFromPEM(pemData) + return len(pool.Subjects()) +} diff --git a/cmd/pinniped/cmd/kubeconfig_test.go b/cmd/pinniped/cmd/kubeconfig_test.go index c9dd5b77..34c78ce3 100644 --- a/cmd/pinniped/cmd/kubeconfig_test.go +++ b/cmd/pinniped/cmd/kubeconfig_test.go @@ -36,8 +36,10 @@ func TestGetKubeconfig(t *testing.T) { testOIDCCABundlePath := filepath.Join(tmpdir, "testca.pem") require.NoError(t, ioutil.WriteFile(testOIDCCABundlePath, testOIDCCA.Bundle(), 0600)) + testConciergeCA, err := certauthority.New(pkix.Name{CommonName: "Test Concierge CA"}, 1*time.Hour) + require.NoError(t, err) testConciergeCABundlePath := filepath.Join(tmpdir, "testconciergeca.pem") - require.NoError(t, ioutil.WriteFile(testConciergeCABundlePath, []byte("test-concierge-ca"), 0600)) + require.NoError(t, ioutil.WriteFile(testConciergeCABundlePath, testConciergeCA.Bundle(), 0600)) tests := []struct { name string @@ -324,7 +326,7 @@ func TestGetKubeconfig(t *testing.T) { }, wantError: true, wantStderr: here.Doc(` - Error: could not autodiscover --concierge-mode and none was provided + Error: could not autodiscover --concierge-mode `), }, { @@ -375,6 +377,8 @@ func TestGetKubeconfig(t *testing.T) { }, wantLogs: []string{ `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + `"level"=0 "msg"="discovered Concierge operating in impersonation proxy mode"`, + `"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://impersonation-endpoint"`, }, wantError: true, wantStderr: here.Doc(` @@ -410,10 +414,10 @@ func TestGetKubeconfig(t *testing.T) { }, wantLogs: []string{ `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, - `"level"=0 "msg"="detected Concierge in TokenCredentialRequest API mode"`, + `"level"=0 "msg"="discovered Concierge operating in TokenCredentialRequest API mode"`, + `"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://fake-server-url-value"`, + `"level"=0 "msg"="discovered Concierge certificate authority bundle" "roots"=0`, `"level"=0 "msg"="discovered WebhookAuthenticator" "name"="test-authenticator"`, - `"level"=0 "msg"="detected concierge endpoint" "endpoint"="https://fake-server-url-value"`, - `"level"=0 "msg"="detected concierge CA bundle" "length"=37`, }, wantError: true, wantStderr: here.Doc(` @@ -433,11 +437,22 @@ func TestGetKubeconfig(t *testing.T) { Server: "https://concierge-endpoint", CertificateAuthorityData: "ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==", }, + Strategies: []configv1alpha1.CredentialIssuerStrategy{{ + Type: configv1alpha1.KubeClusterSigningCertificateStrategyType, + Status: configv1alpha1.SuccessStrategyStatus, + Reason: configv1alpha1.FetchedKeyStrategyReason, + Message: "Successfully fetched key", + LastUpdateTime: metav1.Now(), + // Simulate a previous version of CredentialIssuer that's missing this Frontend field. + Frontend: nil, + }}, }, }, &conciergev1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}, Spec: conciergev1alpha1.JWTAuthenticatorSpec{ + Issuer: "https://test-issuer.example.com", + Audience: "some-test-audience", TLS: &conciergev1alpha1.TLSSpec{ CertificateAuthorityData: "invalid-base64", }, @@ -446,9 +461,12 @@ func TestGetKubeconfig(t *testing.T) { }, wantLogs: []string{ `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + `"level"=0 "msg"="discovered Concierge operating in TokenCredentialRequest API mode"`, + `"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://fake-server-url-value"`, + `"level"=0 "msg"="discovered Concierge certificate authority bundle" "roots"=0`, `"level"=0 "msg"="discovered JWTAuthenticator" "name"="test-authenticator"`, - `"level"=0 "msg"="detected OIDC issuer" "issuer"=""`, - `"level"=0 "msg"="detected OIDC audience" "audience"=""`, + `"level"=0 "msg"="discovered OIDC issuer" "issuer"="https://test-issuer.example.com"`, + `"level"=0 "msg"="discovered OIDC audience" "audience"="some-test-audience"`, }, wantError: true, wantStderr: here.Doc(` @@ -469,10 +487,18 @@ func TestGetKubeconfig(t *testing.T) { &configv1alpha1.CredentialIssuer{ ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, Status: configv1alpha1.CredentialIssuerStatus{ - KubeConfigInfo: &configv1alpha1.CredentialIssuerKubeConfigInfo{ - Server: "https://concierge-endpoint", - CertificateAuthorityData: "ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==", - }, + Strategies: []configv1alpha1.CredentialIssuerStrategy{{ + Type: configv1alpha1.ImpersonationProxyStrategyType, + Status: configv1alpha1.SuccessStrategyStatus, + Reason: configv1alpha1.ListeningStrategyReason, + Frontend: &configv1alpha1.CredentialIssuerFrontend{ + Type: configv1alpha1.ImpersonationProxyFrontendType, + ImpersonationProxyInfo: &configv1alpha1.ImpersonationProxyInfo{ + Endpoint: "https://impersonation-proxy-endpoint.example.com", + CertificateAuthorityData: base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), + }, + }, + }}, }, }, &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, @@ -496,19 +522,28 @@ func TestGetKubeconfig(t *testing.T) { &configv1alpha1.CredentialIssuer{ ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, Status: configv1alpha1.CredentialIssuerStatus{ - KubeConfigInfo: &configv1alpha1.CredentialIssuerKubeConfigInfo{ - Server: "https://concierge-endpoint", - CertificateAuthorityData: "ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==", - }, + Strategies: []configv1alpha1.CredentialIssuerStrategy{{ + Type: configv1alpha1.ImpersonationProxyStrategyType, + Status: configv1alpha1.SuccessStrategyStatus, + Reason: configv1alpha1.ListeningStrategyReason, + Frontend: &configv1alpha1.CredentialIssuerFrontend{ + Type: configv1alpha1.ImpersonationProxyFrontendType, + ImpersonationProxyInfo: &configv1alpha1.ImpersonationProxyInfo{ + Endpoint: "https://impersonation-proxy-endpoint.example.com", + CertificateAuthorityData: base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), + }, + }, + }}, }, }, &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, }, wantLogs: []string{ `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + `"level"=0 "msg"="discovered Concierge operating in impersonation proxy mode"`, + `"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://impersonation-proxy-endpoint.example.com"`, + `"level"=0 "msg"="discovered Concierge certificate authority bundle" "roots"=1`, `"level"=0 "msg"="discovered WebhookAuthenticator" "name"="test-authenticator"`, - `"level"=0 "msg"="detected concierge endpoint" "endpoint"="https://fake-server-url-value"`, - `"level"=0 "msg"="detected concierge CA bundle" "length"=37`, }, wantError: true, wantStderr: here.Doc(` @@ -536,19 +571,28 @@ func TestGetKubeconfig(t *testing.T) { &configv1alpha1.CredentialIssuer{ ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, Status: configv1alpha1.CredentialIssuerStatus{ - KubeConfigInfo: &configv1alpha1.CredentialIssuerKubeConfigInfo{ - Server: "https://concierge-endpoint", - CertificateAuthorityData: "ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==", - }, + Strategies: []configv1alpha1.CredentialIssuerStrategy{{ + Type: configv1alpha1.KubeClusterSigningCertificateStrategyType, + Status: configv1alpha1.SuccessStrategyStatus, + Reason: configv1alpha1.FetchedKeyStrategyReason, + Frontend: &configv1alpha1.CredentialIssuerFrontend{ + Type: configv1alpha1.TokenCredentialRequestAPIFrontendType, + TokenCredentialRequestAPIInfo: &configv1alpha1.TokenCredentialRequestAPIInfo{ + Server: "https://concierge-endpoint.example.com", + CertificateAuthorityData: base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), + }, + }, + }}, }, }, &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, }, wantLogs: []string{ `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + `"level"=0 "msg"="discovered Concierge operating in TokenCredentialRequest API mode"`, + `"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://fake-server-url-value"`, + `"level"=0 "msg"="discovered Concierge certificate authority bundle" "roots"=0`, `"level"=0 "msg"="discovered WebhookAuthenticator" "name"="test-authenticator"`, - `"level"=0 "msg"="detected concierge endpoint" "endpoint"="https://fake-server-url-value"`, - `"level"=0 "msg"="detected concierge CA bundle" "length"=37`, }, wantStdout: here.Doc(` apiVersion: v1 @@ -597,19 +641,28 @@ func TestGetKubeconfig(t *testing.T) { &configv1alpha1.CredentialIssuer{ ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, Status: configv1alpha1.CredentialIssuerStatus{ - KubeConfigInfo: &configv1alpha1.CredentialIssuerKubeConfigInfo{ - Server: "https://concierge-endpoint", - CertificateAuthorityData: "ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==", - }, + Strategies: []configv1alpha1.CredentialIssuerStrategy{{ + Type: configv1alpha1.KubeClusterSigningCertificateStrategyType, + Status: configv1alpha1.SuccessStrategyStatus, + Reason: configv1alpha1.FetchedKeyStrategyReason, + Frontend: &configv1alpha1.CredentialIssuerFrontend{ + Type: configv1alpha1.TokenCredentialRequestAPIFrontendType, + TokenCredentialRequestAPIInfo: &configv1alpha1.TokenCredentialRequestAPIInfo{ + Server: "https://concierge-endpoint.example.com", + CertificateAuthorityData: base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), + }, + }, + }}, }, }, &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, }, wantLogs: []string{ `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + `"level"=0 "msg"="discovered Concierge operating in TokenCredentialRequest API mode"`, + `"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://fake-server-url-value"`, + `"level"=0 "msg"="discovered Concierge certificate authority bundle" "roots"=0`, `"level"=0 "msg"="discovered WebhookAuthenticator" "name"="test-authenticator"`, - `"level"=0 "msg"="detected concierge endpoint" "endpoint"="https://fake-server-url-value"`, - `"level"=0 "msg"="detected concierge CA bundle" "length"=37`, }, wantStdout: here.Doc(` apiVersion: v1 @@ -657,10 +710,18 @@ func TestGetKubeconfig(t *testing.T) { &configv1alpha1.CredentialIssuer{ ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, Status: configv1alpha1.CredentialIssuerStatus{ - KubeConfigInfo: &configv1alpha1.CredentialIssuerKubeConfigInfo{ - Server: "https://concierge-endpoint", - CertificateAuthorityData: "ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==", - }, + Strategies: []configv1alpha1.CredentialIssuerStrategy{{ + Type: configv1alpha1.KubeClusterSigningCertificateStrategyType, + Status: configv1alpha1.SuccessStrategyStatus, + Reason: configv1alpha1.FetchedKeyStrategyReason, + Frontend: &configv1alpha1.CredentialIssuerFrontend{ + Type: configv1alpha1.TokenCredentialRequestAPIFrontendType, + TokenCredentialRequestAPIInfo: &configv1alpha1.TokenCredentialRequestAPIInfo{ + Server: "https://concierge-endpoint.example.com", + CertificateAuthorityData: base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), + }, + }, + }}, }, }, &conciergev1alpha1.JWTAuthenticator{ @@ -676,12 +737,13 @@ func TestGetKubeconfig(t *testing.T) { }, wantLogs: []string{ `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + `"level"=0 "msg"="discovered Concierge operating in TokenCredentialRequest API mode"`, + `"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://fake-server-url-value"`, + `"level"=0 "msg"="discovered Concierge certificate authority bundle" "roots"=0`, `"level"=0 "msg"="discovered JWTAuthenticator" "name"="test-authenticator"`, - `"level"=0 "msg"="detected OIDC issuer" "issuer"="https://example.com/issuer"`, - `"level"=0 "msg"="detected OIDC audience" "audience"="test-audience"`, - `"level"=0 "msg"="detected OIDC CA bundle" "length"=587`, - `"level"=0 "msg"="detected concierge endpoint" "endpoint"="https://fake-server-url-value"`, - `"level"=0 "msg"="detected concierge CA bundle" "length"=37`, + `"level"=0 "msg"="discovered OIDC issuer" "issuer"="https://example.com/issuer"`, + `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, + `"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`, }, wantStdout: here.Docf(` apiVersion: v1 @@ -731,7 +793,8 @@ func TestGetKubeconfig(t *testing.T) { "--concierge-api-group-suffix", "tuna.io", "--concierge-authenticator-type", "webhook", "--concierge-authenticator-name", "test-authenticator", - "--concierge-endpoint", "https://concierge-endpoint.example.com", + "--concierge-mode", "TokenCredentialRequestAPI", + "--concierge-endpoint", "https://explicit-concierge-endpoint.example.com", "--concierge-ca-bundle", testConciergeCABundlePath, "--oidc-issuer", "https://example.com/issuer", "--oidc-skip-browser", @@ -746,22 +809,33 @@ func TestGetKubeconfig(t *testing.T) { &configv1alpha1.CredentialIssuer{ ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, Status: configv1alpha1.CredentialIssuerStatus{ - KubeConfigInfo: &configv1alpha1.CredentialIssuerKubeConfigInfo{ - Server: "https://concierge-endpoint", - CertificateAuthorityData: "ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==", - }, + Strategies: []configv1alpha1.CredentialIssuerStrategy{{ + Type: configv1alpha1.KubeClusterSigningCertificateStrategyType, + Status: configv1alpha1.SuccessStrategyStatus, + Reason: configv1alpha1.FetchedKeyStrategyReason, + Frontend: &configv1alpha1.CredentialIssuerFrontend{ + Type: configv1alpha1.TokenCredentialRequestAPIFrontendType, + TokenCredentialRequestAPIInfo: &configv1alpha1.TokenCredentialRequestAPIInfo{ + Server: "https://concierge-endpoint.example.com", + CertificateAuthorityData: "dGVzdC10Y3ItYXBpLWNh", + }, + }, + }}, }, }, &conciergev1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}, }, }, + wantLogs: []string{ + `"level"=0 "msg"="loaded Concierge certificate authority bundle" "roots"=1`, + }, wantStdout: here.Docf(` apiVersion: v1 clusters: - cluster: - certificate-authority-data: ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== - server: https://fake-server-url-value + certificate-authority-data: %s + server: https://explicit-concierge-endpoint.example.com name: pinniped contexts: - context: @@ -783,8 +857,8 @@ func TestGetKubeconfig(t *testing.T) { - --concierge-api-group-suffix=tuna.io - --concierge-authenticator-name=test-authenticator - --concierge-authenticator-type=webhook - - --concierge-endpoint=https://concierge-endpoint.example.com - - --concierge-ca-bundle-data=dGVzdC1jb25jaWVyZ2UtY2E= + - --concierge-endpoint=https://explicit-concierge-endpoint.example.com + - --concierge-ca-bundle-data=%s - --concierge-mode=TokenCredentialRequestAPI - --issuer=https://example.com/issuer - --client-id=pinniped-cli @@ -798,22 +872,58 @@ func TestGetKubeconfig(t *testing.T) { command: '.../path/to/pinniped' env: [] provideClusterInfo: true - `, base64.StdEncoding.EncodeToString(testOIDCCA.Bundle())), + `, + base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), + base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), + base64.StdEncoding.EncodeToString(testOIDCCA.Bundle()), + ), wantAPIGroupSuffix: "tuna.io", }, { - name: "configure impersonation proxy with autodetected JWT authenticator", + name: "configure impersonation proxy with autodiscovered JWT authenticator", args: []string{ "--kubeconfig", "./testdata/kubeconfig.yaml", - "--concierge-ca-bundle", testConciergeCABundlePath, - "--concierge-endpoint", "https://impersonation-proxy-endpoint.test", "--concierge-mode", "ImpersonationProxy", "--skip-validation", }, conciergeObjects: []runtime.Object{ &configv1alpha1.CredentialIssuer{ ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, - Status: configv1alpha1.CredentialIssuerStatus{}, + Status: configv1alpha1.CredentialIssuerStatus{ + Strategies: []configv1alpha1.CredentialIssuerStrategy{ + // This TokenCredentialRequestAPI strategy would normally be chosen, but + // --concierge-mode=ImpersonationProxy should force it to be skipped. + { + Type: "SomeType", + Status: configv1alpha1.SuccessStrategyStatus, + Reason: "SomeReason", + Message: "Some message", + LastUpdateTime: metav1.Now(), + Frontend: &configv1alpha1.CredentialIssuerFrontend{ + Type: configv1alpha1.TokenCredentialRequestAPIFrontendType, + TokenCredentialRequestAPIInfo: &configv1alpha1.TokenCredentialRequestAPIInfo{ + Server: "https://token-credential-request-api-endpoint.test", + CertificateAuthorityData: "dGVzdC10Y3ItYXBpLWNh", + }, + }, + }, + // The endpoint and CA from this impersonation proxy strategy should be autodiscovered. + { + Type: "SomeOtherType", + Status: configv1alpha1.SuccessStrategyStatus, + Reason: "SomeOtherReason", + Message: "Some other message", + LastUpdateTime: metav1.Now(), + Frontend: &configv1alpha1.CredentialIssuerFrontend{ + Type: configv1alpha1.ImpersonationProxyFrontendType, + ImpersonationProxyInfo: &configv1alpha1.ImpersonationProxyInfo{ + Endpoint: "https://impersonation-proxy-endpoint.test", + CertificateAuthorityData: base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), + }, + }, + }, + }, + }, }, &conciergev1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}, @@ -828,17 +938,18 @@ func TestGetKubeconfig(t *testing.T) { }, wantLogs: []string{ `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + `"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://impersonation-proxy-endpoint.test"`, + `"level"=0 "msg"="discovered Concierge certificate authority bundle" "roots"=1`, `"level"=0 "msg"="discovered JWTAuthenticator" "name"="test-authenticator"`, - `"level"=0 "msg"="detected OIDC issuer" "issuer"="https://example.com/issuer"`, - `"level"=0 "msg"="detected OIDC audience" "audience"="test-audience"`, - `"level"=0 "msg"="detected OIDC CA bundle" "length"=587`, - `"level"=0 "msg"="switching kubeconfig cluster to point at impersonation proxy endpoint" "endpoint"="https://impersonation-proxy-endpoint.test"`, + `"level"=0 "msg"="discovered OIDC issuer" "issuer"="https://example.com/issuer"`, + `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, + `"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`, }, wantStdout: here.Docf(` apiVersion: v1 clusters: - cluster: - certificate-authority-data: dGVzdC1jb25jaWVyZ2UtY2E= + certificate-authority-data: %s server: https://impersonation-proxy-endpoint.test name: pinniped contexts: @@ -862,7 +973,7 @@ func TestGetKubeconfig(t *testing.T) { - --concierge-authenticator-name=test-authenticator - --concierge-authenticator-type=jwt - --concierge-endpoint=https://impersonation-proxy-endpoint.test - - --concierge-ca-bundle-data=dGVzdC1jb25jaWVyZ2UtY2E= + - --concierge-ca-bundle-data=%s - --concierge-mode=ImpersonationProxy - --issuer=https://example.com/issuer - --client-id=pinniped-cli @@ -872,10 +983,14 @@ func TestGetKubeconfig(t *testing.T) { command: '.../path/to/pinniped' env: [] provideClusterInfo: true - `, base64.StdEncoding.EncodeToString(testOIDCCA.Bundle())), + `, + base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), + base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), + base64.StdEncoding.EncodeToString(testOIDCCA.Bundle()), + ), }, { - name: "autodetect impersonation proxy with autodetected JWT authenticator", + name: "autodetect impersonation proxy with autodiscovered JWT authenticator", args: []string{ "--kubeconfig", "./testdata/kubeconfig.yaml", "--skip-validation", @@ -929,12 +1044,13 @@ func TestGetKubeconfig(t *testing.T) { }, wantLogs: []string{ `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, - `"level"=0 "msg"="detected Concierge in impersonation proxy mode" "endpoint"="https://impersonation-proxy-endpoint.test"`, + `"level"=0 "msg"="discovered Concierge operating in impersonation proxy mode"`, + `"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://impersonation-proxy-endpoint.test"`, + `"level"=0 "msg"="discovered Concierge certificate authority bundle" "roots"=0`, `"level"=0 "msg"="discovered JWTAuthenticator" "name"="test-authenticator"`, - `"level"=0 "msg"="detected OIDC issuer" "issuer"="https://example.com/issuer"`, - `"level"=0 "msg"="detected OIDC audience" "audience"="test-audience"`, - `"level"=0 "msg"="detected OIDC CA bundle" "length"=587`, - `"level"=0 "msg"="switching kubeconfig cluster to point at impersonation proxy endpoint" "endpoint"="https://impersonation-proxy-endpoint.test"`, + `"level"=0 "msg"="discovered OIDC issuer" "issuer"="https://example.com/issuer"`, + `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, + `"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`, }, wantStdout: here.Docf(` apiVersion: v1