Secrets
Aunque sea posible almacenar informaciones usando ConfigMaps, no es el enfoque ideal para datos sensibles, como contraseñas.
Para esos casos, es recomendable utilizar Secrets, diseñadas específicamente para almacenar informaciones críticas, como contraseñas o claves, así como el ConfigMap, pero con capas adicionales de seguridad.
Secrets funcionan exactamente como ConfigMaps. Primero debemos crearlas para ser usadas de forma que sean inyectadas dentro de otros objetos.
Tenemos dos métodos de creación de Secrets.
- Imperativo: Cuando utilizamos directamente la línea de comando.
- Declarativo: Cuando utilizamos archivos.
Un objeto Secret es exactamente igual al objeto ConfigMap en su forma, pero los datos necesitan estar codificados en base64.
apiVersion: v1
kind: Secret
metadata:
data:
Para crear un secret utilizando el modo imperativo podemos hacer:
# Generic es el tipo del secret
kubectl create secret generic mysecret --from-literal=key1=david,key2=david2
# Observe que el valor no es mostrado
kubectl describe secrets mysecret
Name: mysecret
Namespace: default
Labels: <none>
Annotations: <none>
Type: Opaque
Data
====
key1: 17 bytes
# Para eliminar
kubectl delete secrets mysecret
secret "mysecret" deleted
Durante el comando automáticamente el valor de las claves ya quedan codificados.
Si queremos crear esto con archivos necesitamos pasar las claves con valor codificado en base64.
echo -n "david" | base64
ZGF2aWQ=
# solo para mostrar cómo hacer el decode
echo "ZGF2aWQ=" | base64 --decode
david
echo -n "david2" | base64
ZGF2aWQy
echo "ZGF2aWQy" | base64 --decode
david2
apiVersion: v1
kind: Secret
metadata:
name: mysecret
data:
key1: ZGF2aWQ=
key2: ZGF2aWQy
kubectl apply -f mysecret.yaml
secret/mysecret created
kubectl get secrets
NAME TYPE DATA AGE
mysecret Opaque 2 22s
sh.helm.release.v1.istio-base.v1 helm.sh/release.v1 1 12d
sh.helm.release.v1.metrics-server.v1 helm.sh/release.v1 1 12h
Si queremos ver los valores opacos necesitamos obtener la salida completa del yaml.
kubectl get secrets mysecret -o yaml
apiVersion: v1
data:
key1: ZGF2aWQ= #<<<<
key2: ZGF2aWQy #<<<<
kind: Secret
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"v1","data":{"key1":"ZGF2aWQ=","key2":"ZGF2aWQy"},"kind":"Secret","metadata":{"annotations":{},"name":"mysecret","namespace":"default"}}
creationTimestamp: "2024-03-18T14:16:06Z"
name: mysecret
namespace: default
resourceVersion: "2824576"
uid: 20429ec4-c03c-4184-ae0f-
Con esos valores necesitamos hacer el decode como fue mostrado anteriormente.
Ahora vamos a utilizar ese secret en un pod. Sería exactamente como haríamos con el configmap, vea la diferencia.
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
labels:
run: echo
name: echo
spec:
containers:
- image: alpine
name: echo
command: ["sleep"]
args:
- "1000"
# envFrom:
# - configMapRef:
# name: myconfigmap
envFrom:
- secretRef:
name: mysecret
dnsPolicy: ClusterFirst
restartPolicy: Always
EOF
kubectl exec echo -- env | grep key
key1=david
key2=david2
En este caso estamos inyectando el valor de todas las claves key1=david y key2=david2. Para adicionar solamente una de esas claves podemos hacer.
Para obtener solamente una clave del secret que contiene varias podemos hacer.
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
labels:
run: echo2
name: echo2
spec:
containers:
- image: alpine
name: echo
command: ["sleep"]
args:
- "1000"
env:
- name: mykey
valueFrom:
secretKeyRef:
name: mysecret
key: key2
dnsPolicy: ClusterFirst
restartPolicy: Always
EOF
kubectl exec echo2 -- env | grep key
mykey=david2
También podemos inyectar el secret como archivos.
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
labels:
run: echo3
name: echo3
spec:
containers:
- image: alpine
name: echo
command: ["sleep"]
args:
- "1000"
volumeMounts:
- name: mysecret-volume
mountPath: /mysecrets
volumes:
- name: mysecret-volume
secret:
secretName: mysecret
EOF
kubectl exec echo3 -- ls /mysecrets
key1
key2
kubectl exec echo3 -- cat /mysecrets/key1
david%
kubectl exec echo3 -- cat /mysecrets/key2
david2%
En el ejemplo arriba estamos creando un volumen con los valores de las claves y estos serán usados en el volume mount. Tendremos /mysecrets/key1 y /mysecrets/key2 y el contenido será los valores reales de las claves, o sea, no codificados.
Como ese pod está en el worker3, vamos a verificar lo que acontece allá dentro.
k get pods echo3 -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
echo3 1/1 Running 0 5m26s 10.44.0.6 cka-cluster-worker3 <none> <none>
kubectl get pods echo3 -o yaml | grep uid
uid: 08198260-0b94-4934-93a3-b63d252163a7
# LOS COMANDOS ABAJO SERÁN DENTRO DEL WORKER3 PARA DONDE EL POD FUE
root@cka-cluster-worker3:/opt# cd /var/lib/kubelet/pods/
root@cka-cluster-worker3:/var/lib/kubelet/pods# ls
08198260-0b94-4934-93a3-b63d252163a7 576a9764-b33e-48e4-82d3-daa925c59c0b b055b81d-96d4-4fe7-8666-635a9d03f4bf e9870bb1-96dd-4ca2-b2e0-885cac2d26f8 fe715166-b49a-4e50-8e5b-1b6e3b65d5b0
50b04dc3-709a-40fa-a916-4f78d3f3ce1b 67a3c831-9b96-4987-8b98-4695e90b0322 c9626cf9-9095-48e0-b313-e1b8f616a0b8 eb4b3290-e36a-4a8e-9d28-39b0cfbe4ac1
# Mismo uid
root@cka-cluster-worker3:/var/lib/kubelet/pods# cd 08198260-0b94-4934-93a3-b63d252163a7/
root@cka-cluster-worker3:/var/lib/kubelet/pods/08198260-0b94-4934-93a3-b63d252163a7# ls
containers etc-hosts plugins volumes
root@cka-cluster-worker3:/var/lib/kubelet/pods/08198260-0b94-4934-93a3-b63d252163a7# cd volumes/
root@cka-cluster-worker3:/var/lib/kubelet/pods/08198260-0b94-4934-93a3-b63d252163a7/volumes# ls
kubernetes.io~projected kubernetes.io~secret
root@cka-cluster-worker3:/var/lib/kubelet/pods/08198260-0b94-4934-93a3-b63d252163a7/volumes# cd kubernetes.io~secret/
root@cka-cluster-worker3:/var/lib/kubelet/pods/08198260-0b94-4934-93a3-b63d252163a7/volumes/kubernetes.io~secret# ls
mysecret-volume
root@cka-cluster-worker3:/var/lib/kubelet/pods/08198260-0b94-4934-93a3-b63d252163a7/volumes/kubernetes.io~secret# cd mysecret-volume/
root@cka-cluster-worker3:/var/lib/kubelet/pods/08198260-0b94-4934-93a3-b63d252163a7/volumes/kubernetes.io~secret/mysecret-volume# ls
key1 key2
root@cka-cluster-worker3:/var/lib/kubelet/pods/08198260-0b94-4934-93a3-b63d252163a7/volumes/kubernetes.io~secret/mysecret-volume# cat key1
david
Algunos detalles sobre secrets:
- Secrets no son cifrados, pero codificados. Cualquiera puede simplemente durante el código hacer el decode de un secret en base64 así como hicimos manualmente.
- Los secrets no son almacenados cifrados en el etcd, pero es posible hacer el cifrado vía rest.
- Cualquiera con permiso de crear pods/deployments en el mismo namespace que los secrets pueden acceder a los secrets. Es necesario garantizar los privilegios para acceder a los secrets usando RBAC.
- Es bueno considerar otras alternativas para almacenar como AWS, Azure, GCP, Vault, etc
Encryption at Rest
Es posible cifrar los datos cuando son grabados en el ETCD. Eso no quiere decir que los datos después de leídos del ETCD estén cifrados.
Para que eso sea posible el kube-apiserver necesita saber lo que debe ser cifrado o no.
En el kube-apiserver tenemos las configuraciones pasadas.
kubectl get pods -n kube-system kube-apiserver-cka-cluster-control-plane -o=jsonpath='{.spec.containers[*].command}' | jq
[
"kube-apiserver",
"--advertise-address=172.18.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",
"--runtime-config=",
"--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"
]
--encription-provider-config no fue pasado.
Vamos primero a crear ese archivo de configuración para saber lo que debe o no ser cifrado.
No es necesario cifrar todos los recursos solamente aquel que tiene datos sensibles por ejemplo secrets y configmaps.
# Necesitamos generar una clave de cifrado con el comando abajo. Esa clave será usada para cifrar los datos que van para el etcd.
ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)
echo $ENCRYPTION_KEY
5T5/bTFcDZYzYisA4DC1mw1Qpl7NIP4BOhMkfM7zgz8=
cat > encryption-config.yaml <<EOF
apiVersion: v1
kind: EncryptionConfig
resources:
- resources: # ¿Cuáles son los recursos que queremos cifrar??
- secrets
- configmaps
providers:
- aescbc:
keys:
- name: key1
secret: ${ENCRYPTION_KEY}
- identity: {}
EOF
Los providers son los usados para cifrar los recursos. El orden que son pasados importa. Si identity que no posee nada fuera el primero nada será cifrado.
Coloque el archivo dentro de /etc/kubernetes por ejemplo que ya está montado y referencie el path para ese archivo.
De aquí para adelante todos los recursos configmaps y secrets están cifrados en el ETCD.
Si ya tiene secrets y necesita cifrarlos necesitamos recrearlos.
kubectl get secrets --all-namespaces -o yaml > allsecrets.yaml
kubectl replace --force -f allsecrets.yaml