Je me souviens dans mon expérience passée qu’un collègue m’avait parlé d’un truc incroyable (surtout la 2).

Son client était régulièrement visité par des robots scrappeur utilisant des adresses en provenance de Tor.

(Tor est un outil qui permet d’échanger des informations de manière plus sécurisée que le web normal en passant par un réseau parallèle chiffré parano)

Comme j’aime les trucs parfaitement idiots, j’ai décidé de bricoler un bouzin qui fait absolument l’inverse et ne laissera passer sur mon load balancer que les IP en provenance de Tor.

Installe-toi confortablement, aujourd’hui c’est crouton et grosse soupe à l’oignon.🤪

soupe à l’oignon

Encore un truc statique rafraîchit dynamiquement sérieusement ?

Comme les IP de Tor sont mouvantes, on va rafraîchir le truc à intervalles réguliers. Ouais, je sais, encore un truc statique à rafraîchissement dynamique. Le recyclage, c’est dans l’ère du temps.

Tu es qui pour me dire de pas le faire hein ? Je fais ce que je veux 😈😈

C’est pour rigoler hein, vous amusez pas à héberger un site Tor avec du contenu illicite sur GCP. Ils vous trouveront rapidement après y’aura du pénal et tout, le truc chiant quoi 🤷.

Utilisez plutôt OVH, avec un peu de chance votre machine va cramer avant de vous faire prendre 🤭🤭🤭

datacenter d’ovh Strasbourg en flammes

Et après ce n’est pas mes .oignon 🤪

Et comme je suis un cyberpunk à chien et que je ne surveille jamais rien, on va essayer de construire un truc un peu costaud et un minimum tolérant à la panne.

Et pas cher parce que je suis aussi radin, comme vous le savez bien 😂

fait pas le cron Jim, t’as une infra et des enfants

Bon une fois n’est pas coutume, on part sur un backend isolé, le scheduler de Google et un terraform dans ton container 🤪

Un cron, un bout de python, c’est le début du bonheur. Article sponsorisé par python cuisine

gordon ramsay capture un python

Pour les IP de Tor je triche un peu, je pourrais chercher un moyen dynamique et compliqué de le faire, mais la flemme.

https://github.com/SecOps-Institute/Tor-IP-Addresses

Merci monsieur, liste dynamiquement rafraîchie à interval régulier, un coup de git, un coup de terraform, je mets mes pantoufles, j’allume la télé et terminé, bon soir.

Pas de credentials qui trainent, des services account accrochés sur les bons objets. croquant crado, rapido gourmand 🤷

diagramme d’infra, voir description ci dessous

explication détaillée du diagramme

sur la partie en bas à gauche

On a notre backend qui va être chargé de dynamiquement rafraichir notre firewall cloud armor. Github contiendra nos ip rafraichies à intervalles réguliers, on s’en occupe pas c’est pas notre problème, on pique juste les ips qui sont dedans.

Le cloud run contiendra une image docker avec python git et terraform voici un algo rapide de sa logique.

on event
git pull ip list

put deny all in first firewall rule terraform file
for ip in list
  add allow ip list iun firewall rule terraform file

tf init
tf apply autoapprove

Le remote bucket est garant de l’état précédent du state Enfin le scheduler google est chargé de déclencher l’évènement cloudrun à intervalles réguliers en mode cronjob http.

Vous voyez un peu la sauvagerie du truc ? on écrase systématiquement le fichier terraform et apply auto approve sur les règles de firewall.

C’est un peu dangereux si on crash le terraform, mais on s’en fout, on met pas ça en prod héhé. Effet sympa, si des ips sortent de la liste, elle seront automatiquement delete par terraform. Effet moins sympa, l’ordre de la liste peut poser des conflits et forcer la reconstruction de l’index terraform sur tous ou quelques objets.

sur la partie en haut

On a un tank qui représente mes haters du clean web, qui prendront une règle deny si ils arrivent avec des ips qui ne sont pas des ips de ma liste.

À droite du tank on a les gothiques qui mangent des oignons à ne pas confondre avec les vampires, qui eux utilisent les IPs de ma liste et sont donc les seuls autorisés à passer le firewall, et consulter mon compte onlyRage.

Voilà pour la théorie

un peu de code avec ça ? (prank, ça tourne mal)

