Gerenciando secrets kubernetes com o External Secret Operator.
O External Secrets Operator é uma solução de código aberto que amplia a API do Kubernetes, projetada para lidar com um desafio frequente em ambientes de nuvem: a gestão segura de secrets ou credenciais sensíveis. Este operador não apenas permite o gerenciamento de secrets fora do cluster Kubernetes, mas também facilita a integração com sistemas de gerenciamento de secrets dedicados. Exemplos desses sistemas incluem, mas não se limitam a, HashiCorp Vault, AWS Secrets Manager e Google Secret Manager.
Dessa forma, o External Secrets Operator proporciona uma camada adicional de segurança e controle, ajudando a garantir que as credenciais sensíveis sejam manipuladas de maneira apropriada e segura.
Qual problema o External Secrets Resolve?
O External Secrets atua como um consolidador de secrets em uma única fonte confiável, o sistema de gerenciamento de secrets. Este recurso sincroniza automaticamente essas informações sigilosas com os secrets do Kubernetes em seu cluster. Desta forma, qualquer atualização feita em um secret no sistema de gerenciamento de secrets é instantaneamente espelhada no cluster Kubernetes.
Esta funcionalidade descarta a necessidade de duplicação de secrets, otimizando o gerenciamento e reforçando a segurança. Dessa forma, o External Secrets apresenta uma solução unificada e segura para o gerenciamento de dados sensíveis em ambientes de nuvem.
Ademais, com o External Secrets, é possível declarar de forma transparente no repositório quais secrets uma aplicação específica necessita, como variáveis de ambiente, por exemplo. Isso simplifica ainda mais o gerenciamento e a entrega de secrets.
Vamos ao cenário…
Em ambientes modernos de infraestrutura, é comum o uso de ferramentas que facilitam a entrega de configurações para aplicações específicas — por exemplo, Kustomize e Helm. Estas ferramentas permitem a definição de templates, que podem ser reutilizados para acelerar o deploy de objetos no Kubernetes dentro do cluster. Na maioria dos casos, esses templates são armazenados nos mesmos repositórios que o código da aplicação.
No entanto, isso levanta um problema importante: como tratamos a definição de objetos do tipo Secret? Certamente, armazenar a declaração de um Secret diretamente no repositório, mesmo que seja um repositório interno e restrito, está fora de questão devido a preocupações de segurança. É neste contexto que o External Secrets Operator se torna uma solução ideal. Esta ferramenta permite o gerenciamento de secrets fora do cluster Kubernetes, proporcionando uma camada adicional de segurança e controle.
Vamos dividir o restante do post em 4 seções:
- Deploy do External Secrets Operator
- Autenticação com o Sistema de gerenciamento de Secrets
- Criando secrets através do External Secrets
- Usando o external secret num cenário real
Deploy do External Secrets Operator
O operator do External Secret pode ser facilmente instalado através do Helm:
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets \
external-secrets/external-secrets \
-n external-secrets \
--create-namespace \
# --set installCRDs=true
Depois de instalar você pode conferir os objetos do operator conforme abaixo:
$ k get all -n external-secrets
NAME READY STATUS RESTARTS AGE
pod/external-secrets-67bb445f45-l77ps 1/1 Running 0 25h
pod/external-secrets-cert-controller-f6bb8d5df-68q5z 1/1 Running 0 25h
pod/external-secrets-webhook-7f98d8fbb6-2dn94 1/1 Running 0 25h
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/external-secrets-webhook ClusterIP 10.96.4.146 <none> 443/TCP 25h
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/external-secrets 1/1 1 1 25h
deployment.apps/external-secrets-cert-controller 1/1 1 1 25h
deployment.apps/external-secrets-webhook 1/1 1 1 25h
NAME DESIRED CURRENT READY AGE
replicaset.apps/external-secrets-67bb445f45 1 1 1 25h
replicaset.apps/external-secrets-cert-controller-f6bb8d5df 1 1 1 25h
replicaset.apps/external-secrets-webhook-7f98d8fbb6 1 1 1 25h
Antes de estabelecermos a conexão com nosso sistema de gerenciamento de secrets, é necessário selecionar um. Neste contexto, escolhemos o Hashicorp Vault para ser instalado dentro do nosso cluster Kubernetes. Utilizando a ferramenta Helm, facilitaremos o processo de implantação e configuração inicial do Vault
Adicione o repositório:
helm repo add hashicorp https://helm.releases.hashicorp.com
Defina os seguintes valores:
#vault-values.yaml
injector:
enabled: false
server:
standalone:
enabled: true
config:
ui: true
listener:
tcp:
address: "0.0.0.0:8200"
tls_disable: 1
storage:
file:
path: "/vault/data"
ui:
enabled: true
serviceType: 'ClusterIP'
É importante ressaltar que esses valores foram definidos especificamente para este artigo. Em um ambiente de produção real, provavelmente teríamos configurações muito mais complexas.
Deploy do Hashicorp Vault:
helm install vault hashicorp/vault --values vault-values.yaml
Se tudo deu certo, você deverá ver essa saída:
NAME READY STATUS RESTARTS AGE
pod/vault-0 1/1 Running 0 25h
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 25h
service/vault ClusterIP 10.96.46.109 <none> 8200/TCP,8201/TCP 25h
service/vault-internal ClusterIP None <none> 8200/TCP,8201/TCP 25h
service/vault-ui ClusterIP 10.96.201.192 <none> 8200/TCP 25h
NAME READY AGE
statefulset.apps/vault 1/1 25h
Todas as etapas a seguir devem ser realizadas no pod vault-0.
kubectl exec vault-0 -- vault operator init -key-shares=1 -key-threshold=1
Ao executar o comando acima, você deverá receber duas informações importantes:
- Intial root token
- Unseal key
Use essas informações para tirar o selo do vault:
kubectl exec vault-0 -- vault operator unseal <unseal key>
Vamos realizar as configurações iniciais do vault:
# login no vault
vault login -method=token -address="http://vault.default.svc.cluster.local:8200"
No caso atual, procedemos com a implantação do Vault no namespace ‘default’, por isso a URL deve corresponder ao formato acima. Durante essa etapa, é necessário fornecer o token do Vault, uma informação que foi disponibilizada anteriormente durante o processo de ‘unseal’ do Vault. Em seguida, ativaremos a ‘engine’ KV (Key/Value) do Vault, permitindo-nos armazenar secrets no formato chave/valorvault secrets enable -path=secret kv-v2
Por fim, vamos guardar uma secret qualquer no vault como um exemplo:
vault kv put secret/foo my-value=s3cr3t
Autenticação com o Sistema de gerenciamento de Secrets
Agora que temos o Vault e External Secrets Operator configurado precisaremos realizar a integração entre esses dois componentes para que possa haver o sincronismo entre o objeto secret do kubernetes e a secret que está definida no Vault.
Primeiramente precisamos definir o objeto SecretStore.
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
spec:
provider:
vault:
server: "http://vault.default.svc.cluster.local:8200"
path: "secret"
version: "v2"
auth:
tokenSecretRef:
name: "vault-token"
key: "token"
---
apiVersion: v1
kind: Secret
metadata:
name: vault-token
data:
token: M3uR00tT0K3n3mbas364== # "Inital root token em base64"
É IMPORTANTE ressaltar: cada namespace que contém uma aplicação que fará uso dos objetos de ExternalSecret deve ter seu próprio objeto SecretStore. Certifique-se de que o servidor do Vault não possui nenhuma restrição de rede; caso contrário, você pode encontrar problemas de sincronização.
Nesse objeto, precisamos definir três informações principais:
- ‘server’: Refere-se ao servidor do Vault. Neste caso, estamos especificando a URL do servidor do Vault, que está implantado internamente no Cluster.
- ‘path’: Indica o caminho (‘path’) da engine KV criada no Vault.
- ‘auth’: Corresponde ao método de autenticação utilizado para se comunicar com o Vault.
Para simplificar nossa demonstração, usaremos a autenticação via token para permitir que o objeto SecretStore se comunique com o backend do Vault. No entanto, essa abordagem não é recomendada para ambientes de produção. O Vault oferece diversas outras formas de autenticação mais seguras.
Após aplicar o manifesto, você deverá ver o objeto SecretStore com o status de ‘Valid’. Isso indica que a autenticação foi bem-sucedida e que o SecretStore está apto para sincronizar com os objetos ExternalSecrets.
$k get SecretStore
NAME AGE STATUS CAPABILITIES READY
vault-backend 25h Valid ReadWrite True
Criando secrets através do External Secrets
Vamos definir o seguinte manifesto abaixo para fazer o sincronismo com a secret que está Vault:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: vault-example
spec:
refreshInterval: "15s"
secretStoreRef:
name: vault-backend #Nome do objeto SecretStore
kind: SecretStore
target:
name: example-sync #O objeto ExternalSecret irá criar uma secret com esse nome
data:
- secretKey: foobar
remoteRef:
key: foo #Chave dentro do path secret, ou seja, secret/foo
property: my-value #Chave da secret que queremos recuperar.
Existem algumas informações importantes aqui:
- O a chave name dentro de secretStoreRef, deve apontar o nome do objeto SecretStore, basicamente aqui estamos informado o “endpoint” de comunicação do cluster para Vault.
- A chave name dentro de Targer, refere-se ao nome da secret que o operator do ExternalSecrets irá criar dentro do cluster.
Ao aplicar esse manifesto devemos ver a seguinte saída:
$ k get externalSecret
NAME STORE REFRESH INTERVAL STATUS READY
vault-example vault-backend 15s SecretSynced True
Podemos ver também o objeto secret criado dentro do ecossistema do kubernetes pelo External Secret Operator.
$ k get secrets
NAME TYPE DATA AGE
example-sync Opaque 1 24h
Usando o external secret num cenário mais realista
Os passos descritos anteriormente ilustraram a configuração inicial para a integração entre Vault e o External Secrets Operator, e também como podemos recuperar secrets. Retornando à questão inicial: Como podemos empregar o External Secrets para assegurar os manifestos de secrets dentro dos repositórios de forma segura?
No próximo exemplo, utilizaremos uma abordagem de deploy de uma aplicação através do Helm. Examinaremos um exemplo de arquitetura que será o foco deste post:
No modelo apresentado, temos uma aplicação rodando no cluster Kubernetes que precisa acessar o serviço AWS S3. Essa aplicação, que não segue as melhores práticas de segurança, autentica-se por meio do AWS_ACCESS_KEY_ID e do AWS_SECRET_ACCESS_KEY. Além disso, a aplicação é implantada e gerenciada por meio do Helm, o que significa que alguns valores são fornecidos através de um template.
Dado esse cenário, usaremos este Helm chart para inserir as secrets necessárias à aplicação via variáveis de ambiente com o auxílio do External Secrets Operator.
Primeiramente, vamos criar essas secrets no ecossistema do Vault:
vault kv put secret/labs/apps/random-app accessKeyId=<accessKeyId>
vault kv put secret/labs/apps/random-app secretAccessKey=<secretAccessKey>
vault kv put secret/labs/apps/random-app region=<region>
Após essa etapa, precisamos adaptar o template para receber os valores do objeto ExternalSecret:
#externalSecret.yaml
{{- if .Values.createExternalSecret }}
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: {{ .Values.ExternalSecret.name }}
namespace: {{ .Release.Namespace }}
labels:
helm.sh/chart: {{ include "random-app.chart" . }}
annotations:
{{ toYaml .Values.ExternalSecret.annotations | indent 8 }}
spec:
refreshInterval: {{ .Values.ExternalSecret.refreshInterval }}
secretStoreRef:
name: {{ .Values.ExternalSecret.vaultBackend }}
kind: SecretStore
target:
name: {{ .Values.ExternalSecret.targetName }}
data:
{{ toYaml .Values.ExternalSecret.vaultData | nindent 4 }}
{{- end }}
Com um template definido para o objeto ExternalSecrets, agora podemos definir os valores:
#values do helmchart
createExternalSecret: true
#referencia da secret
ExternalSecret:
name: random-app-aws-cred
vaultBackend: vault-backend
refreshInterval: 15s
targetName: random-app-aws-cred
vaultData:
- secretKey: accessKeyId
remoteRef:
key: labs/apps/random-app
property: accessKeyId
- secretKey: region
remoteRef:
key: labs/apps/random-app
property: region
- secretKey: secretAccessKey
remoteRef:
key: labs/apps/random-app
property: secretAccessKey
Agora, precisamos referenciar essas secrets nos valores do deployment, mais especificamente através de variáveis de ambiente, pois é assim que a aplicação espera receber essas informações:
# deployment values (...)
env:
- name: AWS_REGION
valueFrom:
secretKeyRef:
name: random-app-aws-cred
key: region
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: random-app-aws-cred
key: accessKeyId
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: random-app-aws-cred
key: secretAccessKey
E pronto! Só precisamos definir esses valores para que o External Secret possa sincronizar e criar os objetos secrets necessários para a nossa aplicação.
Ao aplicar o chart da nossa configuração, podemos observar a seguinte saída:
$ k describe pods random-app-5f8bf9954b-62gct -n random-app
Name: random-app-5f8bf9954b-62gct
Namespace: random-app
Priority: 0
Node: kind-worker2/172.19.0.6
Start Time: Sun, 14 May 2023 11:18:32 -0300
Labels: app=random-app
pod-template-hash=5f8bf9954b
Annotations: <none>
Status: Running
IP: 10.244.1.8
IPs:
IP: 10.244.1.8
Controlled By: ReplicaSet/random-app-5f8bf9954b
Containers:
random-app:
Container ID: containerd://02817212e0a9c8ce65cd7eda462ed85de3854dfe335449feba3f0e0f1360dd28
Image: docker.io/igoritosousa22/random-app:v3
Image ID: docker.io/igoritosousa22/random-app@sha256:682d67d68b43e72eb7c2ca2e3aa25cedd21aaeaaed98569c1dbec4f2151c7b64
Port: 8080/TCP
Host Port: 0/TCP
State: Running
Started: Sun, 14 May 2023 11:18:37 -0300
Ready: True
Restart Count: 0
Limits:
memory: 128Mi
Requests:
cpu: 10m
memory: 64Mi
Liveness: http-get http://:8080/_healthy delay=5s timeout=1s period=5s #success=1 #failure=3
Readiness: http-get http://:8080/_ready delay=5s timeout=1s period=5s #success=1 #failure=3
Environment:
AWS_REGION: <set to the key 'region' in secret 'random-app-aws-cred'> Optional: false
AWS_ACCESS_KEY_ID: <set to the key 'accessKeyId' in secret 'random-app-aws-cred'> Optional: false
AWS_SECRET_ACCESS_KEY: <set to the key 'secretAccessKey' in secret 'random-app-aws-cred'> Optional: false
(...)
Ao inspecionar os objetos secrets do namespace da aplicação, vemos que a secret “random-app-aws-cred” está presente com as respectivas chaves:
~$ k get secrets random-app-aws-cred -o yaml -n random-app
apiVersion: v1
data:
accessKeyId: Vm9jZSByZWFsbWVudGUgYWNobyBxdWU=
region: RXUgY29sb2NhcmlhIHVtIGhhc2ggZGUgc2VjcmV0cw==
secretAccessKey: ZGEgYXdzIGJlbSBhcXVpIGRlIGZvcm1hIHB1YmxpY2E/
immutable: false
kind: Secret
metadata:
annotations:
meta.helm.sh/release-name: random-app
meta.helm.sh/release-namespace: random-app
reconcile.external-secrets.io/data-hash: 0262414b31a871a30b2f90a57547732d0
creationTimestamp: "2023-05-13T14:18:32Z"
labels:
app.kubernetes.io/managed-by: Helm
helm.sh/chart: random-app-0.0.1
name: random-app-aws-cred
namespace: random-app
ownerReferences:
- apiVersion: external-secrets.io/v1beta1
blockOwnerDeletion: true
controller: true
kind: ExternalSecret
name: random-app-aws-cred
uid: e7134612-195b-47ef-a1c8-2dd11b48a5f1
resourceVersion: "62919"
uid: 679a0765-93d0-44c9-bb1d-ec96fae3ea36
type: Opaque
Respondendo as perguntas…
No exemplo apresentado, exploramos uma abordagem prática que nos permite declarar as secrets de uma aplicação de maneira segura. O External Secrets Operator oferece outras funcionalidades, como o push de secrets para o Gerenciador de Secrets externo, que podem ser extremamente úteis, dependendo da estratégia escolhida.
Espero que esta publicação tenha sido útil e que o conhecimento aqui partilhado possa auxiliá-lo no desafio de gerenciar secrets no Kubernetes.
Até a próxima!