2021-01-07 22:58:09 +00:00
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
2020-12-15 00:38:19 +00:00
// SPDX-License-Identifier: Apache-2.0
package cmd
import (
"context"
2021-03-05 22:35:42 +00:00
"crypto/tls"
"crypto/x509"
2020-12-15 00:38:19 +00:00
"encoding/base64"
"fmt"
"io"
2021-03-05 21:52:17 +00:00
"log"
2021-03-05 22:35:42 +00:00
"net/http"
2020-12-15 00:38:19 +00:00
"os"
"strconv"
"strings"
"time"
2021-01-20 17:54:44 +00:00
"github.com/coreos/go-oidc/v3/oidc"
2021-03-05 21:52:17 +00:00
"github.com/go-logr/logr"
"github.com/go-logr/stdr"
2020-12-15 00:38:19 +00:00
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
2021-03-05 21:52:17 +00:00
_ "k8s.io/client-go/plugin/pkg/client/auth" // Adds handlers for various dynamic auth plugins in client-go
2020-12-15 00:38:19 +00:00
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
2021-02-16 19:00:08 +00:00
conciergev1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
2021-02-25 20:16:40 +00:00
configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
2021-02-16 19:00:08 +00:00
conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
2021-01-13 01:27:41 +00:00
"go.pinniped.dev/internal/groupsuffix"
2020-12-15 00:38:19 +00:00
)
type kubeconfigDeps struct {
getPathToSelf func ( ) ( string , error )
2021-03-04 18:34:59 +00:00
getClientset getConciergeClientsetFunc
2021-03-05 21:52:17 +00:00
log logr . Logger
2020-12-15 00:38:19 +00:00
}
func kubeconfigRealDeps ( ) kubeconfigDeps {
return kubeconfigDeps {
getPathToSelf : os . Executable ,
2021-03-04 18:34:59 +00:00
getClientset : getRealConciergeClientset ,
2021-03-18 14:36:28 +00:00
log : stdr . New ( log . New ( os . Stderr , "" , 0 ) ) ,
2020-12-15 00:38:19 +00:00
}
}
//nolint: gochecknoinits
func init ( ) {
getCmd . AddCommand ( kubeconfigCommand ( kubeconfigRealDeps ( ) ) )
}
type getKubeconfigOIDCParams struct {
issuer string
clientID string
listenPort uint16
scopes [ ] string
skipBrowser bool
sessionCachePath string
debugSessionCache bool
2021-03-08 20:33:38 +00:00
caBundle caBundleFlag
2020-12-15 00:38:19 +00:00
requestAudience string
}
type getKubeconfigConciergeParams struct {
2021-02-25 17:31:11 +00:00
disabled bool
2021-02-25 20:16:40 +00:00
credentialIssuer string
2021-02-25 17:31:11 +00:00
authenticatorName string
authenticatorType string
apiGroupSuffix string
2021-03-08 20:33:38 +00:00
caBundle caBundleFlag
2021-02-25 17:31:11 +00:00
endpoint string
2021-03-08 20:33:38 +00:00
mode conciergeModeFlag
2021-03-09 20:48:16 +00:00
skipWait bool
2020-12-15 00:38:19 +00:00
}
type getKubeconfigParams struct {
kubeconfigPath string
kubeconfigContextOverride string
2021-03-05 22:35:42 +00:00
skipValidate bool
timeout time . Duration
2021-03-05 21:53:30 +00:00
outputPath string
2020-12-15 00:38:19 +00:00
staticToken string
staticTokenEnvName string
oidc getKubeconfigOIDCParams
concierge getKubeconfigConciergeParams
2021-04-05 16:41:06 +00:00
generatedNameSuffix string
2021-04-08 21:00:21 +00:00
credentialCachePath string
credentialCachePathSet bool
2020-12-15 00:38:19 +00:00
}
func kubeconfigCommand ( deps kubeconfigDeps ) * cobra . Command {
var (
2021-02-09 18:59:32 +00:00
cmd = & cobra . Command {
2020-12-15 00:38:19 +00:00
Args : cobra . NoArgs ,
Use : "kubeconfig" ,
Short : "Generate a Pinniped-based kubeconfig for a cluster" ,
SilenceUsage : true ,
}
2021-02-09 18:59:32 +00:00
flags getKubeconfigParams
namespace string // unused now
2020-12-15 00:38:19 +00:00
)
f := cmd . Flags ( )
f . StringVar ( & flags . staticToken , "static-token" , "" , "Instead of doing an OIDC-based login, specify a static token" )
f . StringVar ( & flags . staticTokenEnvName , "static-token-env" , "" , "Instead of doing an OIDC-based login, read a static token from the environment" )
2021-02-25 17:31:11 +00:00
f . BoolVar ( & flags . concierge . disabled , "no-concierge" , false , "Generate a configuration which does not use the Concierge, but sends the credential to the cluster directly" )
f . StringVar ( & namespace , "concierge-namespace" , "pinniped-concierge" , "Namespace in which the Concierge was installed" )
2021-02-25 20:16:40 +00:00
f . StringVar ( & flags . concierge . credentialIssuer , "concierge-credential-issuer" , "" , "Concierge CredentialIssuer object to use for autodiscovery (default: autodiscover)" )
2020-12-15 00:38:19 +00:00
f . StringVar ( & flags . concierge . authenticatorType , "concierge-authenticator-type" , "" , "Concierge authenticator type (e.g., 'webhook', 'jwt') (default: autodiscover)" )
f . StringVar ( & flags . concierge . authenticatorName , "concierge-authenticator-name" , "" , "Concierge authenticator name (default: autodiscover)" )
2021-02-19 18:21:10 +00:00
f . StringVar ( & flags . concierge . apiGroupSuffix , "concierge-api-group-suffix" , groupsuffix . PinnipedDefaultSuffix , "Concierge API group suffix" )
2021-03-09 20:48:16 +00:00
f . BoolVar ( & flags . concierge . skipWait , "concierge-skip-wait" , false , "Skip waiting for any pending Concierge strategies to become ready (default: false)" )
2020-12-15 00:38:19 +00:00
2021-03-08 20:31:13 +00:00
f . Var ( & flags . concierge . caBundle , "concierge-ca-bundle" , "Path to TLS certificate authority bundle (PEM format, optional, can be repeated) to use when connecting to the Concierge" )
2021-02-25 17:31:11 +00:00
f . StringVar ( & flags . concierge . endpoint , "concierge-endpoint" , "" , "API base for the Concierge endpoint" )
f . Var ( & flags . concierge . mode , "concierge-mode" , "Concierge mode of operation" )
2021-01-26 19:39:42 +00:00
2020-12-15 00:38:19 +00:00
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)" )
2020-12-16 03:59:57 +00:00
f . StringSliceVar ( & flags . oidc . scopes , "oidc-scopes" , [ ] string { oidc . ScopeOfflineAccess , oidc . ScopeOpenID , "pinniped:request-audience" } , "OpenID Connect scopes to request during login" )
2020-12-15 00:38:19 +00:00
f . BoolVar ( & flags . oidc . skipBrowser , "oidc-skip-browser" , false , "During OpenID Connect login, skip opening the browser (just print the URL)" )
f . StringVar ( & flags . oidc . sessionCachePath , "oidc-session-cache" , "" , "Path to OpenID Connect session cache file" )
2021-03-08 20:31:13 +00:00
f . Var ( & flags . oidc . caBundle , "oidc-ca-bundle" , "Path to TLS certificate authority bundle (PEM format, optional, can be repeated)" )
2020-12-15 00:38:19 +00:00
f . BoolVar ( & flags . oidc . debugSessionCache , "oidc-debug-session-cache" , false , "Print debug logs related to the OpenID Connect session cache" )
2020-12-16 03:59:57 +00:00
f . StringVar ( & flags . oidc . requestAudience , "oidc-request-audience" , "" , "Request a token with an alternate audience using RFC8693 token exchange" )
2020-12-15 00:38:19 +00:00
f . StringVar ( & flags . kubeconfigPath , "kubeconfig" , os . Getenv ( "KUBECONFIG" ) , "Path to kubeconfig file" )
f . StringVar ( & flags . kubeconfigContextOverride , "kubeconfig-context" , "" , "Kubeconfig context name (default: current active context)" )
2021-03-05 22:35:42 +00:00
f . BoolVar ( & flags . skipValidate , "skip-validation" , false , "Skip final validation of the kubeconfig (default: false)" )
f . DurationVar ( & flags . timeout , "timeout" , 10 * time . Minute , "Timeout for autodiscovery and validation" )
2021-03-05 21:53:30 +00:00
f . StringVarP ( & flags . outputPath , "output" , "o" , "" , "Output file path (default: stdout)" )
2021-04-05 16:41:06 +00:00
f . StringVar ( & flags . generatedNameSuffix , "generated-name-suffix" , "-pinniped" , "Suffix to append to generated cluster, context, user kubeconfig entries" )
2021-04-08 21:00:21 +00:00
f . StringVar ( & flags . credentialCachePath , "credential-cache" , "" , "Path to cluster-specific credentials cache" )
2021-02-09 18:59:32 +00:00
mustMarkHidden ( cmd , "oidc-debug-session-cache" )
mustMarkDeprecated ( cmd , "concierge-namespace" , "not needed anymore" )
mustMarkHidden ( cmd , "concierge-namespace" )
2020-12-15 00:38:19 +00:00
2021-03-05 21:53:30 +00:00
cmd . RunE = func ( cmd * cobra . Command , args [ ] string ) error {
if flags . outputPath != "" {
out , err := os . Create ( flags . outputPath )
if err != nil {
return fmt . Errorf ( "could not open output file: %w" , err )
}
defer func ( ) { _ = out . Close ( ) } ( )
cmd . SetOut ( out )
}
2021-04-08 21:00:21 +00:00
flags . credentialCachePathSet = cmd . Flags ( ) . Changed ( "credential-cache" )
2021-03-05 22:35:42 +00:00
return runGetKubeconfig ( cmd . Context ( ) , cmd . OutOrStdout ( ) , deps , flags )
2021-03-05 21:53:30 +00:00
}
2021-02-09 18:59:32 +00:00
return cmd
2020-12-15 00:38:19 +00:00
}
2021-01-08 22:13:04 +00:00
//nolint:funlen
2021-03-05 22:35:42 +00:00
func runGetKubeconfig ( ctx context . Context , out io . Writer , deps kubeconfigDeps , flags getKubeconfigParams ) error {
ctx , cancel := context . WithTimeout ( ctx , flags . timeout )
defer cancel ( )
2021-01-13 01:27:41 +00:00
// Validate api group suffix and immediately return an error if it is invalid.
if err := groupsuffix . Validate ( flags . concierge . apiGroupSuffix ) ; err != nil {
2021-03-11 22:11:46 +00:00
return fmt . Errorf ( "invalid API group suffix: %w" , err )
2021-01-13 01:27:41 +00:00
}
2020-12-15 00:38:19 +00:00
execConfig := clientcmdapi . ExecConfig {
APIVersion : clientauthenticationv1beta1 . SchemeGroupVersion . String ( ) ,
Args : [ ] string { } ,
Env : [ ] clientcmdapi . ExecEnvVar { } ,
}
var err error
execConfig . Command , err = deps . getPathToSelf ( )
if err != nil {
return fmt . Errorf ( "could not determine the Pinniped executable path: %w" , err )
}
2021-01-08 18:21:59 +00:00
execConfig . ProvideClusterInfo = true
2020-12-15 00:38:19 +00:00
clientConfig := newClientConfig ( flags . kubeconfigPath , flags . kubeconfigContextOverride )
currentKubeConfig , err := clientConfig . RawConfig ( )
if err != nil {
return fmt . Errorf ( "could not load --kubeconfig: %w" , err )
}
2021-04-05 16:41:06 +00:00
currentKubeconfigNames , err := getCurrentContext ( currentKubeConfig , flags )
2020-12-15 00:38:19 +00:00
if err != nil {
return fmt . Errorf ( "could not load --kubeconfig/--kubeconfig-context: %w" , err )
}
2021-04-05 16:41:06 +00:00
cluster := currentKubeConfig . Clusters [ currentKubeconfigNames . ClusterName ]
2021-01-13 01:27:41 +00:00
clientset , err := deps . getClientset ( clientConfig , flags . concierge . apiGroupSuffix )
2020-12-15 00:38:19 +00:00
if err != nil {
return fmt . Errorf ( "could not configure Kubernetes client: %w" , err )
}
2021-04-05 16:41:06 +00:00
// Generate the new context/cluster/user names by appending the --generated-name-suffix to the original values.
newKubeconfigNames := & kubeconfigNames {
ContextName : currentKubeconfigNames . ContextName + flags . generatedNameSuffix ,
UserName : currentKubeconfigNames . UserName + flags . generatedNameSuffix ,
ClusterName : currentKubeconfigNames . ClusterName + flags . generatedNameSuffix ,
}
2020-12-15 00:38:19 +00:00
if ! flags . concierge . disabled {
2021-03-09 21:16:46 +00:00
credentialIssuer , err := waitForCredentialIssuer ( ctx , clientset , flags , deps )
2021-02-25 20:16:40 +00:00
if err != nil {
return err
}
2020-12-15 00:38:19 +00:00
authenticator , err := lookupAuthenticator (
clientset ,
flags . concierge . authenticatorType ,
flags . concierge . authenticatorName ,
2021-03-05 21:52:17 +00:00
deps . log ,
2020-12-15 00:38:19 +00:00
)
if err != nil {
return err
}
2021-03-08 20:31:13 +00:00
if err := discoverConciergeParams ( credentialIssuer , & flags , cluster , deps . log ) ; err != nil {
return err
}
if err := discoverAuthenticatorParams ( authenticator , & flags , deps . log ) ; err != nil {
2020-12-15 00:38:19 +00:00
return err
}
2021-03-08 20:31:13 +00:00
// Append the flags to configure the Concierge credential exchange at runtime.
execConfig . Args = append ( execConfig . Args ,
"--enable-concierge" ,
"--concierge-api-group-suffix=" + flags . concierge . apiGroupSuffix ,
"--concierge-authenticator-name=" + flags . concierge . authenticatorName ,
"--concierge-authenticator-type=" + flags . concierge . authenticatorType ,
"--concierge-endpoint=" + flags . concierge . endpoint ,
"--concierge-ca-bundle-data=" + base64 . StdEncoding . EncodeToString ( flags . concierge . caBundle ) ,
)
// Point kubectl at the concierge endpoint.
cluster . Server = flags . concierge . endpoint
cluster . CertificateAuthorityData = flags . concierge . caBundle
2020-12-15 00:38:19 +00:00
}
2021-04-08 21:00:21 +00:00
// If --credential-cache is set, pass it through.
if flags . credentialCachePathSet {
execConfig . Args = append ( execConfig . Args , "--credential-cache=" + flags . credentialCachePath )
}
2020-12-15 00:38:19 +00:00
// If one of the --static-* flags was passed, output a config that runs `pinniped login static`.
if flags . staticToken != "" || flags . staticTokenEnvName != "" {
if flags . staticToken != "" && flags . staticTokenEnvName != "" {
return fmt . Errorf ( "only one of --static-token and --static-token-env can be specified" )
}
execConfig . Args = append ( [ ] string { "login" , "static" } , execConfig . Args ... )
if flags . staticToken != "" {
execConfig . Args = append ( execConfig . Args , "--token=" + flags . staticToken )
}
if flags . staticTokenEnvName != "" {
execConfig . Args = append ( execConfig . Args , "--token-env=" + flags . staticTokenEnvName )
}
2021-03-05 22:35:42 +00:00
2021-04-05 16:41:06 +00:00
kubeconfig := newExecKubeconfig ( cluster , & execConfig , newKubeconfigNames )
2021-03-05 22:35:42 +00:00
if err := validateKubeconfig ( ctx , flags , kubeconfig , deps . log ) ; err != nil {
return err
}
return writeConfigAsYAML ( out , kubeconfig )
2020-12-15 00:38:19 +00:00
}
// Otherwise continue to parse the OIDC-related flags and output a config that runs `pinniped login oidc`.
execConfig . Args = append ( [ ] string { "login" , "oidc" } , execConfig . Args ... )
if flags . oidc . issuer == "" {
2021-02-25 20:16:40 +00:00
return fmt . Errorf ( "could not autodiscover --oidc-issuer and none was provided" )
2020-12-15 00:38:19 +00:00
}
execConfig . Args = append ( execConfig . Args ,
"--issuer=" + flags . oidc . issuer ,
"--client-id=" + flags . oidc . clientID ,
"--scopes=" + strings . Join ( flags . oidc . scopes , "," ) ,
)
if flags . oidc . skipBrowser {
execConfig . Args = append ( execConfig . Args , "--skip-browser" )
}
if flags . oidc . listenPort != 0 {
execConfig . Args = append ( execConfig . Args , "--listen-port=" + strconv . Itoa ( int ( flags . oidc . listenPort ) ) )
}
2021-03-08 20:31:13 +00:00
if len ( flags . oidc . caBundle ) != 0 {
execConfig . Args = append ( execConfig . Args , "--ca-bundle-data=" + base64 . StdEncoding . EncodeToString ( flags . oidc . caBundle ) )
2020-12-15 00:38:19 +00:00
}
if flags . oidc . sessionCachePath != "" {
execConfig . Args = append ( execConfig . Args , "--session-cache=" + flags . oidc . sessionCachePath )
}
if flags . oidc . debugSessionCache {
execConfig . Args = append ( execConfig . Args , "--debug-session-cache" )
}
if flags . oidc . requestAudience != "" {
execConfig . Args = append ( execConfig . Args , "--request-audience=" + flags . oidc . requestAudience )
}
2021-04-05 16:41:06 +00:00
kubeconfig := newExecKubeconfig ( cluster , & execConfig , newKubeconfigNames )
2021-03-05 22:35:42 +00:00
if err := validateKubeconfig ( ctx , flags , kubeconfig , deps . log ) ; err != nil {
return err
}
return writeConfigAsYAML ( out , kubeconfig )
2020-12-15 00:38:19 +00:00
}
2021-04-05 16:41:06 +00:00
type kubeconfigNames struct { ContextName , UserName , ClusterName string }
func getCurrentContext ( currentKubeConfig clientcmdapi . Config , flags getKubeconfigParams ) ( * kubeconfigNames , error ) {
contextName := currentKubeConfig . CurrentContext
if flags . kubeconfigContextOverride != "" {
contextName = flags . kubeconfigContextOverride
}
ctx := currentKubeConfig . Contexts [ contextName ]
if ctx == nil {
return nil , fmt . Errorf ( "no such context %q" , contextName )
}
if _ , exists := currentKubeConfig . Clusters [ ctx . Cluster ] ; ! exists {
return nil , fmt . Errorf ( "no such cluster %q" , ctx . Cluster )
}
if _ , exists := currentKubeConfig . AuthInfos [ ctx . AuthInfo ] ; ! exists {
return nil , fmt . Errorf ( "no such user %q" , ctx . AuthInfo )
}
return & kubeconfigNames { ContextName : contextName , UserName : ctx . AuthInfo , ClusterName : ctx . Cluster } , nil
}
2021-03-09 21:16:46 +00:00
func waitForCredentialIssuer ( ctx context . Context , clientset conciergeclientset . Interface , flags getKubeconfigParams , deps kubeconfigDeps ) ( * configv1alpha1 . CredentialIssuer , error ) {
credentialIssuer , err := lookupCredentialIssuer ( clientset , flags . concierge . credentialIssuer , deps . log )
if err != nil {
return nil , err
}
if ! flags . concierge . skipWait {
ticker := time . NewTicker ( 2 * time . Second )
defer ticker . Stop ( )
deadline , _ := ctx . Deadline ( )
attempts := 1
for {
if ! hasPendingStrategy ( credentialIssuer ) {
break
}
2021-03-09 22:56:53 +00:00
logStrategies ( credentialIssuer , deps . log )
2021-03-09 21:16:46 +00:00
deps . log . Info ( "waiting for CredentialIssuer pending strategies to finish" ,
"attempts" , attempts ,
"remaining" , time . Until ( deadline ) . Round ( time . Second ) . String ( ) ,
)
select {
case <- ctx . Done ( ) :
return nil , ctx . Err ( )
case <- ticker . C :
credentialIssuer , err = lookupCredentialIssuer ( clientset , flags . concierge . credentialIssuer , deps . log )
if err != nil {
return nil , err
}
}
}
}
return credentialIssuer , nil
}
2021-03-08 20:31:13 +00:00
func discoverConciergeParams ( credentialIssuer * configv1alpha1 . CredentialIssuer , flags * getKubeconfigParams , v1Cluster * clientcmdapi . Cluster , log logr . Logger ) error {
2021-02-25 20:16:40 +00:00
// Autodiscover the --concierge-mode.
2021-03-08 17:43:56 +00:00
frontend , err := getConciergeFrontend ( credentialIssuer , flags . concierge . mode )
if err != nil {
2021-03-09 22:56:53 +00:00
logStrategies ( credentialIssuer , log )
2021-03-08 17:43:56 +00:00
return err
}
// 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
2021-03-02 22:06:19 +00:00
}
2021-03-08 17:43:56 +00:00
log . Info ( "discovered Concierge endpoint" , "endpoint" , flags . concierge . endpoint )
}
2021-03-08 20:31:13 +00:00
// Auto-set --concierge-ca-bundle if it wasn't explicitly set..
if len ( flags . concierge . caBundle ) == 0 {
2021-03-08 17:43:56 +00:00
switch frontend . Type {
case configv1alpha1 . TokenCredentialRequestAPIFrontendType :
2021-03-08 20:31:13 +00:00
flags . concierge . caBundle = v1Cluster . CertificateAuthorityData
2021-03-08 17:43:56 +00:00
case configv1alpha1 . ImpersonationProxyFrontendType :
2021-03-08 20:31:13 +00:00
data , err := base64 . StdEncoding . DecodeString ( frontend . ImpersonationProxyInfo . CertificateAuthorityData )
2021-03-08 17:43:56 +00:00
if err != nil {
return fmt . Errorf ( "autodiscovered Concierge CA bundle is invalid: %w" , err )
2021-03-02 22:06:19 +00:00
}
2021-03-08 20:31:13 +00:00
flags . concierge . caBundle = data
2021-02-25 20:16:40 +00:00
}
2021-03-08 20:31:13 +00:00
log . Info ( "discovered Concierge certificate authority bundle" , "roots" , countCACerts ( flags . concierge . caBundle ) )
2021-02-25 17:31:11 +00:00
}
2021-03-08 20:31:13 +00:00
return nil
}
2021-02-25 17:31:11 +00:00
2021-03-09 22:56:53 +00:00
func logStrategies ( credentialIssuer * configv1alpha1 . CredentialIssuer , log logr . Logger ) {
for _ , strategy := range credentialIssuer . Status . Strategies {
log . Info ( "found CredentialIssuer strategy" ,
"type" , strategy . Type ,
"status" , strategy . Status ,
"reason" , strategy . Reason ,
"message" , strategy . Message ,
)
}
}
2021-03-08 20:31:13 +00:00
func discoverAuthenticatorParams ( authenticator metav1 . Object , flags * getKubeconfigParams , log logr . Logger ) error {
2020-12-15 00:38:19 +00:00
switch auth := authenticator . ( type ) {
case * conciergev1alpha1 . WebhookAuthenticator :
// If the --concierge-authenticator-type/--concierge-authenticator-name flags were not set explicitly, set
// them to point at the discovered WebhookAuthenticator.
if flags . concierge . authenticatorType == "" && flags . concierge . authenticatorName == "" {
2021-03-05 21:52:17 +00:00
log . Info ( "discovered WebhookAuthenticator" , "name" , auth . Name )
2020-12-15 00:38:19 +00:00
flags . concierge . authenticatorType = "webhook"
flags . concierge . authenticatorName = auth . Name
}
case * conciergev1alpha1 . JWTAuthenticator :
// If the --concierge-authenticator-type/--concierge-authenticator-name flags were not set explicitly, set
// them to point at the discovered JWTAuthenticator.
if flags . concierge . authenticatorType == "" && flags . concierge . authenticatorName == "" {
2021-03-05 21:52:17 +00:00
log . Info ( "discovered JWTAuthenticator" , "name" , auth . Name )
2020-12-15 00:38:19 +00:00
flags . concierge . authenticatorType = "jwt"
flags . concierge . authenticatorName = auth . Name
}
// If the --oidc-issuer flag was not set explicitly, default it to the spec.issuer field of the JWTAuthenticator.
if flags . oidc . issuer == "" {
2021-03-08 17:43:56 +00:00
log . Info ( "discovered OIDC issuer" , "issuer" , auth . Spec . Issuer )
2020-12-15 00:38:19 +00:00
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 == "" {
2021-03-08 17:43:56 +00:00
log . Info ( "discovered OIDC audience" , "audience" , auth . Spec . Audience )
2020-12-15 00:38:19 +00:00
flags . oidc . requestAudience = auth . Spec . Audience
}
// If the --oidc-ca-bundle flags was not set explicitly, default it to the
// spec.tls.certificateAuthorityData field of the JWTAuthenticator.
2021-03-08 20:31:13 +00:00
if len ( flags . oidc . caBundle ) == 0 && auth . Spec . TLS != nil && auth . Spec . TLS . CertificateAuthorityData != "" {
2020-12-15 00:38:19 +00:00
decoded , err := base64 . StdEncoding . DecodeString ( auth . Spec . TLS . CertificateAuthorityData )
if err != nil {
2021-02-09 18:59:32 +00:00
return fmt . Errorf ( "tried to autodiscover --oidc-ca-bundle, but JWTAuthenticator %s has invalid spec.tls.certificateAuthorityData: %w" , auth . Name , err )
2020-12-15 00:38:19 +00:00
}
2021-03-08 17:43:56 +00:00
log . Info ( "discovered OIDC CA bundle" , "roots" , countCACerts ( decoded ) )
2021-03-08 20:31:13 +00:00
flags . oidc . caBundle = decoded
2020-12-15 00:38:19 +00:00
}
}
2021-03-08 17:43:56 +00:00
return nil
}
2021-03-08 20:33:38 +00:00
func getConciergeFrontend ( credentialIssuer * configv1alpha1 . CredentialIssuer , mode conciergeModeFlag ) ( * configv1alpha1 . CredentialIssuerFrontend , error ) {
2021-03-08 17:43:56 +00:00
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
2021-02-25 20:16:40 +00:00
}
2021-03-08 17:43:56 +00:00
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 ( ) )
2020-12-15 00:38:19 +00:00
}
2021-04-05 16:41:06 +00:00
func newExecKubeconfig ( cluster * clientcmdapi . Cluster , execConfig * clientcmdapi . ExecConfig , newNames * kubeconfigNames ) clientcmdapi . Config {
2020-12-15 00:38:19 +00:00
return clientcmdapi . Config {
Kind : "Config" ,
APIVersion : clientcmdapi . SchemeGroupVersion . Version ,
2021-04-05 16:41:06 +00:00
Clusters : map [ string ] * clientcmdapi . Cluster { newNames . ClusterName : cluster } ,
AuthInfos : map [ string ] * clientcmdapi . AuthInfo { newNames . UserName : { Exec : execConfig } } ,
Contexts : map [ string ] * clientcmdapi . Context { newNames . ContextName : { Cluster : newNames . ClusterName , AuthInfo : newNames . UserName } } ,
CurrentContext : newNames . ContextName ,
2020-12-15 00:38:19 +00:00
}
}
2021-03-05 21:52:17 +00:00
func lookupCredentialIssuer ( clientset conciergeclientset . Interface , name string , log logr . Logger ) ( * configv1alpha1 . CredentialIssuer , error ) {
2021-02-25 20:16:40 +00:00
ctx , cancelFunc := context . WithTimeout ( context . Background ( ) , time . Second * 20 )
defer cancelFunc ( )
// If the name is specified, get that object.
if name != "" {
return clientset . ConfigV1alpha1 ( ) . CredentialIssuers ( ) . Get ( ctx , name , metav1 . GetOptions { } )
}
// Otherwise list all the available CredentialIssuers and hope there's just a single one
results , err := clientset . ConfigV1alpha1 ( ) . CredentialIssuers ( ) . List ( ctx , metav1 . ListOptions { } )
if err != nil {
return nil , fmt . Errorf ( "failed to list CredentialIssuer objects for autodiscovery: %w" , err )
}
if len ( results . Items ) == 0 {
return nil , fmt . Errorf ( "no CredentialIssuers were found" )
}
if len ( results . Items ) > 1 {
return nil , fmt . Errorf ( "multiple CredentialIssuers were found, so the --concierge-credential-issuer flag must be specified" )
}
2021-03-05 21:52:17 +00:00
result := & results . Items [ 0 ]
log . Info ( "discovered CredentialIssuer" , "name" , result . Name )
return result , nil
2021-02-25 20:16:40 +00:00
}
2021-03-05 21:52:17 +00:00
func lookupAuthenticator ( clientset conciergeclientset . Interface , authType , authName string , log logr . Logger ) ( metav1 . Object , error ) {
2020-12-15 00:38:19 +00:00
ctx , cancelFunc := context . WithTimeout ( context . Background ( ) , time . Second * 20 )
defer cancelFunc ( )
// If one was specified, look it up or error.
if authName != "" && authType != "" {
switch strings . ToLower ( authType ) {
case "webhook" :
2021-02-09 18:59:32 +00:00
return clientset . AuthenticationV1alpha1 ( ) . WebhookAuthenticators ( ) . Get ( ctx , authName , metav1 . GetOptions { } )
2020-12-15 00:38:19 +00:00
case "jwt" :
2021-02-09 18:59:32 +00:00
return clientset . AuthenticationV1alpha1 ( ) . JWTAuthenticators ( ) . Get ( ctx , authName , metav1 . GetOptions { } )
2020-12-15 00:38:19 +00:00
default :
return nil , fmt . Errorf ( ` invalid authenticator type %q, supported values are "webhook" and "jwt" ` , authType )
}
}
// Otherwise list all the available authenticators and hope there's just a single one.
2021-02-09 18:59:32 +00:00
jwtAuths , err := clientset . AuthenticationV1alpha1 ( ) . JWTAuthenticators ( ) . List ( ctx , metav1 . ListOptions { } )
2020-12-15 00:38:19 +00:00
if err != nil {
return nil , fmt . Errorf ( "failed to list JWTAuthenticator objects for autodiscovery: %w" , err )
}
2021-02-09 18:59:32 +00:00
webhooks , err := clientset . AuthenticationV1alpha1 ( ) . WebhookAuthenticators ( ) . List ( ctx , metav1 . ListOptions { } )
2020-12-15 00:38:19 +00:00
if err != nil {
return nil , fmt . Errorf ( "failed to list WebhookAuthenticator objects for autodiscovery: %w" , err )
}
results := make ( [ ] metav1 . Object , 0 , len ( jwtAuths . Items ) + len ( webhooks . Items ) )
for i := range jwtAuths . Items {
results = append ( results , & jwtAuths . Items [ i ] )
}
for i := range webhooks . Items {
results = append ( results , & webhooks . Items [ i ] )
}
if len ( results ) == 0 {
2021-02-09 18:59:32 +00:00
return nil , fmt . Errorf ( "no authenticators were found" )
2020-12-15 00:38:19 +00:00
}
if len ( results ) > 1 {
2021-03-05 21:52:17 +00:00
for _ , jwtAuth := range jwtAuths . Items {
log . Info ( "found JWTAuthenticator" , "name" , jwtAuth . Name )
}
for _ , webhook := range webhooks . Items {
log . Info ( "found WebhookAuthenticator" , "name" , webhook . Name )
}
2021-02-09 18:59:32 +00:00
return nil , fmt . Errorf ( "multiple authenticators were found, so the --concierge-authenticator-type/--concierge-authenticator-name flags must be specified" )
2020-12-15 00:38:19 +00:00
}
return results [ 0 ] , nil
}
func writeConfigAsYAML ( out io . Writer , config clientcmdapi . Config ) error {
output , err := clientcmd . Write ( config )
if err != nil {
return err
}
_ , err = out . Write ( output )
if err != nil {
return fmt . Errorf ( "could not write output: %w" , err )
}
return nil
}
2021-03-05 22:35:42 +00:00
func validateKubeconfig ( ctx context . Context , flags getKubeconfigParams , kubeconfig clientcmdapi . Config , log logr . Logger ) error {
if flags . skipValidate {
return nil
}
kubeContext := kubeconfig . Contexts [ kubeconfig . CurrentContext ]
if kubeContext == nil {
return fmt . Errorf ( "invalid kubeconfig (no context)" )
}
cluster := kubeconfig . Clusters [ kubeContext . Cluster ]
if cluster == nil {
return fmt . Errorf ( "invalid kubeconfig (no cluster)" )
}
kubeconfigCA := x509 . NewCertPool ( )
if ! kubeconfigCA . AppendCertsFromPEM ( cluster . CertificateAuthorityData ) {
return fmt . Errorf ( "invalid kubeconfig (no certificateAuthorityData)" )
}
httpClient := & http . Client {
Transport : & http . Transport {
TLSClientConfig : & tls . Config {
MinVersion : tls . VersionTLS12 ,
RootCAs : kubeconfigCA ,
} ,
Proxy : http . ProxyFromEnvironment ,
TLSHandshakeTimeout : 10 * time . Second ,
} ,
Timeout : 10 * time . Second ,
}
ticker := time . NewTicker ( 2 * time . Second )
defer ticker . Stop ( )
pingCluster := func ( ) error {
reqCtx , cancel := context . WithTimeout ( ctx , 10 * time . Second )
defer cancel ( )
req , err := http . NewRequestWithContext ( reqCtx , http . MethodGet , cluster . Server , nil )
if err != nil {
return fmt . Errorf ( "could not form request to validate cluster: %w" , err )
}
resp , err := httpClient . Do ( req )
if err != nil {
return err
}
_ = resp . Body . Close ( )
if resp . StatusCode >= 500 {
return fmt . Errorf ( "unexpected status code %d" , resp . StatusCode )
}
return nil
}
err := pingCluster ( )
if err == nil {
log . Info ( "validated connection to the cluster" )
return nil
}
log . Info ( "could not immediately connect to the cluster but it may be initializing, will retry until timeout" )
deadline , _ := ctx . Deadline ( )
2021-03-05 22:59:43 +00:00
attempts := 0
2021-03-05 22:35:42 +00:00
for {
select {
case <- ctx . Done ( ) :
return ctx . Err ( )
case <- ticker . C :
2021-03-05 22:59:43 +00:00
attempts ++
2021-03-05 22:35:42 +00:00
err := pingCluster ( )
if err == nil {
2021-03-05 22:59:43 +00:00
log . Info ( "validated connection to the cluster" , "attempts" , attempts )
2021-03-05 22:35:42 +00:00
return nil
}
2021-03-05 22:59:43 +00:00
log . Error ( err , "could not connect to cluster, retrying..." , "attempts" , attempts , "remaining" , time . Until ( deadline ) . Round ( time . Second ) . String ( ) )
2021-03-05 22:35:42 +00:00
}
}
}
2021-03-08 17:43:56 +00:00
func countCACerts ( pemData [ ] byte ) int {
pool := x509 . NewCertPool ( )
pool . AppendCertsFromPEM ( pemData )
return len ( pool . Subjects ( ) )
}
2021-03-09 20:48:16 +00:00
func hasPendingStrategy ( credentialIssuer * configv1alpha1 . CredentialIssuer ) bool {
for _ , strategy := range credentialIssuer . Status . Strategies {
if strategy . Reason == configv1alpha1 . PendingStrategyReason {
return true
}
}
return false
}