diff --git a/k8-operator/api/v1alpha1/infisicalsecret_types.go b/k8-operator/api/v1alpha1/infisicalsecret_types.go index 203394417..3b61bd215 100644 --- a/k8-operator/api/v1alpha1/infisicalsecret_types.go +++ b/k8-operator/api/v1alpha1/infisicalsecret_types.go @@ -4,8 +4,19 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +type Authentication struct { + // +kubebuilder:validation:Optional + ServiceAccount ServiceAccountDetails `json:"serviceAccount"` + // +kubebuilder:validation:Optional + ServiceToken ServiceTokenDetails `json:"serviceToken"` +} + type ServiceTokenDetails struct { + // +kubebuilder:validation:Required ServiceTokenSecretReference KubeSecretReference `json:"serviceTokenSecretReference"` + + // +kubebuilder:validation:Required + SecretsScope SecretScopeInWorkspace `json:"secretsScope"` } type ServiceAccountDetails struct { @@ -14,11 +25,12 @@ type ServiceAccountDetails struct { EnvironmentName string `json:"environmentName"` } -type Authentication struct { - // +kubebuilder:validation:Optional - ServiceAccount ServiceAccountDetails `json:"serviceAccount"` - // +kubebuilder:validation:Optional - ServiceToken ServiceTokenDetails `json:"serviceToken"` +type SecretScopeInWorkspace struct { + // +kubebuilder:validation:Required + SecretsPath string `json:"secretsPath"` + + // +kubebuilder:validation:Required + EnvSlug string `json:"envSlug"` } type KubeSecretReference struct { diff --git a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go index 97c0f4fc7..01000431c 100644 --- a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -157,6 +157,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 *SecretScopeInWorkspace) DeepCopyInto(out *SecretScopeInWorkspace) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretScopeInWorkspace. +func (in *SecretScopeInWorkspace) DeepCopy() *SecretScopeInWorkspace { + if in == nil { + return nil + } + out := new(SecretScopeInWorkspace) + 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 @@ -177,6 +192,7 @@ func (in *ServiceAccountDetails) DeepCopy() *ServiceAccountDetails { func (in *ServiceTokenDetails) DeepCopyInto(out *ServiceTokenDetails) { *out = *in out.ServiceTokenSecretReference = in.ServiceTokenSecretReference + out.SecretsScope = in.SecretsScope } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceTokenDetails. 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 f47efb061..07433f781 100644 --- a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml +++ b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml @@ -63,6 +63,16 @@ spec: type: object serviceToken: properties: + secretsScope: + properties: + envSlug: + type: string + secretsPath: + type: string + required: + - envSlug + - secretsPath + type: object serviceTokenSecretReference: properties: secretName: @@ -77,6 +87,7 @@ spec: - secretNamespace type: object required: + - secretsScope - serviceTokenSecretReference type: object type: object diff --git a/k8-operator/config/samples/sample.yaml b/k8-operator/config/samples/sample.yaml index 971df9f7c..4c5059d9f 100644 --- a/k8-operator/config/samples/sample.yaml +++ b/k8-operator/config/samples/sample.yaml @@ -3,8 +3,8 @@ kind: InfisicalSecret metadata: name: infisicalsecret-sample spec: - hostAPI: http://localhost:7070/api - resyncInterval: 60 + hostAPI: http://localhost:8764/api + resyncInterval: 10 authentication: serviceAccount: serviceAccountSecretReference: @@ -16,10 +16,13 @@ spec: serviceTokenSecretReference: secretName: service-token secretNamespace: default + secretsScope: + envSlug: dev + secretsPath: "/" managedSecretReference: secretName: managed-secret secretNamespace: default - # To be depreciated soon - tokenSecretReference: - secretName: service-token - secretNamespace: default + # # To be depreciated soon + # tokenSecretReference: + # secretName: service-token + # secretNamespace: default diff --git a/k8-operator/controllers/infisicalsecret_helper.go b/k8-operator/controllers/infisicalsecret_helper.go index 32216e2e3..509de94ac 100644 --- a/k8-operator/controllers/infisicalsecret_helper.go +++ b/k8-operator/controllers/infisicalsecret_helper.go @@ -219,7 +219,10 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context fmt.Println("ReconcileInfisicalSecret: Fetched secrets via service account") } else if infisicalToken != "" { - plainTextSecretsFromApi, fullEncryptedSecretsResponse, err = util.GetPlainTextSecretsViaServiceToken(infisicalToken, secretVersionBasedOnETag) + envSlug := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.EnvSlug + secretsPath := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.SecretsPath + + plainTextSecretsFromApi, fullEncryptedSecretsResponse, err = util.GetPlainTextSecretsViaServiceToken(infisicalToken, secretVersionBasedOnETag, envSlug, secretsPath) if err != nil { return fmt.Errorf("\nfailed to get secrets because [err=%v]", err) } diff --git a/k8-operator/packages/util/secrets.go b/k8-operator/packages/util/secrets.go index 5087cd03a..28ba47253 100644 --- a/k8-operator/packages/util/secrets.go +++ b/k8-operator/packages/util/secrets.go @@ -3,6 +3,8 @@ package util import ( "encoding/base64" "fmt" + "path" + "regexp" "strings" "github.com/Infisical/infisical/k8-operator/packages/api" @@ -48,7 +50,7 @@ func GetServiceTokenDetails(infisicalToken string) (api.GetServiceTokenDetailsRe return serviceTokenDetails, nil } -func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV3Response, error) { +func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string, envSlug string, secretPath string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV3Response, 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") @@ -68,9 +70,9 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([ encryptedSecretsResponse, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{ WorkspaceId: serviceTokenDetails.Workspace, - Environment: serviceTokenDetails.Environment, + Environment: envSlug, ETag: etag, - SecretPath: serviceTokenDetails.SecretPath, + SecretPath: secretPath, }) if err != nil { @@ -92,7 +94,10 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([ return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err) } - return plainTextSecrets, encryptedSecretsResponse, nil + // expand secrets that are referenced + expandedSecrets := ExpandSecrets(plainTextSecrets, fullServiceToken) + + return expandedSecrets, encryptedSecretsResponse, nil } // Fetches plaintext secrets from an API endpoint using a service account. @@ -252,3 +257,104 @@ func GetPlainTextSecrets(key []byte, encryptedSecretsResponse api.GetEncryptedSe return plainTextSecrets, nil } + +var secRefRegex = regexp.MustCompile(`\${([^\}]*)}`) + +func recursivelyExpandSecret(expandedSecs map[string]string, interpolatedSecs map[string]string, crossSecRefFetch func(env string, path []string, key string) string, key string) string { + if v, ok := expandedSecs[key]; ok { + return v + } + + interpolatedVal, ok := interpolatedSecs[key] + if !ok { + return "" + // panic(fmt.Errorf("Could not find referred secret with key name %s", key), "Please check it refers a") + } + + refs := secRefRegex.FindAllStringSubmatch(interpolatedVal, -1) + for _, val := range refs { + // key: "${something}" val: [${something},something] + interpolatedExp, interpolationKey := val[0], val[1] + ref := strings.Split(interpolationKey, ".") + + // ${KEY1} => [key1] + if len(ref) == 1 { + val := recursivelyExpandSecret(expandedSecs, interpolatedSecs, crossSecRefFetch, interpolationKey) + interpolatedVal = strings.ReplaceAll(interpolatedVal, interpolatedExp, val) + continue + } + + // cross board reference ${env.folder.key1} => [env folder key1] + if len(ref) > 1 { + secEnv, tmpSecPath, secKey := ref[0], ref[1:len(ref)-1], ref[len(ref)-1] + interpolatedSecs[interpolationKey] = crossSecRefFetch(secEnv, tmpSecPath, secKey) // get the reference value + val := recursivelyExpandSecret(expandedSecs, interpolatedSecs, crossSecRefFetch, interpolationKey) + interpolatedVal = strings.ReplaceAll(interpolatedVal, interpolatedExp, val) + } + + } + expandedSecs[key] = interpolatedVal + return interpolatedVal +} + +func ExpandSecrets(secrets []model.SingleEnvironmentVariable, infisicalToken string) []model.SingleEnvironmentVariable { + expandedSecs := make(map[string]string) + interpolatedSecs := make(map[string]string) + // map[env.secret-path][keyname]Secret + crossEnvRefSecs := make(map[string]map[string]model.SingleEnvironmentVariable) // a cache to hold all cross board reference secrets + + for _, sec := range secrets { + // get all references in a secret + refs := secRefRegex.FindAllStringSubmatch(sec.Value, -1) + // nil means its a secret without reference + if refs == nil { + expandedSecs[sec.Key] = sec.Value // atomic secrets without any interpolation + } else { + interpolatedSecs[sec.Key] = sec.Value + } + } + + for i, sec := range secrets { + // already present pick that up + if expandedVal, ok := expandedSecs[sec.Key]; ok { + secrets[i].Value = expandedVal + continue + } + + expandedVal := recursivelyExpandSecret(expandedSecs, interpolatedSecs, func(env string, secPaths []string, secKey string) string { + secPaths = append([]string{"/"}, secPaths...) + secPath := path.Join(secPaths...) + + secPathDot := strings.Join(secPaths, ".") + uniqKey := fmt.Sprintf("%s.%s", env, secPathDot) + + if crossRefSec, ok := crossEnvRefSecs[uniqKey]; !ok { + // if not in cross reference cache, fetch it from server + refSecs, _, err := GetPlainTextSecretsViaServiceToken(infisicalToken, "", env, secPath) + if err != nil { + fmt.Println("HELLO===>", "MOO", err) + // HandleError(err, fmt.Sprintf("Could not fetch secrets in environment: %s secret-path: %s", env, secPath), "If you are using a service token to fetch secrets, please ensure it is valid") + } + refSecsByKey := getSecretsByKeys(refSecs) + // save it to avoid calling api again for same environment and folder path + crossEnvRefSecs[uniqKey] = refSecsByKey + return refSecsByKey[secKey].Value + } else { + return crossRefSec[secKey].Value + } + }, sec.Key) + + secrets[i].Value = expandedVal + } + return secrets +} + +func getSecretsByKeys(secrets []model.SingleEnvironmentVariable) map[string]model.SingleEnvironmentVariable { + secretMapByName := make(map[string]model.SingleEnvironmentVariable, len(secrets)) + + for _, secret := range secrets { + secretMapByName[secret.Key] = secret + } + + return secretMapByName +}