From 619fe553ef4fa822566d31ea6c561b2a7a0db282 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 16 Apr 2023 16:51:36 -0700 Subject: [PATCH] update k8 operator to use service account --- .../api/v1alpha1/infisicalsecret_types.go | 18 +++- .../api/v1alpha1/zz_generated.deepcopy.go | 33 +++++++ ...ecrets.infisical.com_infisicalsecrets.yaml | 31 +++++++ k8-operator/config/samples/sample.yaml | 9 +- .../samples/serviceAccountCredsSecret.yaml | 9 ++ .../controllers/infisicalsecret_helper.go | 80 ++++++++++++++--- k8-operator/go.mod | 10 +-- k8-operator/go.sum | 10 +++ k8-operator/packages/api/api.go | 57 ++++++++++++ k8-operator/packages/api/models.go | 53 +++++++++++ k8-operator/packages/crypto/crypto.go | 7 ++ k8-operator/packages/model/model.go | 14 +++ k8-operator/packages/util/secrets.go | 87 ++++++++++++++++--- 13 files changed, 388 insertions(+), 30 deletions(-) create mode 100644 k8-operator/config/samples/serviceAccountCredsSecret.yaml create mode 100644 k8-operator/packages/model/model.go diff --git a/k8-operator/api/v1alpha1/infisicalsecret_types.go b/k8-operator/api/v1alpha1/infisicalsecret_types.go index f6027d1da..3d02e55ee 100644 --- a/k8-operator/api/v1alpha1/infisicalsecret_types.go +++ b/k8-operator/api/v1alpha1/infisicalsecret_types.go @@ -4,6 +4,16 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +type Authentication struct { + ServiceAccount ServiceAccountDetails `json:"serviceAccount"` +} + +type ServiceAccountDetails struct { + ServiceAccountSecretReference KubeSecretReference `json:"serviceAccountSecretReference"` + ProjectId string `json:"projectId"` + EnvironmentName string `json:"environmentName"` +} + type KubeSecretReference struct { // The name of the Kubernetes Secret // +kubebuilder:validation:Required @@ -16,10 +26,14 @@ type KubeSecretReference struct { // InfisicalSecretSpec defines the desired state of InfisicalSecret type InfisicalSecretSpec struct { - // +kubebuilder:validation:Required + // +kubebuilder:validation:Optional TokenSecretReference KubeSecretReference `json:"tokenSecretReference,omitempty"` + + // +kubebuilder:validation:Optional + Authentication Authentication `json:"authentication,omitempty"` + // +kubebuilder:validation:Required - ManagedSecretReference KubeSecretReference `json:"managedSecretReference,omitempty"` + ManagedSecretReference KubeSecretReference `json:"managedSecretReference"` // Infisical host to pull secrets from HostAPI string `json:"hostAPI,omitempty"` diff --git a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go index af9eff318..c05e757b8 100644 --- a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -26,6 +26,22 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Authentication) DeepCopyInto(out *Authentication) { + *out = *in + out.ServiceAccount = in.ServiceAccount +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Authentication. +func (in *Authentication) DeepCopy() *Authentication { + if in == nil { + return nil + } + out := new(Authentication) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InfisicalSecret) DeepCopyInto(out *InfisicalSecret) { *out = *in @@ -89,6 +105,7 @@ func (in *InfisicalSecretList) DeepCopyObject() runtime.Object { func (in *InfisicalSecretSpec) DeepCopyInto(out *InfisicalSecretSpec) { *out = *in out.TokenSecretReference = in.TokenSecretReference + out.Authentication = in.Authentication out.ManagedSecretReference = in.ManagedSecretReference } @@ -138,3 +155,19 @@ func (in *KubeSecretReference) DeepCopy() *KubeSecretReference { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceAccountDetails) DeepCopyInto(out *ServiceAccountDetails) { + *out = *in + out.ServiceAccountSecretReference = in.ServiceAccountSecretReference +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceAccountDetails. +func (in *ServiceAccountDetails) DeepCopy() *ServiceAccountDetails { + if in == nil { + return nil + } + out := new(ServiceAccountDetails) + 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 2064d76e8..a62fa651a 100644 --- a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml +++ b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml @@ -35,6 +35,35 @@ spec: spec: description: InfisicalSecretSpec defines the desired state of InfisicalSecret properties: + authentication: + properties: + serviceAccount: + properties: + environmentName: + type: string + projectId: + type: string + serviceAccountSecretReference: + 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 + required: + - environmentName + - projectId + - serviceAccountSecretReference + type: object + required: + - serviceAccount + type: object hostAPI: description: Infisical host to pull secrets from type: string @@ -62,6 +91,8 @@ spec: - secretName - secretNamespace type: object + required: + - managedSecretReference type: object status: description: InfisicalSecretStatus defines the observed state of InfisicalSecret diff --git a/k8-operator/config/samples/sample.yaml b/k8-operator/config/samples/sample.yaml index 236e55baf..adecbedbb 100644 --- a/k8-operator/config/samples/sample.yaml +++ b/k8-operator/config/samples/sample.yaml @@ -3,10 +3,17 @@ kind: InfisicalSecret metadata: name: infisicalsecret-sample spec: - hostAPI: https://app.infisical.com/api + hostAPI: http://localhost:7070/api tokenSecretReference: secretName: service-token secretNamespace: default + authentication: + serviceAccount: + serviceAccountSecretReference: + secretName: service-account + secretNamespace: default + projectId: "6439ec224cfbf7ea2a95b651" + environmentName: "dev" managedSecretReference: secretName: managed-secret secretNamespace: default diff --git a/k8-operator/config/samples/serviceAccountCredsSecret.yaml b/k8-operator/config/samples/serviceAccountCredsSecret.yaml new file mode 100644 index 000000000..859312ee2 --- /dev/null +++ b/k8-operator/config/samples/serviceAccountCredsSecret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: service-account +type: Opaque +stringData: + serviceAccountAccessKey: <> + serviceAccountPrivateKey: <> + serviceAccountPublicKey: <> diff --git a/k8-operator/controllers/infisicalsecret_helper.go b/k8-operator/controllers/infisicalsecret_helper.go index fef8b5a33..e528b19d5 100644 --- a/k8-operator/controllers/infisicalsecret_helper.go +++ b/k8-operator/controllers/infisicalsecret_helper.go @@ -7,6 +7,7 @@ import ( "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" "k8s.io/apimachinery/pkg/api/errors" @@ -14,6 +15,10 @@ import ( "k8s.io/apimachinery/pkg/types" ) +const SERVICE_ACCOUNT_ACCESS_KEY = "serviceAccountAccessKey" +const SERVICE_ACCOUNT_PUBLIC_KEY = "serviceAccountPublicKey" +const SERVICE_ACCOUNT_PRIVATE_KEY = "serviceAccountPrivateKey" + 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" @@ -69,19 +74,47 @@ func (r *InfisicalSecretReconciler) GetInfisicalTokenFromKubeSecret(ctx context. Name: infisicalSecret.Spec.TokenSecretReference.SecretName, }) + if errors.IsNotFound(err) { + return "", nil + } + if err != nil { return "", fmt.Errorf("failed to read Infisical token secret from secret named [%s] in namespace [%s]: with error [%w]", infisicalSecret.Spec.TokenSecretReference.SecretName, infisicalSecret.Spec.TokenSecretReference.SecretNamespace, err) } infisicalServiceToken := tokenSecret.Data[INFISICAL_TOKEN_SECRET_KEY_NAME] - if infisicalServiceToken == nil { - return "", fmt.Errorf("the Infisical token is not set in the Kubernetes secret. Please add the key [%s] with the corresponding token value", INFISICAL_TOKEN_SECRET_KEY_NAME) - } return strings.Replace(string(infisicalServiceToken), " ", "", -1), nil } -func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, secretsFromAPI []util.SingleEnvironmentVariable, encryptedSecretsResponse api.GetEncryptedSecretsV2Response) error { +// 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) { + serviceAccountCredsFromKubeSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{ + Namespace: infisicalSecret.Spec.Authentication.ServiceAccount.ServiceAccountSecretReference.SecretNamespace, + Name: infisicalSecret.Spec.Authentication.ServiceAccount.ServiceAccountSecretReference.SecretName, + }) + + if errors.IsNotFound(err) { + return model.ServiceAccountDetails{}, nil + } + + if err != nil { + return model.ServiceAccountDetails{}, fmt.Errorf("something went wrong when fetching your service account credentials [err=%s]", err) + } + + accessKeyFromSecret := serviceAccountCredsFromKubeSecret.Data[SERVICE_ACCOUNT_ACCESS_KEY] + publicKeyFromSecret := serviceAccountCredsFromKubeSecret.Data[SERVICE_ACCOUNT_PUBLIC_KEY] + privateKeyFromSecret := serviceAccountCredsFromKubeSecret.Data[SERVICE_ACCOUNT_PRIVATE_KEY] + + if accessKeyFromSecret == nil || publicKeyFromSecret == nil || privateKeyFromSecret == nil { + return model.ServiceAccountDetails{}, nil + } + + 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.GetEncryptedSecretsV2Response) error { plainProcessedSecrets := make(map[string][]byte) for _, secret := range secretsFromAPI { plainProcessedSecrets[secret.Key] = []byte(secret.Value) // plain process @@ -109,7 +142,7 @@ func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context return nil } -func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context.Context, managedKubeSecret corev1.Secret, secretsFromAPI []util.SingleEnvironmentVariable, encryptedSecretsResponse api.GetEncryptedSecretsV2Response) error { +func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context.Context, managedKubeSecret corev1.Secret, secretsFromAPI []model.SingleEnvironmentVariable, encryptedSecretsResponse api.GetEncryptedSecretsV2Response) error { plainProcessedSecrets := make(map[string][]byte) for _, secret := range secretsFromAPI { plainProcessedSecrets[secret.Key] = []byte(secret.Value) @@ -131,6 +164,15 @@ func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) error { infisicalToken, err := r.GetInfisicalTokenFromKubeSecret(ctx, infisicalSecret) + if err != nil { + return fmt.Errorf("ReconcileInfisicalSecret: unable to get service token from kube secret [err=%s]", err) + } + + 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) + } + 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) @@ -146,15 +188,33 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context return fmt.Errorf("something went wrong when fetching the managed Kubernetes secret [%w]", err) } + // Get exiting Etag if exists secretVersionBasedOnETag := "" - if managedKubeSecret != nil { secretVersionBasedOnETag = managedKubeSecret.Annotations[SECRET_VERSION_ANNOTATION] } - plainTextSecretsFromApi, fullEncryptedSecretsResponse, err := util.GetPlainTextSecretsViaServiceToken(infisicalToken, secretVersionBasedOnETag) - if err != nil { - return fmt.Errorf("failed to get secrets because [err=%v]\n", err) + var plainTextSecretsFromApi []model.SingleEnvironmentVariable + var fullEncryptedSecretsResponse api.GetEncryptedSecretsV2Response + + if serviceAccountCreds.AccessKey != "" || serviceAccountCreds.PrivateKey != "" || serviceAccountCreds.PublicKey != "" { + plainTextSecretsFromApi, fullEncryptedSecretsResponse, 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 != "" { + plainTextSecretsFromApi, fullEncryptedSecretsResponse, err = util.GetPlainTextSecretsViaServiceToken(infisicalToken, secretVersionBasedOnETag) + if err != nil { + return fmt.Errorf("\nfailed to get secrets because [err=%v]", err) + } + + fmt.Println("ReconcileInfisicalSecret: Fetched secrets via service token") + + } 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 { @@ -162,8 +222,6 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context return nil } - fmt.Println("secret is modified so it needs to be created or updated") - if managedKubeSecret == nil { return r.CreateInfisicalManagedKubeSecret(ctx, infisicalSecret, plainTextSecretsFromApi, fullEncryptedSecretsResponse) } else { diff --git a/k8-operator/go.mod b/k8-operator/go.mod index a4b615409..6de068455 100644 --- a/k8-operator/go.mod +++ b/k8-operator/go.mod @@ -57,12 +57,12 @@ require ( go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.24.0 // indirect - golang.org/x/crypto v0.1.0 - golang.org/x/net v0.7.0 // indirect + golang.org/x/crypto v0.8.0 + golang.org/x/net v0.9.0 // indirect golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect - golang.org/x/sys v0.5.0 // indirect - golang.org/x/term v0.5.0 // indirect - golang.org/x/text v0.7.0 // indirect + golang.org/x/sys v0.7.0 // indirect + golang.org/x/term v0.7.0 // indirect + golang.org/x/text v0.9.0 // indirect golang.org/x/time v0.3.0 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/k8-operator/go.sum b/k8-operator/go.sum index 1ae17974a..5b5f8f3d0 100644 --- a/k8-operator/go.sum +++ b/k8-operator/go.sum @@ -392,6 +392,8 @@ golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38= golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -474,6 +476,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgk golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -566,11 +570,15 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -583,6 +591,8 @@ golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/k8-operator/packages/api/api.go b/k8-operator/packages/api/api.go index ac7c47d6d..bb719f650 100644 --- a/k8-operator/packages/api/api.go +++ b/k8-operator/packages/api/api.go @@ -78,3 +78,60 @@ func CallGetSecretsV2(httpClient *resty.Client, request GetEncryptedSecretsV2Req return encryptedSecretsResponse, nil } + +func CallGetServiceTokenAccountDetailsV2(httpClient *resty.Client) (ServiceAccountDetailsResponse, error) { + var serviceAccountDetailsResponse ServiceAccountDetailsResponse + response, err := httpClient. + R(). + SetResult(&serviceAccountDetailsResponse). + SetHeader("User-Agent", USER_AGENT_NAME). + Get(fmt.Sprintf("%v/v2/service-accounts/me", API_HOST_URL)) + + if err != nil { + return ServiceAccountDetailsResponse{}, fmt.Errorf("CallGetServiceTokenAccountDetailsV2: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return ServiceAccountDetailsResponse{}, fmt.Errorf("CallGetServiceTokenAccountDetailsV2: Unsuccessful response: [response=%s]", response) + } + + return serviceAccountDetailsResponse, nil +} + +func CallGetServiceAccountWorkspacePermissionsV2(httpClient *resty.Client) (ServiceAccountWorkspacePermissions, error) { + var serviceAccountWorkspacePermissionsResponse ServiceAccountWorkspacePermissions + response, err := httpClient. + R(). + SetResult(&serviceAccountWorkspacePermissionsResponse). + SetHeader("User-Agent", USER_AGENT_NAME). + Get(fmt.Sprintf("%v/v2/service-accounts//permissions/workspace", API_HOST_URL)) + + if err != nil { + return ServiceAccountWorkspacePermissions{}, fmt.Errorf("CallGetServiceAccountWorkspacePermissionsV2: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return ServiceAccountWorkspacePermissions{}, fmt.Errorf("CallGetServiceAccountWorkspacePermissionsV2: Unsuccessful response: [response=%s]", response) + } + + return serviceAccountWorkspacePermissionsResponse, nil +} + +func CallGetServiceAccountKeysV2(httpClient *resty.Client, request GetServiceAccountKeysRequest) (GetServiceAccountKeysResponse, error) { + var serviceAccountKeysResponse GetServiceAccountKeysResponse + response, err := httpClient. + R(). + SetResult(&serviceAccountKeysResponse). + SetHeader("User-Agent", USER_AGENT_NAME). + Get(fmt.Sprintf("%v/v2/service-accounts/%v/keys", API_HOST_URL, request.ServiceAccountId)) + + if err != nil { + return GetServiceAccountKeysResponse{}, fmt.Errorf("CallGetServiceAccountKeysV2: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return GetServiceAccountKeysResponse{}, fmt.Errorf("CallGetServiceAccountKeysV2: Unsuccessful response: [response=%s]", response) + } + + return serviceAccountKeysResponse, nil +} diff --git a/k8-operator/packages/api/models.go b/k8-operator/packages/api/models.go index 7a34b8f7a..623cf05c9 100644 --- a/k8-operator/packages/api/models.go +++ b/k8-operator/packages/api/models.go @@ -65,3 +65,56 @@ type GetServiceTokenDetailsResponse struct { Iv string `json:"iv"` Tag string `json:"tag"` } + +type ServiceAccountDetailsResponse struct { + ServiceAccount struct { + ID string `json:"_id"` + Name string `json:"name"` + Organization string `json:"organization"` + PublicKey string `json:"publicKey"` + LastUsed time.Time `json:"lastUsed"` + ExpiresAt time.Time `json:"expiresAt"` + } `json:"serviceAccount"` +} + +type ServiceAccountWorkspacePermission struct { + ID string `json:"_id"` + ServiceAccount string `json:"serviceAccount"` + Workspace struct { + ID string `json:"_id"` + Name string `json:"name"` + AutoCapitalization bool `json:"autoCapitalization"` + Organization string `json:"organization"` + Environments []struct { + Name string `json:"name"` + Slug string `json:"slug"` + ID string `json:"_id"` + } `json:"environments"` + } `json:"workspace"` + Environment string `json:"environment"` + Read bool `json:"read"` + Write bool `json:"write"` +} + +type ServiceAccountWorkspacePermissions struct { + ServiceAccountWorkspacePermission []ServiceAccountWorkspacePermissions `json:"serviceAccountWorkspacePermissions"` +} + +type GetServiceAccountKeysRequest struct { + ServiceAccountId string `json:"id"` +} + +type ServiceAccountKey struct { + ID string `json:"_id"` + EncryptedKey string `json:"encryptedKey"` + Nonce string `json:"nonce"` + Sender string `json:"sender"` + ServiceAccount string `json:"serviceAccount"` + Workspace string `json:"workspace"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type GetServiceAccountKeysResponse struct { + ServiceAccountKeys []ServiceAccountKey `json:"serviceAccountKeys"` +} diff --git a/k8-operator/packages/crypto/crypto.go b/k8-operator/packages/crypto/crypto.go index d5b0a9955..e8633e273 100644 --- a/k8-operator/packages/crypto/crypto.go +++ b/k8-operator/packages/crypto/crypto.go @@ -3,6 +3,8 @@ package crypto import ( "crypto/aes" "crypto/cipher" + + "golang.org/x/crypto/nacl/box" ) func DecryptSymmetric(key []byte, encryptedPrivateKey []byte, tag []byte, IV []byte) ([]byte, error) { @@ -26,3 +28,8 @@ func DecryptSymmetric(key []byte, encryptedPrivateKey []byte, tag []byte, IV []b return plaintext, nil } + +func DecryptAsymmetric(ciphertext []byte, nonce []byte, publicKey []byte, privateKey []byte) (plainText []byte) { + plainTextToReturn, _ := box.Open(nil, ciphertext, (*[24]byte)(nonce), (*[32]byte)(publicKey), (*[32]byte)(privateKey)) + return plainTextToReturn +} diff --git a/k8-operator/packages/model/model.go b/k8-operator/packages/model/model.go new file mode 100644 index 000000000..b7d4780b2 --- /dev/null +++ b/k8-operator/packages/model/model.go @@ -0,0 +1,14 @@ +package model + +type ServiceAccountDetails struct { + AccessKey string + PublicKey string + PrivateKey string +} + +type SingleEnvironmentVariable struct { + Key string `json:"key"` + Value string `json:"value"` + Type string `json:"type"` + ID string `json:"_id"` +} diff --git a/k8-operator/packages/util/secrets.go b/k8-operator/packages/util/secrets.go index 4cbf24648..8134fb882 100644 --- a/k8-operator/packages/util/secrets.go +++ b/k8-operator/packages/util/secrets.go @@ -7,16 +7,10 @@ import ( "github.com/Infisical/infisical/k8-operator/packages/api" "github.com/Infisical/infisical/k8-operator/packages/crypto" + "github.com/Infisical/infisical/k8-operator/packages/model" "github.com/go-resty/resty/v2" ) -type SingleEnvironmentVariable struct { - Key string `json:"key"` - Value string `json:"value"` - Type string `json:"type"` - ID string `json:"_id"` -} - type DecodedSymmetricEncryptionDetails = struct { Cipher []byte IV []byte @@ -54,7 +48,7 @@ func GetServiceTokenDetails(infisicalToken string) (api.GetServiceTokenDetailsRe return serviceTokenDetails, nil } -func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([]SingleEnvironmentVariable, api.GetEncryptedSecretsV2Response, error) { +func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV2Response, error) { serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4) if len(serviceTokenParts) < 4 { return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again") @@ -100,6 +94,77 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([ return plainTextSecrets, encryptedSecretsResponse, 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.GetEncryptedSecretsV2Response, error) { + httpClient := resty.New() + httpClient.SetAuthToken(serviceAccountCreds.AccessKey). + SetHeader("Accept", "application/json") + + serviceAccountDetails, err := api.CallGetServiceTokenAccountDetailsV2(httpClient) + if err != nil { + return nil, api.GetEncryptedSecretsV2Response{}, 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.GetEncryptedSecretsV2Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account key details. [err=%v]", err) + } + + // find key for requested project + var workspaceServiceAccountKey api.ServiceAccountKey + for _, serviceAccountKey := range serviceAccountKeys.ServiceAccountKeys { + if serviceAccountKey.Workspace == projectId { + workspaceServiceAccountKey = serviceAccountKey + } + } + + if workspaceServiceAccountKey.ID == "" || workspaceServiceAccountKey.EncryptedKey == "" || workspaceServiceAccountKey.Nonce == "" || serviceAccountCreds.PublicKey == "" || serviceAccountCreds.PrivateKey == "" { + return nil, api.GetEncryptedSecretsV2Response{}, 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.GetEncryptedSecretsV2Response{}, 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.GetEncryptedSecretsV2Response{}, 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.GetEncryptedSecretsV2Response{}, 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.GetEncryptedSecretsV2Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PrivateKey secrets because [err=%v]", err) + } + + plainTextWorkspaceKey := crypto.DecryptAsymmetric(cipherText, nonce, publickey, privateKey) + + encryptedSecretsResponse, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{ + WorkspaceId: projectId, + Environment: environmentName, + ETag: etag, + }) + + if err != nil { + return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to fetch secrets because [err=%v]", err) + } + + plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse) + if err != nil { + return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get plain text secrets because [err=%v]", err) + } + + return plainTextSecrets, encryptedSecretsResponse, nil +} + func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV string, tag string) (DecodedSymmetricEncryptionDetails, error) { cipherx, err := base64.StdEncoding.DecodeString(cipher) if err != nil { @@ -129,8 +194,8 @@ func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV st }, nil } -func GetPlainTextSecrets(key []byte, encryptedSecretsResponse api.GetEncryptedSecretsV2Response) ([]SingleEnvironmentVariable, error) { - plainTextSecrets := []SingleEnvironmentVariable{} +func GetPlainTextSecrets(key []byte, encryptedSecretsResponse api.GetEncryptedSecretsV2Response) ([]model.SingleEnvironmentVariable, error) { + plainTextSecrets := []model.SingleEnvironmentVariable{} for _, secret := range encryptedSecretsResponse.Secrets { // Decrypt key key_iv, err := base64.StdEncoding.DecodeString(secret.SecretKeyIV) @@ -174,7 +239,7 @@ func GetPlainTextSecrets(key []byte, encryptedSecretsResponse api.GetEncryptedSe return nil, fmt.Errorf("unable to symmetrically decrypt secret value") } - plainTextSecret := SingleEnvironmentVariable{ + plainTextSecret := model.SingleEnvironmentVariable{ Key: string(plainTextKey), Value: string(plainTextValue), Type: string(secret.Type),