Ce tutoriel est l'un des quatre qui mettent en pratique les concepts de Microservices de mars 2022 : Réseau Kubernetes :
Vous souhaitez des conseils détaillés sur l’utilisation de NGINX pour encore plus de cas d’utilisation de réseau Kubernetes ? Téléchargez notre eBook gratuit, Gérer le trafic Kubernetes avec NGINX : Un guide pratique .
Vous travaillez dans le service informatique d'un magasin local populaire qui vend une variété de produits, des oreillers aux vélos. Ils sont sur le point de lancer leur première boutique en ligne, mais ils ont demandé à un expert en sécurité de tester le site avant qu'il ne soit rendu public. Malheureusement, l'expert en sécurité a trouvé un problème ! La boutique en ligne est vulnérable à une injection SQL . L'expert en sécurité a pu exploiter le site pour obtenir des informations sensibles de votre base de données, notamment des noms d'utilisateur et des mots de passe.
Votre équipe est venue vers vous – l’ingénieur Kubernetes – pour sauver la situation. Heureusement, vous savez que l’injection SQL (ainsi que d’autres vulnérabilités) peut être atténuée à l’aide des outils de gestion du trafic Kubernetes. Vous avez déjà déployé un contrôleur Ingress pour exposer l’application et, dans une seule configuration, vous êtes en mesure de garantir que la vulnérabilité ne peut pas être exploitée. La boutique en ligne peut désormais être lancée à temps. Bien joué!
Ce blog accompagne le laboratoire de l'unité 3 de Microservices de mars 2022 - Modèle de sécurité des microservices dans Kubernetes , démontrant comment utiliser NGINX et NGINX Ingress Controller pour bloquer l'injection SQL.
Pour exécuter le tutoriel, vous avez besoin d'une machine avec :
Pour tirer le meilleur parti du laboratoire et du tutoriel, nous vous recommandons, avant de commencer, de :
Ce tutoriel utilise ces technologies :
Les instructions pour chaque défi incluent le texte complet des fichiers YAML utilisés pour configurer les applications. Vous pouvez également copier le texte depuis notre dépôt GitHub . Un lien vers GitHub est fourni avec le texte de chaque fichier YAML.
Ce tutoriel comprend quatre défis :
Dans ce défi, vous déployez un cluster minikube et installez Podinfo en tant qu'exemple d'application présentant des vulnérabilités de sécurité.
Déployer un cluster minikube . Après quelques secondes, un message confirme que le déploiement a réussi.
$ minikube start 🏄 Terminé ! kubectl est maintenant configuré pour utiliser le cluster « minikube » et l'espace de noms « default » par défaut
Ici, vous déployez une application de commerce électronique simple composée de deux microservices :
Procédez comme suit :
À l’aide de l’éditeur de texte de votre choix, créez un fichier YAML appelé 1-app.yaml avec le contenu suivant (ou copiez-le depuis GitHub ).
apiVersion : apps/v1 type : Déploiement
métadonnées :
nom : application
spécification :
sélecteur :
matchLabels :
application : application
modèle :
métadonnées :
étiquettes :
application : application
spécification :
conteneurs :
- nom : application
image : f5devcentral/microservicesmarch:1.0.3
ports :
- containerPort : 80
env:
- nom: MYSQL_USER
valeur : dan
- nom : MYSQL_PASSWORD
valeur : dan
- nom : MYSQL_DATABASE
valeur : sqlitraining
- nom : DATABASE_HOSTNAME
valeur : db.default.svc.cluster.local
---
apiVersion : v1
type : Service
métadonnées :
nom : application
spécification :
ports :
- port : 80
targetPort : 80
NodePort : 30001
sélecteur :
application : application
type : NodePort
---
apiVersion : apps/v1
kind : Déploiement
métadonnées :
nom : db
spécification :
sélecteur :
matchLabels :
application : db
modèle :
métadonnées :
étiquettes :
application : db
spécification :
conteneurs :
- nom : db
image : mariadb:10.3.32-focal
ports :
- containerPort : 3306
env:
- nom: MYSQL_ROOT_PASSWORD
valeur : root
- nom : MYSQL_USER
valeur : dan
- nom : MYSQL_PASSWORD
valeur : dan
- nom : MYSQL_DATABASE
valeur : sqlitraining
---
apiVersion : v1
type : Service
métadonnées :
nom : db
spécification :
ports :
- port : 3306
targetPort : 3306
sélecteur :
application : db
Déployer l’application et l’API :
$ kubectl apply -f 1-app.yaml déploiement.apps/app créé service/app créé déploiement.apps/db créé service/db créé
Confirmez que les pods Podinfo sont déployés, comme indiqué par la valeur En cours d’exécution
dans la colonne STATUS
. Leur déploiement complet peut prendre 30 à 40 secondes. Attendez donc que l’état des deux pods soit En cours d’exécution
avant de passer à l’étape suivante (en réexécutant la commande si nécessaire).
$ kubectl get pods NOM PRÊT ÉTAT RESTARTS AGE app-d65d9b879-b65f2 1/1 En cours d'exécution 0 37 s db-7bbcdc75c-q2kt5 1/1 En cours d'exécution 0 37 s
Ouvrez l'application dans votre navigateur :
$ minikube service app |-----------|------|-------------|--------------| | ESPACE DE NOMS | NOM | PORT CIBLE | URL | |-----------|------|-------------|--------------| | default | app | | Pas de port de nœud | |-----------|------|-------------|--------------| 😿 le service default/app n'a pas de port de nœud 🏃 Démarrage du tunnel pour l'application de service. |-----------|------|-------------------------|------------------------| | ESPACE DE NOMS | NOM | PORT CIBLE | URL | |-----------|------|-------------------------|-------------------------| | default | app | | http://127.0.0.1:55446 | |-----------|------|-------------------------|-------------------------| 🎉 Ouverture du service default/app dans le navigateur par défaut...
L'exemple d'application est plutôt basique. Il comprend une page d'accueil avec une liste d'articles (par exemple, des oreillers) et un ensemble de pages de produits avec des détails comme une description et le prix. Les données sont stockées dans la base de données MariaDB. Chaque fois qu'une page est demandée, une requête SQL est émise sur la base de données.
Lorsque vous ouvrez la page produit des oreillers , vous remarquerez peut-être que l'URL se termine par /product/1 . Le1 est l'ID du produit. Pour éviter l'insertion directe de code malveillant dans la requête SQL, il est recommandé de nettoyer les entrées utilisateur avant de transmettre les requêtes aux services back-end. Mais que se passe-t-il si l’application n’est pas correctement configurée et que l’entrée n’est pas échappée avant d’être insérée dans la requête SQL sur la base de données ?
Pour savoir si l’application échappe correctement les entrées, exécutez une expérience simple en modifiant l’ID par un identifiant qui n’existe pas dans la base de données.
Modifiez manuellement le dernier élément de l'URL à partir de1 à-1 . Le message d'erreur ID
de produit
non valide
« -1 »
indique que l'ID de produit n'est pas échappé ; au lieu de cela, la chaîne est insérée directement dans la requête. Ce n’est pas bon à moins que vous ne soyez un hacker !
Supposons que la requête de base de données ressemble à ceci :
SÉLECTIONNEZ * DANS une_table OÙ id = "1"
Pour exploiter la vulnérabilité causée par le fait de ne pas échapper l'entrée, remplacez 1
avec -1"
<requête_malveillante>
--
//
tel que :
"
) après-1
complète la première requête.--
//
supprime le reste de la requête.Ainsi, par exemple, si vous modifiez l’élément final de l’URL en -1"
ou 1
--
//
, la requête est compilée en :
SÉLECTIONNEZ * DANS une_table OÙ id = "-1" OU 1 -- //" -------------- ^ injecté ^
Cela sélectionne toutes les lignes de la base de données, ce qui est utile dans un hack. Pour savoir si c'est le cas, changez la fin de l'URL en ‑1"
. Le message d'erreur résultant vous donne des informations plus utiles sur la base de données :
Erreur fatale : mysqli_sql_exception non détectée : Vous avez une erreur dans votre syntaxe SQL ; vérifiez le manuel qui correspond à votre version de serveur MariaDB pour la bonne syntaxe à utiliser près de '"-1""' à la ligne 1 dans /var/www/html/product.php:23 Stack trace : #0 /var/www/html/product.php(23) : mysqli->query('SELECT * FROM p...') #1 {main} lancé dans /var/www/html/product.php à la ligne 23
Vous pouvez maintenant commencer à manipuler le code injecté pour tenter de classer les résultats de la base de données par ID :
-1" OU 1 COMMANDER PAR id DESC -- //
Le résultat est la page produit du dernier article de la base de données.
Forcer la base de données à ordonner les résultats est intéressant, mais pas particulièrement utile si le piratage est votre objectif. Essayer d'extraire les noms d'utilisateur et les mots de passe de la base de données en vaut bien plus la peine.
On peut supposer sans risque qu’il existe une table d’utilisateurs dans la base de données avec des noms d’utilisateur et des mots de passe. Mais comment étendre votre accès de la table des produits à la table des utilisateurs ?
La réponse est d'injecter du code comme ceci :
-1" UNION SELECT * FROM utilisateurs -- //
où
-1"
force le retour d'un ensemble vide dès la première requête.UNION
force deux tables de base de données ensemble (dans ce cas, les produits et les utilisateurs ), ce qui vous permet d'obtenir des informations (mots de passe) qui ne figurent pas dans la table d'origine ( produits ).SELECT
*
FROM
users
sélectionne toutes les lignes de la table users .--
//
supprime tout ce qui suit la requête malveillante.Lorsque vous modifiez l'URL pour qu'elle se termine par le code injecté, vous obtenez un nouveau message d'erreur :
Erreur fatale : mysqli_sql_exception non détectée : Les instructions SELECT utilisées ont un nombre différent de colonnes dans /var/www/html/product.php:23 Pile d'exécution : #0 /var/www/html/product.php(23) : mysqli->query('SELECT * FROM p...') #1 {main} lancé dans /var/www/html/product.php à la ligne 23
Ce message révèle que les tables produits et utilisateurs n'ont pas le même nombre de colonnes, donc l'instruction UNION
ne peut pas être exécutée. Mais vous pouvez découvrir le nombre de colonnes par essais et erreurs en ajoutant des colonnes (noms de champs) une par une en tant que paramètres à l'instruction SELECT
. Une bonne estimation du nom d'un champ dans une table d'utilisateurs est password
, alors essayez cela :
# sélectionnez 1 colonne -1" UNION SELECT mot de passe FROM utilisateurs; -- // # sélectionnez 2 colonnes -1" UNION SELECT mot de passe,mot de passe FROM utilisateurs; -- // # sélectionnez 3 colonnes -1" UNION SELECT mot de passe,mot de passe,mot de passe FROM utilisateurs; -- / # sélectionnez 4 colonnes -1" UNION SELECT mot de passe,mot de passe,mot de passe,mot de passe FROM utilisateurs; -- // # sélectionnez 5 colonnes -1" UNION SELECT mot de passe,mot de passe,mot de passe,mot de passe,mot de passe FROM utilisateurs; -- //
La dernière requête réussit (vous indiquant qu'il y a cinq colonnes dans la table des utilisateurs ) et vous voyez un mot de passe utilisateur :
À ce stade, vous ne connaissez pas le nom d'utilisateur qui correspond à ce mot de passe. Mais connaissant le nombre de colonnes dans la table des utilisateurs , vous pouvez utiliser les mêmes types de requêtes que précédemment pour exposer ces informations. Supposons que le nom du champ concerné soit username
. Et cela s’avère être vrai : la requête suivante expose à la fois le nom d’utilisateur et le mot de passe de la table des utilisateurs . C’est génial, à moins que cette application ne soit hébergée sur votre infrastructure !
-1" UNION SELECT nom d'utilisateur, nom d'utilisateur, mot de passe, mot de passe, nom d'utilisateur FROM utilisateurs où id=1 -- //
Le développeur de l’application de la boutique en ligne doit évidemment accorder plus d’attention à la désinfection des entrées utilisateur (par exemple en utilisant des requêtes paramétrées), mais en tant qu’ingénieur Kubernetes, vous pouvez également contribuer à prévenir l’injection SQL en empêchant l’attaque d’atteindre l’application. De cette façon, la vulnérabilité de l’application n’a plus autant d’importance.
Il existe de nombreuses façons de protéger vos applications. Pour le reste de ce laboratoire, nous nous concentrons sur deux éléments :
Dans ce défi, vous injectez un conteneur sidecar dans le pod pour proxy tout le trafic et refuser toute demande contenant UNION
dans l'URL.
Déployez d’abord NGINX Open Source en tant que side-car , puis testez s’il filtre les requêtes malveillantes .
Note: Nous utilisons cette technique à des fins d’illustration uniquement. En réalité, déployer manuellement des proxys en tant que side-cars n’est pas la meilleure solution (nous y reviendrons plus tard).
Créez un fichier YAML appelé 2-app-sidecar.yaml avec le contenu suivant (ou copiez-le depuis GitHub ). Les aspects importants de la configuration incluent :
SELECT
ou UNION
est refusée (voir le premier bloc d'emplacement
dans la section ConfigMap
).apiVersion : apps/v1
type : Déploiement
métadonnées :
nom : application
spécification :
sélecteur :
matchLabels :
application : application
modèle :
métadonnées :
étiquettes :
application : application
spécification :
conteneurs :
- nom : application
image : f5devcentral/microservicesmarch:1.0.3
ports :
- containerPort : 80
env:
- nom: MYSQL_USER
valeur : dan
- nom : MYSQL_PASSWORD
valeur : dan
- nom : MYSQL_DATABASE
valeur : sqlitraining
- nom : DATABASE_HOSTNAME
valeur : db.default.svc.cluster.local
- nom : proxy # <-- sidecar
image : « nginx »
ports :
- containerPort : 8080
volumeMounts :
- mountPath : /etc/nginx
nom : nginx-config
volumes :
- nom : nginx-config
configMap :
nom : sidecar
---
apiVersion : v1
kind : Service
métadonnées :
nom : application
spécification :
ports :
- port : 80
targetPort : 8080 # <-- le trafic est acheminé vers le proxy
nodePort : 30001
sélecteur :
application : application
type : NodePort
---
apiVersion : v1
kind : ConfigMap
métadonnées :
nom : sidecar
données :
nginx.conf : |-
événements {}
http {
serveur {
écouter 8080 default_server ;
écouter [::]:8080 default_server ;
emplacement ~* "(\'|\")(.*)(drop|insert|md5|select|union)" {
refuser tout ;
}
emplacement / {
proxy_pass http://localhost:80/;
}
}
}
---
apiVersion : apps/v1
type : Déploiement
métadonnées :
nom : db
spécification :
sélecteur :
matchLabels :
application : db
modèle :
métadonnées :
étiquettes :
application : db
spécification :
conteneurs :
- nom : db
image : mariadb:10.3.32-focal
ports :
- containerPort : 3306
env:
- nom: MYSQL_ROOT_PASSWORD
valeur : root
- nom : MYSQL_USER
valeur : dan
- nom : MYSQL_PASSWORD
valeur : dan
- nom : MYSQL_DATABASE
valeur : sqlitraining
---
apiVersion : v1
type : Service
métadonnées :
nom : db
spécification :
ports :
- port : 3306
targetPort : 3306
sélecteur :
application : db
Déployer le side-car :
$ kubectl apply -f 2-app-sidecar.yaml déploiement.apps/app configuré service/app configuré configmap/sidecar créé déploiement.apps/db inchangé service/db inchangé
Testez si le side-car filtre le trafic en revenant à l’application et en essayant à nouveau l’injection SQL. NGINX bloque la requête avant qu’elle n’atteigne l’application !
-1" UNION SELECT nom d'utilisateur, nom d'utilisateur, mot de passe, mot de passe, nom d'utilisateur FROM utilisateurs où id=1 -- //
Protéger votre application comme dans le Challenge 3 est intéressant comme expérience pédagogique, mais nous ne le recommandons pas pour la production car :
Une bien meilleure solution consiste à utiliser NGINX Ingress Controller pour étendre la même protection à toutes vos applications ! Les contrôleurs d'entrée peuvent être utilisés pour centraliser toutes sortes de fonctionnalités de sécurité, du blocage des requêtes comme le fait un pare-feu d'application Web (WAF) à l'authentification et à l'autorisation.
Dans ce défi, vous déployez NGINX Ingress Controller , configurez le routage du trafic et vérifiez que le filtre bloque l'injection SQL .
Le moyen le plus rapide d'installer NGINX Ingress Controller est d'utiliser Helm .
Ajoutez le référentiel NGINX à Helm :
$ helm repo ajouter nginx-stable https://helm.nginx.com/stable
Téléchargez et installez le contrôleur d'entrée NGINX Open Source NGINX , qui est géré par F5 NGINX. Notez le paramètre enableSnippets=true
: les extraits sont utilisés pour configurer NGINX afin de bloquer l'injection SQL. La dernière ligne de sortie confirme l’installation réussie.
$ 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 NOM : main DERNIÈREMENT DÉPLOYÉ : Jour Lun JJ hh:mm:ss AAAA ESPACE DE NOM : par défaut STATUT : déployé RÉVISION : 1 SUITE DE TESTS : Aucun REMARQUES : Le contrôleur d'entrée NGINX a été installé.
Confirmez que le pod NGINX Ingress Controller est déployé, comme indiqué par la valeur En cours d’exécution
dans la colonne STATUS
.
$ kubectl get pods NOM ÉTAT PRÊT ... main-nginx-ingress-779b74bb8b-mtdkr 1/1 En cours d'exécution ... ... REDÉMARRE L'ÂGE... 0 18s
Créez un fichier YAML appelé 3-ingress.yaml avec le contenu suivant (ou copiez-le depuis GitHub ). Il définit le manifeste Ingress requis pour acheminer le trafic vers l'application (pas via le proxy side-car cette fois). Notez les annotations :
bloc où un extrait est utilisé pour personnaliser la configuration du contrôleur d'entrée NGINX avec le même bloc d'emplacement
que dans la définition ConfigMap du défi 3 : il rejette toute demande qui inclut (entre autres chaînes de caractères) SELECT
ou UNION
.
apiVersion: v1 type: Service
métadonnées :
nom : app-without-sidecar
spécification :
ports :
- port : 80
targetPort : 80
sélecteur :
application : application
---
apiVersion : networking.k8s.io/v1
type : Entrée
métadonnées :
nom : entrée
annotations :
nginx.org/server-snippets : |
emplacement ~* "(\'|\")(.*)(drop|insert|md5|select|union)" {
refuser tout ;
}
spécification :
ingressClassName : nginx
règles :
- hôte : "exemple.com"
http :
chemins :
- backend :
service :
nom : app-without-sidecar
port :
numéro : 80
chemin : /
type de chemin : Préfixe
$ kubectl apply -f 3-ingress.yaml service/app-without-sidecar créé ingress.networking.k8s.io/entry créé
Lancez un conteneur BusyBox jetable pour émettre une demande au pod NGINX Ingress Controller avec le nom d’hôte correct.
$ kubectl run -ti --rm=true busybox --image=busybox $ wget --header="Hôte : exemple.com" -qO- main-nginx-ingress # ...
Tentez l'injection SQL. Le403
Le code d’état interdit
confirme que NGINX bloque l’attaque !
$ wget --header="Hôte : exemple.com" -qO- 'main-nginx-ingress/product/-1"%20UNION%20SELECT%20nom d'utilisateur,nom d'utilisateur,mot de passe,mot de passe,nom d'utilisateur%20FROM%20utilisateurs%20où%2 0id=1%20--%20//' wget : le serveur a renvoyé une erreur : HTTP/1.1 403 Interdit
Kubernetes n'est pas sécurisé par défaut. Un contrôleur Ingress peut atténuer les vulnérabilités d’injection SQL (et bien d’autres). Mais gardez à l’esprit que le type de fonctionnalité de type WAF que vous venez d’implémenter avec NGINX Ingress Controller ne remplace pas un WAF réel, ni ne remplace l’architecture sécurisée des applications. Un hacker averti peut toujours faire fonctionner le hack UNION
avec quelques petites modifications au code. Pour en savoir plus sur ce sujet, consultez le Guide du pentester sur l'injection SQL (SQLi) .
Cela dit, un contrôleur Ingress reste un outil puissant pour centraliser la majeure partie de votre sécurité, ce qui conduit à une plus grande efficacité et sécurité, y compris les cas d'utilisation d'authentification et d'autorisation centralisés (mTLS, authentification unique) et même un WAF robuste comme F5 NGINX App Protect WAF .
La complexité de vos applications et de votre architecture peut nécessiter un contrôle plus précis. Si votre organisation nécessite Zero Trust et un chiffrement de bout en bout , envisagez un maillage de services tel que le maillage de services F5 NGINX toujours gratuit pour contrôler la communication entre les services du cluster Kubernetes (trafic est-ouest). Nous explorons les maillages de services dans l'unité 4, Stratégies avancées de déploiement de Kubernetes .
Pour plus de détails sur l'obtention et le déploiement de NGINX Open Source, visitez nginx.org .
Pour essayer le contrôleur d'entrée NGINX basé sur NGINX Plus avec NGINX App Protect, démarrez votre essai gratuit de 30 jours dès aujourd'hui ou contactez-nous pour discuter de vos cas d'utilisation .
Pour essayer le contrôleur d'entrée NGINX basé sur NGINX Open Source, consultez les versions du contrôleur d'entrée NGINX sur notre référentiel GitHub ou téléchargez un conteneur prédéfini depuis DockerHub .
« Cet article de blog peut faire référence à des produits qui ne sont plus disponibles et/ou qui ne sont plus pris en charge. Pour obtenir les informations les plus récentes sur les produits et solutions F5 NGINX disponibles, explorez notre famille de produits NGINX . NGINX fait désormais partie de F5. Tous les liens NGINX.com précédents redirigeront vers un contenu NGINX similaire sur F5.com."