diff --git a/cmd/pinniped/cmd/kubeconfig.go b/cmd/pinniped/cmd/kubeconfig.go index 1b108c02..8959370f 100644 --- a/cmd/pinniped/cmd/kubeconfig.go +++ b/cmd/pinniped/cmd/kubeconfig.go @@ -73,11 +73,14 @@ type getKubeconfigOIDCParams struct { } type getKubeconfigConciergeParams struct { - disabled bool - namespace string - authenticatorName string - authenticatorType string - apiGroupSuffix string + disabled bool + namespace string + authenticatorName string + authenticatorType string + apiGroupSuffix string + caBundleData string + endpoint string + useImpersonationProxy bool } type getKubeconfigParams struct { @@ -110,6 +113,10 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command { f.StringVar(&flags.concierge.authenticatorName, "concierge-authenticator-name", "", "Concierge authenticator name (default: autodiscover)") f.StringVar(&flags.concierge.apiGroupSuffix, "concierge-api-group-suffix", "pinniped.dev", "Concierge API group suffix") + f.StringVar(&flags.concierge.caBundleData, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the concierge") + f.StringVar(&flags.concierge.endpoint, "concierge-endpoint", "", "API base for the Pinniped concierge endpoint") + f.BoolVar(&flags.concierge.useImpersonationProxy, "use-impersonation-proxy", false, "Whether the concierge cluster uses an impersonation proxy") + f.StringVar(&flags.oidc.issuer, "oidc-issuer", "", "OpenID Connect issuer URL (default: autodiscover)") f.StringVar(&flags.oidc.clientID, "oidc-client-id", "pinniped-cli", "OpenID Connect client ID (default: autodiscover)") f.Uint16Var(&flags.oidc.listenPort, "oidc-listen-port", 0, "TCP port for localhost listener (authorization code flow only)") @@ -162,6 +169,10 @@ func runGetKubeconfig(out io.Writer, deps kubeconfigDeps, flags getKubeconfigPar if err != nil { return fmt.Errorf("could not load --kubeconfig/--kubeconfig-context: %w", err) } + if flags.concierge.useImpersonationProxy { + cluster.CertificateAuthorityData = []byte(flags.concierge.caBundleData) + cluster.Server = flags.concierge.endpoint + } clientset, err := deps.getClientset(clientConfig, flags.concierge.apiGroupSuffix) if err != nil { return fmt.Errorf("could not configure Kubernetes client: %w", err) @@ -266,6 +277,13 @@ func configureConcierge(authenticator metav1.Object, flags *getKubeconfigParams, } } + if flags.concierge.endpoint == "" { + flags.concierge.endpoint = v1Cluster.Server + } + if flags.concierge.caBundleData == "" { + flags.concierge.caBundleData = base64.StdEncoding.EncodeToString(v1Cluster.CertificateAuthorityData) + } + // Append the flags to configure the Concierge credential exchange at runtime. execConfig.Args = append(execConfig.Args, "--enable-concierge", @@ -273,9 +291,14 @@ func configureConcierge(authenticator metav1.Object, flags *getKubeconfigParams, "--concierge-namespace="+flags.concierge.namespace, "--concierge-authenticator-name="+flags.concierge.authenticatorName, "--concierge-authenticator-type="+flags.concierge.authenticatorType, - "--concierge-endpoint="+v1Cluster.Server, - "--concierge-ca-bundle-data="+base64.StdEncoding.EncodeToString(v1Cluster.CertificateAuthorityData), + "--concierge-endpoint="+flags.concierge.endpoint, + "--concierge-ca-bundle-data="+flags.concierge.caBundleData, ) + if flags.concierge.useImpersonationProxy { + execConfig.Args = append(execConfig.Args, + "--use-impersonation-proxy", + ) + } return nil } diff --git a/cmd/pinniped/cmd/kubeconfig_test.go b/cmd/pinniped/cmd/kubeconfig_test.go index cbae7146..07e0d8c1 100644 --- a/cmd/pinniped/cmd/kubeconfig_test.go +++ b/cmd/pinniped/cmd/kubeconfig_test.go @@ -61,6 +61,8 @@ func TestGetKubeconfig(t *testing.T) { --concierge-api-group-suffix string Concierge API group suffix (default "pinniped.dev") --concierge-authenticator-name string Concierge authenticator name (default: autodiscover) --concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt') (default: autodiscover) + --concierge-ca-bundle-data string CA bundle to use when connecting to the concierge + --concierge-endpoint string API base for the Pinniped concierge endpoint --concierge-namespace string Namespace in which the concierge was installed (default "pinniped-concierge") -h, --help help for kubeconfig --kubeconfig string Path to kubeconfig file @@ -76,6 +78,7 @@ func TestGetKubeconfig(t *testing.T) { --oidc-skip-browser During OpenID Connect login, skip opening the browser (just print the URL) --static-token string Instead of doing an OIDC-based login, specify a static token --static-token-env string Instead of doing an OIDC-based login, read a static token from the environment + --use-impersonation-proxy Whether the concierge cluster uses an impersonation proxy `), }, { @@ -506,6 +509,67 @@ func TestGetKubeconfig(t *testing.T) { `, base64.StdEncoding.EncodeToString(testCA.Bundle())), wantAPIGroupSuffix: "tuna.io", }, + { + name: "configure impersonation proxy with autodetected JWT authenticator", + args: []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--concierge-ca-bundle-data", "blah", // TODO make this more realistic, maybe do some validation? + "--concierge-endpoint", "https://impersonation-proxy-endpoint.test", + "--use-impersonation-proxy", + }, + conciergeObjects: []runtime.Object{ + &conciergev1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator", Namespace: "pinniped-concierge"}, + Spec: conciergev1alpha1.JWTAuthenticatorSpec{ + Issuer: "https://example.com/issuer", + Audience: "test-audience", + TLS: &conciergev1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString(testCA.Bundle()), + }, + }, + }, + }, + wantStdout: here.Docf(` + apiVersion: v1 + clusters: + - cluster: + certificate-authority-data: YmxhaA== + server: https://impersonation-proxy-endpoint.test + name: pinniped + contexts: + - context: + cluster: pinniped + user: pinniped + name: pinniped + current-context: pinniped + kind: Config + preferences: {} + users: + - name: pinniped + user: + exec: + apiVersion: client.authentication.k8s.io/v1beta1 + args: + - login + - oidc + - --enable-concierge + - --concierge-api-group-suffix=pinniped.dev + - --concierge-namespace=pinniped-concierge + - --concierge-authenticator-name=test-authenticator + - --concierge-authenticator-type=jwt + - --concierge-endpoint=https://impersonation-proxy-endpoint.test + - --concierge-ca-bundle-data=blah + - --use-impersonation-proxy + - --issuer=https://example.com/issuer + - --client-id=pinniped-cli + - --scopes=offline_access,openid,pinniped:request-audience + - --ca-bundle-data=%s + - --request-audience=test-audience + command: '.../path/to/pinniped' + env: [] + provideClusterInfo: true + `, base64.StdEncoding.EncodeToString(testCA.Bundle())), + }, } for _, tt := range tests { tt := tt diff --git a/cmd/pinniped/cmd/login_oidc.go b/cmd/pinniped/cmd/login_oidc.go index 7b5f53f3..9d986530 100644 --- a/cmd/pinniped/cmd/login_oidc.go +++ b/cmd/pinniped/cmd/login_oidc.go @@ -16,6 +16,11 @@ import ( "path/filepath" "time" + corev1 "k8s.io/api/core/v1" + + authenticationv1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/authentication/v1alpha1" + loginv1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/login/v1alpha1" + "github.com/coreos/go-oidc/v3/oidc" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -65,6 +70,7 @@ type oidcLoginFlags struct { conciergeEndpoint string conciergeCABundle string conciergeAPIGroupSuffix string + useImpersonationProxy bool } func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command { @@ -94,6 +100,7 @@ func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command { cmd.Flags().StringVar(&flags.conciergeEndpoint, "concierge-endpoint", "", "API base for the Pinniped concierge endpoint") cmd.Flags().StringVar(&flags.conciergeCABundle, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the concierge") cmd.Flags().StringVar(&flags.conciergeAPIGroupSuffix, "concierge-api-group-suffix", "pinniped.dev", "Concierge API group suffix") + cmd.Flags().BoolVar(&flags.useImpersonationProxy, "use-impersonation-proxy", false, "Whether the concierge cluster uses an impersonation proxy") mustMarkHidden(&cmd, "debug-session-cache") mustMarkRequired(&cmd, "issuer") @@ -171,12 +178,21 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - if concierge != nil { + // do a credential exchange request, unless impersonation proxy is configured + if concierge != nil && !flags.useImpersonationProxy { cred, err = deps.exchangeToken(ctx, concierge, token.IDToken.Token) if err != nil { return fmt.Errorf("could not complete concierge credential exchange: %w", err) } } + if concierge != nil && flags.useImpersonationProxy { + // TODO add the right header??? + req, err := execCredentialForImpersonationProxy(token, flags) + if err != nil { + return err + } + return json.NewEncoder(cmd.OutOrStdout()).Encode(req) + } return json.NewEncoder(cmd.OutOrStdout()).Encode(cred) } func makeClient(caBundlePaths []string, caBundleData []string) (*http.Client, error) { @@ -238,3 +254,36 @@ func mustGetConfigDir() string { } return filepath.Join(home, ".config", xdgAppName) } + +func execCredentialForImpersonationProxy(token *oidctypes.Token, flags oidcLoginFlags) (*clientauthv1beta1.ExecCredential, error) { + reqJSON, err := json.Marshal(&loginv1alpha1.TokenCredentialRequest{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: flags.conciergeNamespace, + }, + TypeMeta: metav1.TypeMeta{ + Kind: "TokenCredentialRequest", + APIVersion: loginv1alpha1.GroupName + "/v1alpha1", + }, + Spec: loginv1alpha1.TokenCredentialRequestSpec{ + Token: token.AccessToken.Token, // TODO + Authenticator: corev1.TypedLocalObjectReference{ + APIGroup: &authenticationv1alpha1.SchemeGroupVersion.Group, + Kind: os.Getenv(flags.conciergeAuthenticatorType), + Name: os.Getenv(flags.conciergeAuthenticatorName), + }, + }, + }) + if err != nil { + return nil, err + } + encodedToken := base64.RawURLEncoding.EncodeToString(reqJSON) + return &clientauthv1beta1.ExecCredential{ + TypeMeta: metav1.TypeMeta{ + Kind: "ExecCredential", + APIVersion: "client.authentication.k8s.io/v1beta1", + }, + Status: &clientauthv1beta1.ExecCredentialStatus{ + Token: encodedToken, + }, + }, nil +} diff --git a/cmd/pinniped/cmd/login_oidc_test.go b/cmd/pinniped/cmd/login_oidc_test.go index 41607e4e..70c81924 100644 --- a/cmd/pinniped/cmd/login_oidc_test.go +++ b/cmd/pinniped/cmd/login_oidc_test.go @@ -74,6 +74,7 @@ func TestLoginOIDCCommand(t *testing.T) { --scopes strings OIDC scopes to request during login (default [offline_access,openid,pinniped:request-audience]) --session-cache string Path to session cache file (default "` + cfgDir + `/sessions.yaml") --skip-browser Skip opening the browser (just print the URL) + --use-impersonation-proxy Whether the concierge cluster uses an impersonation proxy `), }, {