/vault-plugin-database-k8s-controller

A fork of Vault's database credential plugin allowing the use of annotations on service accounts as parameters in statements

Primary LanguageGoMozilla Public License 2.0MPL-2.0

vault-plugin-database-k8s-controller

A fork of Vault's database credential plugin allowing the use of an annotation on service accounts to dynamically specify user creation statements. Essentially, you can have many virtual 'roles' which each have tightly scoped database user creation statements, where the roles are in fact defined in Kubernetes. At Monzo we use this to allow for hundreds of Cassandra clients which each are allowed access to a single independent keyspace. Vault is not aware of individual services; simply logging in with your Kubernetes service account token is enough to get permission to issue the correct database credentials.

Non-builtin database plugins are not supported, as sadly custom plugins cannot call out to other custom plugins. However, all of Vault's builtin database plugins are bundled into this binary and should work as normal.

Currently based on https://github.com/hashicorp/vault/tree/v1.3.0/builtin/logical/database

To rebase:

make rebase

Instructions

This plugin differs from the default database plugin only if kubernetes config is provided

# if your plugin is registered as database-k8s
vault secrets enable -path=database -plugin-name=database-k8s database-k8s
vault write database/kubeconfig kubernetes_host=https://127.0.0.1 kubernetes_ca_cert=@cert jwt=@jwt

If this is provided, the plugin will attempt to maintain an in memory cache of all service accounts in Kubernetes. If any service accounts contain an annotation monzo.com/keyspace, the mapping from service account name to the annotation is also stored in Vault. This is so that the mapping can be used before the cache is built from the k8s API.

The purpose of this is to interpolate this annotation into any creation statements of a role, to create essentially a dynamic role for every service account. If you provide a role named like k8s_rw_s-ledger_default and this role does not explicitly exist then instead the plugin will look up the concrete role named rw, and will look up the service account s-ledger in the namespace default.

It will then replace all instances of {{annotation}} in the creation statements of the concrete rw role with the value of the annotation on that service account.

You can also set an annotation monzo.com/cluster which allows you to override the db name of the concrete rw role with the value of the annotation.

Annotation keys can be overridden with the kubeconfig endpoint, using keyspace_annotation and db_name_annotation.

The role names are designed such that they can support a vault policy as follows:

path "database/creds/k8s_rw_{{identity.entity.aliases.kubernetes.metadata.service_account_name}}_{{identity.entity.aliases.kubernetes.metadata.service_account_namespace}}"
{
  capabilities = ["read"]
}

The role name is used for these parameters so that the plugin has the same API as its upstream.

Example

$ vault write database/roles/rw \
    db_name=my-cassandra-database \
    creation_statements="CREATE USER '{{username}}' WITH PASSWORD '{{password}}' NOSUPERUSER;" \
    creation_statements="GRANT ALL PERMISSIONS ON KEYSPACE \"{{annotation}}\" TO {{username}};" \
    default_ttl="1h" \
    max_ttl="24h"
Success! Data written to: database/roles/rw

kubectl create serviceaccount s-ledger
kubectl annotate serviceaccount s-ledger monzo.com/keyspace='ledger'

$ vault read database/roles/k8s_rw_s-ledger_default
Key                      Value
---                      -----
creation_statements      [CREATE USER '{{username}}' WITH PASSWORD '{{password}}' NOSUPERUSER; GRANT ALL PERMISSIONS ON KEYSPACE "ledger" TO {{username}};]
db_name                  my-cassandra-database
default_ttl              1h
max_ttl                  24h
renew_statements         []
revocation_statements    []
rollback_statements      []