From 5215090625c07ff0be296a11dec02715e186b551 Mon Sep 17 00:00:00 2001 From: Sukrat Kashyap Date: Fri, 25 Sep 2020 04:25:25 +0100 Subject: [PATCH] feat: Cluster level default settings for Hashicorp Vault (#472) --- README.md | 35 ++++++++++++------- ...ernetes-client.io_externalsecrets_crd.yaml | 3 -- config/environment.js | 4 +++ config/index.js | 8 ++++- lib/backends/vault-backend.js | 10 +++--- lib/backends/vault-backend.test.js | 27 +++++++++++++- 6 files changed, 65 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 22ca31ad..2e7ff146 100644 --- a/README.md +++ b/README.md @@ -227,11 +227,11 @@ by default an `ExternalSecret` may access arbitrary keys from the backend e.g. name: password ``` -An enforced naming convention helps to keep the structure tidy and limits the access according -to your naming schema. +An enforced naming convention helps to keep the structure tidy and limits the access according +to your naming schema. -Configure the schema as regular expression in the namespace using an annotation. -This allows `ExternalSecrets` in `core-namespace` just to access secrets that start with +Configure the schema as regular expression in the namespace using an annotation. +This allows `ExternalSecrets` in `core-namespace` just to access secrets that start with `/dev/cluster1/core-namespace/`: ```yaml @@ -342,6 +342,13 @@ spec: kubernetes-external-secrets supports fetching secrets from [Hashicorp Vault](https://www.vaultproject.io/), using the [Kubernetes authentication method](https://www.vaultproject.io/docs/auth/kubernetes). +```yml +env: + VAULT_ADDR: https://vault.domain.tld + DEFAULT_VAULT_MOUNT_POINT: "k8s-auth" # optional, default value to be used if not specified in the ExternalSecret + DEFAULT_VAULT_ROLE: "k8s-auth-role" # optional, default value to be used if not specified in the ExternalSecret +``` + You will need to set the `VAULT_ADDR` environment variables so that kubernetes-external-secrets knows which endpoint to connect to, then create `ExternalSecret` definitions as follows: ```yml @@ -352,10 +359,12 @@ metadata: spec: backendType: vault # Your authentication mount point, e.g. "kubernetes" + # Overrides cluster DEFAULT_VAULT_MOUNT_POINT vaultMountPoint: my-kubernetes-vault-mount-point # The vault role that will be used to fetch the secrets # This role will need to be bound to kubernetes-external-secret's ServiceAccount; see Vault's documentation: # https://www.vaultproject.io/docs/auth/kubernetes.html + # Overrides cluster DEFAULT_VAULT_ROLE vaultRole: my-vault-role data: - name: password @@ -474,7 +483,7 @@ kubernetes-external-secrets supports fetching secrets from [GCP Secret Manager]( The external secret will poll for changes to the secret according to the value set for POLLER_INTERVAL_MILLISECONDS in env. Depending on the time interval this is set to you may incur additional charges as Google Secret Manager [charges](https://cloud.google.com/secret-manager/pricing) per a set number of API calls. -A service account is required to grant the controller access to pull secrets. +A service account is required to grant the controller access to pull secrets. #### Add a secret @@ -493,7 +502,7 @@ echo -n '{"value": "my-secret-value-with-update"}' | gcloud secrets versions Instructions are here: [Enable Workload Identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity#enable_workload_identity_on_a_new_cluster). To enable workload identity on an existing cluster (which is not covered in that document), first enable it on the cluster like so: gcloud container clusters update $CLUSTER_NAME --workload-pool=$PROJECT_NAME.svc.id.goog - + Next enable workload metadata config on the node pool in which the pod will run: gcloud beta container node-pools update $POOL --cluster $CLUSTER_NAME --workload-metadata-from-node=GKE_METADATA_SERVER @@ -509,7 +518,7 @@ If enabling it only for a particular pool, make sure to add any relevant tolerat operator: "Equal" effect: "NoSchedule" value: "node-pool-taint" - + affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: @@ -519,11 +528,11 @@ If enabling it only for a particular pool, make sure to add any relevant tolerat operator: In values: - node-pool - + You can add an annotation which is needed for workload identity by passing it in via Helm: serviceAccount: - annotations: + annotations: iam.gke.io/gcp-service-account: my-secrets-sa@$PROJECT.iam.gserviceaccount.com Create the policy binding: @@ -559,10 +568,10 @@ Uncomment GOOGLE_APPLICATION_CREDENTIALS in the values file as well as the follo gcp-creds: secret: gcp-creds mountPath: /app/gcp-creds - + This will mount the secret at /app/gcp-creds/gcp-creds.json and make it available via the GOOGLE_APPLICATION_CREDENTIALS environment variable. -#### Usage +#### Usage Once you have kubernetes-external-secrets installed, you can create an external secret with YAML like the following: ```yml @@ -585,11 +594,11 @@ The field "key" is the name of the secret in Google Secret Manager. The field " To retrieve external secrets, you can use the following command: kubectl get externalsecrets -n $NAMESPACE - + To retrieve the secrets themselves, you can use the regular: kubectl get secrets -n $NAMESPACE - + To retrieve an individual secret's content, use the following where "mysecret" is the key to the secret content under the "data" field: kubectl get secret my-secret -o 'go-template={{index .data "mysecret"}}' | base64 -D diff --git a/charts/kubernetes-external-secrets/crds/kubernetes-client.io_externalsecrets_crd.yaml b/charts/kubernetes-external-secrets/crds/kubernetes-client.io_externalsecrets_crd.yaml index 64f9a650..0040a8ab 100644 --- a/charts/kubernetes-external-secrets/crds/kubernetes-client.io_externalsecrets_crd.yaml +++ b/charts/kubernetes-external-secrets/crds/kubernetes-client.io_externalsecrets_crd.yaml @@ -102,9 +102,6 @@ spec: backendType: enum: - vault - required: - - vaultRole - - vaultMountPoint - properties: backendType: enum: diff --git a/config/environment.js b/config/environment.js index 46f73500..dbd1ba70 100644 --- a/config/environment.js +++ b/config/environment.js @@ -20,6 +20,8 @@ const vaultEndpoint = process.env.VAULT_ADDR || 'http://127.0.0.1:8200' // Grab the vault namespace from the environment const vaultNamespace = process.env.VAULT_NAMESPACE || null const vaultTokenRenewThreshold = process.env.VAULT_TOKEN_RENEW_THRESHOLD || null +const defaultVaultMountPoint = process.env.DEFAULT_VAULT_MOUNT_POINT || null +const defaultVaultRole = process.env.DEFAULT_VAULT_ROLE || null const pollerIntervalMilliseconds = process.env.POLLER_INTERVAL_MILLISECONDS ? Number(process.env.POLLER_INTERVAL_MILLISECONDS) : 10000 @@ -42,6 +44,8 @@ module.exports = { vaultEndpoint, vaultNamespace, vaultTokenRenewThreshold, + defaultVaultMountPoint, + defaultVaultRole, environment, pollerIntervalMilliseconds, metricsPort, diff --git a/config/index.js b/config/index.js index 5a7bd2f0..951e8a29 100644 --- a/config/index.js +++ b/config/index.js @@ -82,7 +82,13 @@ const vaultClient = vault(vaultOptions) // expires and with at least one remaining poll opportunty to retry renewal if it fails. const vaultTokenRenewThreshold = envConfig.vaultTokenRenewThreshold ? Number(envConfig.vaultTokenRenewThreshold) : 3 * envConfig.pollerIntervalMilliseconds / 1000 -const vaultBackend = new VaultBackend({ client: vaultClient, tokenRenewThreshold: vaultTokenRenewThreshold, logger }) +const vaultBackend = new VaultBackend({ + client: vaultClient, + tokenRenewThreshold: vaultTokenRenewThreshold, + logger: logger, + defaultVaultMountPoint: envConfig.defaultVaultMountPoint, + defaultVaultRole: envConfig.defaultVaultRole +}) const azureKeyVaultBackend = new AzureKeyVaultBackend({ credential: azureConfig.azureKeyVault(), logger diff --git a/lib/backends/vault-backend.js b/lib/backends/vault-backend.js index 5f93f30d..3ebedf36 100644 --- a/lib/backends/vault-backend.js +++ b/lib/backends/vault-backend.js @@ -10,10 +10,12 @@ class VaultBackend extends KVBackend { * @param {Number} tokenRenewThreshold - tokens are renewed when ttl reaches this threshold * @param {Object} logger - Logger for logging stuff. */ - constructor ({ client, tokenRenewThreshold, logger }) { + constructor ({ client, tokenRenewThreshold, logger, defaultVaultMountPoint, defaultVaultRole }) { super({ logger }) this._client = client this._tokenRenewThreshold = tokenRenewThreshold + this.defaultVaultMountPoint = defaultVaultMountPoint + this.defaultVaultRole = defaultVaultRole } /** @@ -38,13 +40,13 @@ class VaultBackend extends KVBackend { * @param {number} specOptions.kvVersion - K/V Version 1 or 2 * @returns {Promise} Promise object representing secret property values. */ - async _get ({ key, specOptions: { vaultMountPoint, vaultRole, kvVersion = 2 } }) { + async _get ({ key, specOptions: { vaultMountPoint = null, vaultRole = null, kvVersion = 2 } }) { if (!this._client.token) { const jwt = this._fetchServiceAccountToken() this._logger.debug('fetching new token from vault') await this._client.kubernetesLogin({ - mount_point: vaultMountPoint, - role: vaultRole, + mount_point: vaultMountPoint || this.defaultVaultMountPoint, + role: vaultRole || this.defaultVaultRole, jwt: jwt }) } else { diff --git a/lib/backends/vault-backend.test.js b/lib/backends/vault-backend.test.js index 9e4ae9b4..52fe1c92 100644 --- a/lib/backends/vault-backend.test.js +++ b/lib/backends/vault-backend.test.js @@ -15,6 +15,8 @@ const logger = pino({ describe('VaultBackend', () => { let clientMock let vaultBackend + const defaultFakeMountPoint = 'defaultFakeMountPoint' + const defaultFakeRole = 'defaultFakeRole' const mountPoint = 'fakeMountPoint' const role = 'fakeRole' const secretKey = 'fakeSecretKey' @@ -52,7 +54,9 @@ describe('VaultBackend', () => { vaultBackend = new VaultBackend({ client: clientMock, tokenRenewThreshold: vaultTokenRenewThreshold, - logger + logger: logger, + defaultVaultMountPoint: defaultFakeMountPoint, + defaultVaultRole: defaultFakeRole }) }) @@ -95,6 +99,27 @@ describe('VaultBackend', () => { expect(secretPropertyValue).equals(quotedSecretValue) }) + it('if vaultRole and vaultMountPoint not specified use the default one', async () => { + const secretPropertyValue = await vaultBackend._get({ + specOptions: { + }, + key: secretKey + }) + + // First, we log into Vault... + sinon.assert.calledWith(clientMock.kubernetesLogin, { + mount_point: defaultFakeMountPoint, + role: defaultFakeRole, + jwt: jwt + }) + + // ... then we fetch the secret ... + sinon.assert.calledWith(clientMock.read, secretKey) + + // ... and expect to get its proper value + expect(secretPropertyValue).equals(quotedSecretValue) + }) + it('logs in and returns secret property value - kv version 1', async () => { clientMock.read = sinon.stub().returns(kv1Secret)