Skip to main content

Restringir Acceso a la API Kubernetes

El kube-apiserver es el componente central de Kubernetes haciendo la comunicación entre todos los componentes. Es una API reutilizable y extensible siendo esta una de las principales razones de la popularidad de Kubernetes.

Cuando enviamos una request a Kubernetes es con el apiserver con el que estamos interactuando siendo este responsable de:

  • Autenticación a través de certificados (1 nivel)
    • ¿Quién eres?
  • Autorización a través de roles y rolebindings (2 nivel)
    • ¿Tienes permiso para ejecutar esta acción en este recurso?
  • Admisión (3 nivel)
    • ¿Estás infringiendo alguna regla de límite impuesta en el clúster|namespace?

Admission Controllers hacen verificaciones adicionales sobre límites o personalizaciones customizadas. Podemos crear nuestras propias limitaciones con herramientas de terceros como por ejemplo OPA (Open Policy Agent).

Solamente si los 3 niveles pasan la acción será ejecutada.

  • Una request puede venir de:
    • Normal user
    • Service Account
    • Anónimo
  • Toda request debe ser autenticada
    • O debe ser tratada como un usuario anónimo.

¿Qué podemos hacer para mejorar la seguridad del apiserver?

  • No permitir acceso anónimo:

    root@cks-master:~# curl https://localhost:6443 -k
    {
    "kind": "Status",
    "apiVersion": "v1",
    "metadata": {},
    "status": "Failure",
    "message": "forbidden: User \"system:anonymous\" cannot get path \"/\"",
    "reason": "Forbidden",
    "details": {},
    "code": 403
    }
    # Esto significa que está siendo permitido usuario anónimo, vamos a cambiar.
    # añade - --anonymous-auth=false en el manifiesto del kube-apiserver y prueba.

    root@cks-master:~# curl https://localhost:6443 -k
    {
    "kind": "Status",
    "apiVersion": "v1",
    "metadata": {},
    "status": "Failure",
    "message": "Unauthorized",
    "reason": "Unauthorized",
    "code": 401
    }
  • Deshabilitar puertos inseguros: Era posible liberar HTTP en el apiserver, pero a partir de la versión 1.22 ya no es posible. Si necesitamos certificados para autenticar el usuario, habilitar los puertos inseguros de Kubernetes hará un bypass de la autenticación y autorización aunque el admission sea analizado. Esto solamente era utilizado para propósitos de debug, pero es bueno conocer.

  • Prevenir pods de acceder a la API: Deshabilitar el automount del token del service account en los pods, ya fue mostrado anteriormente.

  • Prevenir acceso no autorizado (RBAC). Garantizar authorization-mode con RBAC. alt text

  • Apiserver port debería estar detrás de un firewall / Permitir solamente rango específico de IP: Esta protección está en un nivel fuera del examen y fuera de la competencia de Kubernetes.

  • No exponer el ApiServer fuera del clúster. Voy a mostrar abajo en Manual Api Query. En caso necesario usar el ítem anterior ↑. Existen otras maneras y servicios que podemos exponer en el clúster para aplicar un manifiesto.

    • Ejecutar un pod o un job kubectl dentro del clúster.
    • SSH a un bastion que dentro de la red tenga acceso a la API.
    • Soluciones CI/CD.
    • VPN.
  • Restringir accesos de los Nodes al Apiserver (NodeRestriction)

Podemos enviar requisiciones de diferentes formas siendo la más común el uso de la CLI kubectl, pero podemos utilizar libs para diferentes lenguajes.

Es necesario restringir el acceso al API server una vez que este es la puerta de entrada al clúster.

Manual API Query

Si queremos hacer una llamada a la API usando kubectl pasando los certificados manualmente es posible.


# Este comando muestra el kubeconfig que estamos utilizando sin exponer los certificados
root@cks-master:~# k config view
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: DATA+OMITTED
server: https://10.128.0.5:6443
name: kubernetes
contexts:
- context:
cluster: kubernetes
user: david
name: david
- context:
cluster: kubernetes
user: kubernetes-admin
name: kubernetes-admin@kubernetes
current-context: kubernetes-admin@kubernetes
kind: Config
preferences: {}
users:
- name: david
user:
client-certificate-data: DATA+OMITTED
client-key-data: DATA+OMITTED
- name: kubernetes-admin
user:
client-certificate-data: DATA+OMITTED
client-key-data: DATA+OMITTED

