Skip to main content
Version: 1.2.x

PostgreSQL Credentials

Before you get started, you must have:
✓ Vault 1.3.1 or newer
✓ Vault Injector 0.3.0 or newer

Setup Vault

Install Vault (it does not need to be installed in the Kubernetes cluster, but should be reachable from inside the Kubernetes cluster). The Vault Injector (agent-injector) must be installed into the cluster and configured to inject sidecars. This is automatically done by the Helm chart v0.5.0+ which installs Vault 0.12+ and Vault-Injector 0.3.0+. The example below assumes that Vault is installed in the tsb namespace.

For more details, check the Vault documentation.

helm install --name=vault --set='server.dev.enabled=true' ./vault-helm

Set up the database secret engine for PostgreSQL

Enable the database secrets engine in Vault.

vault secrets enable database

Expected output:

Success! Enabled the database secrets engine at: database/

By default, the secrets engine is enabled at the name of the engine. To enable the secrets engine at a different path, use the -path argument.

Configure Vault with the proper plugin and connection information. In the connection_url parameter, replace postgres.tsb.svc:5432/tsb with the full host:port/db_name of your PostgreSQL cluster. Only change the lower username and password with your own, don't edit the one between {{ }} in the URL, it is used as a template:

vault write database/config/tsb \
plugin_name=postgresql-database-plugin \
allowed_roles="pg-role" \
connection_url="postgresql://{{username}}:{{password}}@postgres.tsb.svc:5432/tsb?sslmode=disable" \
username="<postgres-username>" \
password="<postgres-password>"

You can review the configuration by using the read action:

vault read  database/config/tsb
# Key Value
# --- -----
# allowed_roles [pg-role]
# connection_details map[connection_url:postgresql://{{username}}:{{password}}@postgres.tsb.svc:5432/?sslmode=disable username:postgres]
# plugin_name postgresql-database-plugin
# root_credentials_rotate_statements []

Configure a role that maps a name in Vault to a templated SQL statement which Vault can execute to create database credentials.
The max_ttl defines how long the new credentials are valid.
The default_ttl defines a lease time, and the Vault-Injector will renew it until we reach the max_ttl.

TTL values must be paired with the application's database connection lifetime to ensure they are all closed before the TTL expires.

Run the command below, ensuring that you don’t edit the parameters between {{ }} because they are used as a template by Vault:

vault write database/roles/pg-role \
db_name=tsb \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
GRANT ALL ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="12h" \
max_ttl="24h"

Success! Data written to: database/roles/pg-role

Again, use the read action to verify the setup:

vault read  database/roles/pg-role
# Key Value
# --- -----
# creation_statements [CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO "{{name}}";]
# db_name tsb
# default_ttl 24h
# max_ttl 24h
# renew_statements []
# revocation_statements []
# rollback_statements []

Now, generate a new credential by reading from the /creds endpoint with the name of the role. This is the mechanism that will be used by the Vault-Injector to grab credentials for your Kubernetes application:

vault read database/creds/pg-role
Key Value
--- -----
lease_id database/creds/pg-role/tUEs8eogkk9KL5erU5rLv7hD
lease_duration 24h
lease_renewable true
password A1a-1ZYMcUHKJIJH6rrc
username v-token-pg-role-KQ4ze3GYi5He0D70tEmo-1587973449

Set up Kubernetes Secret Engine

Configure a policy named “pg-auth”. This is a very non-restrictive policy, and in a production setting, you should add more restrictions.

vault policy write pg-auth - <<EOF
path "database/creds/*" {
capabilities = ["read"]
}
EOF
Success! Uploaded policy: pg-auth

Configure Vault to enable access to the Kubernetes API. This example assumes that you are running commands in the Vault pod using kubectl exec. If you are not, you will need to find the right JWT Token, Kubernetes API URL (that Vault will use to connect to Kubernetes) and the CA certificate of the vaultserver service account as described in Vault documentation:

vault auth enable kubernetes
vault write auth/kubernetes/config \
token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
kubernetes_host=https://${KUBERNETES_PORT_443_TCP_ADDR}:443 \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt

Attach the database policy to service accounts from the management namespace (tsb namespace here):

vault write auth/kubernetes/role/pg \
bound_service_account_names=* \
bound_service_account_namespaces=tsb \
policies=pg-auth \
ttl=24h

To add more restrictions create one role per ServiceAccount. For PostgreSQL, you will need one for tsb-iam, tsb-spm and default service accounts because TSB API pods run with default service accounts:

vault write auth/kubernetes/role/pg \
bound_service_account_names=default,tsb-spm,tsb-iam \
bound_service_account_namespaces=tsb \
policies=pg-auth \
ttl=24h

Inject secrets into the pod

To use Vault Agent Injector in combination with PostgreSQL, add the following deployment pod annotations and environment variables to the ManagementPlane custom resource.

An overlay is used to re-configure the deployment on the fly:

