Este tutorial es uno de los cuatro que ponen en práctica conceptos de «Microservicios de marzo de 2022: redes de Kubernetes»:
- Reduzca la latencia de Kubernetes con el escalado automático
- Protege las API de Kubernetes mediante la limitación de solicitudes
- Protege las aplicaciones de Kubernetes frente a la inyección SQL (esta publicación)
- Mejore el tiempo de actividad y la resiliencia con un despliegue canary
¿Quieres una guía detallada sobre cómo usar NGINX para aún más casos de uso de redes en Kubernetes? Descarga gratis nuestro eBook, «Gestión del tráfico de Kubernetes con NGINX: guía práctica».
Trabajas en el departamento de TI de una conocida tienda local que vende una gran variedad de productos, desde almohadas hasta bicicletas. Están a punto de lanzar su primera tienda online, pero antes de hacerla pública pidieron a un experto en seguridad que realizara una prueba de penetración del sitio. Por desgracia, el experto en seguridad encontró un problema: la tienda online es vulnerable a la inyección de SQL. El experto pudo aprovechar esta vulnerabilidad para obtener información confidencial de la base de datos, incluidos nombres de usuario y contraseñas.
Tu equipo ha recurrido a ti —el ingeniero de Kubernetes— para salvar la situación. Por suerte, sabes que la inyección SQL (al igual que otras vulnerabilidades) puede mitigarse mediante las herramientas de gestión del tráfico de Kubernetes. Ya has implementado un controlador Ingress para exponer la aplicación y, con una única configuración, puedes garantizar que la vulnerabilidad no pueda explotarse. Ahora, la tienda online puede lanzarse a tiempo. ¡Buen trabajo!
Descripción general del laboratorio y el tutorial
Este blog complementa el laboratorio de la unidad 3 de Microservices March 2022 – «Patrón de seguridad de microservicios en Kubernetes», en el que se muestra cómo utilizar NGINX y NGINX Ingress Controller para bloquear la inyección de código SQL.
Para seguir el tutorial, necesitas un equipo con:
- 2 CPU o más
- 2 GB de memoria libre
- 20 GB de espacio libre en disco
- Conexión a Internet
- Gestor de contenedores o máquinas virtuales, como Docker, Hyperkit, Hyper-V, KVM, Parallels, Podman, VirtualBox o VMware Fusion/Workstation
- minikube instalado
- Helm instalado
- Una configuración que permita abrir una ventana del navegador. Si no es posible, tendrás que averiguar cómo acceder a los servicios pertinentes mediante un navegador.
Para sacar el máximo partido al laboratorio y al tutorial, le recomendamos que, antes de empezar:
- Mira la grabación de la presentación conceptual retransmitida en directo
- Consulta los blogs, el seminario web y el vídeo con información contextual
- Vea el vídeo de 16 minutos que resume el laboratorio:
 
