Sécurisation de mon point d'entré web des menaces externes
J'héberge actuellement des services web privés accessibles depuis Internet. Afin de protéger ces applications, j'avais besoin d'un moyen plutôt sécurisé pour protéger l'accès à ces dernier.
Comme vous le savez peut-être déjà, il existe des tonnes de robots/bots qui analysent en permanence toutes les adresses IP publiques d'Internet à la recherche de potentielles vulnérabilités. Des ports ouverts, des services web non sécurisés ou des failles de sécurité. Il existe des organisations privées qui permettent de découvrir ces vulnérabilités comme Shodan. Dans mon cas particulier, c'est la seule information qu'ils ont pu collecter sur mon IP publique :
Je dois donc exposer mes services et y accéder de n'importe où sur Internet sans trop de complexité tout en m'assurant que je suis le seul à pouvoir les utiliser et que personne d'autre ne puisse accéder à mes données.
Théorie
J'utilise le modèle Zero trust security. Donc, par défaut, personne ni aucun appareil n'est digne de confiance.
La première étape consiste à configurer le pare-feu pour supprimer tout le trafic entrant, fermer tous les ports réseau, désactiver la connexion pour tous les utilisateurs, supprimer tous les packages inutiles et exécuter les démons.
Avertissement : Vous devez être extrêmement prudent lorsque vous fermez tout ou vous pourriez être bloqué hors de votre réseau/serveur sans possibilité de récupérer l'accès.
Disposition
Voici donc une vue grosse maille de ma configuration réseau :
Internet --> IP publique --> Pare-feu --> Serveur de passerelle --> Service d'entrée --> Réseau privé
Mise en œuvre
Pas à pas
IP publique
Bon ici il n'y a pas grand chose à sécuriser. L'adresse IP est fournie de manière statique par mon FAI et est publiquement visible et accessible.
Pare-feu
Cet appareil est la première ligne de sécurité.
Comme mentionné précédemment, j'utilise le modèle Zero trust security, donc la première étape consiste à supprimer tout le trafic entrant par défaut. Maintenant, personne ne peut m'atteindre de l'extérieur, mais le trafic de retour ne le peut pas non plus. Lorsque je fais une requête à un service extérieur, les données renvoyées sont supprimées au niveau du pare-feu. Pas très utile en effet.
La deuxième étape consiste ensuite à autoriser le retour du trafic (tout en continuant à supprimer tout le reste du trafic). Je peux le faire avec une simple traduction d'adresse réseau ou NAT. De cette façon, lorsqu'un serveur du réseau privé fait une requête à un service public, la réponse peut traverser le pare-feu en toute sécurité. Cela peut s'avérer utile si vous souhaitez mettre à jour des packages sur votre serveur ! Tous les pare-feu prennent en charge nativement NAT, il n'est donc pas très difficile de l'activer.
Ensuite, je veux autoriser l'accès public à mes services qui exposent les ports HTTPS, donc 443/tcp
. J'ai activé la redirection de ports sur mon pare-feu afin que lorsque le trafic entrant/la requête arrive sur le port 443
avec le protocole TCP, il soit transmis à mon serveur de passerelle au sein de mon réseau privé.
Remarque : Le port exposé par mon serveur de passerelle n'a aucun impact sur la règle de transfert. Cela pourrait être
443
pour être cohérent ou cela pourrait être8888
. Ce n'est pas grave.
Serveur de passerelle
Mon serveur de passerelle actuel est basé sur Linux. La distribution utilisée dans mon cas n'est pas pertinente.
Pare-feu local
Il est livré avec firewalld pré-installé qui me permet d'avoir un pare-feu local sur mon serveur. Étant donné que ma passerelle reçoit du trafic du monde extérieur, je dois contrôler ce qui peut entrer et sortir de ce serveur.
Pour ma configuration, j'ai choisi de rester simple. J'autorise uniquement le trafic entrant depuis le port 443/tcp
(et 22/tcp
depuis mon réseau privé pour administrer le serveur avec SSH). Ensuite, j'autorise le trafic sortant (comme le NAT) pour le flux de retour.
Avec la ligne de commande firewalld suivante, je peux voir ma configuration actuelle :
Je reçois :public (active)
target: default
icmp-block-inversion: no
interfaces: eth0
sources:
services: https ssh
ports:
protocols:
forward: no
masquerade: no
forward-ports:
source-ports:
icmp-blocks:
rich rules:
Service Ingress
J'utilise NGINX comme service Ingress. Il est exécuté sur mon serveur de passerelle. Son objectif est d'acheminer et de filtrer le trafic HTTPS entrant.
Filtrer le trafic indésirable
La première étape consiste à filtrer tous les HTTPS qui ne vont pas à mon service exposé. Une première étape simple consiste à supprimer par défaut toutes les requêtes entrantes. Dans une configuration du site par défaut /etc/nginx/conf.d/default.conf
, j'ai :
La première ligne avec le mot clé listen
indique à NGINX d'écouter sur le port 443
et de commencer une connexion sécurisée avec le client.
Dans cette configuration, server_name _;
correspond à tout les Server Name Identification ou SNI (ou indéfinis) dans les requêtes entrantes.
La ligne return 444;
renvoie le code HTTP 444
qui indique à NGINX de fermer la connexion immédiatement.
Comme j'utilise de l'HTTPS, je dois fournir un certificat X.509 pour les communications TLS. J'ai fait une petite blague sur celui-ci parce que bon ... si vous êtes un étranger essayant d'accéder à mes trucs privés GTFO. Cela me donne :
server {
listen 443 default_server ssl;
server_name _;
ssl_certificate /etc/pki/tls/certs/go.fuck.yourself.now.crt;
ssl_certificate_key /etc/pki/tls/private/go.fuck.yourself.now.key;
return 444;
}
J'utilise actuellement une clé privée RSA 4096
bits de long.
Maintenant, si j'essaie de faire une requête stupide à mon service d'entrée comme :
J'ai la réponse :* Trying XXX.XXX.XXX.XXX...
* TCP_NODELAY set
* Connected to mygateway.local (XXX.XXX.XXX.XXX) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
* CAfile: /etc/ssl/cert.pem
CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server accepted to use http/1.1
* Server certificate:
* subject: C=FR; L=Paris; O=Wabbit; CN=go.fuck.yourself.now; emailAddress=admin@wabbit
* start date: May 29 15:55:36 2021 GMT
* expire date: May 29 15:55:36 2022 GMT
* issuer: C=FR; L=Paris; O=Wabbit; CN=wabbit; emailAddress=admin@wabbit
* SSL certificate verify ok.
> GET / HTTP/1.1
> Host: dumb.example.org
> User-Agent: curl/7.64.1
> Accept: */*
>
* Empty reply from server
* Connection #0 to host mygateway.local left intact
curl: (52) Empty reply from server
* Closing connection 0
Mon serveur Ingress m'envoie le certificat avec le nom commun go.fuck.yourself.now
pour une communication sécurisée. Mon client fait une requête GET
sur le chemin d'URI /
avec l'hôte dumb.example.org
.
Étant donné que mon serveur Ingress n'est pas configuré pour accepter les demandes de service portant le nom dumb.example.org
, il ferme la connexion sans avertissement et mon client reçoit une Empty reply from server
.
Améliorer la connexion sécurisée
Par défaut NGINX est très ouvert en terme de protocoles et de chiffrement permettant d'établir une connexion sécurisée. Dans le fichier de configuration principal NGINX, situé dans /etc/nginx/nginx.conf
, dans la section http
, j'ai remplacé les options ssl_
par :
Maintenant, mon service Ingress n'autorisera que TLS 1.2
minimum (jusqu'à 1.3
) avec uniquement des chiffrements HIGH
, pas NULL
et pas MD5
pour établir des connexions sécurisées avec le client. Le ssl_prefer_server_ciphers
pour que le client essaie les chiffrements avec l'ordre fourni par le service Ingress.
Recommandations TLS supplémentaires
Tout logguer
Par défaut, NGINX log toutes les requêtes dans le fichier /var/log/nginx/access.log
et toutes les erreurs dans /var/log/nginx/error.log
. J'ai vérifié si c'était le cas pour mon instance. Dans le fichier de configuration principal /etc/nginx/nginx.conf
, j'ai :
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
Dans /var/log/nginx/access.log
, je peux voir ma requête de test précédente :
Exposer un service
Maintenant que toutes mes connexions sont filtrées, je souhaite exposer mes services. Pour ce faire, j'ai ajouté un nouveau fichier de configuration dans /etc/nginx/conf.d
avec le contenu suivant :
server {
listen 443 ssl;
server_name myapp.example.org;
ssl_certificate /etc/pki/tls/certs/myapp.example.org.crt;
ssl_certificate_key /etc/pki/tls/private/myapp.example.org.key;
location / {
proxy_pass https://myapp.local;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Si une demande arrive avec le SNI myapp.example.org
, elle est acceptée par le service Ingress. NGINX va utiliser le certificat myapp.example.org.crt
pour établir une connexion sécurisée avec le client. Ensuite, il transférera tout le trafic vers mon application privée sur https://myapp.local
en utilisant la configuration proxy d'NGINX.
Identification des clients
Étant donné que je souhaite accéder à mes applications depuis n'importe où en dehors de mon réseau privé, je dois autoriser les requêtes entrantes de toutes les adresses IP publiques potentielles. Mais je veux toujours pouvoir identifier mes appareils parmi toutes les IP publiques.
J'ai décidé d'implémenter l'authentification par [certificat client][certificat-client-wikipedia]. J'ai généré un certificat X.509 privé et unique pour chacun de mes appareils (PC, téléphone, ...) et je les ai installés.
J'ai ensuite configuré mon service Ingress pour demander le certificat client lorsqu'un client souhaite accéder à un service exposé. En complétant la configuration précédente, j'obtiens :
server {
listen 443 ssl;
server_name myapp.example.org;
ssl_certificate /etc/pki/tls/certs/myapp.example.org.crt;
ssl_certificate_key /etc/pki/tls/private/myapp.example.org.key;
# make verification optional, so we can display a 403 message to those
# who fail authentication
ssl_verify_client on;
ssl_client_certificate /etc/pki/tls/certs/users.example.org.crt;
location / {
proxy_pass https://myapp.local;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
ssl_verify_client
force l'utilisateur à fournir un certificat client pour l'authentification. Sinon, la connexion est fermée avec le code 400
.
ssl_certificate_key
fournit l'autorité de certification ou CA qui a signé les certificats des clients.
Maintenant, si j'essaie de faire une demande sur myapp.example.org
sans le certificat client :
HTTP/1.1 400 Bad Request
Server: nginx/1.20.1
Date: Sun, 10 Oct 2021 17:35:05 GMT
Content-Type: text/html
Content-Length: 237
Connection: close
<html>
<head><title>400 No required SSL certificate was sent</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<center>No required SSL certificate was sent</center>
<hr><center>nginx/1.20.1</center>
</body>
</html>
Le serveur Ingress rejette ma demande avec le code 400
car je n'ai pas fourni le certificat client requis.
Essayons à nouveau en fournissant le certificat client :
Je reçois :HTTP/1.1 302 Found
Server: nginx/1.20.1
Date: Sun, 10 Oct 2021 17:35:36 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 30
Connection: keep-alive
Content-Language: en
Location: /login
Referrer-Policy: same-origin
Vary: Accept, Accept-Encoding
X-Content-Type-Options: nosniff
X-Frame-Options: deny
X-Xss-Protection: 1; mode=block
Found. Redirecting to /login
Ca fonctionne !
Fail2ban
Maintenant que je peux accéder en toute sécurité à mes services privés, je veux toujours empêcher les utilisateurs ou les bots indésirables de faire du brute force ou des DDOS sur mon serveur de passerelle.
J'utilise actuellement Fail2ban pour bloquer ces comportements. Il analyse en permanence les logs du service Ingress pour identifier les demandes malveillantes faites par les clients publics.
J'ai créé un filter
personnalisé qui correspond à toutes les demandes renvoyant un code 444
. Si vous vous souvenez bien, ce code est renvoyé par ma configuration NGINX si le client demande un SNI inconnu.
La configuration du filtre (/etc/fail2ban/filter.d/nginx-444.conf
) ressemble à :
Ensuite, j'ai une jail
qui implémente ce filter
. Le fichier /etc/fail2ban/jail.d/block-malicious-users.conf
contient :
[DEFAULT]
ignoreip = 127.0.0.1 XXX.XXX.XXX.XXX YYY.YYY.YYY.YYY
findtime = 3600
bantime = 31536000
maxretry = 1
[nginx-444]
enabled = true
logpath = /var/log/nginx/*.log
# Ban IP
action = %(banaction_allports)s
Si Fail2ban trouve dans le fichier logpath
, au moins maxretry
requêtes échouées dans une fenêtre de temps findtime
. Il interdira l'IP pendant bantime
secondes.
Remarques : J'ai ignoré l'adresse de l'hôte local, l'IP de mon PC d'administration (
XXX.XXX.XXX.XXX
) et l'IP publique de la passerelleYYY.YYY.YYY.YYY
pour empêcher Fail2ban de me bannir lorsque je teste la configuration.
Vérifier les IP bannies
Je peux déjà voir des client banni des précédentes requêtes ayant échoué. Quand j'utilise la commande Fail2ban de statuts :
J'ai :Status for the jail: nginx-444
|- Filter
| |- Currently failed: 0
| |- Total failed: 3
| `- File list: /var/log/nginx/error.log /var/log/nginx/custom-access.log /var/log/nginx/access.log
`- Actions
|- Currently banned: 3
|- Total banned: 3
`- Banned IP list: XXX.XXX.XXX.XXX YYY.YYY.YYY.YYY ZZZ.ZZZ.ZZZ.ZZZ
En vérifiant firewalld pour les règles de suppressions de paquets provenant de ces IP avec la commande :
J'ai :public (active)
target: default
icmp-block-inversion: no
interfaces: eth0
sources:
services: dhcpv6-client https mdns ssh
ports:
protocols:
forward: no
masquerade: no
forward-ports:
source-ports:
icmp-blocks:
rich rules:
rule family="ipv4" source address="XXX.XXX.XXX.XXX" port port="0-65535" protocol="tcp" reject type="icmp-port-unreachable"
rule family="ipv4" source address="YYY.YYY.YYY.YYY" port port="0-65535" protocol="tcp" reject type="icmp-port-unreachable"
rule family="ipv4" source address="ZZZ.ZZZ.ZZZ.ZZZ" port port="0-65535" protocol="tcp" reject type="icmp-port-unreachable"
Cela semble bon !
Je banni généralement environ 6,5
IP par jour :
Conclusion
Sur chacune de mes couches j'ai implémenté :
-
Pare-feu :
- Supprimer tout le trafic entrant
- NAT
- Redirection de port sur le port
443/tcp
-
Serveur passerelle :
-
Service Ingress :
- Supprimer tout le trafic avec un SNI inconnu
- Utilisez uniquement des protocoles et des chiffrements sécurisés TLS
- Appliquer [certificat client][certificat-client-wikipedia] pour l'authentification
- Utilisez des certificats X.509 à l'état de l'art