spec:
dataStore:
postgres:
connectionLifetime: 1h # Set connection lifetime
components:
apiServer:
kubeSpec:
deployment:
podAnnotations:
vault.hashicorp.com/agent-inject: 'true'
vault.hashicorp.com/agent-init-first: 'true'
vault.hashicorp.com/agent-inject-secret-config.yaml: 'database/creds/pg-role'
vault.hashicorp.com/agent-inject-template-config.yaml: |
{{- with secret "database/creds/pg-role" -}}
data:
username: {{ .Data.username }}
password: {{ .Data.password }}
{{- end -}}
vault.hashicorp.com/role: 'pg'
vault.hashicorp.com/secret-volume-path: /etc/dbvault
overlays:
- apiVersion: v1
kind: Deployment
name: tsb
patches:
- path: spec.template.spec.containers[name:tsb].args.[:/etc/db/config\.yaml]
value: /etc/dbvault/config.yaml
- path: spec.template.spec.initContainers[name:migration].args.[:/etc/db/config\.yaml]
value: /etc/dbvault/config.yaml
- apiVersion: apps/v1
kind: Deployment
name: tsb-data-reconcile
patches:
- path: spec.template.spec.containers[name:tsb-data-reconcile].args.[:/etc/db/config\.yaml]
value: /etc/dbvault/config.yaml
iamServer:
kubeSpec:
deployment:
podAnnotations:
vault.hashicorp.com/agent-inject: 'true'
vault.hashicorp.com/agent-init-first: 'true'
vault.hashicorp.com/agent-inject-secret-config.yaml: 'database/creds/pg-role'
vault.hashicorp.com/agent-inject-template-config.yaml: |
{{- with secret "database/creds/pg-role" -}}
data:
username: {{ .Data.username }}
password: {{ .Data.password }}
{{- end -}}
vault.hashicorp.com/role: 'pg'
vault.hashicorp.com/secret-volume-path: /etc/dbvault
overlays:
- apiVersion: v1
kind: Deployment
name: iam
patches:
- path: spec.template.spec.containers[name:iam].args.[:/etc/db/config\.yaml]
value: /etc/dbvault/config.yaml
spmServer:
kubeSpec:
deployment:
podAnnotations:
vault.hashicorp.com/agent-inject: 'true'
vault.hashicorp.com/agent-init-first: 'true'
vault.hashicorp.com/agent-inject-secret-config.yaml: 'database/creds/pg-role'
vault.hashicorp.com/agent-inject-template-config.yaml: |
{{- with secret "database/creds/pg-role" -}}
data:
username: {{ .Data.username }}
password: {{ .Data.password }}
{{- end -}}
vault.hashicorp.com/role: 'pg'
vault.hashicorp.com/secret-volume-path: /etc/dbvault
job:
podAnnotations:
vault.hashicorp.com/agent-inject: 'true'
vault.hashicorp.com/agent-init-first: 'true'
vault.hashicorp.com/agent-inject-secret-config.yaml: 'database/creds/pg-role'
vault.hashicorp.com/agent-pre-populate-only: "true"
vault.hashicorp.com/agent-inject-template-config.yaml: |
{{- with secret "database/creds/pg-role" -}}
data:
username: {{ .Data.username }}
password: {{ .Data.password }}
{{- end -}}
vault.hashicorp.com/role: 'pg'
vault.hashicorp.com/secret-volume-path: /etc/dbvault
overlays:
- apiVersion: v1
kind: Deployment
name: spm
patches:
- path: spec.template.spec.containers[name:spm].args.[:/etc/db/config\.yaml]
value: /etc/dbvault/config.yaml
- apiVersion: v1
kind: CronJob
name: spmsync
patches:
- path: spec.jobTemplate.spec.template.spec.containers[name:spmsync].args.[:/etc/db/config\.yaml]
value: /etc/dbvault/config.yaml

Debugging

Check roles in PostgreSQL

Use the PostgreSQL command line client psql to check the role creation inside the target database, tsb:

psql -h postgres -p 5432 -U tsb -d tsb

Once connected to the database, you can use the \du command to list all the current roles for the database:

\du

List of roles
Role name | Attributes | Member of
----------------------------------------------------+------------------------------------------------------------+-------------------------------------------------------------
rds_ad | Cannot login | {}
rds_iam | Cannot login | {}
rds_password | Cannot login | {}
rds_replication | Cannot login | {}
rds_superuser | Cannot login | {pg_monitor,pg_signal_backend,rds_replication,rds_password}
rdsadmin | Superuser, Create role, Create DB, Replication, Bypass RLS+| {}
| Password valid until infinity |
rdsrepladmin | No inheritance, Cannot login, Replication | {}
tsb | Create role, Create DB +| {rds_superuser}
| Password valid until infinity |
| | {}
v-kubernet-pg-role-5OUfsUQv3xAASWZkbECV-1589890098 | Password valid until 2020-05-20 12:08:23+00 | {}
v-kubernet-pg-role-7uiTkWgsxphogXub0qpp-1589887199 | Password valid until 2020-05-20 11:20:04+00 | {}
...

Here you can see the role tsb that was used to configure the database inside Vault and some roles like v-kubernet-pg-role-5OUfsUQv3xAASWZkbECV-1589890098 which correspond to dynamic roles created by the vault-Injector sidecar.

You can also list the access rights that were granted to the dynamic roles. Here is an example with the role v-kubernet-pg-role-5OUfsUQv3xAASWZkbECV-1589890098:

SELECT grantee AS user, CONCAT(table_schema, '.', table_name) AS table,
CASE
WHEN COUNT(privilege_type) = 7 THEN 'ALL'
ELSE ARRAY_TO_STRING(ARRAY_AGG(privilege_type), ', ')
END AS grants
FROM information_schema.role_table_grants
WHERE grantee='v-kubernet-pg-role-5OUfsUQv3xAASWZkbECV-1589890098'
GROUP BY table_name, table_schema, grantee;

user | table | grants
----------------------------------------------------+------------------------+--------
v-kubernet-pg-role-5OUfsUQv3xAASWZkbECV-1589890098 | public.application | ALL
v-kubernet-pg-role-5OUfsUQv3xAASWZkbECV-1589890098 | public.assignment | ALL
v-kubernet-pg-role-5OUfsUQv3xAASWZkbECV-1589890098 | public.association | ALL
...