Este tutorial utiliza estas tecnologías:
- NGINX Open Source
- Controlador de Ingress de NGINX (basado en NGINX Open Source)
- BusyBox
- Helm
- minikube
- Una aplicación sencilla con vulnerabilidades de seguridad, desarrollada para este laboratorio
Las instrucciones de cada reto incluyen el texto completo de los archivos YAML utilizados para configurar las aplicaciones. También puedes copiar el texto de nuestro repositorio de GitHub. Junto al texto de cada archivo YAML se proporciona un enlace a GitHub.
Este tutorial incluye cuatro desafíos:
- Desplegar un clúster y una aplicación vulnerable
- Hackea la aplicación
- Usar un contenedor sidecar de NGINX para bloquear determinadas solicitudes
- Configurar NGINX Ingress Controller para filtrar solicitudes
Desafío 1: Desplegar un clúster y una aplicación vulnerable
En este reto, desplegarás un clúster de minikube e instalarás Podinfo como aplicación de ejemplo con vulnerabilidades de seguridad.
Crear un clúster de Minikube
Despliega un clúster de minikube. Tras unos segundos, aparecerá un mensaje que confirma que el despliegue se ha realizado correctamente.
$ minikube start
🏄 Done! kubectl is now configured to use "minikube" cluster and "default" namespace by defaultInstalar la aplicación vulnerable
Aquí desplegarás una sencilla aplicación de comercio electrónico que consta de dos microservicios:
- Una base de datos MariaDB
- Un microservicio PHP que se conecta a la base de datos y recupera datos
Sigue estos pasos:
- Con el editor de texto que prefiera, cree un archivo YAML llamado 1-app.yaml con el siguiente contenido (o cópielo de GitHub).
apiVersion: apps/v1 kind: Deployment
metadata:
name: app
spec:
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
containers:
- name: app
image: f5devcentral/microservicesmarch:1.0.3
ports:
- containerPort: 80
env:
- name: MYSQL_USER
value: dan
- name: MYSQL_PASSWORD
value: dan
- name: MYSQL_DATABASE
value: sqlitraining
- name: DATABASE_HOSTNAME
value: db.default.svc.cluster.local
---
apiVersion: v1
kind: Service
metadata:
name: app
spec:
ports:
- port: 80
targetPort: 80
nodePort: 30001
selector:
app: app
type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: db
spec:
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: db
image: mariadb:10.3.32-focal
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: root
- name: MYSQL_USER
value: dan
- name: MYSQL_PASSWORD
value: dan
- name: MYSQL_DATABASE
value: sqlitraining
---
apiVersion: v1
kind: Service
metadata:
name: db
spec:
ports:
- port: 3306
targetPort: 3306
selector:
app: db - Despliega la aplicación y la API:
$ kubectl apply -f 1-app.yaml deployment.apps/app created
service/app created
deployment.apps/db created
service/db created - Confirma que los pods de Podinfo se hayan desplegado, tal como indica el valor «
Running» en la columna «STATUS». Pueden tardar entre 30 y 40 segundos en desplegarse por completo, así que espera hasta que el estado de ambos pods sea «Running» antes de continuar con el siguiente paso (vuelve a ejecutar el comando si es necesario).
$ kubectl get podsNAME READY STATUS RESTARTS AGE
app-d65d9b879-b65f2 1/1 Running 0 37s
db-7bbcdc75c-q2kt5 1/1 Running 0 37s - Abre la aplicación en tu navegador:
$ minikube service app|-----------|------|-------------|--------------|
| NAMESPACE | NAME | TARGET PORT | URL |
|-----------|------|-------------|--------------|
| default | app | | No node port |
|-----------|------|-------------|--------------|
😿 service default/app has no node port
🏃 Starting tunnel for service app.
|-----------|------|-------------|------------------------|
| NAMESPACE | NAME | TARGET PORT | URL |
|-----------|------|-------------|------------------------|
| default | app | | http://127.0.0.1:55446 |
|-----------|------|-------------|------------------------|
🎉 Opening service default/app in default browser... Desafío 2: Hackea la aplicación
La aplicación de ejemplo es bastante básica. Incluye una página de inicio con una lista de artículos (por ejemplo, almohadas) y varias páginas de productos con información como la descripción y el precio. Los datos se almacenan en la base de datos MariaDB. Cada vez que se solicita una página, se ejecuta una consulta SQL en la base de datos.
- Para la página de inicio, se recuperan todos los artículos de la base de datos.
- Para una página de producto, el artículo se obtiene por su ID.
Al abrir la página del producto almohadas, puede que observes que la URL termina en /product/1. El 1 es el ID del producto. Para evitar la inserción directa de código malicioso en la consulta SQL, se recomienda sanear los datos introducidos por el usuario antes de reenviar las solicitudes a los servicios de backend. Pero ¿qué ocurre si la aplicación no está configurada correctamente y los datos introducidos no se escapan antes de insertarlos en la consulta SQL a la base de datos?
Explotación 1
Para comprobar si la aplicación escapa correctamente los datos de entrada, haz una prueba sencilla cambiando el ID por uno que no exista en la base de datos.
Cambia manualmente el último elemento de la URL de 1 a -1. El mensaje de error Invalid product id "-1" indica que el ID del producto no se está escapando; en su lugar, la cadena se inserta directamente en la consulta. ¡Eso no es bueno, a menos que seas un hacker!