Dentro del kubeconfig podemos apuntar los paths de los certificados y claves en vez de utilizar el propio contenido del certificado embedado. Por comodidad de tener solamente un único archivo se acaba incluyendo los certificados con plaintext en base64 en el kubeconfig. El comando abajo muestra el archivo completo en ~/.kube/config lo mismo que hacer un cat ~/.kube/config.

# Voy a reducir las salidas para que sea fácil de leer
root@cks-master:~# k config view --raw
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: LS0tLS1CRUdJT...RklDQVRFLS0tLS0K
server: https://10.128.0.5:6443
name: kubernetes
contexts:
- context:
cluster: kubernetes
user: kubernetes-admin
name: kubernetes-admin@kubernetes
current-context: kubernetes-admin@kubernetes
kind: Config
preferences: {}
users:
- name: kubernetes-admin
user:
client-certificate-data: LS0tLS1CRUdJT...RFLS0tLS0K
client-key-data: LS0tLS1CRUdJT...tLS0tCg==

Vamos a extraer esos certificados a archivo ya haciendo el base64 decode.


# Voy a remover algunos de los caracteres del base64 para facilitar la lectura
root@cks-master:~# echo "LS0tLS1C....FLS0tLS0K" | base64 --decode > ca

root@cks-master:~# cat ca
-----BEGIN CERTIFICATE-----
MIIDBTCCAe2gAwIBAgIIfy9qvG3J3uEwDQYJKoZIhvcNAQELBQAwFTETMBEGA1UE
AxMKa3ViZXJuZXRlczAeFw0yNDA4MTUyMDM2MDJaFw0zNDA4MTMyMDQxMDJaMBUx
EzARBgNVBAMTCmt1YmVybmV0ZXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQDxJ40vI2VhF+5F9SFNNfliOdjbi2+Ml+9SFMfd5WDbNEMqp3hmOpJngXGk
#Reducido
JU7NAYRagu7ypyzH9P3F2V6pThYBG0hRtt8UJippp399SDLXXLlOJKEKbY1DGeNa
MIxgD80KO92iey328zS1YxBAPC3D3StC4+14kGUAC1qOXa6uF1YvNgYDMZSz8AXL
SHLMRY9GfN2L5GQAcRQgprQeb83kaV0X7mqsnvMbQlrcZCub7wZ9M44WuQ+Qomjn
mOyo9Yb6q+QU

root@cks-master:~# echo "LS0tLS1...VRFLS0tLS0K" | base64 --decode > crt
-----END CERTIFICATE-----

root@cks-master:~# cat crt
-----BEGIN CERTIFICATE-----
MIIDKTCCAhGgAwIBAgIIHvhZBgu4UWkwDQYJKoZIhvcNAQELBQAwFTETMBEGA1UE
AxMKa3ViZXJuZXRlczAeFw0yNDA4MTUyMDM2MDJaFw0yNTA4MTUyMDQxMDVaMDwx
...
wcfJ3r/K5DpOova1rSprr/2RjbqBE+XH82Q9gXsYGLNs+JFsy1Rl4W4DiiftCKZT
Syh1SuqHG8OWRVUNoV9D7rMV5Ix9hwQJaFOCRx2zDqnTRizNwI2L9NqRtUBz9jB0
YiBtpfXMmEGQJdQBU7YiD2zpD3RY9RIh2r0T9ZtXVU0VJx2QgpbwdXekBb3X
-----END CERTIFICATE-----

root@cks-master:~# echo "LS0tLS...0tLS0tCg==" | base64 --decode > key

