Skip to content
This repository has been archived by the owner on Jul 26, 2022. It is now read-only.

Commit

Permalink
feat: Cluster level default settings for Hashicorp Vault (#472)
Browse files Browse the repository at this point in the history
  • Loading branch information
sukratkashyap authored Sep 25, 2020
1 parent 9318f68 commit 5215090
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 22 deletions.
35 changes: 22 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,6 @@ spec:
backendType:
enum:
- vault
required:
- vaultRole
- vaultMountPoint
- properties:
backendType:
enum:
Expand Down
4 changes: 4 additions & 0 deletions config/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -42,6 +44,8 @@ module.exports = {
vaultEndpoint,
vaultNamespace,
vaultTokenRenewThreshold,
defaultVaultMountPoint,
defaultVaultRole,
environment,
pollerIntervalMilliseconds,
metricsPort,
Expand Down
8 changes: 7 additions & 1 deletion config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions lib/backends/vault-backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand All @@ -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 {
Expand Down
27 changes: 26 additions & 1 deletion lib/backends/vault-backend.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -52,7 +54,9 @@ describe('VaultBackend', () => {
vaultBackend = new VaultBackend({
client: clientMock,
tokenRenewThreshold: vaultTokenRenewThreshold,
logger
logger: logger,
defaultVaultMountPoint: defaultFakeMountPoint,
defaultVaultRole: defaultFakeRole
})
})

Expand Down Expand Up @@ -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)

Expand Down

0 comments on commit 5215090

Please sign in to comment.