diff --git a/Dockerfile b/Dockerfile index d9ab2d93b..ad2bdc8ce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.14.0 +FROM alpine:3.14.1 ARG TARGETOS ARG TARGETARCH COPY bin/external-secrets-${TARGETOS}-${TARGETARCH} /bin/external-secrets diff --git a/apis/externalsecrets/v1alpha1/secretstore_gcpsm_types.go b/apis/externalsecrets/v1alpha1/secretstore_gcpsm_types.go index e410db65c..709914880 100644 --- a/apis/externalsecrets/v1alpha1/secretstore_gcpsm_types.go +++ b/apis/externalsecrets/v1alpha1/secretstore_gcpsm_types.go @@ -31,7 +31,8 @@ type GCPSMAuthSecretRef struct { // GCPSMProvider Configures a store to sync secrets using the GCP Secret Manager provider. type GCPSMProvider struct { // Auth defines the information necessary to authenticate against GCP - Auth GCPSMAuth `json:"auth"` + // +optional + Auth GCPSMAuth `json:"auth,omitempty"` // ProjectID project where secret is located ProjectID string `json:"projectID,omitempty"` diff --git a/apis/meta/v1/types.go b/apis/meta/v1/types.go index 0bb3a2514..ffc6c4e5a 100644 --- a/apis/meta/v1/types.go +++ b/apis/meta/v1/types.go @@ -18,7 +18,7 @@ package v1 // In some instances, `key` is a required field. type SecretKeySelector struct { // The name of the Secret resource being referred to. - Name string `json:"name"` + Name string `json:"name,omitempty"` // Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults // to the namespace of the referent. // +optional diff --git a/deploy/charts/external-secrets/Chart.yaml b/deploy/charts/external-secrets/Chart.yaml index 95090b709..f2ebd97e8 100644 --- a/deploy/charts/external-secrets/Chart.yaml +++ b/deploy/charts/external-secrets/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: external-secrets description: External secret management for Kubernetes type: application -version: "0.3.3" -appVersion: "v0.3.3" +version: "0.3.4" +appVersion: "v0.3.4" kubeVersion: ">= 1.11.0-0" keywords: - kubernetes-external-secrets diff --git a/deploy/charts/external-secrets/README.md b/deploy/charts/external-secrets/README.md index 5a71de466..70c40be99 100644 --- a/deploy/charts/external-secrets/README.md +++ b/deploy/charts/external-secrets/README.md @@ -4,7 +4,7 @@ [//]: # (README.md generated by gotmpl. DO NOT EDIT.) -![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![Version: 0.3.3](https://img.shields.io/badge/Version-0.3.3-informational?style=flat-square) +![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![Version: 0.3.4](https://img.shields.io/badge/Version-0.3.4-informational?style=flat-square) External secret management for Kubernetes @@ -49,11 +49,13 @@ The command removes all the Kubernetes components associated with the chart and | podAnnotations | object | `{}` | | | podLabels | object | `{}` | | | podSecurityContext | object | `{}` | | +| priorityClassName | string | `""` | Pod priority class name. | | prometheus.enabled | bool | `false` | Specifies whether to expose Service resource for collecting Prometheus metrics | | prometheus.service.port | int | `8080` | | | rbac.create | bool | `true` | Specifies whether role and rolebinding resources should be created. | | replicaCount | int | `1` | | | resources | object | `{}` | | +| scopedNamespace | string | `""` | If set external secrets are only reconciled in the provided namespace | | securityContext | object | `{}` | | | serviceAccount.annotations | object | `{}` | Annotations to add to the service account. | | serviceAccount.create | bool | `true` | Specifies whether a service account should be created. | diff --git a/deploy/charts/external-secrets/templates/deployment.yaml b/deploy/charts/external-secrets/templates/deployment.yaml index 7ef0cbcb9..338b698a8 100644 --- a/deploy/charts/external-secrets/templates/deployment.yaml +++ b/deploy/charts/external-secrets/templates/deployment.yaml @@ -2,6 +2,7 @@ apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "external-secrets.fullname" . }} + namespace: {{ .Release.Namespace | quote }} labels: {{- include "external-secrets.labels" . | nindent 4 }} spec: @@ -38,11 +39,14 @@ spec: {{- end }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} - {{- if or (.Values.leaderElect) (.Values.extraArgs) }} + {{- if or (.Values.leaderElect) (.Values.scopedNamespace) (.Values.extraArgs) }} args: {{- if .Values.leaderElect }} - --enable-leader-election=true {{- end }} + {{- if .Values.scopedNamespace }} + - --namespace={{ .Values.scopedNamespace }} + {{- end }} {{- range $key, $value := .Values.extraArgs }} {{- if $value }} - --{{ $key }}={{ $value }} @@ -74,3 +78,6 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} + {{- if .Values.priorityClassName }} + priorityClassName: {{ .Values.priorityClassName }} + {{- end }} diff --git a/deploy/charts/external-secrets/templates/serviceaccount.yaml b/deploy/charts/external-secrets/templates/serviceaccount.yaml index 911638fb4..d3e58f78b 100644 --- a/deploy/charts/external-secrets/templates/serviceaccount.yaml +++ b/deploy/charts/external-secrets/templates/serviceaccount.yaml @@ -3,6 +3,7 @@ apiVersion: v1 kind: ServiceAccount metadata: name: {{ include "external-secrets.serviceAccountName" . }} + namespace: {{ .Release.Namespace | quote }} labels: {{- include "external-secrets.labels" . | nindent 4 }} {{- with .Values.serviceAccount.annotations }} diff --git a/deploy/charts/external-secrets/values.yaml b/deploy/charts/external-secrets/values.yaml index 419b06473..738733fc9 100644 --- a/deploy/charts/external-secrets/values.yaml +++ b/deploy/charts/external-secrets/values.yaml @@ -17,6 +17,10 @@ fullnameOverride: "" # than one instance of external-secrets operates at a time. leaderElect: false +# -- If set external secrets are only reconciled in the +# provided namespace +scopedNamespace: "" + serviceAccount: # -- Specifies whether a service account should be created. create: true @@ -66,3 +70,6 @@ nodeSelector: {} tolerations: [] affinity: {} + +# -- Pod priority class name. +priorityClassName: "" diff --git a/deploy/crds/external-secrets.io_clustersecretstores.yaml b/deploy/crds/external-secrets.io_clustersecretstores.yaml index eac6a56fc..be66d8f8a 100644 --- a/deploy/crds/external-secrets.io_clustersecretstores.yaml +++ b/deploy/crds/external-secrets.io_clustersecretstores.yaml @@ -108,8 +108,6 @@ spec: cluster-scoped defaults to the namespace of the referent. type: string - required: - - name type: object secretAccessKeySecretRef: description: The SecretAccessKey is used for authentication @@ -130,8 +128,6 @@ spec: cluster-scoped defaults to the namespace of the referent. type: string - required: - - name type: object type: object type: object @@ -179,8 +175,6 @@ spec: to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent. type: string - required: - - name type: object clientSecret: description: The Azure ClientSecret of the service principle @@ -200,8 +194,6 @@ spec: to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent. type: string - required: - - name type: object required: - clientId @@ -249,8 +241,6 @@ spec: cluster-scoped defaults to the namespace of the referent. type: string - required: - - name type: object type: object required: @@ -259,8 +249,6 @@ spec: projectID: description: ProjectID project where secret is located type: string - required: - - auth type: object ibm: description: IBM configures this store to sync secrets using IBM @@ -291,8 +279,6 @@ spec: cluster-scoped defaults to the namespace of the referent. type: string - required: - - name type: object type: object required: @@ -351,8 +337,6 @@ spec: cluster-scoped defaults to the namespace of the referent. type: string - required: - - name type: object required: - path @@ -384,8 +368,6 @@ spec: cluster-scoped defaults to the namespace of the referent. type: string - required: - - name type: object secretRef: description: SecretRef to a key in a Secret resource @@ -408,8 +390,6 @@ spec: cluster-scoped defaults to the namespace of the referent. type: string - required: - - name type: object type: object jwt: @@ -441,8 +421,6 @@ spec: cluster-scoped defaults to the namespace of the referent. type: string - required: - - name type: object type: object kubernetes: @@ -483,8 +461,6 @@ spec: cluster-scoped defaults to the namespace of the referent. type: string - required: - - name type: object serviceAccountRef: description: Optional service account field containing @@ -537,8 +513,6 @@ spec: cluster-scoped defaults to the namespace of the referent. type: string - required: - - name type: object username: description: Username is a LDAP user name used to @@ -566,8 +540,6 @@ spec: to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent. type: string - required: - - name type: object type: object caBundle: diff --git a/deploy/crds/external-secrets.io_secretstores.yaml b/deploy/crds/external-secrets.io_secretstores.yaml index b84336b5a..9c6cae9ee 100644 --- a/deploy/crds/external-secrets.io_secretstores.yaml +++ b/deploy/crds/external-secrets.io_secretstores.yaml @@ -108,8 +108,6 @@ spec: cluster-scoped defaults to the namespace of the referent. type: string - required: - - name type: object secretAccessKeySecretRef: description: The SecretAccessKey is used for authentication @@ -130,8 +128,6 @@ spec: cluster-scoped defaults to the namespace of the referent. type: string - required: - - name type: object type: object type: object @@ -179,8 +175,6 @@ spec: to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent. type: string - required: - - name type: object clientSecret: description: The Azure ClientSecret of the service principle @@ -200,8 +194,6 @@ spec: to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent. type: string - required: - - name type: object required: - clientId @@ -249,8 +241,6 @@ spec: cluster-scoped defaults to the namespace of the referent. type: string - required: - - name type: object type: object required: @@ -259,8 +249,6 @@ spec: projectID: description: ProjectID project where secret is located type: string - required: - - auth type: object ibm: description: IBM configures this store to sync secrets using IBM @@ -291,8 +279,6 @@ spec: cluster-scoped defaults to the namespace of the referent. type: string - required: - - name type: object type: object required: @@ -351,8 +337,6 @@ spec: cluster-scoped defaults to the namespace of the referent. type: string - required: - - name type: object required: - path @@ -384,8 +368,6 @@ spec: cluster-scoped defaults to the namespace of the referent. type: string - required: - - name type: object secretRef: description: SecretRef to a key in a Secret resource @@ -408,8 +390,6 @@ spec: cluster-scoped defaults to the namespace of the referent. type: string - required: - - name type: object type: object jwt: @@ -441,8 +421,6 @@ spec: cluster-scoped defaults to the namespace of the referent. type: string - required: - - name type: object type: object kubernetes: @@ -483,8 +461,6 @@ spec: cluster-scoped defaults to the namespace of the referent. type: string - required: - - name type: object serviceAccountRef: description: Optional service account field containing @@ -537,8 +513,6 @@ spec: cluster-scoped defaults to the namespace of the referent. type: string - required: - - name type: object username: description: Username is a LDAP user name used to @@ -566,8 +540,6 @@ spec: to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent. type: string - required: - - name type: object type: object caBundle: diff --git a/docs/provider-google-secrets-manager.md b/docs/provider-google-secrets-manager.md index b3f622a70..8f43e5a4b 100644 --- a/docs/provider-google-secrets-manager.md +++ b/docs/provider-google-secrets-manager.md @@ -2,11 +2,7 @@ External Secrets Operator integrates with [GCP Secret Manager](https://cloud.google.com/secret-manager) for secret management. -### Authentication - -At the moment, we only support [service account key](https://cloud.google.com/iam/docs/creating-managing-service-account-keys) authentication. - -#### Service account key authentication +### Service account key authentication A service account key is created and the JSON keyfile is stored in a `Kind=Secret`. The `project_id` and `private_key` should be configured for the project. @@ -33,3 +29,63 @@ The operator will fetch the GCP Secret Manager secret and inject it as a `Kind=S ``` kubectl get secret secret-to-be-created -n | -o jsonpath='{.data.dev-secret-test}' | base64 -d ``` + +## Authentication with Workload Identity + +This makes it possible for your Google Kubernetes Engine (GKE) applications to consume services provided by Google APIs, namely Secrets Manager service in this case. + +Here we will assume that you installed ESO using helm and that you named the chart installation `external-secrets` and the namespace where it lives `es` like: + +```sh +helm install external-secrets external-secrets/external-secrets --namespace es +``` + +Then most of the resources would have this name, the important one here being the k8s service account attached to the external-secrets operator deployment: + +``` +# ... + containers: + - image: ghcr.io/external-secrets/external-secrets:vVERSION + name: external-secrets + ports: + - containerPort: 8080 + protocol: TCP + restartPolicy: Always + schedulerName: default-scheduler + serviceAccount: external-secrets + serviceAccountName: external-secrets # <--- here +``` + +### Following the documentation + +You can find the documentation for Workload Identity under [this url](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity). We will walk you through how to navigate it here. + +#### Changing Values + +Search [the documment](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) for this editable values and change them to your values: + +- CLUSTER_NAME: The name of your cluster +- PROJECT_ID: Your project ID (not your Project number nor your Project name) +- K8S_NAMESPACE: For us folowing these steps here it will be `es`, but this will be the namespace where you deployed the external-secrets operator +- KSA_NAME: external-secrets (if you are not creating a new one to attach to the deployemnt) +- GSA_NAME: external-secrets for simplicity, or something else if you have to follow different naming convetions for cloud resources +- ROLE_NAME: roles/secretmanager.secretAccessor so you make the pod only be able to access secrets on Secret Manager + +#### Following through + +You can follow through the documentation and adapt it to your specific use case. If you want to just use the serviceaccount that we deployed with the helm chart, for example, you don't need to create a new service account on 2 of [Authenticating to Google Cloud](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity#authenticating_to). + +#### SecretStore with WorkloadIdentity + +To use workload identity you can just omit the auth field of the secret store and let the operator client fall back to defaults using the roles attached to your service account. + +``` +apiVersion: external-secrets.io/v1alpha1 +kind: SecretStore +metadata: + name: example +spec: + provider: + gcpsm: + projectID: pid +``` \ No newline at end of file diff --git a/docs/spec.md b/docs/spec.md index c2bc64649..1c4096455 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -543,7 +543,9 @@ ExternalSecretStatus Description -

"Ready"

+

"Deleted"

+ +

"Ready"

@@ -1151,6 +1153,7 @@ GCPSMAuth +(Optional)

Auth defines the information necessary to authenticate against GCP

diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index aefcdab0e..d74793ffe 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -40,6 +40,9 @@ var _ = SynchronizedBeforeSuite(func() []byte { By("installing eso") addon.InstallGlobalAddon(addon.NewESO(), cfg) + + By("installing scoped eso") + addon.InstallGlobalAddon(addon.NewScopedESO(), cfg) return nil }, func([]byte) {}) diff --git a/e2e/framework/addon/eso.go b/e2e/framework/addon/eso.go index c09fa3875..428e55c7b 100644 --- a/e2e/framework/addon/eso.go +++ b/e2e/framework/addon/eso.go @@ -27,3 +27,14 @@ func NewESO() *ESO { }, } } + +func NewScopedESO() *ESO { + return &ESO{ + &HelmChart{ + Namespace: "default", + ReleaseName: "eso-aws-sm", + Chart: "/k8s/deploy/charts/external-secrets", + Values: []string{"/k8s/eso.scoped.values.yaml"}, + }, + } +} diff --git a/e2e/k8s/eso.scoped.values.yaml b/e2e/k8s/eso.scoped.values.yaml new file mode 100644 index 000000000..cfe52f1ce --- /dev/null +++ b/e2e/k8s/eso.scoped.values.yaml @@ -0,0 +1,12 @@ +installCRDs: false +image: + repository: local/external-secrets + tag: test +scopedNamespace: test +extraEnv: + - name: AWS_SECRETSMANAGER_ENDPOINT + value: "http://localstack.default" + - name: AWS_STS_ENDPOINT + value: "http://localstack.default" + - name: AWS_SSM_ENDPOINT + value: "http://localstack.default" diff --git a/main.go b/main.go index 2598f64db..6825d55f7 100644 --- a/main.go +++ b/main.go @@ -46,12 +46,14 @@ func main() { var controllerClass string var enableLeaderElection bool var loglevel string + var namespace string flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&controllerClass, "controller-class", "default", "the controller is instantiated with a specific controller name and filters ES based on this property") flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") flag.StringVar(&loglevel, "loglevel", "info", "loglevel to use, one of: debug, info, warn, error, dpanic, panic, fatal") + flag.StringVar(&namespace, "namespace", "", "watch external secrets scoped in the provided namespace only") flag.Parse() var lvl zapcore.Level @@ -69,6 +71,7 @@ func main() { Port: 9443, LeaderElection: enableLeaderElection, LeaderElectionID: "external-secrets-controller", + Namespace: namespace, }) if err != nil { setupLog.Error(err, "unable to start manager") diff --git a/pkg/provider/gcp/secretmanager/secretsmanager.go b/pkg/provider/gcp/secretmanager/secretsmanager.go index 576b505af..e55c671bd 100644 --- a/pkg/provider/gcp/secretmanager/secretsmanager.go +++ b/pkg/provider/gcp/secretmanager/secretsmanager.go @@ -39,12 +39,12 @@ const ( defaultVersion = "latest" errGCPSMStore = "received invalid GCPSM SecretStore resource" - errGCPSMCredSecretName = "invalid GCPSM SecretStore resource: missing GCP Secret Access Key" errClientClose = "unable to close SecretManager client: %w" errInvalidClusterStoreMissingSAKNamespace = "invalid ClusterSecretStore: missing GCP SecretAccessKey Namespace" errFetchSAKSecret = "could not fetch SecretAccessKey secret: %w" errMissingSAK = "missing SecretAccessKey" errUnableProcessJSONCredentials = "failed to process the provided JSON credentials: %w" + errUnableProcessDefaultCredentials = "failed to process the default credentials: %w" errUnableCreateGCPSMClient = "failed to create GCP secretmanager client: %w" errUninitalizedGCPProvider = "provider GCP is not initialized" errClientGetSecretAccess = "unable to access Secret from SecretManager Client: %w" @@ -73,9 +73,6 @@ type gClient struct { func (c *gClient) setAuth(ctx context.Context) error { credentialsSecret := &corev1.Secret{} credentialsSecretName := c.store.Auth.SecretRef.SecretAccessKey.Name - if credentialsSecretName == "" { - return fmt.Errorf(errGCPSMCredSecretName) - } objectKey := types.NamespacedName{ Name: credentialsSecretName, Namespace: c.namespace, @@ -88,7 +85,10 @@ func (c *gClient) setAuth(ctx context.Context) error { } objectKey.Namespace = *c.store.Auth.SecretRef.SecretAccessKey.Namespace } - + if credentialsSecretName == "" { + c.credentials = nil + return nil + } err := c.kube.Get(ctx, objectKey, credentialsSecret) if err != nil { return fmt.Errorf(errFetchSAKSecret, err) @@ -122,12 +122,23 @@ func (sm *ProviderGCP) NewClient(ctx context.Context, store esv1alpha1.GenericSt sm.projectID = cliStore.store.ProjectID - config, err := google.JWTConfigFromJSON(cliStore.credentials, CloudPlatformRole) - if err != nil { - return nil, fmt.Errorf(errUnableProcessJSONCredentials, err) + if cliStore.credentials != nil { + config, err := google.JWTConfigFromJSON(cliStore.credentials, CloudPlatformRole) + if err != nil { + return nil, fmt.Errorf(errUnableProcessJSONCredentials, err) + } + ts := config.TokenSource(ctx) + clientGCPSM, err := secretmanager.NewClient(ctx, option.WithTokenSource(ts)) + if err != nil { + return nil, fmt.Errorf(errUnableCreateGCPSMClient, err) + } + sm.SecretManagerClient = clientGCPSM + return sm, nil + } + ts, err := google.DefaultTokenSource(ctx, CloudPlatformRole) + if err != nil { + return nil, fmt.Errorf(errUnableProcessDefaultCredentials, err) } - ts := config.TokenSource(ctx) - clientGCPSM, err := secretmanager.NewClient(ctx, option.WithTokenSource(ts)) if err != nil { return nil, fmt.Errorf(errUnableCreateGCPSMClient, err)