root@cks-master:~# cat key
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAzcGoyR3lmL/uPCCwnd7dMIOv+JSl+lW7BUOLup57sLZvvPKY
HgUpJ/VEKWek+fYM+uoa1Xr1tVYo70LgfGiDNHRJhsm6nMCQJAJEfe6/FfIcr8v/
SvDP18QnP91UIGfrgpr9RWI1IYn8DxAxExkSGbbiGv+IgXp3p3vzIQPghlzv5E0g
...
HxTs4wKBgCqZmA+9QW52ii63i61YBY3m6rQA8FKd3hiPaC6USaXN53hMqcTfNLmk
sspEI8PVyIs75bD0qyK+WJYUjVlu2+iSXVQC9M26v4P1jfLdHeDuRuWsa/kmunJb
qa6+xdpKHvMAyd6pJg2m/T9afh9L2+0AGyqOiDbUzBB/ClyNef9c
-----END RSA PRIVATE KEY-----

Y podemos hacer el curl pasando los certificados.

root@cks-master:~# curl https://10.128.0.5:6443
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

# Si no pasamos los certificados del cliente entonces somos anónimos
root@cks-master:~# curl https://10.128.0.5:6443 --cacert ca
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "forbidden: User \"system:anonymous\" cannot get path \"/\"",
"reason": "Forbidden",
"details": {},
"code": 403
}

root@cks-master:~# curl https://10.128.0.5:6443 --cacert ca --cert crt
curl: (58) unable to set private key file: 'crt' type PEM

root@cks-master:~# curl https://10.128.0.5:6443 --cacert ca --cert crt --key key
{
"paths": [
"/.well-known/openid-configuration",
"/api",
"/api/v1",
"/apis",
"/apis/",
"/apis/admissionregistration.k8s.io",
# Removido para disminuir la salida del estudio
## ....
"/readyz/poststarthook/start-system-namespaces-controller",
"/readyz/poststarthook/storage-object-count-tracker-hook",
"/readyz/shutdown",
"/version"
]

Si queremos acceder a Kubernetes fuera del clúster necesitamos atender al service que expone la API.

# En el momento el service es un ClusterIP o sea, solamente accedido dentro del clúster, para que sea expuesto es necesario cambiar esto a NodePort
root@cks-master:~# k get svc kubernetes
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 6d12h

root@cks-master:~# k edit svc kubernetes
service/kubernetes edited

root@cks-master:~# k get svc kubernetes
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes NodePort 10.96.0.1 <none> 443:31305/TCP 6d12h

# Podemos obtener el IP por la interfaz gráfica o usar el metadata server en el caso del GCP
curl -H "Metadata-Flavor: Google" "http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/access-configs/0/external-ip"
34.30.155.135

# O incluso usando un sitio externo que devuelve la información que necesitamos
curl ifconfig.me
34.30.155.135

Usando dirección IP externa de cualquiera de los nodes en el puerto 31305 podemos llegar al clúster.

En mi máquina local.

# Y llegamos allá, pero necesitamos de un kubeconfig configurado o pasar los certificados.
curl https://34.30.155.135:31305
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

Si copiamos el kubeconfig que tenemos en el node master a un archivo y apuntar el kubeconfig en vez de configurar también funciona, pero con algunos ajustes.

# creando el config con el kubeconfig de nuestro clúster tal como está
vim config

# No funciona porque el IP del clúster del config está apuntando a un IP interno del clúster, vamos a ajustar
❯ kubectl --kubeconfig config get pods
819678 memcache.go:265] couldnt get current server API group list: Get "https://10.128.0.5:6443/api?timeout=32s": dial tcp 10.128.0.5:6443: i/o timeout

# Vamos a alterar la línea abajo para nuestro https://34.30.155.135:31305
cat config | grep server
server: https://10.128.0.5:6443

vim config

cat config | grep server
server: https://34.30.155.135:31305

kubectl --kubeconfig config get pods
E0822 06:08:03.646491 838578 memcache.go:265] couldnt get current server API group list: Get "https://34.30.155.135:31305/api?timeout=32s": tls: failed to verify certificate: x509: certificate is valid for 10.96.0.1, 10.128.0.5, not 34.30.155.135

# Vamos a arreglar y explicamos después.
# Vamos a forzar que el nombre kubernetes sea traducido al ip usando el archivo de hosts
sudo bash -c 'echo "34.30.155.135 kubernetes" >>/etc/hosts'

