diff --git a/docs/integrations/platforms/kubernetes.mdx b/docs/integrations/platforms/kubernetes.mdx index 7ddb616b2..29ca9c4ba 100644 --- a/docs/integrations/platforms/kubernetes.mdx +++ b/docs/integrations/platforms/kubernetes.mdx @@ -12,7 +12,7 @@ The operator continuously updates secrets and can also reload dependent deployme ## Install Operator -The operator can be install via [Helm](helm.sh) or [kubectl](https://github.com/kubernetes/kubectl) +The operator can be install via [Helm](https://helm.sh) or [kubectl](https://github.com/kubernetes/kubectl) @@ -61,23 +61,38 @@ Once you have installed the operator to your cluster, you'll need to create a `I apiVersion: secrets.infisical.com/v1alpha1 kind: InfisicalSecret metadata: - # Name of of this InfisicalSecret resource - name: infisicalsecret-sample + name: infisicalsecret-sample + labels: + label-to-be-passed-to-managed-secret: sample-value + annotations: + example.com/annotation-to-be-passed-to-managed-secret: "sample-value" spec: - # The host that should be used to pull secrets from. If left empty, the value specified in Global configuration will be used - hostAPI: https://app.infisical.com/api - resyncInterval: 60 - authentication: - serviceToken: - serviceTokenSecretReference: - secretName: service-token + hostAPI: https://app.infisical.com/api + resyncInterval: 10 + authentication: + # Make sure to only have 1 authentication method defined, serviceToken/universalAuth. + # If you have multiple authentication methods defined, it may cause issues. + universalAuth: + secretsScope: + projectSlug: + envSlug: # "dev", "staging", "prod", etc.. + secretsPath: "" # Root is "/" + credentialsRef: + secretName: universal-auth-credentials + secretNamespace: default + + serviceToken: + serviceTokenSecretReference: + secretName: service-token + secretNamespace: default + secretsScope: + envSlug: + secretsPath: # Root is "/" + + managedSecretReference: + secretName: managed-secret secretNamespace: default - secretsScope: - envSlug: dev - secretsPath: "/" - managedSecretReference: - secretName: managed-secret # <-- the name of kubernetes secret that will be created - secretNamespace: default # <-- where the kubernetes secret should be created + # secretType: kubernetes.io/dockerconfigjson ``` ### InfisicalSecret CRD properties @@ -105,11 +120,60 @@ Default re-sync interval is every 1 minute. - This block defines the method that will be used to authenticate with Infisical so that secrets can be fetched. Currently, only [Service Tokens](../../documentation/platform/token) can be used to authenticate with Infisical. + This block defines the method that will be used to authenticate with Infisical so that secrets can be fetched - - The service token required to authenticate with Infisical needs to be stored in a Kubernetes secret. This block defines the reference to the name and name space of secret that stores this service token. + + The universal machine identity authentication method is used to authenticate with Infisical. The client ID and client secret needs to be stored in a Kubernetes secret. This block defines the reference to the name and namespace of secret that stores these credentials. + + + + You need to create a machine identity, and give it access to the project(s) you want to interact with. You can [read more about machine identities here](/documentation/platform/identities/universal-auth). + + + Once you have created your machine identity and added it to your project(s), you will need to create a Kubernetes secret containing the identity credentials. + To quickly create a Kubernetes secret containing the identity credentials, you can run the command below. + + Make sure you replace `` with the identity client ID and `` with the identity client secret. + + ``` bash + kubectl create secret generic universal-auth-credentials --from-literal=clientId="" --from-literal=clientSecret="" + ``` + + + + Once the secret is created, add the `secretName` and `secretNamespace` of the secret that was just created under `authentication.universalAuth.credentialsRef` field in the InfisicalSecret resource. + + + + + + + Make sure to also populate the `secretsScope` field with the project slug _`projectSlug`_, environment slug _`envSlug`_, and secrets path _`secretsPath`_ that you want to fetch secrets from. Please see the example below. + + + ## Example + ```yaml + apiVersion: secrets.infisical.com/v1alpha1 + kind: InfisicalSecret + metadata: + name: infisicalsecret-sample-crd + spec: + authentication: + universalAuth: + secretsScope: + projectSlug: # <-- project slug + envSlug: # "dev", "staging", "prod", etc.. + secretsPath: "" # Root is "/" + credentialsRef: + secretName: universal-auth-credentials # <-- name of the Kubernetes secret that stores our machine identity credentials + secretNamespace: default # <-- namespace of the Kubernetes secret that stores our machine identity credentials + ... + ``` + + + + The service token required to authenticate with Infisical needs to be stored in a Kubernetes secret. This block defines the reference to the name and namespace of secret that stores this service token. Follow the instructions below to create and store the service token in a Kubernetes secrets and reference it in your CRD. #### 1. Generate service token @@ -122,13 +186,17 @@ Default re-sync interval is every 1 minute. To quickly create a Kubernetes secret containing the generated service token, you can run the command below. Make sure you replace `` with your service token. ``` bash - kubectl create secret generic service-token --from-literal=infisicalToken= + kubectl create secret generic service-token --from-literal=infisicalToken="" ``` #### 3. Add reference for the Kubernetes secret containing service token Once the secret is created, add the name and namespace of the secret that was just created under `authentication.serviceToken.serviceTokenSecretReference` field in the InfisicalSecret resource. + + Make sure to also populate the `secretsScope` field with the, environment slug _`envSlug`_, and secrets path _`secretsPath`_ that you want to fetch secrets from. Please see the example below. + + ## Example ```yaml apiVersion: secrets.infisical.com/v1alpha1 @@ -141,25 +209,13 @@ Default re-sync interval is every 1 minute. serviceTokenSecretReference: secretName: service-token # <-- name of the Kubernetes secret that stores our service token secretNamespace: option # <-- namespace of the Kubernetes secret that stores our service token + secretsScope: + envSlug: # "dev", "staging", "prod", etc.. + secretsPath: # Root is "/" ... ``` - - This block defines the scope of what secrets should be fetched. This is needed as your service token can have access to multiple folders and environments. - A scope is defined by `envSlug` and `secretsPath`. - - #### envSlug - - This refers to the short hand name of an environment. For example for the `development` environment the environment slug is `dev`. You can locate the slug of your environment by heading to your project settings in the Infisical dashboard. - - #### secretsPath - - secretsPath is the path to the secret in the given environment. For example a path of `/` would refer to the root of the environment whereas `/folder1` would refer to the secrets in folder1 from the root. - - Both fields are required. - - The `managedSecretReference` field is used to define the target location for storing secrets retrieved from an Infisical project. This field requires specifying both the name and namespace of the Kubernetes secret that will hold these secrets. diff --git a/helm-charts/secrets-operator/templates/infisicalsecret-crd.yaml b/helm-charts/secrets-operator/templates/infisicalsecret-crd.yaml index 4ac32fca4..61e7a6719 100644 --- a/helm-charts/secrets-operator/templates/infisicalsecret-crd.yaml +++ b/helm-charts/secrets-operator/templates/infisicalsecret-crd.yaml @@ -90,6 +90,38 @@ spec: - secretsScope - serviceTokenSecretReference type: object + universalAuth: + properties: + credentialsRef: + properties: + secretName: + description: The name of the Kubernetes Secret + type: string + secretNamespace: + description: The name space where the Kubernetes Secret + is located + type: string + required: + - secretName + - secretNamespace + type: object + secretsScope: + properties: + envSlug: + type: string + projectSlug: + type: string + secretsPath: + type: string + required: + - envSlug + - projectSlug + - secretsPath + type: object + required: + - credentialsRef + - secretsScope + type: object type: object hostAPI: description: Infisical host to pull secrets from diff --git a/helm-charts/secrets-operator/values.yaml b/helm-charts/secrets-operator/values.yaml index a5692b061..26f7a1938 100644 --- a/helm-charts/secrets-operator/values.yaml +++ b/helm-charts/secrets-operator/values.yaml @@ -32,7 +32,7 @@ controllerManager: - ALL image: repository: infisical/kubernetes-operator - tag: v0.4.0 # fixed to prevent accidental upgrade + tag: v0.5.0 # fixed to prevent accidental upgrade resources: limits: cpu: 500m diff --git a/k8-operator/api/v1alpha1/infisicalsecret_types.go b/k8-operator/api/v1alpha1/infisicalsecret_types.go index cae966fe0..c29761b06 100644 --- a/k8-operator/api/v1alpha1/infisicalsecret_types.go +++ b/k8-operator/api/v1alpha1/infisicalsecret_types.go @@ -9,12 +9,20 @@ type Authentication struct { ServiceAccount ServiceAccountDetails `json:"serviceAccount"` // +kubebuilder:validation:Optional ServiceToken ServiceTokenDetails `json:"serviceToken"` + // +kubebuilder:validation:Optional + UniversalAuth UniversalAuthDetails `json:"universalAuth"` +} + +type UniversalAuthDetails struct { + // +kubebuilder:validation:Required + CredentialsRef KubeSecretReference `json:"credentialsRef"` + // +kubebuilder:validation:Required + SecretsScope MachineIdentityScopeInWorkspace `json:"secretsScope"` } type ServiceTokenDetails struct { // +kubebuilder:validation:Required ServiceTokenSecretReference KubeSecretReference `json:"serviceTokenSecretReference"` - // +kubebuilder:validation:Required SecretsScope SecretScopeInWorkspace `json:"secretsScope"` } @@ -28,11 +36,19 @@ type ServiceAccountDetails struct { type SecretScopeInWorkspace struct { // +kubebuilder:validation:Required SecretsPath string `json:"secretsPath"` - // +kubebuilder:validation:Required EnvSlug string `json:"envSlug"` } +type MachineIdentityScopeInWorkspace struct { + // +kubebuilder:validation:Required + SecretsPath string `json:"secretsPath"` + // +kubebuilder:validation:Required + EnvSlug string `json:"envSlug"` + // +kubebuilder:validation:Required + ProjectSlug string `json:"projectSlug"` +} + type KubeSecretReference struct { // The name of the Kubernetes Secret // +kubebuilder:validation:Required diff --git a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go index 5b6befcbc..23251a194 100644 --- a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -31,6 +31,7 @@ func (in *Authentication) DeepCopyInto(out *Authentication) { *out = *in out.ServiceAccount = in.ServiceAccount out.ServiceToken = in.ServiceToken + out.UniversalAuth = in.UniversalAuth } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Authentication. @@ -157,6 +158,21 @@ func (in *KubeSecretReference) DeepCopy() *KubeSecretReference { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachineIdentityScopeInWorkspace) DeepCopyInto(out *MachineIdentityScopeInWorkspace) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineIdentityScopeInWorkspace. +func (in *MachineIdentityScopeInWorkspace) DeepCopy() *MachineIdentityScopeInWorkspace { + if in == nil { + return nil + } + out := new(MachineIdentityScopeInWorkspace) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MangedKubeSecretConfig) DeepCopyInto(out *MangedKubeSecretConfig) { *out = *in @@ -219,3 +235,20 @@ func (in *ServiceTokenDetails) DeepCopy() *ServiceTokenDetails { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UniversalAuthDetails) DeepCopyInto(out *UniversalAuthDetails) { + *out = *in + out.CredentialsRef = in.CredentialsRef + out.SecretsScope = in.SecretsScope +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UniversalAuthDetails. +func (in *UniversalAuthDetails) DeepCopy() *UniversalAuthDetails { + if in == nil { + return nil + } + out := new(UniversalAuthDetails) + in.DeepCopyInto(out) + return out +} diff --git a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml index 7d4e88b14..811eb9669 100644 --- a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml +++ b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml @@ -90,6 +90,38 @@ spec: - secretsScope - serviceTokenSecretReference type: object + universalAuth: + properties: + credentialsRef: + properties: + secretName: + description: The name of the Kubernetes Secret + type: string + secretNamespace: + description: The name space where the Kubernetes Secret + is located + type: string + required: + - secretName + - secretNamespace + type: object + secretsScope: + properties: + envSlug: + type: string + projectSlug: + type: string + secretsPath: + type: string + required: + - envSlug + - projectSlug + - secretsPath + type: object + required: + - credentialsRef + - secretsScope + type: object type: object hostAPI: description: Infisical host to pull secrets from diff --git a/k8-operator/config/samples/machineIdentitySecret.yaml b/k8-operator/config/samples/machineIdentitySecret.yaml new file mode 100644 index 000000000..c2a69b438 --- /dev/null +++ b/k8-operator/config/samples/machineIdentitySecret.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + name: universal-auth-credentials +type: Opaque +stringData: + clientId: + clientSecret: diff --git a/k8-operator/config/samples/sample.yaml b/k8-operator/config/samples/sample.yaml index ca0025a53..65347dec7 100644 --- a/k8-operator/config/samples/sample.yaml +++ b/k8-operator/config/samples/sample.yaml @@ -1,35 +1,42 @@ apiVersion: secrets.infisical.com/v1alpha1 kind: InfisicalSecret metadata: - name: infisicalsecret-sample - labels: - label-to-be-passed-to-managed-secret: sample-value - annotations: - reflector.v1.k8s.emberstack.com/reflection-allowed: 'true' + name: infisicalsecret-sample + labels: + label-to-be-passed-to-managed-secret: sample-value + annotations: + example.com/annotation-to-be-passed-to-managed-secret: "sample-value" spec: - hostAPI: http://localhost:8888/api - resyncInterval: 10 - authentication: - serviceAccount: - serviceAccountSecretReference: - secretName: service-account - secretNamespace: default - projectId: "6439ec224cfbf7ea2a95b651" - environmentName: "dev" - serviceToken: - serviceTokenSecretReference: - secretName: service-token - secretNamespace: default - secretsScope: - envSlug: dev - secretsPath: "/" - managedSecretReference: - secretName: managed-secret - secretNamespace: default - creationPolicy: "Orphan" ## Owner | Orphan - # secretType: kubernetes.io/dockerconfigjson + hostAPI: https://app.infisical.com/api + resyncInterval: 10 + authentication: + # Make sure to only have 1 authentication method defined, serviceToken/universalAuth. + # If you have multiple authentication methods defined, it may cause issues. + serviceToken: + serviceTokenSecretReference: + secretName: service-token + secretNamespace: default + secretsScope: + envSlug: + secretsPath: # Root is "/" - # # To be depreciated soon - # tokenSecretReference: - # secretName: service-token - # secretNamespace: default + universalAuth: + secretsScope: + projectSlug: + envSlug: # "dev", "staging", "prod", etc.. + secretsPath: "" # Root is "/" + + credentialsRef: + secretName: universal-auth-credentials + secretNamespace: default + + managedSecretReference: + secretName: managed-secret + secretNamespace: default + creationPolicy: "Orphan" ## Owner | Orphan + # secretType: kubernetes.io/dockerconfigjson + + # # To be depreciated soon + # tokenSecretReference: + # secretName: service-token + # secretNamespace: default diff --git a/k8-operator/config/samples/serviceAccountCredsSecret.yaml b/k8-operator/config/samples/serviceAccountCredsSecret.yaml deleted file mode 100644 index 859312ee2..000000000 --- a/k8-operator/config/samples/serviceAccountCredsSecret.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: service-account -type: Opaque -stringData: - serviceAccountAccessKey: <> - serviceAccountPrivateKey: <> - serviceAccountPublicKey: <> diff --git a/k8-operator/controllers/infisicalsecret_controller.go b/k8-operator/controllers/infisicalsecret_controller.go index 7aca83651..5eba262ac 100644 --- a/k8-operator/controllers/infisicalsecret_controller.go +++ b/k8-operator/controllers/infisicalsecret_controller.go @@ -53,11 +53,11 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ if infisicalSecretCR.Spec.ResyncInterval != 0 { requeueTime = time.Second * time.Duration(infisicalSecretCR.Spec.ResyncInterval) - fmt.Println("Manual re-sync interval set", "requeueAfter", requeueTime) + fmt.Printf("\nManual re-sync interval set. Interval: %v\n", requeueTime) + } else { + fmt.Printf("\nRe-sync interval set. Interval: %v\n", requeueTime) } - fmt.Println("Requeue duration set", "requeueAfter", requeueTime) - // Check if the resource is already marked for deletion if infisicalSecretCR.GetDeletionTimestamp() != nil { return ctrl.Result{ diff --git a/k8-operator/controllers/infisicalsecret_helper.go b/k8-operator/controllers/infisicalsecret_helper.go index 4523b42bb..4ef378fe2 100644 --- a/k8-operator/controllers/infisicalsecret_helper.go +++ b/k8-operator/controllers/infisicalsecret_helper.go @@ -6,7 +6,6 @@ import ( "strings" "github.com/Infisical/infisical/k8-operator/api/v1alpha1" - "github.com/Infisical/infisical/k8-operator/packages/api" "github.com/Infisical/infisical/k8-operator/packages/model" "github.com/Infisical/infisical/k8-operator/packages/util" corev1 "k8s.io/api/core/v1" @@ -20,12 +19,29 @@ 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", +} + +var machineIdentityTokenInstance *util.MachineIdentityToken + func (r *InfisicalSecretReconciler) GetInfisicalConfigMap(ctx context.Context) (configMap map[string]string, errToReturn error) { // default key values defaultConfigMapData := make(map[string]string) @@ -100,6 +116,28 @@ func (r *InfisicalSecretReconciler) GetInfisicalTokenFromKubeSecret(ctx context. return strings.Replace(string(infisicalServiceToken), " ", "", -1), nil } +func (r *InfisicalSecretReconciler) GetInfisicalUniversalAuthFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (machineIdentityDetails model.MachineIdentityDetails, err error) { + + universalAuthCredsFromKubeSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{ + Namespace: infisicalSecret.Spec.Authentication.UniversalAuth.CredentialsRef.SecretNamespace, + Name: infisicalSecret.Spec.Authentication.UniversalAuth.CredentialsRef.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 +165,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 +194,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 +225,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 +250,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.GetInfisicalUniversalAuthFromKubeSecret(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 +293,60 @@ 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 && machineIdentityTokenInstance == nil { + // Create new machine identity token instance + 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) + var plainTextSecretsFromApi []model.SingleEnvironmentVariable + var updateDetails model.RequestUpdateUpdateDetails + + if authStrategy == AuthStrategy.SERVICE_ACCOUNT { // Service Account + plainTextSecretsFromApi, updateDetails, 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, updateDetails, 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 := machineIdentityTokenInstance.GetToken() + + if err != nil { + return fmt.Errorf("%s", "Waiting for access token to become available") + } + scope := infisicalSecret.Spec.Authentication.UniversalAuth.SecretsScope + plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaUniversalAuth(accessToken, secretVersionBasedOnETag, scope) + + if err != nil { + return fmt.Errorf("\nfailed to get secrets because [err=%v]", err) + } + fmt.Println("ReconcileInfisicalSecret: Fetched secrets via universal auth") } 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 !updateDetails.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, updateDetails.ETag) } else { - return r.UpdateInfisicalManagedKubeSecret(ctx, *managedKubeSecret, plainTextSecretsFromApi, fullEncryptedSecretsResponse) + return r.UpdateInfisicalManagedKubeSecret(ctx, *managedKubeSecret, plainTextSecretsFromApi, updateDetails.ETag) } } diff --git a/k8-operator/packages/api/api.go b/k8-operator/packages/api/api.go index 15de04321..05a58e8cb 100644 --- a/k8-operator/packages/api/api.go +++ b/k8-operator/packages/api/api.go @@ -58,7 +58,6 @@ func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Req R(). SetResult(&secretsResponse). SetHeader("User-Agent", USER_AGENT_NAME). - SetHeader("If-None-Match", request.ETag). SetQueryParam("environment", request.Environment). SetQueryParam("include_imports", "true"). // TODO needs to be set as a option SetQueryParam("workspaceId", request.WorkspaceId) @@ -77,13 +76,10 @@ func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Req return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response) } - if response.Header().Get("etag") == request.ETag { - secretsResponse.Modified = false - } else { - secretsResponse.Modified = true - } + responseETag := response.Header().Get("etag") - secretsResponse.ETag = response.Header().Get("etag") + secretsResponse.Modified = request.ETag != responseETag + secretsResponse.ETag = responseETag return secretsResponse, nil } @@ -107,6 +103,76 @@ func CallGetServiceTokenAccountDetailsV2(httpClient *resty.Client) (ServiceAccou return serviceAccountDetailsResponse, nil } +func CallUniversalMachineIdentityLogin(request MachineIdentityUniversalAuthLoginRequest) (MachineIdentityDetailsResponse, error) { + var machineIdentityDetailsResponse MachineIdentityDetailsResponse + + response, err := resty.New(). + R(). + SetResult(&machineIdentityDetailsResponse). + SetBody(request). + SetHeader("User-Agent", USER_AGENT_NAME). + Post(fmt.Sprintf("%v/v1/auth/universal-auth/login", API_HOST_URL)) + + if err != nil { + return MachineIdentityDetailsResponse{}, fmt.Errorf("CallUniversalMachineIdentityLogin: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return MachineIdentityDetailsResponse{}, fmt.Errorf("CallUniversalMachineIdentityLogin: Unsuccessful response: [response=%s]", response) + } + + return machineIdentityDetailsResponse, nil +} + +func CallUniversalMachineIdentityRefreshAccessToken(request MachineIdentityUniversalAuthRefreshRequest) (MachineIdentityDetailsResponse, error) { + var universalAuthRefreshResponse MachineIdentityDetailsResponse + + response, err := resty.New(). + R(). + SetResult(&universalAuthRefreshResponse). + SetHeader("User-Agent", USER_AGENT_NAME). + SetBody(request). + Post(fmt.Sprintf("%v/v1/auth/token/renew", API_HOST_URL)) + + if err != nil { + return MachineIdentityDetailsResponse{}, fmt.Errorf("CallUniversalAuthRefreshAccessToken: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return MachineIdentityDetailsResponse{}, fmt.Errorf("CallUniversalAuthRefreshAccessToken: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String()) + } + + return universalAuthRefreshResponse, nil +} + +func CallGetDecryptedSecretsV3(httpClient *resty.Client, request GetDecryptedSecretsV3Request) (GetDecryptedSecretsV3Response, error) { + var decryptedSecretsResponse GetDecryptedSecretsV3Response + + response, err := httpClient. + R(). + SetResult(&decryptedSecretsResponse). + SetHeader("User-Agent", USER_AGENT_NAME). + SetQueryParam("secretPath", request.SecretPath). + SetQueryParam("workspaceSlug", request.ProjectSlug). + SetQueryParam("environment", request.Environment). + Get(fmt.Sprintf("%v/v3/secrets/raw", API_HOST_URL)) + + if err != nil { + return GetDecryptedSecretsV3Response{}, fmt.Errorf("CallGetDecryptedSecretsV3: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return GetDecryptedSecretsV3Response{}, fmt.Errorf("CallGetDecryptedSecretsV3: Unsuccessful response: [response=%s]", response) + } + + responseETag := response.Header().Get("etag") + + decryptedSecretsResponse.Modified = request.ETag != responseETag + decryptedSecretsResponse.ETag = responseETag + + return decryptedSecretsResponse, nil +} + func CallGetServiceAccountWorkspacePermissionsV2(httpClient *resty.Client) (ServiceAccountWorkspacePermissions, error) { var serviceAccountWorkspacePermissionsResponse ServiceAccountWorkspacePermissions response, err := httpClient. diff --git a/k8-operator/packages/api/models.go b/k8-operator/packages/api/models.go index a72ccfc49..ff4ea3e10 100644 --- a/k8-operator/packages/api/models.go +++ b/k8-operator/packages/api/models.go @@ -65,6 +65,17 @@ type EncryptedSecretV3 struct { UpdatedAt time.Time `json:"updatedAt"` } +type DecryptedSecretV3 struct { + ID string `json:"id"` + Workspace string `json:"workspace"` + Environment string `json:"environment"` + Version int `json:"version"` + Type string `json:"string"` + SecretKey string `json:"secretKey"` + SecretValue string `json:"secretValue"` + SecretComment string `json:"secretComment"` +} + type ImportedSecretV3 struct { Environment string `json:"environment"` FolderId string `json:"folderId"` @@ -79,6 +90,19 @@ type GetEncryptedSecretsV3Response struct { ETag string `json:"ETag,omitempty"` } +type GetDecryptedSecretsV3Response struct { + Secrets []DecryptedSecretV3 `json:"secrets"` + ETag string `json:"ETag,omitempty"` + Modified bool `json:"modified,omitempty"` +} + +type GetDecryptedSecretsV3Request struct { + ProjectSlug string `json:"workspaceSlug"` + Environment string `json:"environment"` + SecretPath string `json:"secretPath"` + ETag string `json:"etag,omitempty"` +} + type GetServiceTokenDetailsResponse struct { ID string `json:"_id"` Name string `json:"name"` @@ -101,6 +125,13 @@ type ServiceAccountDetailsResponse struct { } `json:"serviceAccount"` } +type MachineIdentityDetailsResponse struct { + AccessToken string `json:"accessToken"` + ExpiresIn int `json:"expiresIn"` + AccessTokenMaxTTL int `json:"accessTokenMaxTTL"` + TokenType string `json:"tokenType"` +} + type ServiceAccountWorkspacePermission struct { ID string `json:"_id"` ServiceAccount string `json:"serviceAccount"` @@ -128,6 +159,15 @@ type GetServiceAccountKeysRequest struct { ServiceAccountId string `json:"id"` } +type MachineIdentityUniversalAuthLoginRequest struct { + ClientId string `json:"clientId"` + ClientSecret string `json:"clientSecret"` +} + +type MachineIdentityUniversalAuthRefreshRequest struct { + AccessToken string `json:"accessToken"` +} + type ServiceAccountKey struct { ID string `json:"_id"` EncryptedKey string `json:"encryptedKey"` diff --git a/k8-operator/packages/model/model.go b/k8-operator/packages/model/model.go index b7d4780b2..3d16f3a84 100644 --- a/k8-operator/packages/model/model.go +++ b/k8-operator/packages/model/model.go @@ -6,6 +6,16 @@ type ServiceAccountDetails struct { PrivateKey string } +type MachineIdentityDetails struct { + ClientId string + ClientSecret string +} + +type RequestUpdateUpdateDetails struct { + Modified bool + ETag string +} + type SingleEnvironmentVariable struct { Key string `json:"key"` Value string `json:"value"` diff --git a/k8-operator/packages/util/machine-identity-token.go b/k8-operator/packages/util/machine-identity-token.go new file mode 100644 index 000000000..055e869ad --- /dev/null +++ b/k8-operator/packages/util/machine-identity-token.go @@ -0,0 +1,170 @@ +package util + +import ( + "fmt" + "os" + "sync" + "time" + + "github.com/Infisical/infisical/k8-operator/packages/api" + "github.com/go-resty/resty/v2" +) + +type MachineIdentityToken struct { + accessTokenTTL time.Duration + accessTokenMaxTTL time.Duration + accessTokenFetchedTime time.Time + accessTokenRefreshedTime time.Time + + mutex sync.Mutex + + accessToken string + clientSecret string + clientId string +} + +func NewMachineIdentityToken(clientId string, clientSecret string) *MachineIdentityToken { + + token := MachineIdentityToken{ + clientSecret: clientSecret, + clientId: clientId, + } + + go token.HandleTokenLifecycle() + + return &token +} + +func (t *MachineIdentityToken) HandleTokenLifecycle() error { + + for { + accessTokenMaxTTLExpiresInTime := t.accessTokenFetchedTime.Add(t.accessTokenMaxTTL - (5 * time.Second)) + accessTokenRefreshedTime := t.accessTokenRefreshedTime + + if accessTokenRefreshedTime.IsZero() { + accessTokenRefreshedTime = t.accessTokenFetchedTime + } + + nextAccessTokenExpiresInTime := accessTokenRefreshedTime.Add(t.accessTokenTTL - (5 * time.Second)) + + if t.accessTokenFetchedTime.IsZero() && t.accessTokenRefreshedTime.IsZero() { + // case: init login to get access token + fmt.Println("\nInfisical Authentication: attempting to authenticate...") + err := t.FetchNewAccessToken() + if err != nil { + fmt.Printf("\nInfisical Authentication: unable to authenticate universal auth because %v. Will retry in 30 seconds", err) + + // wait a bit before trying again + time.Sleep((30 * time.Second)) + continue + } + } else if time.Now().After(accessTokenMaxTTLExpiresInTime) { + fmt.Printf("\nInfisical Authentication: machine identity access token has reached max ttl, attempting to re authenticate...") + err := t.FetchNewAccessToken() + if err != nil { + fmt.Printf("\nInfisical Authentication: unable to authenticate universal auth because %v. Will retry in 30 seconds", err) + + // wait a bit before trying again + time.Sleep((30 * time.Second)) + continue + } + } else { + err := t.RefreshAccessToken() + if err != nil { + fmt.Printf("\nInfisical Authentication: unable to refresh universal auth token because %v. Will retry in 30 seconds", err) + + // wait a bit before trying again + time.Sleep((30 * time.Second)) + continue + } + } + + if accessTokenRefreshedTime.IsZero() { + accessTokenRefreshedTime = t.accessTokenFetchedTime + } else { + accessTokenRefreshedTime = t.accessTokenRefreshedTime + } + + nextAccessTokenExpiresInTime = accessTokenRefreshedTime.Add(t.accessTokenTTL - (5 * time.Second)) + accessTokenMaxTTLExpiresInTime = t.accessTokenFetchedTime.Add(t.accessTokenMaxTTL - (5 * time.Second)) + + if nextAccessTokenExpiresInTime.After(accessTokenMaxTTLExpiresInTime) { + // case: Refreshed so close that the next refresh would occur beyond max ttl (this is because currently, token renew tries to add +access-token-ttl amount of time) + // example: access token ttl is 11 sec and max ttl is 30 sec. So it will start with 11 seconds, then 22 seconds but the next time you call refresh it would try to extend it to 33 but max ttl only allows 30, so the token will be valid until 30 before we need to reauth + time.Sleep(t.accessTokenTTL - nextAccessTokenExpiresInTime.Sub(accessTokenMaxTTLExpiresInTime)) + } else { + time.Sleep(t.accessTokenTTL - (5 * time.Second)) + } + } +} + +func (t *MachineIdentityToken) RefreshAccessToken() error { + httpClient := resty.New() + httpClient.SetRetryCount(10000). + SetRetryMaxWaitTime(20 * time.Second). + SetRetryWaitTime(5 * time.Second) + + accessToken, err := t.GetToken() + + if err != nil { + return err + } + + response, err := api.CallUniversalMachineIdentityRefreshAccessToken(api.MachineIdentityUniversalAuthRefreshRequest{AccessToken: accessToken}) + if err != nil { + return err + } + + accessTokenTTL := time.Duration(response.ExpiresIn * int(time.Second)) + accessTokenMaxTTL := time.Duration(response.AccessTokenMaxTTL * int(time.Second)) + t.accessTokenRefreshedTime = time.Now() + + t.SetToken(response.AccessToken, accessTokenTTL, accessTokenMaxTTL) + + return nil +} + +// Fetches a new access token using client credentials +func (t *MachineIdentityToken) FetchNewAccessToken() error { + + loginResponse, err := api.CallUniversalMachineIdentityLogin(api.MachineIdentityUniversalAuthLoginRequest{ + ClientId: t.clientId, + ClientSecret: t.clientSecret, + }) + if err != nil { + return err + } + + accessTokenTTL := time.Duration(loginResponse.ExpiresIn * int(time.Second)) + accessTokenMaxTTL := time.Duration(loginResponse.AccessTokenMaxTTL * int(time.Second)) + + if accessTokenTTL <= time.Duration(5)*time.Second { + fmt.Println("\nInfisical Authentication: At this time, k8 operator does not support refresh of tokens with 5 seconds or less ttl. Please increase access token ttl and try again") + os.Exit(1) + } + + t.accessTokenFetchedTime = time.Now() + t.SetToken(loginResponse.AccessToken, accessTokenTTL, accessTokenMaxTTL) + + return nil +} + +func (t *MachineIdentityToken) SetToken(token string, accessTokenTTL time.Duration, accessTokenMaxTTL time.Duration) { + t.mutex.Lock() + defer t.mutex.Unlock() + + t.accessToken = token + t.accessTokenTTL = accessTokenTTL + t.accessTokenMaxTTL = accessTokenMaxTTL +} + +func (t *MachineIdentityToken) GetToken() (string, error) { + t.mutex.Lock() + defer t.mutex.Unlock() + + if t.accessToken == "" { + return "", fmt.Errorf("no machine identity access token available") + } + + return t.accessToken, nil +} diff --git a/k8-operator/packages/util/secrets.go b/k8-operator/packages/util/secrets.go index d3e13f536..a8c4a4cc5 100644 --- a/k8-operator/packages/util/secrets.go +++ b/k8-operator/packages/util/secrets.go @@ -7,6 +7,7 @@ import ( "regexp" "strings" + "github.com/Infisical/infisical/k8-operator/api/v1alpha1" "github.com/Infisical/infisical/k8-operator/packages/api" "github.com/Infisical/infisical/k8-operator/packages/crypto" "github.com/Infisical/infisical/k8-operator/packages/model" @@ -50,10 +51,44 @@ func GetServiceTokenDetails(infisicalToken string) (api.GetServiceTokenDetailsRe return serviceTokenDetails, nil } -func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string, envSlug string, secretPath string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV3Response, error) { +func GetPlainTextSecretsViaUniversalAuth(accessToken string, etag string, secretScope v1alpha1.MachineIdentityScopeInWorkspace) ([]model.SingleEnvironmentVariable, model.RequestUpdateUpdateDetails, error) { + + httpClient := resty.New() + httpClient.SetAuthScheme("Bearer") + httpClient.SetAuthToken(accessToken) + + secretsResponse, err := api.CallGetDecryptedSecretsV3(httpClient, api.GetDecryptedSecretsV3Request{ + ProjectSlug: secretScope.ProjectSlug, + Environment: secretScope.EnvSlug, + SecretPath: secretScope.SecretsPath, + ETag: etag, + }) + + if err != nil { + return nil, model.RequestUpdateUpdateDetails{}, err + } + + var secrets []model.SingleEnvironmentVariable + + for _, secret := range secretsResponse.Secrets { + secrets = append(secrets, model.SingleEnvironmentVariable{ + Key: secret.SecretKey, + Value: secret.SecretValue, + Type: secret.Type, + ID: secret.ID, + }) + } + + return secrets, model.RequestUpdateUpdateDetails{ + Modified: secretsResponse.Modified, + ETag: secretsResponse.ETag, + }, nil +} + +func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string, envSlug string, secretPath string) ([]model.SingleEnvironmentVariable, model.RequestUpdateUpdateDetails, error) { serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4) if len(serviceTokenParts) < 4 { - return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again") + return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again") } serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2]) @@ -65,7 +100,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string, en serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient) if err != nil { - return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to get service token details. [err=%v]", err) + return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("unable to get service token details. [err=%v]", err) } encryptedSecretsResponse, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{ @@ -76,51 +111,54 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string, en }) if err != nil { - return nil, api.GetEncryptedSecretsV3Response{}, err + return nil, model.RequestUpdateUpdateDetails{}, err } decodedSymmetricEncryptionDetails, err := GetBase64DecodedSymmetricEncryptionDetails(serviceTokenParts[3], serviceTokenDetails.EncryptedKey, serviceTokenDetails.Iv, serviceTokenDetails.Tag) if err != nil { - return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decode symmetric encryption details [err=%v]", err) + return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("unable to decode symmetric encryption details [err=%v]", err) } plainTextWorkspaceKey, err := crypto.DecryptSymmetric([]byte(serviceTokenParts[3]), decodedSymmetricEncryptionDetails.Cipher, decodedSymmetricEncryptionDetails.Tag, decodedSymmetricEncryptionDetails.IV) if err != nil { - return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decrypt the required workspace key") + return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("unable to decrypt the required workspace key") } plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse.Secrets) if err != nil { - return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err) + return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err) } plainTextSecretsMergedWithImports, err := InjectImportedSecret(plainTextWorkspaceKey, plainTextSecrets, encryptedSecretsResponse.ImportedSecrets) if err != nil { - return nil, api.GetEncryptedSecretsV3Response{}, err + return nil, model.RequestUpdateUpdateDetails{}, err } // expand secrets that are referenced expandedSecrets := ExpandSecrets(plainTextSecretsMergedWithImports, fullServiceToken) - return expandedSecrets, encryptedSecretsResponse, nil + return expandedSecrets, model.RequestUpdateUpdateDetails{ + Modified: encryptedSecretsResponse.Modified, + ETag: encryptedSecretsResponse.ETag, + }, nil } // Fetches plaintext secrets from an API endpoint using a service account. // The function fetches the service account details and keys, decrypts the workspace key, fetches the encrypted secrets for the specified project and environment, and decrypts the secrets using the decrypted workspace key. // Returns the plaintext secrets, encrypted secrets response, and any errors that occurred during the process. -func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccountDetails, projectId string, environmentName string, etag string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV3Response, error) { +func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccountDetails, projectId string, environmentName string, etag string) ([]model.SingleEnvironmentVariable, model.RequestUpdateUpdateDetails, error) { httpClient := resty.New() httpClient.SetAuthToken(serviceAccountCreds.AccessKey). SetHeader("Accept", "application/json") serviceAccountDetails, err := api.CallGetServiceTokenAccountDetailsV2(httpClient) if err != nil { - return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account details. [err=%v]", err) + return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account details. [err=%v]", err) } serviceAccountKeys, err := api.CallGetServiceAccountKeysV2(httpClient, api.GetServiceAccountKeysRequest{ServiceAccountId: serviceAccountDetails.ServiceAccount.ID}) if err != nil { - return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account key details. [err=%v]", err) + return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account key details. [err=%v]", err) } // find key for requested project @@ -132,28 +170,28 @@ func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccou } if workspaceServiceAccountKey.ID == "" || workspaceServiceAccountKey.EncryptedKey == "" || workspaceServiceAccountKey.Nonce == "" || serviceAccountCreds.PublicKey == "" || serviceAccountCreds.PrivateKey == "" { - return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to find key for [projectId=%s] [err=%v]. Ensure that the given service account has access to given projectId", projectId, err) + return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("unable to find key for [projectId=%s] [err=%v]. Ensure that the given service account has access to given projectId", projectId, err) } cipherText, err := base64.StdEncoding.DecodeString(workspaceServiceAccountKey.EncryptedKey) if err != nil { - return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode EncryptedKey secrets because [err=%v]", err) + return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode EncryptedKey secrets because [err=%v]", err) } nonce, err := base64.StdEncoding.DecodeString(workspaceServiceAccountKey.Nonce) if err != nil { - return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode nonce secrets because [err=%v]", err) + return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode nonce secrets because [err=%v]", err) } publickey, err := base64.StdEncoding.DecodeString(serviceAccountCreds.PublicKey) if err != nil { - return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PublicKey secrets because [err=%v]", err) + return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PublicKey secrets because [err=%v]", err) } privateKey, err := base64.StdEncoding.DecodeString(serviceAccountCreds.PrivateKey) if err != nil { - return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PrivateKey secrets because [err=%v]", err) + return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PrivateKey secrets because [err=%v]", err) } plainTextWorkspaceKey := crypto.DecryptAsymmetric(cipherText, nonce, publickey, privateKey) @@ -165,15 +203,18 @@ func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccou }) if err != nil { - return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to fetch secrets because [err=%v]", err) + return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("unable to fetch secrets because [err=%v]", err) } plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse.Secrets) if err != nil { - return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get plain text secrets because [err=%v]", err) + return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get plain text secrets because [err=%v]", err) } - return plainTextSecrets, encryptedSecretsResponse, nil + return plainTextSecrets, model.RequestUpdateUpdateDetails{ + Modified: encryptedSecretsResponse.Modified, + ETag: encryptedSecretsResponse.ETag, + }, nil } func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV string, tag string) (DecodedSymmetricEncryptionDetails, error) {