Quand j’ai commencé ce projet, j’étais saucé de fou, la réalité de la vie m’a offert un gros parpaing dans la gueule.

Je vous propose de vous montrer ce que j’ai commencé à faire et après je propose une alternative.

Mais PUTAIN DE BORDEL DE MERDE, oui, je le dis, j’ai la rage contre GCP.

chien qui a la rage

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
def main():
    with open ("Tor-IP-Addresses/tor-exit-nodes.lst")  as file:
        lines = file.readlines()
        ips = [line.rstrip() for line in lines]
        ips = check_ip(ips)

        template = template_shlagos(ips)

        with open ('terraform/allow_tor.tf', "w") as tf_file:
            tf_file.write(template)


def check_ip(ips):

    import socket

    ok_ips=[]    
    for addr in ips:
        try:
            socket.inet_aton(addr)
            ok_ips.append(addr)
        except socket.error:
            print (f'address {addr}, not validate by sock inet')
    return ok_ips

def template_shlagos(ips): 
    tf_soupe = ""
    head = """
resource "google_compute_security_policy" "policy_allow_tor" {
  name = "allow-tor-ips"

  project = var.project_id

  rule {
    action   = "deny(403)"
    priority = "2147483647"
    match {
      versioned_expr = "SRC_IPS_V1"
      config {
        src_ip_ranges = ["*"]
      }
    }
    description = "Deny access to all ips"
  }

"""
    tf_soupe = head
    i = 0

    for ip10 in  ([ips[i:i+10] for i in range(0, len(ips), 10)]):
        i += 1

        rule=("""
  rule {
    action   = "allow"
    priority = "%s"
    match {
      versioned_expr = "SRC_IPS_V1"
      config {
        src_ip_ranges = %s
      }
    }
    description = "allow tor ips"
  }

""" % (str(1000+i),  ip10 ))

        tf_soupe += rule

    tf_soupe = tf_soupe[:-1] # remove last ,
    tf_soupe += "}"
    tf_soupe = tf_soupe.replace("'", '"') # need " not ' because tf is crying like a baby
    return tf_soupe

if __name__ == "__main__":
	 main()

Putain mais qu’est-ce que c’est que ce gros code de merde ?

Et bien ça c’est ce qui arrive quand on utilise python pour templater du terraform de facon crade, le pire c’est que je n’en ai pas honte.

ça fait quoi et pourquoi CKC ?

  • lire le fichier Tor-IP-Addresses/tor-exit-nodes.lst qui contient les IPs de tor
  • récupérer les IPs et les mettre dans un tableau
  • ouvrir un socket pour voir si l’ip répond ou pas
  • templater le fichier terraform avec une poutre IPN en guise de chausse pied.

C’était moche et rapide, le terraform plan passe, bonne vibe quoi, j’étais presque content de mon coup

Et la PAF, CKC à cause des quotas de GCP.

Pas plus de 100 rules, maximum 10 ips par rule, mais on est où là ? Mais putain, je paye, laisse moi faire de la merde non ?

meme el risitas

J’étais content, c’était moche et rapide, le terraform plan passe, et l’apply casse (je vous ai déjà dit que je déteste terraform ? )

c’est con j’avais que environs 1800 IPs à rentrer, franchement, une broutille, quel indignité …

nicolas sarkozy, débat présidentiel, quel indignité

google_compute_security_policy.policy_allow_tor: Still creating... [10s elapsed]
╷
│ Error: Error waiting for Creating SecurityPolicy "allow-tor-ips": Quota 'SECURITY_POLICY_RULES' exceeded.  Limit: 100.0 globally.

bon manifestement c’est pas le bon outil pour faire ça, du coup on va plutôt faire un reverse proxy avec tonton nginx.

repli stratégique, ça part en compute

un truc bête un peu comme ça mais genre avec 1800 lignes, rien de bien méchant, hein cloud armor ?

1
2
3
4
5
location / {
   deny all;
   allow 9.8.7.6;
   allow [...];
 }

Le templating sera bien moins compliqué et dégueux sans terraform.

On va trouver un truc pour templater la conf et la balancer dans gcs. Une fois dans gcs on peut tirer la conf sur 1..N compute et scale si on veut plusieurs frontaux pour des questions d’HA

On va faire un compute de ringard, quitte à être dans la loose.

