Skip to main content

Secrets

Vamos fazer um review sobre secrets usando o conteúdo do CKA.

Usamos secrets para armazenar:

  • Passwords
  • API keys
  • Credenciais
  • Certificados
  • Conexões de banco

Um dos maiores erros de segurança é colocar secrets hardcoded dentro de um repositório, por favor nunca deixe isso acontecer.

Ainda é possível encriptar uma secret e trabalhar com ela hardcoded no repositório e desencriptar essa secrets dentro da aplicação. Isso ainda é utilizado hoje em dia e da perspectiva da segurança esta tudo bem, mas toda vez que uma secret precisa ser alterada é necessário redeployar a aplicação.

O kubernetes consegue desacoplar e injetar as secrets como variáveis de ambiente e como arquivos montados dentro do pod assim que o pod subir.

Vamos montar o seguinte cenário:

  • secret1 com a key user e valor admin montado como volume.
  • secret2 com a key password e valor 123456abcdef disponível como variável de ambiente.
  • pod com nginx e com essas secrets.
root@cks-master:~# kubectl create secret generic secret1 --from-literal user=admin
secret/secret1 created
root@cks-master:~# kubectl create secret generic secret2 --from-literal password=123456abcdef
secret/secret2 created
root@cks-master:~# kubectl run nginx --image=nginx -o yaml --dry-run=client > nginx.yaml

Altere nginx.yaml para adicionar as secrets.

apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: nginx
name: nginx
spec:
containers:
- image: nginx
name: nginx
resources: {}
### Esse bloco adicione
env:
- name: password
valueFrom:
secretKeyRef:
name: secret2
key: password
volumeMounts:
- name: secret1
mountPath: "/etc/secret1"
readOnly: true
volumes:
- name: secret1
secret:
secretName: secret1
###
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}

Aplicando...

root@cks-master:~# vim nginx.yaml
root@cks-master:~# k apply -f nginx.yaml
pod/nginx created
root@cks-master:~# k exec pods/nginx -- env | grep password
password=123456abcdef

root@cks-master:~# k exec pods/nginx -- cat /etc/secret1/user
admin

# O pod foi para o cks-worker
root@cks-master:~# k get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx 1/1 Running 0 2m8s 192.168.1.8 cks-worker <none> <none>

Agora vamos tentar hackear as secrets, vamos para o node onde o pode esta rodando e fazer algumas analises.

root@cks-worker:~# crictl ps | grep nginx
b9ca02ba11f99 5ef79149e0ec8 3 minutes ago Running nginx 0 7146adfb818d4 nginx
124380aa60e89 a80c8fd6e5229 11 minutes ago Running controller 1 4d9c694b089a7 ingress-nginx-controller-7d4db76476-xxqvt

