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

Commit

Permalink
feat(vault): Support for Hashicorp Vault (#198)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pluies authored and Silas Boyd-Wickizer committed Nov 8, 2019
1 parent a2a9dff commit d61312c
Show file tree
Hide file tree
Showing 12 changed files with 341 additions and 2 deletions.
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ The following table lists the configurable parameters of the `kubernetes-externa
| `env.METRICS_PORT` | Specify the port for the prometheus metrics server | `3001` |
| `env.ROLE_PERMITTED_ANNOTATION` | Specify the annotation key where to lookup the role arn permission boundaries | `iam.amazonaws.com/permitted` |
| `env.POLLER_INTERVAL_MILLISECONDS` | Set POLLER_INTERVAL_MILLISECONDS in Deployment Pod | `10000` |
| `env.VAULT_ADDR` | Endpoint for the Vault backend, if using Vault | `http://127.0.0.1:8200 |
| `envVarsFromSecret.AWS_ACCESS_KEY_ID` | Set AWS_ACCESS_KEY_ID (from a secret) in Deployment Pod | |
| `envVarsFromSecret.AWS_SECRET_ACCESS_KEY` | Set AWS_SECRET_ACCESS_KEY (from a secret) in Deployment Pod | |
| `image.repository` | kubernetes-external-secrets Image name | `godaddy/kubernetes-external-secrets` |
Expand Down Expand Up @@ -217,7 +218,7 @@ data:

## Backends

kubernetes-external-secrets supports both AWS Secrets Manager and AWS System Manager.
kubernetes-external-secrets supports AWS Secrets Manager, AWS System Manager, and Hashicorp Vault.

### AWS Secrets Manager

Expand Down Expand Up @@ -289,6 +290,36 @@ spec:
property: password
```

### Hashicorp Vault

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.html).

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
apiVersion: 'kubernetes-client.io/v1'
kind: ExternalSecret
metadata:
name: hello-vault-service
spec:
backendType: vault
# Your authentication mount point, e.g. "kubernetes"
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
vaultRole: my-vault-role
data:
- name: password
# The full path of the secret to read, as in `vault read secret/data/hello-service/credentials`
key: secret/data/hello-service/credentials
property: password
# Vault values are matched individually. If you have several keys in your Vault secret, you will need to add them all separately
- name: api-key
key: secret/data/hello-service/credentials
property: api-key
```
## Metrics
kubernetes-external-secrets exposes the following metrics over a prometheus endpoint:
Expand Down
4 changes: 4 additions & 0 deletions charts/kubernetes-external-secrets/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ spec:
name: {{ $value.secretKeyRef | quote }}
key: {{ $value.key | quote }}
{{- end }}
{{- with .Values.securityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
Expand Down
18 changes: 18 additions & 0 deletions charts/kubernetes-external-secrets/templates/rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,22 @@ subjects:
- name: {{ template "kubernetes-external-secrets.serviceAccountName" . }}
namespace: {{ .Release.Namespace | quote }}
kind: ServiceAccount
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: {{ include "kubernetes-external-secrets.fullname" . }}-auth
labels:
app.kubernetes.io/name: {{ include "kubernetes-external-secrets.name" . }}
helm.sh/chart: {{ include "kubernetes-external-secrets.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- name: {{ template "kubernetes-external-secrets.serviceAccountName" . }}
namespace: {{ .Release.Namespace | quote }}
kind: ServiceAccount
{{- end -}}
1 change: 1 addition & 0 deletions charts/kubernetes-external-secrets/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ env:
POLLER_INTERVAL_MILLISECONDS: 10000
LOG_LEVEL: info
METRICS_PORT: 3001
VAULT_ADDR: http://127.0.0.1:8200

# Create environment variables from exists k8s secrets
# envVarsFromSecret:
Expand Down
2 changes: 2 additions & 0 deletions config/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ if (environment === 'development') {
require('dotenv').config()
}

const vaultEndpoint = process.env.VAULT_ADDR || 'http://127.0.0.1:8200'
const pollerIntervalMilliseconds = process.env.POLLER_INTERVAL_MILLISECONDS
? Number(process.env.POLLER_INTERVAL_MILLISECONDS) : 10000

Expand All @@ -26,6 +27,7 @@ const rolePermittedAnnotation = process.env.ROLE_PERMITTED_ANNOTATION || 'iam.am
const metricsPort = process.env.METRICS_PORT || 3001

module.exports = {
vaultEndpoint,
environment,
pollerIntervalMilliseconds,
metricsPort,
Expand Down
7 changes: 6 additions & 1 deletion config/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict'

const vault = require('node-vault')
const kube = require('kubernetes-client')
const KubeRequest = require('kubernetes-client/backends/request')
const pino = require('pino')
Expand All @@ -10,6 +11,7 @@ const CustomResourceManager = require('../lib/custom-resource-manager')
const customResourceManifest = require('../custom-resource-manifest.json')
const SecretsManagerBackend = require('../lib/backends/secrets-manager-backend')
const SystemManagerBackend = require('../lib/backends/system-manager-backend')
const VaultBackend = require('../lib/backends/vault-backend')

const kubeconfig = new kube.KubeConfig()
kubeconfig.loadFromDefault()
Expand Down Expand Up @@ -38,9 +40,12 @@ const systemManagerBackend = new SystemManagerBackend({
assumeRole: awsConfig.assumeRole,
logger
})
const vaultClient = vault({ apiVersion: 'v1', endpoint: envConfig.vaultEndpoint })
const vaultBackend = new VaultBackend({ client: vaultClient, logger })
const backends = {
secretsManager: secretsManagerBackend,
systemManager: systemManagerBackend
systemManager: systemManagerBackend,
vault: vaultBackend
}

// backwards compatibility
Expand Down
12 changes: 12 additions & 0 deletions examples/hello-service-external-secret-vault.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: 'kubernetes-client.io/v1'
kind: ExternalSecret
metadata:
name: hello-service
spec:
backendType: vault
vaultMountPoint: my-kubernetes-vault-mount-point
vaultRole: my-vault-role
data:
- name: password
key: secret/data/hello-service/password
property: password
13 changes: 13 additions & 0 deletions external-secrets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ rules:
resources: ["externalsecrets/status"]
verbs: ["get", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: kubernetes-external-secrets-cluster-role-binding-auth
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: kubernetes-external-secrets-service-account
namespace: kubernetes-external-secrets
---
apiVersion: v1
kind: Namespace
metadata:
Expand Down
97 changes: 97 additions & 0 deletions lib/backends/vault-backend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
'use strict'

const KVBackend = require('./kv-backend')

/** Vault backend class. */
class VaultBackend extends KVBackend {
/**
* Create Vault backend.
* @param {Object} client - Client for interacting with Vault.
* @param {Object} logger - Logger for logging stuff.
*/
constructor ({ client, logger }) {
super({ logger })
this._client = client
}

/**
* Fetch Kubernetes service account token.
* @returns {string} String representing the token of the service account running this pod.
*/
_fetchServiceAccountToken () {
if (!this._serviceAccountToken) {
const fs = require('fs')
this._serviceAccountToken = fs.readFileSync('/var/run/secrets/kubernetes.io/serviceaccount/token', 'utf8')
}
return this._serviceAccountToken
}

/**
* Fetch Kubernetes secret property values.
* @param {Object[]} secretProperties - Kubernetes secret properties.
* @param {string} secretProperties[].key - Secret key in the backend.
* @param {string} secretProperties[].name - Kubernetes Secret property name.
* @param {string} secretProperties[].property - If the backend secret is an
* object, this is the property name of the value to use.
* @returns {Promise} Promise object representing secret property values.
*/
_fetchSecretPropertyValues ({ vaultMountPoint, vaultRole, jwt, externalData }) {
return Promise.all(externalData.map(async secretProperty => {
this._logger.info(`fetching secret property ${secretProperty.key}`)
const value = await this._get({ vaultMountPoint: vaultMountPoint, vaultRole: vaultRole, jwt: jwt, secretKey: secretProperty.key })

return value[secretProperty.property]
}))
}

/**
* Get secret property value from Vault.
* @param {string} secretKey - Key used to store secret property value in Vault.
* @returns {Promise} Promise object representing secret property value.
*/
async _get ({ vaultMountPoint, vaultRole, secretKey }) {
if (!this._client.token) {
const jwt = this._fetchServiceAccountToken()
this._logger.debug(`fetching new token from vault`)
const vault = await this._client.kubernetesLogin({
mount_point: vaultMountPoint,
role: vaultRole,
jwt: jwt
})
this._client.token = vault.auth.client_token
} else {
this._logger.debug(`renewing existing token from vault`)
this._client.tokenRenewSelf()
}

this._logger.debug(`reading secret key ${secretKey} from vault`)
const secretResponse = await this._client.read(secretKey)

return secretResponse.data.data
}

/**
* Fetch Kubernetes secret manifest data.
* @param {ExternalSecretSpec} spec - Kubernetes ExternalSecret spec.
* @returns {Promise} Promise object representing Kubernetes secret manifest data.
*/
async getSecretManifestData ({ spec }) {
const data = {}
const vaultMountPoint = spec.vaultMountPoint
const vaultRole = spec.vaultRole

// Also support spec.properties to be backwards compatible.
const externalData = spec.data || spec.properties
const secretPropertyValues = await this._fetchSecretPropertyValues({
vaultMountPoint,
vaultRole,
externalData
})
externalData.forEach((secret, index) => {
data[secret.name] = (Buffer.from(secretPropertyValues[index], 'utf8')).toString('base64')
})
return data
}
}

module.exports = VaultBackend
95 changes: 95 additions & 0 deletions lib/backends/vault-backend.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/* eslint-env mocha */
'use strict'

const { expect } = require('chai')
const sinon = require('sinon')
const pino = require('pino')

const VaultBackend = require('./vault-backend')
const logger = pino({
serializers: {
err: pino.stdSerializers.err
}
})

describe('VaultBackend', () => {
let clientMock
let vaultBackend

beforeEach(() => {
clientMock = sinon.mock()

vaultBackend = new VaultBackend({
client: clientMock,
logger
})
})

describe('_get', () => {
const mountPoint = 'fakeMountPoint'
const role = 'fakeRole'
const secretKey = 'fakeSecretKey'
const jwt = 'this-is-a-jwt-token'

beforeEach(() => {
clientMock.read = sinon.stub().returns({
data: {
data: 'fakeSecretPropertyValue'
}
})
clientMock.tokenRenewSelf = sinon.stub().returns(true)
clientMock.kubernetesLogin = sinon.stub().returns({
auth: {
client_token: '1234'
}
})

vaultBackend._fetchServiceAccountToken = sinon.stub().returns(jwt)

clientMock.token = undefined
})

it('logs in and returns secret property value', async () => {
const secretPropertyValue = await vaultBackend._get({
vaultMountPoint: mountPoint,
vaultRole: role,
secretKey: secretKey
})

// First, we log into Vault...
sinon.assert.calledWith(clientMock.kubernetesLogin, {
mount_point: 'fakeMountPoint',
role: 'fakeRole',
jwt: jwt
})

// ... then we fetch the secret ...
sinon.assert.calledWith(clientMock.read, 'fakeSecretKey')

// ... and expect to get its proper value
expect(secretPropertyValue).equals('fakeSecretPropertyValue')
})

it('returns secret property value after renewing token if a token exists', async () => {
clientMock.token = 'an-existing-token'

const secretPropertyValue = await vaultBackend._get({
vaultMountPoint: mountPoint,
vaultRole: role,
secretKey: secretKey
})

// No logging into Vault...
sinon.assert.notCalled(clientMock.kubernetesLogin)

// ... but renew the token instead ...
sinon.assert.calledOnce(clientMock.tokenRenewSelf)

// ... then we fetch the secret ...
sinon.assert.calledWith(clientMock.read, 'fakeSecretKey')

// ... and expect to get its proper value
expect(secretPropertyValue).equals('fakeSecretPropertyValue')
})
})
})
Loading

0 comments on commit d61312c

Please sign in to comment.