# Ahora editamos nuevamente el archivo config sustituyendo el IP por kubernetes
vim config
13s
cat config | grep server
server: https://kubernetes:31305

❯ kubectl --kubeconfig config get pods
NAME READY STATUS RESTARTS AGE
app1 1/1 Running 1 (8h ago) 3d17h
app2 1/1 Running 1 (8h ago) 3d17h
kube-bench-master-vdsxd 0/1 Completed 0 2d13h

Lo que pasó fue que ese certificado no es válido para ese IP que estamos usando. Vamos a conferir en el master. Si estamos llegando al apiserver necesitamos conferir el certificado de él.

root@cks-master:~# cd /etc/kubernetes/pki/
root@cks-master:/etc/kubernetes/pki# ls
apiserver-etcd-client.crt apiserver.key front-proxy-ca.key
apiserver-etcd-client.key ca.crt front-proxy-client.crt
apiserver-kubelet-client.crt ca.key front-proxy-client.key
apiserver-kubelet-client.key etcd sa.key
apiserver.crt front-proxy-ca.crt sa.pub
root@cks-master:/etc/kubernetes/pki#

# Verificando el certificado
root@cks-master:/etc/kubernetes/pki# openssl x509 -in apiserver.crt -text
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 7696322783196691690 (0x6aced4cb612f3cea)
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN = kubernetes
Validity
Not Before: Aug 15 20:36:02 2024 GMT
Not After : Aug 15 20:41:02 2025 GMT
Subject: CN = kube-apiserver
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Modulus:
00:c5:fa:73:fd:19:53:b4:b9:8f:22:fe:68:41:9d:
28:7d:43:1e:51:10:ca:71:49:84:a9:af:29:59:4a:
2a:d4:86:72:d0:bf:a2:57:8d:70:72:ff:97:22:41:
f1:2a:8d:b5:db:96:7a:57:8e:d5:6e:10:ed:3e:d6:
ba:f4:55:7e:a4:2e:49:4b:df:9b:10:44:6a:e7:86:
c4:0b:fa:f0:2e:41:0a:08:f7:e1:6f:48:ae:ea:1a:
c2:46:66:77:f3:82:5a:41:82:d7:9f:84:08:ee:6d:
6d:92:43:a8:e5:80:b0:90:12:1b:22:3d:f6:c5:e4:
ad:55:99:37:44:3f:6a:33:7d:82:95:7f:c1:50:3e:
c2:e4:83:4d:8c:2b:3a:12:63:0d:47:42:ef:aa:89:
49:06:82:b8:2b:84:5c:24:b0:d0:10:e1:82:69:29:
32:ed:ce:74:00:4d:2e:97:5f:db:68:33:61:51:5a:
78:d9:14:ad:75:80:d2:87:84:91:e9:99:af:e0:6f:
12:44:6e:04:4d:da:95:3e:53:fc:f4:61:57:bc:96:
5c:bc:6e:30:05:ab:c6:cb:e9:c6:7e:5d:01:86:99:
32:b1:9b:85:b8:be:c5:f8:c1:0b:2d:38:54:06:a4:
5e:b4:5b:2b:83:98:ee:e0:99:96:85:5b:82:b2:f7:
a6:6d
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Authority Key Identifier:
keyid:BA:A3:CC:43:88:3F:85:E0:68:92:58:7A:6B:22:55:06:74:FA:01:8E
# Observe que estos son todos los nombres que él acepta
# El IP 34.30.155.135 no está ahí.
# Usamos kubernetes, pero podría ser cks-master o cualquier otro de estos DNSs.
X509v3 Subject Alternative Name:
DNS:cks-master, DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, IP Address:10.96.0.1, IP Address:10.128.0.5
Signature Algorithm: sha256WithRSAEncryption
d0:9a:bf:c8:c5:87:69:5c:0b:4d:17:ba:38:15:d8:02:6f:f1:
7d:63:b9:6a:a9:49:1f:ed:f0:6c:4f:84:1a:94:57:40:f0:0c:
92:a4:c7:45:81:87:6f:bf:f0:8c:64:49:5c:6f:0d:3d:3d:d5:
c1:bb:ea:46:b7:8d:4f:a6:b3:7d:52:2a:b4:a3:92:de:49:ad:
42:84:99:7e:26:65:84:a9:0e:2b:de:55:c3:44:aa:ea:3a:08:
04:2f:97:e6:17:c0:fe:6f:a6:43:4b:8b:ea:e4:43:06:0f:16:
2e:d2:22:f1:b1:99:31:c7:ef:0b:3a:80:2d:de:e7:ee:f7:2d:
02:d0:cb:34:5e:0b:a9:23:ba:0a:86:5c:a6:53:51:dc:25:39:
0f:c5:39:8e:22:c4:6f:8e:69:c0:09:44:d8:00:d0:99:6e:a1:
40:31:54:11:ee:9d:04:77:ea:d2:86:12:3c:1a:fd:da:88:39:
0e:c6:77:0c:7c:50:a2:f8:c5:bb:2c:df:83:c0:74:cc:58:92:
69:58:31:9a:7e:7f:bd:62:d1:84:78:bc:62:21:07:2f:9e:5c:
d9:3d:23:82:e9:fe:95:01:8c:44:2a:b2:ed:a3:52:63:1e:a6:
41:11:a9:8e:b4:7d:d5:4b:bb:1e:61:47:6b:5e:c3:e3:98:c6:
5a:78:43:c5
-----BEGIN CERTIFICATE-----
MIIDiTCCAnGgAwIBAgIIas7Uy2EvPOowDQYJKoZIhvcNAQELBQAwFTETMBEGA1UE
AxMKa3ViZXJuZXRlczAeFw0yNDA4MTUyMDM2MDJaFw0yNTA4MTUyMDQxMDJaMBkx
FzAVBgNVBAMTDmt1YmUtYXBpc2VydmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAxfpz/RlTtLmPIv5oQZ0ofUMeURDKcUmEqa8pWUoq1IZy0L+iV41w
cv+XIkHxKo2125Z6V47VbhDtPta69FV+pC5JS9+bEERq54bEC/rwLkEKCPfhb0iu
6hrCRmZ384JaQYLXn4QI7m1tkkOo5YCwkBIbIj32xeStVZk3RD9qM32ClX/BUD7C
5INNjCs6EmMNR0LvqolJBoK4K4RcJLDQEOGCaSky7c50AE0ul1/baDNhUVp42RSt
dYDSh4SR6Zmv4G8SRG4ETdqVPlP89GFXvJZcvG4wBavGy+nGfl0BhpkysZuFuL7F
+MELLThUBqRetFsrg5ju4JmWhVuCsvembQIDAQABo4HYMIHVMA4GA1UdDwEB/wQE
AwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMB8GA1UdIwQY
MBaAFLqjzEOIP4XgaJJYemsiVQZ0+gGOMH8GA1UdEQR4MHaCCmNrcy1tYXN0ZXKC
Cmt1YmVybmV0ZXOCEmt1YmVybmV0ZXMuZGVmYXVsdIIWa3ViZXJuZXRlcy5kZWZh
dWx0LnN2Y4Ika3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FshwQK
YAABhwQKgAAFMA0GCSqGSIb3DQEBCwUAA4IBAQDQmr/IxYdpXAtNF7o4FdgCb/F9
Y7lqqUkf7fBsT4QalFdA8AySpMdFgYdvv/CMZElcbw09PdXBu+pGt41PprN9Uiq0
o5LeSa1ChJl+JmWEqQ4r3lXDRKrqOggEL5fmF8D+b6ZDS4vq5EMGDxYu0iLxsZkx
x+8LOoAt3ufu9y0C0Ms0XgupI7oKhlymU1HcJTkPxTmOIsRvjmnACUTYANCZbqFA
MVQR7p0Ed+rShhI8Gv3aiDkOxncMfFCi+MW7LN+DwHTMWJJpWDGafn+9YtGEeLxi
IQcvnlzZPSOC6f6VAYxEKrLto1JjHqZBEamOtH3VS7seYUdrXsPjmMZaeEPF
-----END CERTIFICATE-----

