Na demonstração tecnica a seguir veremos a integração do Vault Sidecar Injector mais a utilização do Database secret engine no ecossistema do Kubernetes. A ideia é apresentar um flxuo de trabalho onde o injector do Vault seja capaz de renderizar secrets das engines do Vault (Database Engine e Key Value Engine).
O Vault Sidecar Injector aproveita o webhook de admissão de mutação do kubernetes para interceptar e argumentar (ou alterar) definições de pod especificamente anotadas para injeções de segredos.
Observação: Estamos assumindo que você esteja familizariado com operações no ecossistema do kubernetes e tenha o helm e terraform instalados também.
IMPORTANTE: As aplicações utilizadas aqui são exclusivamente para testes e estudos. Use por sua conta em risco e de preferência num abiente controlado não produtivo.
Minimamente você precisará ter:
- Um Cluster Kubernetes em execução
- Uma instancia de banco de dados acessível (Aqui estamos usando postgresql no RDS, mas pode ser qulquer Banco de dados suportado pelo Vault). Aqui você encontra um exemplo para deploy do postgresql dentro do ambiente K8S caso nao queira subir uma instância RDS.
- Uma aplicação que se conecte ao banco de dados acima. Esse repositório contém uma aplicação de exemplo para se conectar à um banco postgresql.
- https://learn.hashicorp.com/tutorials/vault/database-secrets
- https://learn.hashicorp.com/tutorials/vault/kubernetes-sidecar
- https://github.com/jweissig/vault-k8s-sidecar-demo
- https://www.vaultproject.io/docs/platform/k8s/injector/annotations
- https://docs.aws.amazon.com/emr/latest/EMR-on-EKS-DevelopmentGuide/setting-up-enable-IAM.html
git clone https://github.com/stone-payments/lab-hashicorp-vault-injector.git
cd lab-hashicorp-vault-injector
Estamos assumindo que você já tenha um cluster Kubernetes em funcionamento. Sendo assim, para fazer o deploy do Vault, bem como a sua configuração faça:
cd setup && terraform init && terraform apply
O código terraform faz:
- Deploy e unseal do Vault
- Habilita a engine Database
- Habilita e configura os backends de autenticação: Kubernetes, AppRole e AWS.
- Configura as policies
- Configura as roles dos backends de autenticação
- Habilita e Configura conexão com a base de dados
- Configura as roles de criação
Para fins didáticos vamos dar uma olhada num explo do que você NÃO DEVE fazer: O Hardcode.
apiVersion: apps/v1
kind: Deployment
metadata:
name: vida-loka-hashi-app
namespace: vault-hashitalks
labels:
app: hashi-app
spec:
selector:
matchLabels:
app: hashi-app
replicas: 1
template:
metadata:
labels:
app: hashi-app
spec:
serviceAccountName: vault-hashitalks
containers:
- name: evil-hashiapp
image: <sua_docker_image>:<tag>
command: ['/bin/bash']
args: ['-c', '/app/postgresql-live']
envFrom:
- configMapRef:
name: hashi-data
---
apiVersion: v1
data:
DATABASE_PASSWORD: #NÃO FAÇA ISSO.
DATABASE_USER: #NÃO FAÇA ISSO
DATABASE_NAME: #NÃO FAÇA ISSO
DATABASE_HOST: #NÃO FAÇA ISSO
kind: ConfigMap
metadata:
name: hashi-data
namespace: vault-hashitalks
Esse modelo é extremamente inseguro, pois as credenciais estão expostas. Qualquer pessoal com acesso a esse manifesto pode comprometer a aplicação.
O modelo abaixo utiliza o backend de autenticação do Vault App Role. Esse modelo adiciona uma camada de segurança porém ainda é ncessário criar um objeto de secret no K8S com os dados da role-id e secret-id. Essa etapa adiciona essa ação manual de criação da secret e pode ser que no longo prazo não seja tão eficiente.
k apply -f k8s-manifests/app-role-auth-hashiapp.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-approle-auth-hashiapp
namespace: vault-hashitalks
labels:
app: app-approle
spec:
selector:
matchLabels:
app: app-approle
replicas: 1
template:
metadata:
annotations:
vault.hashicorp.com/agent-inject: 'true'
vault.hashicorp.com/role: 'vault-hashitalks-approle'
vault.hashicorp.com/agent-extra-secret: 'approle'
vault.hashicorp.com/auth-type: 'approle'
vault.hashicorp.com/auth-path: 'auth/approle'
vault.hashicorp.com/auth-config-role-id-file-path: '/vault/custom/role-id'
vault.hashicorp.com/auth-config-secret-id-file-path: '/vault/custom/secret-id'
vault.hashicorp.com/agent-inject-secret-db-creds: 'database/rds/postgres/vault-hashi-talks-mock/creds/readonly'
vault.hashicorp.com/agent-inject-template-db-creds: |
{{ with secret "database/rds/postgres/vault-hashi-talks-mock/creds/readwrite" -}}
export DATABASE_USER="{{ .Data.username }}"
{{- end }}
{{ with secret "database/rds/postgres/vault-hashi-talks-mock/creds/readwrite" -}}
export DATABASE_PASSWORD="{{ .Data.password }}"
{{- end }}
labels:
app: app-approle
spec:
serviceAccountName: vault-hashitalks
containers:
- name: hashiapp
image: <sua_docker_image>:<tag>
command: ['/bin/bash']
args: ['-c', '. /vault/secrets/db-creds && /app/postgresql-live']
envFrom:
- configMapRef:
name: hashi-data
---
apiVersion: v1
data:
DATABASE_NAME: "app_db"
DATABASE_HOST: "mydatabase.local:5432"
kind: ConfigMap
metadata:
name: hashi-data
namespace: vault-hashitalks
O modelo abaixo utiliza o backend de autenticação do Vault Kubernetes. Esse modelo já é mais eficiente que anterior, pois não há qualquer ação manual além de instrumentar o manifesto k8s da aplicação. Será necessário atachar ao PoD uma service account que fará o bound nas configurações de Role do Vault. Além disso o manifesto K8S da aplicação precisará ter annotations específicas e também alguma instrução de onde e como renderizar as secrets.
k apply -f k8s-manifests/k8s-auth-hashiapp.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: k8s-auth-hashi-app
namespace: vault-hashitalks
labels:
app: hashi-app
spec:
selector:
matchLabels:
app: hashi-app
replicas: 1
template:
metadata:
annotations:
vault.hashicorp.com/agent-inject: 'true' # annotation que diz ao injector para realizar a mutação no pod
vault.hashicorp.com/agent-inject-secret-database-config: 'database/rds/postgres/vault-hashi-talks-mock/creds/readonly' # path da secret engined database
vault.hashicorp.com/agent-inject-template-database-config: |
{{ with secret "database/rds/postgres/vault-hashi-talks-mock/creds/readonly" -}}
export DATABASE_USER="{{ .Data.username }}"
{{- end }}
{{ with secret "database/rds/postgres/vault-hashi-talks-mock/creds/readonly" -}}
export DATABASE_PASSWORD="{{ .Data.password }}"
{{- end }}
vault.hashicorp.com/role: 'vault-hashitalks-role' # role que possui a policy de acesso
labels:
app: hashi-app
spec:
serviceAccountName: vault-hashitalks
containers:
- name: hashiapp
image: <sua_docker_image>:<tag>
command: ['/bin/bash']
args: ['-c', '. /vault/secrets/database-config && /app/postgresql-live'] #Indica para aplicação qual o diretório e como renderizar as credenciais
envFrom:
- configMapRef:
name: hashi-data
---
apiVersion: v1
data:
DATABASE_NAME: "app_db"
DATABASE_HOST: "mydatabase.local:5432"
kind: ConfigMap
metadata:
name: hashi-data
namespace: vault-hashitalks
O modelo abaixo utiliza o backend de autenticação do Vault AWS auth. Esse modelo é tão eficiente quanto ao anterior (Kubernetes auth). Uma das vantagens é a possibilidade de utilizar o IAM da aws em conjunto com o vault no processo de autenticação. Com esse método além de autenticar no Vault, a aplicação pode usar a mesma service accoount para se autenticar e acessar recursos no ecossistema da AWS via IAM Role Service Account (IRSA). Lembrando que esse modelo de autenticação é específico para AWS e só irá funcionar para escopos que se utilizem do serviço de compute da AWS (EC2) como o EKS.
k apply -f k8s-manifests/aws-auth-hashiapp.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: aws-auth-hashi-app
namespace: app-example-iam-auth
labels:
app: hashi-app
spec:
selector:
matchLabels:
app: hashi-app
replicas: 1
template:
metadata:
annotations:
vault.hashicorp.com/agent-inject: 'true' # annotation que diz ao injector para realizar a mutação no pod
vault.hashicorp.com/agent-configmap: 'aws-auth-config'
labels:
app: hashi-app
spec:
serviceAccountName: app-example-iam-auth
containers:
- name: hashiapp
image: <sua_docker_image>:<tag>
command: ['/bin/bash']
args: ['-c', '. /vault/secrets/database-config && /app/postgresql-live']
envFrom:
- configMapRef:
name: hashi-data
---
apiVersion: v1
data:
DATABASE_NAME: "app_db"
DATABASE_HOST: "mydatabase.local:5432"
kind: ConfigMap
metadata:
name: hashi-data
namespace: app-example-iam-auth
---
apiVersion: v1
kind: ConfigMap
metadata:
name: aws-auth-config
namespace: app-example-iam-auth
data:
config.hcl: |
"auto_auth" = {
"method" "aws" {
"mount_path" = "auth/aws"
"config" = {
"role" = "app-example-iam-auth"
"type" = "iam"
}
}
"sink" = {
"config" = {
"path" = "/home/vault/.vault-token"
}
"type" = "file"
}
}
"exit_after_auth" = false
"pid_file" = "/home/vault/.pid"
"template" = {
"contents" = "{{ with secret \"database/rds/postgres/vault-hashi-talks-mock/creds/readonly\" -}}\n export DATABASE_USER=\"{{ .Data.username }}\"\n{{- end }}\n{{ with secret \"database/rds/postgres/vault-hashi-talks-mock/creds/readonly\" -}}\n export DATABASE_PASSWORD=\"{{ .Data.password }}\"\n{{- end }}"
"destination" = "/vault/secrets/database-config"
}
"vault" = {
"address" = "http://vault.vault-hashitalks.svc:8200"
}
config-init.hcl: |
"auto_auth" = {
"method" "aws" {
"mount_path" = "auth/aws"
"config" = {
"role" = "app-example-iam-auth"
"type" = "iam"
}
}
"sink" = {
"config" = {
"path" = "/home/vault/.vault-token"
}
"type" = "file"
}
}
"template" = {
"contents" = "{{ with secret \"database/rds/postgres/vault-hashi-talks-mock/creds/readonly\" -}}\n export DATABASE_USER=\"{{ .Data.username }}\"\n{{- end }}\n{{ with secret \"database/rds/postgres/vault-hashi-talks-mock/creds/readonly\" -}}\n export DATABASE_PASSWORD=\"{{ .Data.password }}\"\n{{- end }}"
"destination" = "/vault/secrets/database-config"
}
"exit_after_auth" = true
"pid_file" = "/home/vault/.pid"
"vault" = {
"address" = "http://vault.vault-hashitalks.svc:8200"
}
Cada um dos modelos de autenticação apresentados acima, irá montar as credenciais da Database Engine num volume compartilhado entre container da Aplicação e Vault-Agent.
IMPORTANTE: Cada aplicação irá receber as credeniais de uma maneira específica. A aplicação desse repositório renderiza as credenciais como variáveis de ambiente. Você pode alterar esse comportamento de acordo com a necessidade da aplicação utilizada.