# Vamos fazer uma inspeção nesse container
# Vou remover um pouco da saída e deixar alguns pontos somente para facilitar a leitura
root@cks-worker:~# crictl inspect b9ca02ba11f99
{
...
"info": {
"sandboxID": "7146adfb818d45c305d21f2743dfa380a05ed7cfff32dc19d3c5b44b00148fc3",
"pid": 6488, # PID do processo no host que vamos usar também
"removing": false,
"snapshotKey": "b9ca02ba11f99b42e0a66ea5cea07b1175c625d723e60742808f2676d066c1d6",
"snapshotter": "overlayfs",
"runtimeType": "io.containerd.runc.v2",
"runtimeOptions": {
"systemd_cgroup": true
},
...
{
...
"envs": [
{
"key": "password",
"value": "123456abcdef" # A secret aqui
},
{
"key": "APP1_PORT",
"value": "tcp://10.105.1.235:80"
},
{
"key": "APP1_PORT_80_TCP",
"value": "tcp://10.105.1.235:80"
},
...
],
...
"mounts": [
{ # Aqui o mount da secret dentro do host usado pelo kubelet
"container_path": "/etc/secret1",
"host_path": "/var/lib/kubelet/pods/842f7ae7-ac2b-4654-88b4-b26ed6ac315c/volumes/kubernetes.io~secret/secret1",
"readonly": true
},
...
}

# Conferindo a outra secret montada no host pelo ponto de montagem
root@cks-worker:~# cat /var/lib/kubelet/pods/842f7ae7-ac2b-4654-88b4-b26ed6ac315c/volumes/kubernetes.io~secret/secret1/user
admin
root@cks-worker:~#

# Ou através do pid podemos ir direto no filesystem do processo
root@cks-worker:~# cat /proc/6488/root/etc/secret1/user
admin

Se alguém tiver acesso ao container runtime ou ao root de um node é suficiente para conseguir acesso as secret. Não é considerado um problema de segurança, o problema é dar o esse poder de acessar os nodes como root ou permissão no container runtime para quem não precisa ter.

Outra forma de conseguir acesso as secrets é conseguir acesso ao ETCD.

# Verificando onde estão os certificados do etcd
root@cks-master:~# cat /etc/kubernetes/manifests/kube-apiserver.yaml | grep etcd
- --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt
- --etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt
- --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key
- --etcd-servers=https://127.0.0.1:2379

# Usando o certificados para testar a conexão
# Não precisamos especificar o endopoint pois estamos no mesmo host
root@cks-master:~# ETCDCTL_API=3 etcdctl --cert /etc/kubernetes/pki/apiserver-etcd-client.crt --key /etc/kubernetes/pki/apiserver-etcd-client.key --cacert /etc/kubernetes/pki/etcd/ca.crt endpoint health
127.0.0.1:2379 is healthy: successfully committed proposal: took = 21.755638ms

# Como as secrets estão no namespace default passamos /registry/secrets/namespace_name/secret_name
root@cks-master:~# ETCDCTL_API=3 etcdctl --cert /etc/kubernetes/pki/apiserver-etcd-client.crt --key /etc/kubernetes/pki/apiserver-etcd-client.key --cacert /etc/kubernetes/pki/etcd/ca.crt get /registry/secrets/default/secret1
/registry/secrets/default/secret1
k8s


v1Secret�

secret1�default"*$0857e5b5-0553-4977-b61b-de3016a62c242�����a
kubectl-createUpdate�v����FieldsV1:-
+{"f:data":{".":{},"f:user":{}},"f:type":{}}B
useradmin�Opaque�" #<<<<<

root@cks-master:~# ETCDCTL_API=3 etcdctl --cert /etc/kubernetes/pki/apiserver-etcd-client.crt --key /etc/kubernetes/pki/apiserver-etcd-client.key --cacert /etc/kubernetes/pki/etcd/ca.crt get /registry/secrets/default/secret2
/registry/secrets/default/secret2
k8s


v1Secret�

secret2�default"*$ae064b8d-eac6-4c76-8f29-21db3c9d125b2�����e
kubectl-createUpdate�v����FieldsV1:1
/{"f:data":{".":{},"f:password":{}},"f:type":{}}B
password
123456abcdef�Opaque�" #<<<<<
root@cks-master:~#

E vemos que as secrets estão armazenadas sem encriptação. O que podemos fazer é encriptar as secrets para caso alguém consiga acesso ao ETCD não consiga essa informação plaintext como vemos aqui. O kubernetes pode ter o ETCD completamente separado em qualquer outro lugar.

Nesse caso o apiserver pode ser o responsável por encriptar e desencriptar as chaves armazenadas no ETCD. Será necessário passar um argumento (--encryption-provider-config) no manifesto ou serviço do apiserver para que ele execute esta ação.

O apiserver irá procurar o objeto de tipo EncryptionConfig que definará o que e como deve ser encriptado.

Vamos fazer uma análise do objeto.

apiVersion: v1
kind: EncryptionConfig
resources:
- resources: # Quais os recursos que queremos encriptar??
- secrets
providers: # Um array de multiplos possíveis providers que são executados em ordem.
- identity: {} # Esse provider é o default e nada deve ser encriptado, mas armazenado em plaintext
- aesgcm: # Algorítmo de encriptação
keys:
- name: key1
secret: c2VjcmV0IGlzIHNlY3VyZQ==
- name: key2
secret: dGhpcyBpcyBwYXNzd29yZA==
- aescbc: # Algorítmo de encriptação
keys:
- name: key1
secret: c2VjcmV0IGlzIHNlY3VyZQ==
- name: key2
secret: dGhpcyBpcyBwYXNzd29yZA==

Uma coisa importante é que os providers trabalham em ordem sendo que o primeiro é usado para encriptação quando novos recursos são criados.

Recursos que já foram salvo anteriormente permanecerão como estavam.

Olhando para o yaml acima, as novas secrets não serão armazenadas encriptadas pois o - identity: {} é o primeiro da lista. Porém para leitura, podemos ler secrets desencriptadas e encriptadas com os algorítmos aesgcm e aescbc tentando fazer a decriptação com as chaves definidas.

Só para ficar melhor entendido no exemplo abaixo temos o inverso.


```yaml
apiVersion: v1
kind: EncryptionConfig
resources:
- resources: # Quais os recursos que queremos encriptar??
- secrets
providers:
- aesgcm: # Algorítmo de encriptação que será usado para salvar os json dos novos recursos, nesse caso somente secrets.
keys:
- name: key1
secret: c2VjcmV0IGlzIHNlY3VyZQ==
- name: key2
secret: dGhpcyBpcyBwYXNzd29yZA==
- identity: {}

Se não definirmos por ultimo o - identity: {} não será possível ler secrets que estejam desencriptadas. Esse é um exemplo clássico de quando passamos a encriptar os recursos, mas já temos secrets criadas desencriptadas no ETCD.

Para encriptar todas as secrets, uma vez definido objeto acima, podemos recriar todas as secrets que ele será encriptado com aesgcm

kubectl get secrets --all-namespaces -o json | kubectl replace -f -

Se quisermos desencriptar somente mudamos a ordem para e reaplicamos o comando novamente que todas elas serão armazenada sem encriptação.

apiVersion: v1
kind: EncryptionConfig
resources:
- resources:
- secrets
providers:
- identity: {}
- aescbc: # outro exemplo somente
keys:
- name: key1
secret: <BASE 64 ENCODED SECRET> # lembrando que a secret aqui precisa estar em base64

Vamos testar e encriptar todas as secrets do cluster com aescbc e um password da nossa escolha.

Vamos criar o manifesto com uma senha já em base64 e com aescbc como o primeiro da lista.

# É necessário que o password tenha 32 bits ou seja, 32 caracteres
root@cks-master:/etc/kubernetes/etcd# echo -n "1234567890abcdefghijklmnopqrstuv" | base64
MTIzNDU2Nzg5MGFiY2RlZmdoaWprbG1ub3BxcnN0dXY=

# Pode ser gerado com o comando abaixo também como mostra a documentação.
root@cks-master:/etc/kubernetes/manifests# head -c 32 /dev/urandom | base64
sxpf6fmJM6KLYEtx5FbeypRInerEMcOarM+bPx8ep6I=

root@cks-master:/etc/kubernetes/manifests# echo "sxpf6fmJM6KLYEtx5FbeypRInerEMcOarM+bPx8ep6I=" | base64 --decode
��_���3��`Kq�V�ʔH���1Ú�ϛ?��root@cks-master:/etc/kubernetes/manifests#

root@cks-master:~# cd /etc/kubernetes/

root@cks-master:/etc/kubernetes# mkdir etcd

root@cks-master:/etc/kubernetes# cd etcd/

root@cks-master:/etc/kubernetes/etcd# vim ec.yaml

root@cks-master:/etc/kubernetes/etcd# cat ec.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: MTIzNDU2Nzg5MGFiY2RlZmdoaWprbG1ub3BxcnN0dXY=
- identity: {}

# Agora vamos adicionar o parâmetro no kube-apiserver.yaml e montar o diretório onde temos o arquivo.
root@cks-master:/etc/kubernetes/etcd# cd ../manifests/
# Alterando o que é necessário
root@cks-master:/etc/kubernetes/manifests# vim kube-apiserver.yaml
root@cks-master:/etc/kubernetes/manifests# cat kube-apiserver.yaml
apiVersion: v1
kind: Pod
metadata:
annotations:
kubeadm.kubernetes.io/kube-apiserver.advertise-address.endpoint: 10.128.0.5:6443
creationTimestamp: null
labels:
component: kube-apiserver
tier: control-plane
name: kube-apiserver
namespace: kube-system
spec:
containers:
- command:
- kube-apiserver
- --encryption-provider-config=/etc/kubernetes/etcd/ec.yaml # Apontando o manifesto
- --advertise-address=10.128.0.5
- --allow-privileged=true
- --authorization-mode=Node,RBAC
- --client-ca-file=/etc/kubernetes/pki/ca.crt
- --enable-admission-plugins=NodeRestriction
- --enable-bootstrap-token-auth=true
- --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt
- --etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt
- --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key
- --etcd-servers=https://127.0.0.1:2379
- --kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt
- --kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key
- --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
- --proxy-client-cert-file=/etc/kubernetes/pki/front-proxy-client.crt
- --proxy-client-key-file=/etc/kubernetes/pki/front-proxy-client.key
- --requestheader-allowed-names=front-proxy-client
- --requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt
- --requestheader-extra-headers-prefix=X-Remote-Extra-
- --requestheader-group-headers=X-Remote-Group
- --requestheader-username-headers=X-Remote-User
- --secure-port=6443
- --service-account-issuer=https://kubernetes.default.svc.cluster.local
- --service-account-key-file=/etc/kubernetes/pki/sa.pub
- --service-account-signing-key-file=/etc/kubernetes/pki/sa.key
- --service-cluster-ip-range=10.96.0.0/12
- --tls-cert-file=/etc/kubernetes/pki/apiserver.crt
- --tls-private-key-file=/etc/kubernetes/pki/apiserver.key
image: registry.k8s.io/kube-apiserver:v1.31.0
imagePullPolicy: IfNotPresent
livenessProbe:
failureThreshold: 8
httpGet:
host: 10.128.0.5
path: /livez
port: 6443
scheme: HTTPS
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 15
name: kube-apiserver
readinessProbe:
failureThreshold: 3
httpGet:
host: 10.128.0.5
path: /readyz
port: 6443
scheme: HTTPS
periodSeconds: 1
timeoutSeconds: 15
resources:
requests:
cpu: 250m
startupProbe:
failureThreshold: 24
httpGet:
host: 10.128.0.5
path: /livez
port: 6443
scheme: HTTPS
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 15
volumeMounts:
- mountPath: /etc/ssl/certs
name: ca-certs
readOnly: true
- mountPath: /etc/ca-certificates
name: etc-ca-certificates
readOnly: true
- mountPath: /etc/kubernetes/pki
name: k8s-certs
readOnly: true
# Montando o volume que contem o manifesto ec.yaml
- mountPath: /etc/kubernetes/etcd
name: etcd # << esse volume sera montado no path acima
readOnly: true
- mountPath: /usr/local/share/ca-certificates
name: usr-local-share-ca-certificates
readOnly: true
- mountPath: /usr/share/ca-certificates
name: usr-share-ca-certificates
readOnly: true
hostNetwork: true
priority: 2000001000
priorityClassName: system-node-critical
securityContext:
seccompProfile:
type: RuntimeDefault
volumes:
- hostPath:
path: /etc/ssl/certs
type: DirectoryOrCreate
name: ca-certs
- hostPath:
path: /etc/ca-certificates
type: DirectoryOrCreate
name: etc-ca-certificates
# Volume mapeado para ser montado
- hostPath:
path: /etc/kubernetes/etcd
type: DirectoryOrCreate
name: etcd
- hostPath:
path: /etc/kubernetes/pki
type: DirectoryOrCreate
name: k8s-certs
- hostPath:
path: /usr/local/share/ca-certificates
type: DirectoryOrCreate
name: usr-local-share-ca-certificates
- hostPath:
path: /usr/share/ca-certificates
type: DirectoryOrCreate
name: usr-share-ca-certificates
status: {}

# Se o kube-apiserver não subir, é porque a chave não tinha 32 caracteres

Vamos conferir agora se podermos ler a secrets

# Ainda podemos...
root@cks-master:~# ETCDCTL_API=3 etcdctl --cert /etc/kubernetes/pki/apiserver-etcd-client.crt --key /etc/kubernetes/pki/apiserver-etcd-client.key --cacert /etc/kubernetes/pki/etcd/ca.crt get /registry/secrets/default/secret1
/registry/secrets/default/secret1
k8s


v1Secret�

secret1�default"*$0857e5b5-0553-4977-b61b-de3016a62c242�����a
kubectl-createUpdate�v����FieldsV1:-
+{"f:data":{".":{},"f:user":{}},"f:type":{}}B
useradmin�Opaque�"


# Vamos aplicar só para uma secret e conferir
root@cks-master:~# kubectl get secrets secret1 -o json | kubectl replace -f -
secret/secret1 replaced

# E Já temos nossa secret encriptada no etcd
root@cks-master:~# ETCDCTL_API=3 etcdctl --cert /etc/kubernetes/pki/apiserver-etcd-client.crt --key /etc/kubernetes/pki/apiserver-etcd-client.key --cacert /etc/kubernetes/pki/etcd/ca.crt get /registry/secrets/default/secret1
/registry/secrets/default/secret1
k8s:enc:aescbc:v1:key1:ɔ;c8���%��R|����x��{���5L�]��(�̓�SE�B�A�bϷ������^�>�_a��*; 36@���w..�j����jP��K�����d/j�����n�v��:|�7�I�V�b�죈��Q
�H��|��[|z�]�����9�Ԋ�.�L�?��'�[VѾz��<��{FN]ӏq�2��%��A�� zr���+}��ȫN�l��'�5�|/u�l�2��)d�t����VX�Il�sm

# Analisando a secret podemos ver que o kube apiserver fez a decriptação e trouxe em base64 que é o padrão que estava antes.
root@cks-master:~# k get secrets secret1 -o yaml
apiVersion: v1
data:
user: YWRtaW4=
kind: Secret
metadata:
creationTimestamp: "2024-08-27T13:33:24Z"
name: secret1
namespace: default
resourceVersion: "1031144"
uid: 0857e5b5-0553-4977-b61b-de3016a62c24
type: Opaque
root@cks-master:~# echo "YWRtaW4=" | base64 --decode
adminroot@cks-master:~#

Claro que se criar uma nova secret ela já estará encriptada no ETCD.

root@cks-master:~# k get secrets --all-namespaces -o yaml | kubectl replace -f -
...

root@cks-master:~# k get secrets -n kube-system bootstrap-token-xny0k4
NAME TYPE DATA AGE
bootstrap-token-xny0k4 bootstrap.kubernetes.io/token 5 11d

# Veja que ele ja começa avisando qual a encriptação aescbc
root@cks-master:~# ETCDCTL_API=3 etcdctl --cert /etc/kubernetes/pki/apiserver-etcd-client.crt --key /etc/kubernetes/pki/apiserver-etcd-client.key --cacert /etc/kubernetes/pki/etcd/ca.crt get /registry/secrets/kube-system/bootstrap-token-xny0k4
/registry/secrets/kube-system/bootstrap-token-xny0k4
k8s:enc:aescbc:v1:key1:l"E��>!А��/J�����넞:A
?�P4���.
y�����
`s ��?|%5�i���m'aU����}�i=b�H�jF�Ŧ��r����f'7�c�u�� ®�YҜ��=����CH�--,��4�M)
j
��ι�&DP��O>^��K�U�����'ء���8�4�
��z[3�˗x�=�?t���l��h�CZb�͠��nU�R�v�|"�*�� d`�a��9`ӽ�5��09��F� �Gϸ�3.�{��ZD�jAK�<�'����@���|�,�]�R��R��&���SxB�z�5�9���b����U��"��6�h�zbޥ1�Ž4����TޢD�Q��0i��>�ע�<6)���[`���Z-����g�Okؑ�!��E}��l%���(E�g�E@�)
������j+�
�$��M�v�F��Zʤ՛Ӷ�r��]�Ư�wG�:�'H���(�-)��;�K����_�o�ا�D�-$��>/�Dui��wbE���v��֯�'3�U7)y���k
A�C����=e�=�+"�8�q(
root@cks-master:~#

Agora que que temos tudo encriptado e recriado podemos remover o identify se quiser.

Apesar de possível não precisamos encriptar todos os recursos encriptar e desencriptar recursos pode causar problemas. Faça isso somente para recursos que realmente possuem informações de credenciais.

Usamos uma encriptação com uma chave estática montada direto no apiserver que não é a melhor solução para gerenciar a chave. Se um atacante conseguir acesso ao control plane e conseguir ler o filesystem ainda sim será posśivel fazer a leitura das secrets como vimos acima.

Em produção geralmente as secrets dependem de alguma ferramenta de terceiros. No exame provalmente não será pedido pois sai fora do escopo.

alt text

Só um detalhe para lembrar, base64 não é encriptação.