Cómo proteger un clúster k3s de un solo nodo
Cierra los riesgos de k3s en un VPS: TCP 6443, kubelet en 10250, kubeconfig con permisos inseguros, NodePort fuera del firewall y pods privilegiados.
Qué expone un clúster k3s de un solo nodo el primer día
Un clúster k3s de un solo nodo en un VPS público queda expuesto en cinco puntos concretos al día siguiente de ejecutar el instalador de una línea: el servidor de API de Kubernetes en TCP 6443, kubelet en TCP 10250, el archivo kubeconfig almacenado en disco, el rango de NodePort que el firewall no puede detectar y cualquier pod autorizado a solicitar privileged o hostPath. Cada punto tiene una solución que se aplica en minutos. Esta guía asume que k3s ya está en ejecución. Si no lo está, empiece por instalar k3s de un solo nodo en un VPS y vuelva después.
Compruebe qué está escuchando antes de cambiar nada.
sudo ss -tulpn | grep -E '6443|10250|10256|8472'Una instalación predeterminada muestra 6443 (el servidor de API), 10250 (kubelet), 10256 (la comprobación de estado de kube-proxy) y 8472/udp (la red superpuesta de flannel, que usa VXLAN, virtual extensible LAN). k3s se enlaza a 0.0.0.0 de forma predeterminada, por lo que todos esos puertos quedan en la dirección pública y no sólo en loopback.
Por qué el puerto 6443 es todo el clúster
Cualquier persona o proceso que pueda autenticarse en el puerto 6443 con permisos de administrador puede crear un pod, y un pod puede obtener acceso root en el host. El puerto 6443 es la puerta de entrada a la máquina.
Un puerto 6443 abierto no implica un compromiso inmediato, porque Kubernetes no acepta contraseñas. Requiere un certificado de cliente o un bearer token. Aun así, se cumplen dos condiciones.
En primer lugar, el servidor de API responde a algunas solicitudes sin ninguna credencial. El control de acceso basado en roles (RBAC) predeterminado de Kubernetes vincula el grupo system:unauthenticated a un rol llamado system:public-info-viewer, que permite /version, /healthz, /livez y /readyz. Desde otra máquina:
curl -sk https://YOUR_SERVER_IP:6443/versionEsto devuelve la versión exacta de Kubernetes, que sirve como entrada para buscar CVE (common vulnerabilities and exposures) y explica por qué un escáner considera interesante el sistema. Todas las rutas posteriores se rechazan, y el rechazo incluye su identidad:
forbidden: User "system:anonymous" cannot get path "/api"En segundo lugar, cualquier error del servidor de API es accesible de forma remota mientras el puerto esté abierto. A partir de ese momento, aplicar los parches deja de ser opcional.
La corrección sencilla es añadir una regla de firewall. ufw necesita más de una línea en este caso, porque k3s encamina el tráfico del clúster mediante el mismo kernel.
sudo ufw default deny incoming
sudo ufw allow OpenSSH
sudo ufw allow from 203.0.113.10 to any port 6443 proto tcp
sudo ufw allow from 10.42.0.0/16 to any
sudo ufw allow from 10.43.0.0/16 to any
sudo ufw enableLas dos últimas reglas proceden directamente de la documentación de k3s. 10.42.0.0/16 es la red de pods predeterminada y 10.43.0.0/16 es la red de servicios predeterminada. Sin ellas, ufw descarta el tráfico interno del clúster, por lo que los pods pierden el acceso al servidor de API y entre sí. El ejemplo de k3s permite 6443 desde cualquier origen; sustituirlo por su propia dirección es el cambio que merece la pena realizar. Si ufw es nuevo para usted, los conceptos básicos del firewall ufw para un VPS explican las políticas predeterminadas de las que depende esta configuración.
La documentación de k3s es clara sobre el puerto de overlay: "El puerto VXLAN de los nodos no debe exponerse a Internet, porque permite que cualquiera acceda a la red del clúster". Una política predeterminada que deniegue el tráfico entrante resuelve este problema sin especificar el puerto.
La corrección más sólida consiste en dejar de acceder al servidor de API mediante la dirección pública y utilizar en su lugar una dirección de VPN o de una red mesh. El certificado del servidor debe incluir la dirección a la que se conecta, por lo que debe añadirla como SAN (subject alternative name) en /etc/rancher/k3s/config.yaml:
tls-san:
- 10.8.0.1
- k3s.example.com
secrets-encryption: truesudo systemctl restart k3s
sudo k3s secrets-encrypt statussecrets-encryption: true cifra los objetos Secret en el almacén de datos. La documentación de k3s indica que "Secrets-encryption no se puede habilitar en un servidor existente sin reiniciarlo", y los Secrets escritos antes del cambio mantienen su formato anterior hasta que ejecute sudo k3s secrets-encrypt reencrypt. Es importante entender qué ofrece esta medida. Protege un archivo del almacén de datos copiado desde una copia de seguridad. No protege frente a alguien que pueda comunicarse con el servidor de API, porque el servidor de API descifra los Secrets para cualquiera que tenga permiso para leerlos. La misma distinción se aplica a cualquier almacén de secretos autohospedado. Por eso una revisión de seguridad de Vaultwarden se centra en el token de administrador y en el archivo de copia de seguridad, no en el cifrado en sí.
Por qué es importante kubelet en el puerto 10250
kubelet es el agente que inicia los contenedores. Su API en el puerto 10250 enumera los pods y ejecuta comandos dentro de ellos. Un kubelet que acepta solicitudes anónimas proporciona un shell remoto a todas las cargas de trabajo de la máquina.
Compruebe el suyo:
curl -sk https://127.0.0.1:10250/pods | head -c 60Un k3s actualizado responde Unauthorized, porque kubelet solicita al servidor de API que autentique y autorice a cada cliente. Si devuelve una lista de pods en JSON, el acceso anónimo está habilitado y cualquiera que pueda acceder al puerto 10250 puede leer datos de sus contenedores y ejecutar comandos en ellos.
Cierre el acceso desde el exterior en cualquier caso. En un nodo único, el único cliente de kubelet es el plano de control del mismo equipo, y ese tráfico entra por la interfaz de loopback, que ufw acepta de forma predeterminada. Denegar el puerto 10250 desde Internet no tiene ningún coste. Si ha llegado aquí desde un kubectl top con problemas o desde un metrics-server que no termina de estabilizarse, las causas se recopilan en errores del puerto 10250 de kubelet.
Tu kubeconfig es una credencial de administrador del clúster
k3s escribe /etc/rancher/k3s/k3s.yaml con root como propietario y con permisos 600. La documentación indica la consecuencia de cambiar esos permisos: "El archivo kubeconfig pertenece a root y se escribe con permisos predeterminados 600. Cambiar los permisos a 644 permitirá que otros usuarios sin privilegios del host puedan leerlo."
Interprétalo así: los permisos 644 convierten a cada cuenta local en administrador del clúster. Muchos tutoriales recomiendan exactamente eso, normalmente mediante --write-kubeconfig-mode 644, para que kubectl funcione sin sudo. Funciona porque entrega la credencial de administrador a cualquiera que tenga acceso a un shell.
Copia el archivo para un solo usuario.
mkdir -p ~/.kube
sudo install -o "$USER" -g "$USER" -m 600 /etc/rancher/k3s/k3s.yaml ~/.kube/config
kubectl get nodesDespués, confirma que el archivo original sigue protegido:
stat -c '%a %U:%G' /etc/rancher/k3s/k3s.yaml600 root:root es la respuesta que buscas. Ese archivo contiene un certificado de cliente para un miembro de system:masters, el grupo que el servidor de API considera permitido sin condiciones, por lo que nunca se consultan las reglas de RBAC para ese certificado. Kubernetes no tiene una lista de revocación de certificados, por lo que una copia filtrada sigue siendo válida hasta que renueves la autoridad certificadora del clúster. Trátalo como una clave privada de SSH y limita el conjunto de cuentas que pueden acceder a él, por el mismo motivo que las cuentas de usuario con privilegios mínimos en un VPS.
El firewall no detecta el tráfico de NodePort
Un servicio de tipo type: NodePort abre un puerto entre 30000 y 32767 en todas las direcciones que tiene el nodo, incluida la dirección pública. Un servicio de tipo type: LoadBalancer va más allá en k3s: ServiceLB, el balanceador de carga incluido, programa un pod pequeño por servicio en kube-system que reclama directamente el puerto del servicio en el host.
kubectl -n kube-system get pods | grep svclb
sudo ss -tulpn | grep -E ':3[0-2][0-9]{3}'Aquí está la parte que suele sorprender. Bloquea ese puerto con ufw y sigue respondiendo.
sudo ufw deny 30080/tcp
curl http://YOUR_SERVER_IP:30080La página sigue cargando por la ruta que sigue el paquete. kube-proxy escribe reglas DNAT (traducción de direcciones de red de destino) en la cadena PREROUTING de la tabla nat, y PREROUTING se ejecuta antes de que se tome cualquier decisión de filtrado. El destino se convierte en la dirección de un pod, que no es el host, por lo que el kernel envía el paquete por la cadena FORWARD y nunca por INPUT. Las reglas de ufw están en INPUT. El paquete nunca pasa por ellas. El orden de los saltos se puede ver aquí:
sudo iptables -S PREROUTING -t nat | head
sudo iptables -S FORWARD | headKUBE-SERVICES está al principio de PREROUTING, y los saltos de Kubernetes en FORWARD están por encima de las cadenas propias de ufw. Este es el mismo mecanismo que permite a Docker publicar puertos pasando por ufw, y las soluciones son las mismas.
- Filtra en el firewall de red de tu proveedor. Se ejecuta delante de la máquina y no depende de cómo enrute los paquetes tu kernel.
- Evita NodePort y LoadBalancer. Deja los servicios en
ClusterIPy accede a ellos conkubectl port-forwarda través de la sesión SSH que ya tienes. - Expón un único ingress en 80 y 443, y ningún otro puerto.
- Reduce el rango con
service-node-port-rangeenkube-apiserver-arg, para que un NodePort accidental quede en una zona que estés supervisando.
Sigue siendo recomendable ejecutar ufw. Controla el tráfico dirigido al propio host, como SSH y el servidor de API. Simplemente no filtra el tráfico de los pods, y esperar que lo haga es la forma de acabar con una base de datos accesible desde fuera.
Un pod con hostPath o privileged tiene acceso equivalente a root en tu VPS
Los contenedores son procesos normales que se ejecutan sobre tu kernel, pero con una vista restringida de él. Varios campos de un pod eliminan esa restricción.
securityContext.privileged: trueproporciona al contenedor todas las capacidades de Linux y acceso a los dispositivos del host.hostPathmonta un directorio del host en el pod. Un pod que monta/con permisos de lectura y escritura puede añadir una clave a/root/.ssh/authorized_keys.hostPID: truecoloca el contenedor en el espacio de nombres de procesos del host. Desde ahí,nsentercontra el PID 1 abre un shell del host.hostNetwork: truelo coloca en la pila de red del host. Así puede enlazar puertos del host y acceder a servicios enlazados a loopback.
Por tanto, «quién puede crear pods aquí» equivale a «quién tiene root en este VPS». Cualquier ServiceAccount con create sobre pods en cualquier namespace tiene acceso equivalente a root, salvo que algo rechace primero el pod.
Ese control es la admisión de Pod Security, integrada en el API server. La configuración rápida consiste en una etiqueta por namespace y no requiere reiniciar.
kubectl label namespace default \
pod-security.kubernetes.io/enforce=baseline \
pod-security.kubernetes.io/enforce-version=latest \
pod-security.kubernetes.io/warn=restrictedbaseline rechaza los cuatro campos anteriores. restricted va más allá y exige un usuario que no sea root, un perfil seccomp (secure computing mode), que no haya escalada de privilegios y que las capacidades se reduzcan a ALL. Esto rompe muchos charts publicados. Aplicar baseline y limitarse a emitir advertencias sobre restricted permite comprobar qué se rompería antes de adoptar la política.
Comprueba que funciona:
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: pstest
spec:
containers:
- name: app
image: busybox
command: ["sleep", "60"]
securityContext:
privileged: true
EOFEl API server lo rechaza e indica qué campo rechazó:
Error from server (Forbidden): error when creating "STDIN": pods "pstest" is forbidden: violates PodSecurity "baseline:latest": privileged (container "app" must not set securityContext.privileged=true)Para establecer un valor predeterminado para todo el clúster en lugar de añadir una etiqueta a cada namespace, la documentación de k3s describe un archivo de configuración de admisión en /var/lib/rancher/k3s/server/psa.yaml:
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
configuration:
apiVersion: pod-security.admission.config.k8s.io/v1beta1
kind: PodSecurityConfiguration
defaults:
enforce: "baseline"
enforce-version: "latest"
warn: "restricted"
warn-version: "latest"
exemptions:
namespaces: [kube-system]Indica al API server que lo use mediante /etc/rancher/k3s/config.yaml y reinicia k3s:
kube-apiserver-arg:
- 'admission-control-config-file=/var/lib/rancher/k3s/server/psa.yaml'La excepción kube-system no es opcional. Los pods de ServiceLB propios de k3s usan puertos del host, algo que baseline prohíbe. Si omites kube-system de la lista, esos pods se rechazarán la próxima vez que algún proceso los cree de nuevo. Mantén abierta una segunda sesión SSH cuando reinicies k3s después de cambiar la configuración de admisión.
Además, k3s incluye un controlador de políticas de red y lo habilita de forma predeterminada. Por tanto, los objetos NetworkPolicy tienen efecto en este clúster sin instalar nada más. Esto no ocurre en todas las distribuciones de Kubernetes. Es la herramienta que impide que un pod comprometido acceda al resto.
Elimine los componentes incluidos que no utiliza
El instalador despliega un conjunto de complementos. Cada uno añade un listener y otro componente que debe actualizarse. --disable acepta estos valores: coredns, servicelb, traefik, local-storage, metrics-server, runtimes.
Mantenga coredns. Ningún elemento del clúster puede resolver un nombre sin él. Los demás son opcionales. En /etc/rancher/k3s/config.yaml:
disable:
- traefik
- servicelb
disable-helm-controller: truesudo systemctl restart k3s
kubectl get pods -Ak3s elimina los componentes que deshabilite, por lo que los pods de traefik y los pods de svclb- desaparecen automáticamente. Conozca primero las consecuencias. Si se elimina servicelb, todos los servicios type: LoadBalancer permanecen en <pending> para siempre, porque ningún componente les asigna una dirección. Si se elimina traefik, no hay ningún controlador de ingress, por lo que los objetos Ingress no hacen nada. Deshabilítelos cuando exponga el tráfico de otra forma, por ejemplo mediante un reverse proxy en el host, y déjelos habilitados cuando los utilice. disable-helm-controller: true elimina el controlador que supervisa los recursos HelmChart, un componente con privilegios que no necesita si ejecuta helm por su cuenta.
Dejar de montar automáticamente el token predeterminado de ServiceAccount
Cada pod recibe un token de ServiceAccount en /var/run/secrets/kubernetes.io/serviceaccount/token, a menos que se indique lo contrario. La ServiceAccount default no tiene permisos de RBAC, por lo que el token por sí solo no permite hacer mucho. Lo que obtiene un atacante dentro de un contenedor comprometido es una credencial válida y un servidor de API accesible. Este es el primer paso en la mayoría de los análisis de escalada en el clúster.
La guía de bastionado de k3s lo desactiva por espacio de nombres:
kubectl patch serviceaccount --namespace default default --patch '{"automountServiceAccountToken": false}'
kubectl patch serviceaccount --namespace kube-node-lease default --patch '{"automountServiceAccountToken": false}'
kubectl patch serviceaccount --namespace kube-public default --patch '{"automountServiceAccountToken": false}'Compruebe que el cambio se haya aplicado. Espere unos segundos a que el pod se inicie.
kubectl run t --image=busybox --restart=Never --command -- sleep 30
kubectl exec t -- ls /var/run/secrets/kubernetes.io/serviceaccountLa ruta ya no existe, por lo que ls muestra No such file or directory. Una carga de trabajo que necesite realmente acceso a la API establece automountServiceAccountToken: true en su propia especificación de pod. Por tanto, nada queda bloqueado de forma permanente. Esto ya supone una pequeña mejora. La mejora más importante consiste en no asignar a una carga de trabajo una ServiceAccount con permisos reales. Puede comprobar qué valor tiene actualmente un token:
kubectl auth can-i --list --as=system:serviceaccount:default:defaultLímites de recursos para que un pod no pueda detener todo el clúster
En un solo nodo, el plano de control y las cargas de trabajo comparten el mismo kernel y el mismo conjunto de memoria. Un pod que pierde memoria no siempre termina por sí solo. El asesino OOM (sin memoria disponible) del kernel selecciona la víctima según una puntuación que favorece a los procesos grandes, y k3s es un proceso grande y de larga duración. Por eso, puede desaparecer el clúster en lugar del pod. Después, ningún componente reinicia las cargas de trabajo, porque el componente que las reinicia es el que ha terminado.
Un LimitRange establece límites para los pods que no definen ninguno:
apiVersion: v1
kind: LimitRange
metadata:
name: defaults
namespace: default
spec:
limits:
- type: Container
default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 50m
memory: 128MiUn ResourceQuota limita lo que puede reclamar el namespace completo:
apiVersion: v1
kind: ResourceQuota
metadata:
name: cap
namespace: default
spec:
hard:
limits.cpu: "3"
limits.memory: 3Gi
pods: "20"A continuación, reserve espacio para el propio k3s en /etc/rancher/k3s/config.yaml:
kubelet-arg:
- 'system-reserved=cpu=250m,memory=512Mi'La diferencia se observa en dos lugares. Un contenedor terminado por superar su propio límite informa de Reason: OOMKilled en Last State dentro de kubectl describe pod, y el resto del nodo continúa funcionando. Un nodo que se queda completamente sin memoria deja una línea Killed process en dmesg y normalmente arrastra consigo a los nodos vecinos. El primer caso indica que el límite está cumpliendo su función. El segundo es lo que los límites deben evitar.
Analizar los manifiestos en CI y el clúster de k3s según un calendario
El análisis debe hacerse en dos lugares, y cada uno detecta problemas distintos. Instale Trivy primero. El proyecto publica un paquete Debian con cada versión, y 0.74.0 era la versión actual en agosto de 2026. Consulte la página de versiones para comprobar si existe una versión más reciente antes de fijar esta versión en la automatización.
sudo apt-get install -y wget
wget -q https://github.com/aquasecurity/trivy/releases/download/v0.74.0/trivy_0.74.0_Linux-64bit.deb
sudo dpkg -i trivy_0.74.0_Linux-64bit.deb
trivy --versionEl primer lugar son los manifiestos, antes de que lleguen al clúster.
trivy fs --scanners misconfig --severity HIGH,CRITICAL --exit-code 1 ./deploy--exit-code 1 hace que el trabajo de CI (integración continua) falle cuando encuentra un problema. Cada resultado indica el campo que falla y su gravedad. Así, un privileged: true que no pretendía confirmar en el repositorio hace que falle la compilación en lugar de llegar al servidor de API. Un problema que haya decidido aceptar se incluye en un archivo .trivyignore. De este modo, la decisión queda en git junto al manifiesto que la originó.
El segundo lugar es el clúster en ejecución, según un calendario.
trivy k8s --compliance=k8s-cis-1.23 --report summaryConviene aclarar un aspecto de ese comando. trivy k8s implementa un pod recopilador de nodos que necesita acceso al host para inspeccionar la configuración de los nodos. En un clúster en el que acaba de empezar a rechazar pods privilegiados, debe tenerlo en cuenta en lugar de buscar una forma de evitarlo. trivy k8s --report summary --disable-node-collector omite el recopilador y, con él, pierde las comprobaciones del nivel de nodo.
kube-bench cubre el lado del host: permisos de archivos y flags de procesos. Ahí es donde se encuentra la mayor parte del benchmark de CIS (Center for Internet Security). Incluye un perfil para k3s. Tome el manifiesto Job del proyecto original y ajústelo.
curl -sfL -o kube-bench-job.yaml https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yamlCambie el comando del contenedor a ["kube-bench", "--benchmark", "k3s-cis-1.7"] y sustituya los montajes /etc/kubernetes y /var/lib/etcd por /etc/rancher y /var/lib/rancher, porque ahí es donde k3s guarda sus archivos. Después:
kubectl apply -f kube-bench-job.yaml
kubectl logs -l app=kube-bench --tail=-1Observe qué es ese Job: hostPID: true más montajes de directorios del host. Es exactamente la estructura de pod que empezó a rechazar hace una sección. Ejecútelo en el espacio de nombres exento kube-system, lea la salida y después kubectl delete job kube-bench. Que un analizador necesite privilegios no es motivo para dejar de prohibir las cargas de trabajo privilegiadas.
El contenido de las imágenes es otra cuestión. trivy image ghcr.io/example/app:1.4 lee la base de datos de paquetes dentro de una imagen y muestra los que tienen vulnerabilidades conocidas. Hace la misma comprobación que cuando comprueba si su servidor tiene CVE conocidas.
Ahora, la parte importante sobre la salida del analizador. Un clúster de un solo nodo para uso personal no superará una lista larga de controles CIS. Además, la mayoría de esos fallos son correctos, pero no relevantes para usted. El benchmark está escrito para un clúster multi-nodo y multi-tenant: etcd en hosts separados, registros de auditoría enviados fuera de la máquina, una autoridad certificadora independiente para kubelet y plugins de admisión para un régimen de cumplimiento que no le afecta. k3s ejecuta deliberadamente el plano de control como un único proceso con un archivo de configuración. Por eso, los controles que comprueban los permisos de un archivo de manifiesto kube-scheduler no pueden pasar: ese archivo no existe.
Lea los fallos en este orden y deténgase cuando deje de aportar valor: modos y propietarios de archivos bajo /etc/rancher y /var/lib/rancher; cualquier elemento que mencione acceso anónimo o no autenticado; cualquier elemento que indique que un componente está enlazado a 0.0.0.0; y cualquier contenedor que se ejecute como UID 0 sin una razón válida. El resto puede esperar hasta que tenga un segundo nodo o una segunda persona con acceso. Un informe de cien líneas que ignora vale menos que uno de cinco líneas sobre el que actúa.
Orden de refuerzo de seguridad de k3s para un nodo único
- Establezca la política predeterminada de entrada de ufw en deny, permita SSH, permita 6443 desde su propia dirección y permita las redes de pods y servicios.
- Copie el kubeconfig en su usuario con el modo 600 y no establezca nunca
--write-kubeconfig-mode 644. - Confirme que kubelet en 10250 rechaza las solicitudes anónimas y mantenga el puerto fuera de Internet.
- Desactive los componentes incluidos que no utilice y reinicie k3s.
- Etiquete los namespaces para la admisión de Pod Security con
enforce=baselineywarn=restricted. - Desactive el montaje automático predeterminado del token de ServiceAccount.
- Añada un LimitRange y un ResourceQuota, y reserve CPU y memoria para k3s.
- Integre
trivy fs --scanners misconfigen CI y ejecute un análisis CIS mensualmente.
El host subyacente sigue necesitando el mismo cuidado que cualquier otro servidor, y k3s añade una particularidad. Se instaló mediante un script en lugar de apt, por lo que apt upgrade nunca lo actualiza. Mantenga el sistema operativo actualizado según su propio calendario con actualizaciones desatendidas en Ubuntu y actualice k3s de forma deliberada volviendo a ejecutar su instalador con el canal o la versión que desee. Es fácil olvidar que hay dos rutas de actualización en una misma máquina, así que anote qué componente sigue cada una.
FAQ
¿Es seguro exponer el servidor de API de k3s en el puerto 6443 a Internet?
Por sí solo, no es una puerta abierta, porque el servidor de API requiere un certificado de cliente o un token y rechaza todo lo demás con forbidden: User "system:anonymous". Aun así, quedan dos riesgos. Los clientes anónimos todavía pueden leer /version, que indica a un escáner qué versión de Kubernetes debe buscar. Además, cualquier vulnerabilidad futura del servidor de API será accesible de forma remota mientras el puerto esté abierto. En un clúster de un solo nodo, nada externo necesita 6443 salvo tu propio kubectl. Por tanto, permite el acceso desde tu dirección con sudo ufw allow from YOUR_IP to any port 6443 proto tcp y deja que la política de denegación predeterminada gestione el resto.
¿Por qué mi regla de ufw no bloquea un servicio NodePort?
Porque el paquete nunca llega a la cadena donde está ubicada la regla. kube-proxy coloca reglas DNAT en la cadena PREROUTING de la tabla nat. Esta cadena se ejecuta primero y reescribe el destino a una dirección de pod. Después, el paquete se reenvía en lugar de entregarse localmente. Por eso atraviesa FORWARD y omite INPUT, donde residen las reglas de ufw. Confírmalo con sudo iptables -S PREROUTING -t nat | head. Filtra los NodePort en el firewall de red de tu proveedor, o evita type: NodePort y accede a los servicios con kubectl port-forward.
¿Debo ejecutar k3s con --write-kubeconfig-mode 644?
No. La documentación de k3s explica claramente su efecto: "Cambiar el modo a 644 permitirá que otros usuarios sin privilegios del host puedan leerlo." Ese archivo contiene un certificado de cliente para system:masters. Por tanto, el modo 644 convierte a todas las cuentas locales en administradores del clúster. Cópialo para un solo usuario con sudo install -o "$USER" -g "$USER" -m 600 /etc/rancher/k3s/k3s.yaml ~/.kube/config y deja el archivo original con 600 root:root.
¿Qué benchmark de kube-bench debo usar para k3s?
Usa k3s-cis-1.7. La documentación de kube-bench indica: "kube-bench incluye benchmarks para la plataforma Rancher K3S. Para ejecutarlo, debes especificar --benchmark k3s-cis-1.7 al ejecutar el comando kube-bench." Pásalo explícitamente, porque la detección automática presupone una estructura de kubeadm, mientras que k3s mantiene sus archivos en /etc/rancher y /var/lib/rancher. Es normal que aparezcan fallos que no se aplican a un solo nodo. Prioriza los permisos de archivos y el acceso anónimo.
¿La admisión de Pod Security romperá los componentes incluidos de k3s?
Lo hará si la aplicas de forma obligatoria en kube-system. Los pods ServiceLB que k3s crea para los servicios type: LoadBalancer solicitan puertos del host. baseline prohíbe los puertos del host, por lo que esos pods serán rechazados la próxima vez que se creen. Excluye kube-system en el archivo de configuración de admisión, o aplica Pod Security sólo mediante etiquetas en los namespaces que administras. Empieza con enforce=baseline y warn=restricted para poder leer qué componentes rompería restricted antes de activarla.