On pourrait mettre nginx dans cloudrun, mais c’est pas terrible pour plusieurs raisons:

  • les colds start du container apportent une latence qui peut être importante. On veut éviter au maximum les latences sur un reverse proxy
  • on veux charger une conf nginx avec un fichier énorme et marginal
  • un gros compute réduit la baisse de perf apportée par docker

meme superbus, j’ai des dockerfile, je compile en pagaille

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
FROM python:3.9-slim
ENV PYTHONUNBUFFERED True

RUN apt-get update && apt-get upgrade -y && apt-get install git -y
ENV APP_HOME /app
WORKDIR $APP_HOME
COPY . ./

RUN pip install --no-cache-dir -r requirements.txt
CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app

le code dans mon cloudrun

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import os

from flask import Flask
from google.cloud import storage

app = Flask(__name__)


@app.route("/")
def update_tor_address():

    bucket_name=os.getenv("BUCKET_NAME")
    if not bucket_name:
        print ("ERROR: env BUCKET_NAME is missing")
        return 500

    get_command_execution_result('git clone https://github.com/SecOps-Institute/Tor-IP-Addresses.git'.split())    

    with open ("Tor-IP-Addresses/tor-exit-nodes.lst")  as file:
        lines = file.readlines()
        ips = [line.rstrip() for line in lines]
        ips = check_ip(ips)
        template = template_shlagos(ips)
        with open('vhost', "w") as file:
            file.write(template)

    storage_client = storage.Client()
    bucket = storage_client.get_bucket(bucket_name)

    blob = bucket.blob("vhost")
    blob.upload_from_filename("vhost")
        
    return "200"


def get_command_execution_result(command):
    import subprocess
    result = subprocess.run(command, capture_output=True, text=True)
    return result.returncode, result.stdout, result.stderr


def check_ip(ips):

    import socket

    ok_ips=[]
    for addr in ips:
        try:
            socket.inet_aton(addr)
            ok_ips.append(addr)
        except socket.error:
            print (f'address {addr}, not validate by sock inet')
    return ok_ips

def template_shlagos(ips):
    template = """
server {
    listen 80;
    listen [::]:80;

    server_name example.com;

    root /var/www/example.com;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
        deny all;
        allow 127.0.0.1;
        allow %s;
    }
}
    """ % ";\n        allow ".join(ips)
    return template


if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))

Du coup on a recyclé le précèdent code pour faire un template plus simple et on balance tout ça dans un bucket gcs, l’astuce c’est que cloudrun a le droit de lire et d’écrire dans les buckets GCS du projet. On lui donne une variable d’env avec le nom du bucket et c’est parti.

On choisi le mode authenticated et seul cloud scheduler pourra le contacter pour lancer le scrap des nouvelles ips. J’ai laissé la conf par defaut nginx dans mon template mais ça s’adapte facilement, démerde toi 🤷

dernière étape, récupération de la conf dans mon compute nginx

Dernier truc un peu sale, on reload la conf en mode cron tous les jours dans le compute. On aurait pu mettre du Ansible directement dans le cloudrun et appliquer la conf à chaud au moment du scrap.

meme stromae, jamel, excuse moi de te déranger, mais tu serais pas en train de faire de la merde ?

Je ne le fais pas pour les raisons suivantes:

  • on veux éviter un fort couplage avec les computes
  • des conf en dur dans ansible avec les ips de machines
    • ça scale mal si on met plus de compute ou qu’on construit/détruit souvent

On garde une infra découplée des frontaux nginx et cloisonnement de responsabilité.

On pose un cron dans root avec ça dans le script. Il vous faut gcloud avec un SA qui a le droit de faire du gcs.

1
2
cp gs://{ton bucket}/vhost /etc/nginx/sites-enabled/
systemctl restart

et paf tu rentre pas

1
2
3
4
5
6
7
8
root@debian:/home/debian# curl 127.0.0.1:80
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.18.0</center>
</body>
</html>

Conclusion

Donc on a appris quoi aujourd’hui ?

  • les oignons c’est bon, mais faut en manger avec modération
  • cloud Armor est un outil super con, et super limité
  • l’important quand on chute c’est de tomber dans une poubelle pas trop loin
  • le darknet ce n’est pas que pour les gothiques, c’est aussi pour les robots
  • on peut faire des trucs complètement cons mais techniquement corrects 🤷
  • gcp est parfois décevant 😿