diff --git a/k8-operator/controllers/infisicalsecret_helper.go b/k8-operator/controllers/infisicalsecret_helper.go index 4523b42bb..9de1121fd 100644 --- a/k8-operator/controllers/infisicalsecret_helper.go +++ b/k8-operator/controllers/infisicalsecret_helper.go @@ -20,12 +20,27 @@ const SERVICE_ACCOUNT_ACCESS_KEY = "serviceAccountAccessKey" const SERVICE_ACCOUNT_PUBLIC_KEY = "serviceAccountPublicKey" const SERVICE_ACCOUNT_PRIVATE_KEY = "serviceAccountPrivateKey" +const INFISICAL_MACHINE_IDENTITY_CLIENT_ID = "clientId" +const INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET = "clientSecret" + const INFISICAL_TOKEN_SECRET_KEY_NAME = "infisicalToken" const SECRET_VERSION_ANNOTATION = "secrets.infisical.com/version" // used to set the version of secrets via Etag const OPERATOR_SETTINGS_CONFIGMAP_NAME = "infisical-config" const OPERATOR_SETTINGS_CONFIGMAP_NAMESPACE = "infisical-operator-system" const INFISICAL_DOMAIN = "https://app.infisical.com/api" +type AuthStrategyType string + +var AuthStrategy = struct { + SERVICE_TOKEN AuthStrategyType + SERVICE_ACCOUNT AuthStrategyType + UNIVERSAL_MACHINE_IDENTITY AuthStrategyType +}{ + SERVICE_TOKEN: "SERVICE_TOKEN", + SERVICE_ACCOUNT: "SERVICE_ACCOUNT", + UNIVERSAL_MACHINE_IDENTITY: "UNIVERSAL_MACHINE_IDENTITY", +} + func (r *InfisicalSecretReconciler) GetInfisicalConfigMap(ctx context.Context) (configMap map[string]string, errToReturn error) { // default key values defaultConfigMapData := make(map[string]string) @@ -100,6 +115,28 @@ func (r *InfisicalSecretReconciler) GetInfisicalTokenFromKubeSecret(ctx context. return strings.Replace(string(infisicalServiceToken), " ", "", -1), nil } +func (r *InfisicalSecretReconciler) GetInfisicalUniversalAuthMachineIdentityFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (machineIdentityDetails model.MachineIdentityDetails, err error) { + + universalAuthCredsFromKubeSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{ + Namespace: infisicalSecret.Spec.Authentication.UniversalAuthMachineIdentity.Credentials.SecretNamespace, + Name: infisicalSecret.Spec.Authentication.UniversalAuthMachineIdentity.Credentials.SecretName, + }) + + if errors.IsNotFound(err) { + return model.MachineIdentityDetails{}, nil + } + + if err != nil { + return model.MachineIdentityDetails{}, fmt.Errorf("something went wrong when fetching your machine identity credentials [err=%s]", err) + } + + clientIdFromSecret := universalAuthCredsFromKubeSecret.Data[INFISICAL_MACHINE_IDENTITY_CLIENT_ID] + clientSecretFromSecret := universalAuthCredsFromKubeSecret.Data[INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET] + + return model.MachineIdentityDetails{ClientId: string(clientIdFromSecret), ClientSecret: string(clientSecretFromSecret)}, nil + +} + // Fetches service account credentials from a Kubernetes secret specified in the infisicalSecret object, extracts the access key, public key, and private key from the secret, and returns them as a ServiceAccountCredentials object. // If any keys are missing or an error occurs, returns an empty object or an error object, respectively. func (r *InfisicalSecretReconciler) GetInfisicalServiceAccountCredentialsFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (serviceAccountDetails model.ServiceAccountDetails, err error) { @@ -127,7 +164,7 @@ func (r *InfisicalSecretReconciler) GetInfisicalServiceAccountCredentialsFromKub return model.ServiceAccountDetails{AccessKey: string(accessKeyFromSecret), PrivateKey: string(privateKeyFromSecret), PublicKey: string(publicKeyFromSecret)}, nil } -func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, secretsFromAPI []model.SingleEnvironmentVariable, encryptedSecretsResponse api.GetEncryptedSecretsV3Response) error { +func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error { plainProcessedSecrets := make(map[string][]byte) secretType := infisicalSecret.Spec.ManagedSecretReference.SecretType @@ -156,7 +193,7 @@ func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context } } - annotations[SECRET_VERSION_ANNOTATION] = encryptedSecretsResponse.ETag + annotations[SECRET_VERSION_ANNOTATION] = ETag // create a new secret as specified by the managed secret spec of CRD newKubeSecretInstance := &corev1.Secret{ @@ -187,16 +224,15 @@ func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context return nil } -func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context.Context, managedKubeSecret corev1.Secret, secretsFromAPI []model.SingleEnvironmentVariable, encryptedSecretsResponse api.GetEncryptedSecretsV3Response) error { +func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context.Context, managedKubeSecret corev1.Secret, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error { plainProcessedSecrets := make(map[string][]byte) for _, secret := range secretsFromAPI { plainProcessedSecrets[secret.Key] = []byte(secret.Value) } managedKubeSecret.Data = plainProcessedSecrets - managedKubeSecret.ObjectMeta.Annotations = map[string]string{ - SECRET_VERSION_ANNOTATION: encryptedSecretsResponse.ETag, - } + managedKubeSecret.ObjectMeta.Annotations = map[string]string{} + managedKubeSecret.ObjectMeta.Annotations[SECRET_VERSION_ANNOTATION] = ETag err := r.Client.Update(ctx, &managedKubeSecret) if err != nil { @@ -213,11 +249,28 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context return fmt.Errorf("ReconcileInfisicalSecret: unable to get service token from kube secret [err=%s]", err) } + var authStrategy AuthStrategyType + serviceAccountCreds, err := r.GetInfisicalServiceAccountCredentialsFromKubeSecret(ctx, infisicalSecret) if err != nil { return fmt.Errorf("ReconcileInfisicalSecret: unable to get service account creds from kube secret [err=%s]", err) } + infisicalMachineIdentityCreds, err := r.GetInfisicalUniversalAuthMachineIdentityFromKubeSecret(ctx, infisicalSecret) + if err != nil { + return fmt.Errorf("ReconcileInfisicalSecret: unable to get machine identity creds from kube secret [err=%s]", err) + } + + if serviceAccountCreds.AccessKey != "" || serviceAccountCreds.PrivateKey != "" || serviceAccountCreds.PublicKey != "" { + authStrategy = AuthStrategy.SERVICE_ACCOUNT + } else if infisicalToken != "" { + authStrategy = AuthStrategy.SERVICE_TOKEN + } else if infisicalMachineIdentityCreds.ClientId != "" && infisicalMachineIdentityCreds.ClientSecret != "" { + authStrategy = AuthStrategy.UNIVERSAL_MACHINE_IDENTITY + } else { + return fmt.Errorf("no authentication method provided. You must provide either a valid service token or a service account details to fetch secrets") + } + r.SetInfisicalTokenLoadCondition(ctx, &infisicalSecret, err) if err != nil { return fmt.Errorf("unable to load Infisical Token from the specified Kubernetes secret with error [%w]", err) @@ -239,41 +292,66 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context secretVersionBasedOnETag = managedKubeSecret.Annotations[SECRET_VERSION_ANNOTATION] } - var plainTextSecretsFromApi []model.SingleEnvironmentVariable - var fullEncryptedSecretsResponse api.GetEncryptedSecretsV3Response + if authStrategy == AuthStrategy.UNIVERSAL_MACHINE_IDENTITY && util.MachineIdentityTokenInstance == nil { + // Create new machine identity token instance + util.MachineIdentityTokenInstance = util.NewMachineIdentityToken(infisicalMachineIdentityCreds.ClientId, infisicalMachineIdentityCreds.ClientSecret) + } - if serviceAccountCreds.AccessKey != "" || serviceAccountCreds.PrivateKey != "" || serviceAccountCreds.PublicKey != "" { - plainTextSecretsFromApi, fullEncryptedSecretsResponse, err = util.GetPlainTextSecretsViaServiceAccount(serviceAccountCreds, infisicalSecret.Spec.Authentication.ServiceAccount.ProjectId, infisicalSecret.Spec.Authentication.ServiceAccount.EnvironmentName, secretVersionBasedOnETag) + // TODO: Also save a timestamp of when the token expires, so we know when to refetch an access token + + // if infisicalMachineIdentityCreds.ClientId != "" && infisicalMachineIdentityCreds.ClientSecret != "" { + // } + + var plainTextSecretsFromApi []model.SingleEnvironmentVariable + var updateAttributes api.UpdateAttributes + + if authStrategy == AuthStrategy.SERVICE_ACCOUNT { // Service Account + plainTextSecretsFromApi, updateAttributes, err = util.GetPlainTextSecretsViaServiceAccount(serviceAccountCreds, infisicalSecret.Spec.Authentication.ServiceAccount.ProjectId, infisicalSecret.Spec.Authentication.ServiceAccount.EnvironmentName, secretVersionBasedOnETag) if err != nil { return fmt.Errorf("\nfailed to get secrets because [err=%v]", err) } fmt.Println("ReconcileInfisicalSecret: Fetched secrets via service account") - } else if infisicalToken != "" { + } else if authStrategy == AuthStrategy.SERVICE_TOKEN { // Service Tokens (deprecated) envSlug := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.EnvSlug secretsPath := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.SecretsPath - plainTextSecretsFromApi, fullEncryptedSecretsResponse, err = util.GetPlainTextSecretsViaServiceToken(infisicalToken, secretVersionBasedOnETag, envSlug, secretsPath) + plainTextSecretsFromApi, updateAttributes, err = util.GetPlainTextSecretsViaServiceToken(infisicalToken, secretVersionBasedOnETag, envSlug, secretsPath) if err != nil { return fmt.Errorf("\nfailed to get secrets because [err=%v]", err) } fmt.Println("ReconcileInfisicalSecret: Fetched secrets via service token") + } else if authStrategy == AuthStrategy.UNIVERSAL_MACHINE_IDENTITY { // Machine Identity + accessToken, err := util.MachineIdentityTokenInstance.GetToken() + + if err != nil { + fmt.Println("\nReconcileInfisicalSecret: Waiting for access token to become available") + return nil + } + + scope := infisicalSecret.Spec.Authentication.UniversalAuthMachineIdentity.SecretsScope + plainTextSecretsFromApi, updateAttributes, err = util.GetPlainTextSecretsViaUniversalAuthMachineIdentity(accessToken, secretVersionBasedOnETag, scope) + + fmt.Println("ReconcileInfisicalSecret: Fetched secrets via universal auth") + if err != nil { + return fmt.Errorf("\nfailed to get secrets because [err=%v]", err) + } } else { return fmt.Errorf("no authentication method provided. You must provide either a valid service token or a service account details to fetch secrets") } - if !fullEncryptedSecretsResponse.Modified { - fmt.Println("No secrets modified so reconcile not needed", "Etag:", fullEncryptedSecretsResponse.ETag, "Modified:", fullEncryptedSecretsResponse.Modified) + if !updateAttributes.Modified { + fmt.Println("No secrets modified so reconcile not needed") return nil } if managedKubeSecret == nil { - return r.CreateInfisicalManagedKubeSecret(ctx, infisicalSecret, plainTextSecretsFromApi, fullEncryptedSecretsResponse) + return r.CreateInfisicalManagedKubeSecret(ctx, infisicalSecret, plainTextSecretsFromApi, updateAttributes.ETag) } else { - return r.UpdateInfisicalManagedKubeSecret(ctx, *managedKubeSecret, plainTextSecretsFromApi, fullEncryptedSecretsResponse) + return r.UpdateInfisicalManagedKubeSecret(ctx, *managedKubeSecret, plainTextSecretsFromApi, updateAttributes.ETag) } }