Impersonation proxy cli arguments

This commit is contained in:
Margo Crawford 2021-01-26 11:39:42 -08:00
parent 64aff7b983
commit 07b7b743b4
4 changed files with 145 additions and 8 deletions

View File

@ -78,6 +78,9 @@ type getKubeconfigConciergeParams struct {
authenticatorName string authenticatorName string
authenticatorType string authenticatorType string
apiGroupSuffix string apiGroupSuffix string
caBundleData string
endpoint string
useImpersonationProxy bool
} }
type getKubeconfigParams struct { 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.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.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.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.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)") 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 { if err != nil {
return fmt.Errorf("could not load --kubeconfig/--kubeconfig-context: %w", err) 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) clientset, err := deps.getClientset(clientConfig, flags.concierge.apiGroupSuffix)
if err != nil { if err != nil {
return fmt.Errorf("could not configure Kubernetes client: %w", err) 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. // Append the flags to configure the Concierge credential exchange at runtime.
execConfig.Args = append(execConfig.Args, execConfig.Args = append(execConfig.Args,
"--enable-concierge", "--enable-concierge",
@ -273,9 +291,14 @@ func configureConcierge(authenticator metav1.Object, flags *getKubeconfigParams,
"--concierge-namespace="+flags.concierge.namespace, "--concierge-namespace="+flags.concierge.namespace,
"--concierge-authenticator-name="+flags.concierge.authenticatorName, "--concierge-authenticator-name="+flags.concierge.authenticatorName,
"--concierge-authenticator-type="+flags.concierge.authenticatorType, "--concierge-authenticator-type="+flags.concierge.authenticatorType,
"--concierge-endpoint="+v1Cluster.Server, "--concierge-endpoint="+flags.concierge.endpoint,
"--concierge-ca-bundle-data="+base64.StdEncoding.EncodeToString(v1Cluster.CertificateAuthorityData), "--concierge-ca-bundle-data="+flags.concierge.caBundleData,
) )
if flags.concierge.useImpersonationProxy {
execConfig.Args = append(execConfig.Args,
"--use-impersonation-proxy",
)
}
return nil return nil
} }

View File

@ -61,6 +61,8 @@ func TestGetKubeconfig(t *testing.T) {
--concierge-api-group-suffix string Concierge API group suffix (default "pinniped.dev") --concierge-api-group-suffix string Concierge API group suffix (default "pinniped.dev")
--concierge-authenticator-name string Concierge authenticator name (default: autodiscover) --concierge-authenticator-name string Concierge authenticator name (default: autodiscover)
--concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt') (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") --concierge-namespace string Namespace in which the concierge was installed (default "pinniped-concierge")
-h, --help help for kubeconfig -h, --help help for kubeconfig
--kubeconfig string Path to kubeconfig file --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) --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 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 --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())), `, base64.StdEncoding.EncodeToString(testCA.Bundle())),
wantAPIGroupSuffix: "tuna.io", 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 { for _, tt := range tests {
tt := tt tt := tt

View File

@ -16,6 +16,11 @@ import (
"path/filepath" "path/filepath"
"time" "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/coreos/go-oidc/v3/oidc"
"github.com/spf13/cobra" "github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -65,6 +70,7 @@ type oidcLoginFlags struct {
conciergeEndpoint string conciergeEndpoint string
conciergeCABundle string conciergeCABundle string
conciergeAPIGroupSuffix string conciergeAPIGroupSuffix string
useImpersonationProxy bool
} }
func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command { 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.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.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().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") mustMarkHidden(&cmd, "debug-session-cache")
mustMarkRequired(&cmd, "issuer") 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) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() 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) cred, err = deps.exchangeToken(ctx, concierge, token.IDToken.Token)
if err != nil { if err != nil {
return fmt.Errorf("could not complete concierge credential exchange: %w", err) 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) return json.NewEncoder(cmd.OutOrStdout()).Encode(cred)
} }
func makeClient(caBundlePaths []string, caBundleData []string) (*http.Client, error) { func makeClient(caBundlePaths []string, caBundleData []string) (*http.Client, error) {
@ -238,3 +254,36 @@ func mustGetConfigDir() string {
} }
return filepath.Join(home, ".config", xdgAppName) 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
}

View File

@ -74,6 +74,7 @@ func TestLoginOIDCCommand(t *testing.T) {
--scopes strings OIDC scopes to request during login (default [offline_access,openid,pinniped:request-audience]) --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") --session-cache string Path to session cache file (default "` + cfgDir + `/sessions.yaml")
--skip-browser Skip opening the browser (just print the URL) --skip-browser Skip opening the browser (just print the URL)
--use-impersonation-proxy Whether the concierge cluster uses an impersonation proxy
`), `),
}, },
{ {