NodeRestriction

Mirando esta misma imagen podemos observar que tenemos el parámetro --enable-admission-plugins.

alt text

Como habíamos comentado podemos crear y utilizar plugins para definir límites además de los impuestos por el RBAC.

Activar el NodeRestriction limita las labels de los nodes que el kubelet puede modificar. Activar este plugin limita que un kubelet de un node solamente modifique labels de su propio node y pods garantizando un aislamiento en los workloads.

Imagine que tenemos un pod que necesita ejecutarse en un determinado tipo de node y utilizamos affinities y selectors para hacer que el scheduler envíe el pod a un node específico en el que los pods se ejecutan en sandbox para garantizar más seguridad. Si haces un match para schedular el pod para un node con la label grupo: nodesec (podría ser cualquier cosa) es para allá que irá.

El kubelet de otro node podría simplemente colocar la label en su propio node liberando que ese pod fuera agendado para él.

Pero existe una label que él no consigue alterar, incluso siendo de su propio node. Toda label que comience con node-restriction.kubernetes.io/* ni el kubelet del propio node es capaz de alterar, pues no tiene permiso para eso. Si hiciésemos un affinity usando esas labels (Definidas anteriormente por el admin), garantizaríamos la seguridad de que un intruso en un node no conseguiría jalar ese pod para aquel node.

Vamos a probar en el worker.

root@cks-worker:~# kubectl get nodes
E0822 12:24:09.174116 166884 memcache.go:265] couldnt get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
E0822 12:24:09.174665 166884 memcache.go:265] couldnt get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
E0822 12:24:09.175920 166884 memcache.go:265] couldnt get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
E0822 12:24:09.176467 166884 memcache.go:265] couldnt get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
E0822 12:24:09.178492 166884 memcache.go:265] couldnt get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
The connection to the server localhost:8080 was refused - did you specify the right host or port?

# No conseguimos ejecutar porque no tenemos un config configurado en el ~/.kube/config en los workers
root@cks-worker:~# kubectl config view
apiVersion: v1
clusters: null
contexts: null
current-context: ""
kind: Config
preferences: {}
users: null

# El kubectl busca primero en esa variable de entorno si está configurada, vamos a configurar
root@cks-worker:~# export KUBECONFIG=/etc/kubernetes/kubelet.conf

# El kubelet no tiene permiso para los namespaces
root@cks-worker:~# kubectl get ns
Error from server (Forbidden): namespaces is forbidden: User "system:node:cks-worker" cannot list resource "namespaces" in API group "" at the cluster scope

root@cks-worker:~# kubectl get nodes
NAME STATUS ROLES AGE VERSION
cks-master Ready control-plane 6d15h v1.30.3
cks-worker Ready <none> 6d15h v1.30.3

# Podemos configurar labels en el propio node.
root@cks-worker:~# kubectl label node cks-worker cks/test=yes
node/cks-worker labeled

# Pero no en otro node
root@cks-worker:~# kubectl label node cks-master cks/test=yes
Error from server (Forbidden): nodes "cks-master" is forbidden: node "cks-worker" is not allowed to modify node "cks-master"

# Y no podemos configurar una label iniciada con node-restriction.kubernetes.io/*
root@cks-worker:~# kubectl label node cks-worker node-restriction.kubernetes.io/test=yes
Error from server (Forbidden): nodes "cks-worker" is forbidden: is not allowed to modify labels: node-restriction.kubernetes.io/test

# Cuidado porque la /* hace diferencia
root@cks-worker:~# kubectl label node cks-worker node-restriction.kubernetes.io=yes
node/cks-worker labeled

root@cks-worker:~# k get nodes cks-worker --show-labels
NAME STATUS ROLES AGE VERSION LABELS
cks-worker Ready <none> 6d15h v1.30.3 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,cks/test=yes,kubernetes.io/arch=amd64,kubernetes.io/hostname=cks-worker,kubernetes.io/os=linux,node-restriction.kubernetes.io=yes

Siempre que necesario haga los filtros de las affinities usando esas labels.