Supongamos que la consulta a la base de datos es algo así:
SELECT * FROM some_table WHERE id = "1"Para aprovechar la vulnerabilidad causada por no escapar la entrada, sustituye 1 por -1" <malicious_query> -- // de modo que:
- Las comillas (
") después de-1completan la primera consulta. - Puedes añadir tu propia consulta maliciosa después de las comillas.
- La secuencia
--//descarta el resto de la consulta.
Así, por ejemplo, si cambia el último elemento de la URL por ‑1" o 1 -- //, la consulta resultante es:
SELECT * FROM some_table WHERE id = "-1" OR 1 -- //"
--------------
^ injected ^ Esto selecciona todas las filas de la base de datos, lo cual resulta útil en un ataque. Para comprobar si este es el caso, cambia el final de la URL a ‑1". El mensaje de error resultante proporciona más información útil sobre la base de datos:
Fatal error: Uncaught mysqli_sql_exception: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '"-1""' at line 1 in /var/www/html/product.php:23 Stack trace: #0 /var/www/html/product.php(23): mysqli->query('SELECT * FROM p...') #1 {main} thrown in /var/www/html/product.php on line 23Ahora puedes empezar a manipular el código inyectado para intentar ordenar los resultados de la base de datos por ID:
-1" OR 1 ORDER BY id DESC -- //El resultado es la página de producto del último artículo de la base de datos.
Explotación 2
Obligar a la base de datos a ordenar los resultados es interesante, pero no resulta especialmente útil si tu objetivo es hackear el sistema. Merece mucho más la pena intentar extraer nombres de usuario y contraseñas de la base de datos.
Es razonable suponer que en la base de datos hay una tabla de usuarios con nombres de usuario y contraseñas. Pero ¿cómo puedes ampliar tu acceso desde la tabla de productos hasta la tabla de usuarios?
La respuesta consiste en inyectar código como este:
-1" UNION SELECT * FROM users -- //donde
‑1"fuerza a que la primera consulta devuelva un conjunto vacío.UNIONfuerza la unión de dos tablas de la base de datos; en este caso, products y users, lo que permite obtener información (contraseñas) que no se encuentra en la tabla original (products).SELECT*FROMusersselecciona todas las filas de la tabla users.- La secuencia
--//descarta todo lo que aparece después de la consulta maliciosa.
Al modificar la URL para que termine con el código inyectado, aparece un nuevo mensaje de error:
Fatal error: Uncaught mysqli_sql_exception: The used SELECT statements have a different number of columns in /var/www/html/product.php:23 Stack trace: #0 /var/www/html/product.php(23): mysqli->query('SELECT * FROM p...') #1 {main} thrown in /var/www/html/product.php on line 23Este mensaje revela que las tablas products y users no tienen el mismo número de columnas, por lo que no se puede ejecutar la instrucción UNION. Sin embargo, puedes averiguar el número de columnas mediante ensayo y error, añadiendo columnas (nombres de campos) de una en una como parámetros de la instrucción SELECT. Una buena opción para el nombre de un campo de una tabla users es password, así que prueba con ese:
# select 1 column-1" UNION SELECT password FROM users; -- //
# select 2 columns
-1" UNION SELECT password,password FROM users; -- //
# select 3 columns
-1" UNION SELECT password,password,password FROM users; -- /
# select 4 columns
-1" UNION SELECT password,password,password,password FROM users; -- //
# select 5 columns
-1" UNION SELECT password,password,password,password,password FROM users; -- //La última consulta se ejecuta correctamente (lo que indica que hay cinco columnas en la tabla users) y se muestra la contraseña de un usuario:

En este punto, no sabes qué nombre de usuario corresponde a esta contraseña. Pero, como conoces el número de columnas de la tabla users, puedes usar los mismos tipos de consulta que antes para obtener esa información. Supongamos que el nombre del campo relevante es username. Y resulta que es correcto: la siguiente consulta obtiene tanto el nombre de usuario como la contraseña de la tabla users. Lo cual está muy bien... ¡a menos que esta aplicación esté alojada en tu infraestructura!
-1" UNION SELECT username,username,password,password,username FROM users where id=1 -- //
Desafío 3: Usa un contenedor sidecar de NGINX para bloquear determinadas solicitudes
Está claro que el desarrollador de la aplicación de la tienda online debe prestar más atención a la validación y limpieza de los datos introducidos por los usuarios (por ejemplo, mediante consultas parametrizadas), pero, como ingeniero de Kubernetes, también puedes ayudar a evitar las inyecciones SQL impidiendo que el ataque llegue a la aplicación. De este modo, el hecho de que la aplicación sea vulnerable tiene menos importancia.
Hay muchas formas de proteger tus aplicaciones. Durante el resto de este laboratorio, nos centraremos en dos:
- En este reto, se inserta un contenedor sidecar en el pod para que actúe como proxy de todo el tráfico y rechace cualquier solicitud que contenga
UNIONen la URL. Primero, hay que desplegar NGINX Open Source como sidecar y, después, comprobar si filtra las consultas maliciosas. Nota: Esta técnica se utiliza únicamente con fines ilustrativos. En la práctica, desplegar proxies manualmente como sidecars no es la mejor solución (se explicará más adelante). - En el Reto 4, utilizamos NGINX Ingress Controller para filtrar todo el tráfico que entra en el clúster.
Implementar NGINX Open Source como sidecar
- Cree un archivo YAML llamado 2-app-sidecar.yaml con el siguiente contenido (o cópielo de GitHub). Entre los aspectos importantes de la configuración se incluyen:
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
containers:
- name: app
image: f5devcentral/microservicesmarch:1.0.3
ports:
- containerPort: 80
env:
- name: MYSQL_USER
value: dan
- name: MYSQL_PASSWORD
value: dan
- name: MYSQL_DATABASE
value: sqlitraining
- name: DATABASE_HOSTNAME
value: db.default.svc.cluster.local
- name: proxy # <-- sidecar
image: "nginx"
ports:
- containerPort: 8080
volumeMounts:
- mountPath: /etc/nginx
name: nginx-config
volumes:
- name: nginx-config
configMap:
name: sidecar
---
apiVersion: v1
kind: Service
metadata:
name: app
spec:
ports:
- port: 80
targetPort: 8080 # <-- the traffic is routed to the proxy
nodePort: 30001
selector:
app: app
type: NodePort
---
apiVersion: v1
kind: ConfigMap
metadata:
name: sidecar
data:
nginx.conf: |-
events {}
http {
server {
listen 8080 default_server;
listen [::]:8080 default_server;
location ~* "(\'|\")(.*)(drop|insert|md5|select|union)" {
deny all;
}
location / {
proxy_pass http://localhost:80/;
}
}
}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: db
spec:
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: db
image: mariadb:10.3.32-focal
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: root
- name: MYSQL_USER
value: dan
- name: MYSQL_PASSWORD
value: dan
- name: MYSQL_DATABASE
value: sqlitraining
---
apiVersion: v1
kind: Service
metadata:
name: db
spec:
ports:
- port: 3306
targetPort: 3306
selector:
app: db- Se inicia un contenedor sidecar que ejecuta NGINX Open Source en el puerto 8080.
- NGINX reenvía todo el tráfico a la aplicación.
- Se deniega cualquier solicitud que incluya (entre otras cadenas de caracteres)
SELECToUNION(consulte el primer bloquelocationde la secciónConfigMap). - El servicio de la aplicación dirige primero todo el tráfico al contenedor de NGINX.
- Despliega el sidecar:
$ kubectl apply -f 2-app-sidecar.yaml deployment.apps/app configured
service/app configured
configmap/sidecar created
deployment.apps/db unchanged
service/db unchanged Probar el sidecar como filtro
Comprueba si el sidecar está filtrando el tráfico volviendo a la aplicación e intentando de nuevo la inyección SQL. ¡NGINX bloquea la solicitud antes de que llegue a la aplicación!
-1" UNION SELECT username,username,password,password,username FROM users where id=1 -- // 
Desafío 4: Configurar el controlador de Ingress de NGINX para filtrar solicitudes
Proteger tu aplicación como en el Reto 3 resulta interesante como experiencia educativa, pero no lo recomendamos para entornos de producción porque:
- No es una solución de seguridad completa.
- No es escalable (no se puede aplicar fácilmente esta protección a varias aplicaciones).
- Actualizarlo es complicado e ineficiente.
Una solución mucho mejor es utilizar NGINX Ingress Controller para extender la misma protección a todas tus aplicaciones. Los controladores de Ingress permiten centralizar todo tipo de funciones de seguridad, desde el bloqueo de solicitudes, como hace un firewall de aplicaciones web (WAF), hasta la autenticación y la autorización.

En este reto, desplegarás NGINX Ingress Controller, configurarás el enrutamiento del tráfico, y comprobarás que el filtro bloquea la inyección SQL.
Implementar NGINX Ingress Controller
La forma más rápida de instalar NGINX Ingress Controller es mediante Helm.
- Añada el repositorio de NGINX a Helm:
$ helm repo add nginx-stable https://helm.nginx.com/stable - Descargue e instale el controlador basado en NGINX Open Source NGINX Ingress Controller, cuyo mantenimiento corre a cargo de F5 NGINX. Tenga en cuenta el parámetro
enableSnippets=true: los fragmentos se utilizan para configurar NGINX de modo que bloquee la inyección de SQL. La última línea de la salida confirma que la instalación se ha realizado correctamente.
$ helm install main nginx-stable/nginx-ingress \ --set controller.watchIngressWithoutClass=true --set controller.service.type=NodePort \ --set controller.service.httpPort.nodePort=30005 \ --set controller.enableSnippets=true
NAME: main
LAST DEPLOYED: Day Mon DD hh:mm:ss YYYY
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES: The NGINX Ingress Controller has been installed. - Confirme que el pod de NGINX Ingress Controller se haya desplegado, tal como indica el valor «
Running» en la columna «STATUS».
$ kubectl get pods NAME READY STATUS ...
main-nginx-ingress-779b74bb8b-mtdkr 1/1 Running ...
... RESTARTS AGE
... 0 18sDirige el tráfico a tu aplicación
- Cree un archivo YAML llamado 3-ingress.yaml con el siguiente contenido (o cópielo de GitHub). Este archivo define el manifiesto de Ingress necesario para dirigir el tráfico a la aplicación (esta vez, sin pasar por el proxy sidecar). Observe el bloque
annotations:en el que se utiliza un fragmento para personalizar la configuración del controlador de Ingress de NGINX con el mismo bloquelocationque en la definición del ConfigMap del reto 3: rechaza cualquier solicitud que incluya (entre otras cadenas de caracteres)SELECToUNION.
apiVersion: v1 kind: Service
metadata:
name: app-without-sidecar
spec:
ports:
- port: 80
targetPort: 80
selector:
app: app
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: entry
annotations:
nginx.org/server-snippets: |
location ~* "(\'|\")(.*)(drop|insert|md5|select|union)" {
deny all;
}
spec:
ingressClassName: nginx
rules:
- host: "example.com"
http:
paths:
- backend:
service:
name: app-without-sidecar
port:
number: 80
path: /
pathType: Prefix - Despliega el recurso Ingress:
$ kubectl apply -f 3-ingress.yaml service/app-without-sidecar created
ingress.networking.k8s.io/entry created Verificar el funcionamiento del filtro
- Inicia un contenedor desechable de BusyBox para enviar una solicitud al pod del controlador de Ingress de NGINX con el nombre de host correcto.
$ kubectl run -ti --rm=true busybox --image=busybox$ wget --header="Host: example.com" -qO- main-nginx-ingress
<!DOCTYPE html>
<html lang="en">
<head>
# ...- Intenta realizar la inyección SQL. El código de estado
403Prohibidoconfirma que NGINX bloquea el ataque.
$ wget --header="Host: example.com" -qO- 'main-nginx-ingress/product/-1"%20UNION%20SELECT%20username,username,password,password,username%20FROM%20users%20where%2 0id=1%20--%20//'
wget: server returned error: HTTP/1.1 403 Forbidden Próximos pasos
Kubernetes no es seguro de forma predeterminada. Un controlador Ingress puede mitigar las vulnerabilidades de inyección SQL (y muchas otras). Sin embargo, tenga en cuenta que la funcionalidad similar a la de un WAF que acaba de implementar con NGINX Ingress Controller no sustituye a un WAF real ni a un diseño seguro de las aplicaciones. Un hacker experimentado aún puede conseguir que el ataque UNION funcione con unos pequeños cambios en el código. Para obtener más información sobre este tema, consulte Guía de inyección SQL (SQLi) para pentesters.
Dicho esto, un controlador de Ingress sigue siendo una herramienta potente para centralizar la mayor parte de la seguridad, lo que permite mejorar la eficiencia y la protección, incluidos casos de uso de autenticación y autorización centralizadas (mTLS, inicio de sesión único) e incluso un WAF robusto como F5 NGINX App Protect WAF.
La complejidad de sus aplicaciones y su arquitectura puede requerir un control más granular. Si su organización necesita un modelo de confianza cero y cifrado de extremo a extremo, considere una malla de servicios gratuita para siempre como F5 NGINX Service Mesh para controlar la comunicación entre los servicios del clúster de Kubernetes (tráfico este-oeste). Analizamos las mallas de servicios en la Unidad 4, Estrategias avanzadas de implementación de Kubernetes.
Para obtener más información sobre cómo obtener e implementar NGINX Open Source, visite nginx.org.
Para probar NGINX Ingress Controller basado en NGINX Plus con NGINX App Protect, inicie hoy mismo su prueba gratuita de 30 días o póngase en contacto con nosotros para hablar sobre sus casos de uso.
Para probar NGINX Ingress Controller basado en NGINX Open Source, consulta Versiones de NGINX Ingress Controller en nuestro repositorio de GitHub o descarga un contenedor precompilado de DockerHub.
Sobre el autor
Artículos relacionados del blog
Únete a F5 NGINX en AppWorld 2024
Descubre las numerosas razones para asistir al evento insignia de F5, que incluirá un emocionante anuncio sobre la cartera de productos NGINX.
Rotación de certificados SSL/TLS sin reinicios en NGINX Open Source
La función de diccionario compartido permite configurar NGINX sin necesidad de reiniciarlo al renovar los certificados SSL/TLS.
Exposición de APIs en Kubernetes
La unidad 2 de Microservices March 2022 se centra en cómo exponer tus API en Kubernetes. Aquí tienes enlaces a los recursos que necesitas: la retransmisión en directo del 14 de marzo, artículos de blog y otros recursos de preparación, y el laboratorio práctico «Protege las API de Kubernetes mediante la limitación de solicitudes».
NGINX Sprint 2.0: visión clara, código renovado y nuevos compromisos con el código abierto
Hoy, en NGINX Sprint 2.0, anunciamos cómo respaldaremos nuestra visión del futuro de las aplicaciones modernas con una mayor presencia en la comunidad de código abierto, incluida la participación en el proyecto comunitario Kubernetes, nuevos proyectos de software de código abierto y la arquitectura de referencia para aplicaciones modernas.
Presentamos NGINX Plus R22
NGINX Plus R22 incorpora nuevas funcionalidades, como la compatibilidad con OCSP para verificar la revocación de certificados en TLS mutuo, el uso de varios proveedores de identidad OIDC en una configuración, el seguimiento en tiempo real de la limitación de solicitudes y conexiones en el panel de NGINX Plus y mejoras en NGINX JavaScript.
Presentamos NGINX Plus R17
NGINX Plus R17 incorpora compatibilidad con la limitación de velocidad en dos fases y con TLS 1.3, la versión más reciente del protocolo Transport Layer Security. La configuración de OpenID Connect es más sencilla y NGINX ModSecurity WAF es el doble de rápido que antes. También se ha actualizado el módulo JavaScript de NGINX.
