feat(k8s): automatic service account token creation for k8s auth

This commit is contained in:
Daniel Hougaard
2025-04-01 23:16:38 +04:00
parent cd4f2cccf8
commit 82b828c10e
7 changed files with 110 additions and 4 deletions

View File

@@ -49,6 +49,14 @@ type GenericKubernetesAuth struct {
IdentityID string `json:"identityId"` IdentityID string `json:"identityId"`
// +kubebuilder:validation:Required // +kubebuilder:validation:Required
ServiceAccountRef KubernetesServiceAccountRef `json:"serviceAccountRef"` ServiceAccountRef KubernetesServiceAccountRef `json:"serviceAccountRef"`
// Optionally automatically create a service account token for the configured service account.
// If this is set to `true`, the operator will automatically create a service account token for the configured service account.
// +kubebuilder:validation:Optional
AutoCreateServiceAccountToken bool `json:"autoCreateServiceAccountToken"`
// The audiences to use for the service account token. This is only relevant if `autoCreateServiceAccountToken` is true.
// +kubebuilder:validation:Optional
ServiceAccountTokenAudiences []string `json:"serviceAccountTokenAudiences"`
} }
type TLSConfig struct { type TLSConfig struct {

View File

@@ -38,6 +38,14 @@ type KubernetesAuthDetails struct {
// +kubebuilder:validation:Required // +kubebuilder:validation:Required
SecretsScope MachineIdentityScopeInWorkspace `json:"secretsScope"` SecretsScope MachineIdentityScopeInWorkspace `json:"secretsScope"`
// Optionally automatically create a service account token for the configured service account.
// If this is set to `true`, the operator will automatically create a service account token for the configured service account.
// +kubebuilder:validation:Optional
AutoCreateServiceAccountToken bool `json:"autoCreateServiceAccountToken"`
// The audiences to use for the service account token. This is only relevant if `autoCreateServiceAccountToken` is true.
// +kubebuilder:validation:Optional
ServiceAccountTokenAudiences []string `json:"serviceAccountTokenAudiences"`
} }
type KubernetesServiceAccountRef struct { type KubernetesServiceAccountRef struct {

View File

@@ -45,6 +45,9 @@ func (r *InfisicalDynamicSecretReconciler) GetLogger(req ctrl.Request) logr.Logg
// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;delete // +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;delete
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=list;watch;get;update // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=list;watch;get;update
// +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch // +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch
//+kubebuilder:rbac:groups="",resources=pods,verbs=get;list
//+kubebuilder:rbac:groups="authentication.k8s.io",resources=tokenreviews,verbs=create
//+kubebuilder:rbac:groups="",resources=serviceaccounts/token,verbs=create
func (r *InfisicalDynamicSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { func (r *InfisicalDynamicSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {

View File

@@ -48,6 +48,9 @@ func (r *InfisicalPushSecretReconciler) GetLogger(req ctrl.Request) logr.Logger
//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;delete //+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;delete
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=list;watch;get;update //+kubebuilder:rbac:groups=apps,resources=deployments,verbs=list;watch;get;update
//+kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch //+kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch
//+kubebuilder:rbac:groups="",resources=pods,verbs=get;list
//+kubebuilder:rbac:groups="authentication.k8s.io",resources=tokenreviews,verbs=create
//+kubebuilder:rbac:groups="",resources=serviceaccounts/token,verbs=create
// Reconcile is part of the main kubernetes reconciliation loop which aims to // Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state. // move the current state of the cluster closer to the desired state.

View File

@@ -44,6 +44,9 @@ func (r *InfisicalSecretReconciler) GetLogger(req ctrl.Request) logr.Logger {
//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;delete //+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;delete
//+kubebuilder:rbac:groups=apps,resources=deployments;daemonsets;statefulsets,verbs=list;watch;get;update //+kubebuilder:rbac:groups=apps,resources=deployments;daemonsets;statefulsets,verbs=list;watch;get;update
//+kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch //+kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch
//+kubebuilder:rbac:groups="",resources=pods,verbs=get;list
//+kubebuilder:rbac:groups="authentication.k8s.io",resources=tokenreviews,verbs=create
//+kubebuilder:rbac:groups="",resources=serviceaccounts/token,verbs=create
// Reconcile is part of the main kubernetes reconciliation loop which aims to // Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state. // move the current state of the cluster closer to the desired state.

View File

@@ -8,12 +8,50 @@ import (
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
authenticationv1 "k8s.io/api/authentication/v1"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1" "github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/aws/smithy-go/ptr"
infisicalSdk "github.com/infisical/go-sdk" infisicalSdk "github.com/infisical/go-sdk"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
) )
func GetServiceAccountToken(k8sClient client.Client, namespace string, serviceAccountName string) (string, error) { func GetServiceAccountToken(k8sClient client.Client, namespace string, serviceAccountName string, autoCreateServiceAccountToken bool, serviceAccountTokenAudiences []string) (string, error) {
if autoCreateServiceAccountToken {
if len(serviceAccountTokenAudiences) == 0 {
return "", fmt.Errorf("serviceAccountTokenAudiences is required when autoCreateServiceAccountToken is true")
}
restClient, err := GetRestClientFromClient()
if err != nil {
return "", fmt.Errorf("failed to get REST client: %w", err)
}
tokenRequest := &authenticationv1.TokenRequest{
Spec: authenticationv1.TokenRequestSpec{
Audiences: serviceAccountTokenAudiences,
ExpirationSeconds: ptr.Int64(600), // 10 minutes. the token only needs to be valid for when we do the initial k8s login.
},
}
result := &authenticationv1.TokenRequest{}
err = restClient.
Post().
Namespace(namespace).
Resource("serviceaccounts").
Name(serviceAccountName).
SubResource("token").
Body(tokenRequest).
Do(context.Background()).
Into(result)
if err != nil {
return "", fmt.Errorf("failed to create token: %w", err)
}
return result.Status.Token, nil
}
serviceAccount := &corev1.ServiceAccount{} serviceAccount := &corev1.ServiceAccount{}
err := k8sClient.Get(context.TODO(), client.ObjectKey{Name: serviceAccountName, Namespace: namespace}, serviceAccount) err := k8sClient.Get(context.TODO(), client.ObjectKey{Name: serviceAccountName, Namespace: namespace}, serviceAccount)
@@ -172,7 +210,9 @@ func HandleKubernetesAuth(ctx context.Context, reconcilerClient client.Client, s
Namespace: infisicalPushSecret.Spec.Authentication.KubernetesAuth.ServiceAccountRef.Namespace, Namespace: infisicalPushSecret.Spec.Authentication.KubernetesAuth.ServiceAccountRef.Namespace,
Name: infisicalPushSecret.Spec.Authentication.KubernetesAuth.ServiceAccountRef.Name, Name: infisicalPushSecret.Spec.Authentication.KubernetesAuth.ServiceAccountRef.Name,
}, },
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{}, SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
AutoCreateServiceAccountToken: infisicalPushSecret.Spec.Authentication.KubernetesAuth.AutoCreateServiceAccountToken,
ServiceAccountTokenAudiences: infisicalPushSecret.Spec.Authentication.KubernetesAuth.ServiceAccountTokenAudiences,
} }
case SecretCrd.INFISICAL_DYNAMIC_SECRET: case SecretCrd.INFISICAL_DYNAMIC_SECRET:
@@ -188,7 +228,9 @@ func HandleKubernetesAuth(ctx context.Context, reconcilerClient client.Client, s
Namespace: infisicalDynamicSecret.Spec.Authentication.KubernetesAuth.ServiceAccountRef.Namespace, Namespace: infisicalDynamicSecret.Spec.Authentication.KubernetesAuth.ServiceAccountRef.Namespace,
Name: infisicalDynamicSecret.Spec.Authentication.KubernetesAuth.ServiceAccountRef.Name, Name: infisicalDynamicSecret.Spec.Authentication.KubernetesAuth.ServiceAccountRef.Name,
}, },
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{}, SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
AutoCreateServiceAccountToken: infisicalDynamicSecret.Spec.Authentication.KubernetesAuth.AutoCreateServiceAccountToken,
ServiceAccountTokenAudiences: infisicalDynamicSecret.Spec.Authentication.KubernetesAuth.ServiceAccountTokenAudiences,
} }
} }
@@ -196,7 +238,14 @@ func HandleKubernetesAuth(ctx context.Context, reconcilerClient client.Client, s
return AuthenticationDetails{}, ErrAuthNotApplicable return AuthenticationDetails{}, ErrAuthNotApplicable
} }
serviceAccountToken, err := GetServiceAccountToken(reconcilerClient, kubernetesAuthSpec.ServiceAccountRef.Namespace, kubernetesAuthSpec.ServiceAccountRef.Name) serviceAccountToken, err := GetServiceAccountToken(
reconcilerClient,
kubernetesAuthSpec.ServiceAccountRef.Namespace,
kubernetesAuthSpec.ServiceAccountRef.Name,
kubernetesAuthSpec.AutoCreateServiceAccountToken,
kubernetesAuthSpec.ServiceAccountTokenAudiences,
)
if err != nil { if err != nil {
return AuthenticationDetails{}, fmt.Errorf("unable to get service account token [err=%s]", err) return AuthenticationDetails{}, fmt.Errorf("unable to get service account token [err=%s]", err)
} }

View File

@@ -9,6 +9,9 @@ import (
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
k8Errors "k8s.io/apimachinery/pkg/api/errors" k8Errors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
) )
@@ -58,3 +61,32 @@ func GetInfisicalUniversalAuthFromKubeSecret(ctx context.Context, reconcilerClie
return model.MachineIdentityDetails{ClientId: string(clientIdFromSecret), ClientSecret: string(clientSecretFromSecret)}, nil return model.MachineIdentityDetails{ClientId: string(clientIdFromSecret), ClientSecret: string(clientSecretFromSecret)}, nil
} }
func getKubeClusterConfig() (*rest.Config, error) {
config, err := rest.InClusterConfig()
if err != nil {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
configOverrides := &clientcmd.ConfigOverrides{}
kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
return kubeConfig.ClientConfig()
}
return config, nil
}
func GetRestClientFromClient() (rest.Interface, error) {
config, err := getKubeClusterConfig()
if err != nil {
return nil, err
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, err
}
return clientset.CoreV1().RESTClient(), nil
}