Post

Infra as code : premiers pas

Infra as code : premiers pas

Ce lab est un premier pas vers l’Infrastructure as code. J’ai encore énormément de choses à apprendre pour fluidifier les déploiements, et améliorer la sécurité. Voici la documentation de ce premier lab.


1. Architecture et Sécurisation du Serveur (Hardening)

Sécurisation du pare-feu Cloud

La première ligne de défense filtre le trafic avant même d’atteindre le VPS.

  • Ports autorisés : TCP 22 (SSH), TCP 80 (HTTP), TCP 443 (HTTPS), UDP 5060 (SIP), UDP 10000-20000 (RTP).
  • Durcissement : Suppression de tous les autres ports (réduction de la surface d’attaque).

Mise à jour et Utilisateur

1
2
3
4
sudo apt update && sudo apt upgrade -y
adduser user
usermod -aG sudo user
su - user

Authentification SSH

Déploiement d’une clé Ed25519 gérée via un agent SSH sécurisé.

1
2
3
4
5
mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/authorized_keys
# Coller la clé publique
chmod 600 ~/.ssh/authorized_keys

Durcissement SSH (/etc/ssh/sshd_config)

Attention : si vous utilisez Cloud-Init, il peut être nécessaire de désactiver ses inclusions pour forcer l’usage exclusif des clés.

1
2
3
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no

Pare-feu Local (UFW)

1
2
3
4
5
6
7
8
9
sudo apt install ufw -y
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 5060/udp
sudo ufw allow 10000:20000/udp
sudo ufw enable

2. Écosystème Docker et Réseau

1
2
3
4
5
sudo usermod -aG docker user
sudo chown root:docker /var/run/docker.sock
sudo chmod 660 /var/run/docker.sock
docker network create proxy
docker network create secret

3. Déploiement Traefik v3.6 (Reverse Proxy)

Traefik a deux pattes réseau; une pour l’exposition, et une pour l’administration. Le dashboard est accessible uniquement depuis un réseau privé, en https, autosigné. J’ai choisi tailscale. J’ai également défini une configuration statique, définissant les options dont j’ai besoin (distribution de certificats, HTTPS, plugin pour appsec, provider pour la configuration dynamique). En isolant les middlewares dans des fichiers YAML indépendants, on rend l’infrastructure modulaire. On ne configure plus la sécurité service par service, mais par politiques globales que l’on applique à la demande.

Docker compose de traefik

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
services:
  traefik:
    image: traefik:v3.6
    container_name: traefik
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    networks:
      - proxy
      - secret
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ${PATH_LAB}/traefik/data/acme.json:/acme.json
      - ${PATH_LAB}/traefik/data/logs:/var/log/traefik
      - ${PATH_LAB}/traefik/data/middlewares:/etc/traefik/dynamic_conf:ro
      - ${PATH_LAB}/traefik/data/traefik.yml:/etc/traefik/traefik.yml:ro
    command:
      - --configFile=/etc/traefik/traefik.yml
    labels:
      - traefik.enable=true

      # --- ROUTAGE PRIVÉ (VPN TAILSCALE) ---
      - "traefik.http.routers.traefik.rule=Host(`${VPN_IP}`) || Host(`${VPN_DOMAIN}`)"
      - "traefik.http.routers.traefik.entrypoints=websecure"
      - "traefik.http.routers.traefik.service=api@internal"
      
      # TLS activé sans resolver pour forcer l'auto-signé en privé
      - "traefik.http.routers.traefik.tls=true"
      # Middlewares
      - "traefik.http.routers.traefik.middlewares=tailscale-only@file,crowdsec-waf@file,auth-dashboard"
      - "traefik.http.middlewares.auth-dashboard.basicauth.users=${DASHBOARD_USER}:${DASHBOARD_PASSWORD_HASH}"

networks:
  proxy:
    external: true
  secret:
    external: true

Config Statique (traefik.yml)

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
api:
  dashboard: true
  insecure: false 

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https

  websecure:
    address: ":443"
    forwardedHeaders:
      trustedIPs:
        - "127.0.0.1/32"
        - "172.16.0.0/12"
        - "172.18.0.0/16"
        - "100.64.0.0/10" 
        - "${VPS_IP}/32"   
    http:
      tls: {}

providers:
  docker:
    exposedByDefault: false
  file:
    directory: /etc/traefik/dynamic_conf
    watch: true

experimental:
  plugins:
    bouncer:
      moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
      version: "v1.5.1"

certificatesResolvers:
  mon-solveur:
    acme:
      email: ${ACME_EMAIL}
      storage: /acme.json
      httpChallenge:
        entryPoint: web

log:
  level: INFO
accessLog:
  filePath: /var/log/traefik/access.log

Middleware AppSec (dynamic_conf/crowdsec-waf.yml)

1
2
3
4
5
6
7
8
9
10
11
12
13
http:
  middlewares:
    crowdsec-waf:
      plugin:
        bouncer:
          enabled: true
          crowdsecLapiHost: "crowdsec:8080"
          crowdsecLapiKey: "VOTRE_API_KEY"
          crowdsecMode: "live"
          crowdsecAppsecEnabled: true
          crowdsecAppsecHost: "crowdsec:7422"
          appsecConfig: "custom-rules"
          crowdsecAppsecFailureBlock: true

Middleware Tailscale

1
2
3
4
5
6
7
8
http:
  middlewares:
    tailscale-only:
      ipAllowList:
        sourceRange:
          - "100.64.0.0/10"
          - "172.16.0.0/12" 
          - "127.0.0.1/32"

4. Sécurité Active & suppervision : CrowdSec/AppSec/Kuma

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
services:
  crowdsec:
    image: crowdsecurity/crowdsec:latest
    container_name: crowdsec
    restart: unless-stopped
    networks:
      - secret  
    environment:
      - GID=1000
      - COLLECTIONS=crowdsecurity/traefik crowdsecurity/sshd crowdsecurity/linux crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules
      - BOUNCER_KEY_TRAEFIK=${CROWDSEC_BOUNCER_KEY}
    volumes:
      - /votre/chemin/crowdsec/config:/etc/crowdsec
      - /votre/chemin/crowdsec/data:/var/lib/crowdsec/data
      - /votre/chemin/traefik/logs:/var/log/traefik:ro

networks:
  secret:
    external: true

Acquisition (acquis.yaml)

1
2
3
4
5
6
7
8
9
listen_addr: 0.0.0.0:7422
appsec_config: custom-rules
source: appsec
labels:
  type: appsec
---
filename: /etc/crowdsec/traefik_logs/access.log
labels:
  type: traefik

Custom Rules (appsec-configs/custom-rules.yaml)

1
2
3
4
5
6
name: custom-rules
default_remediation: ban
inband_rules:
  - crowdsecurity/base-config
  - crowdsecurity/vpatch-*
  - crowdsecurity/crs

KUMA

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
services:
  uptime-kuma:
    image: louislam/uptime-kuma:1
    container_name: uptime-kuma
    restart: unless-stopped
    volumes:
      - ./data:/app/data
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - secret  # Isolation dans le réseau d'administration
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:3001/ || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 5
    labels:
      - "traefik.enable=true"

      # --- ROUTAGE PRIVÉ UNIQUEMENT (tailscale) ---
      - "traefik.http.routers.kuma.rule=Host(`${VPN_MACHINE_NAME}`) || Host(`${VPN_STATUS_DOMAIN}`) || Host(`${VPN_STATUS_DOMAIN}.`)"
      - "traefik.http.routers.kuma.entrypoints=websecure"
      - "traefik.http.routers.kuma.tls=true"

      # --- middlewares ---
      - "traefik.http.routers.kuma.middlewares=tailscale-only@file,crowdsec-waf@file"
      
      # --- CONFIGURATION RÉSEAU INTERNE ---
      - "traefik.docker.network=secret"
      - "traefik.http.services.kuma.loadbalancer.server.port=3001"

networks:
  secret:
    external: true

5. Applications exposées

En parallèle de la sécurité et des outils de supervision qui sont isolés, j’ai également des applications exposées. Ici, j’ai choisi d’exposer peppermint. Je peux être amené à consulter ce service depuis n’importe quel réseau. La base de données est isolée et ne communique qu’avec le service, pour assurer sa sécurité au maximum.

Peppermint

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
services:
  peppermint_db:
    container_name: peppermint_postgres
    image: postgres:14-alpine
    restart: always
    networks:
      - peppermint-internal  
    volumes:
      - ./data/pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: peppermint
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
      interval: 5s
      timeout: 5s
      retries: 5

  peppermint:
    container_name: peppermint
    image: pepperlabs/peppermint:latest
    restart: always
    depends_on:
      peppermint_db:
        condition: service_healthy
    networks:
      - proxy                
      - peppermint-internal  
    environment:
      - DB_USERNAME=${DB_USER}
      - DB_PASSWORD=${DB_PASSWORD}
      - DB_HOST=peppermint_postgres
      - SECRET=${PEPPERMINT_SECRET}
      - BASE_URL=https://${DOMAIN_HELPDESK}
      - NEXTAUTH_URL=https://${DOMAIN_HELPDESK}
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.peppermint.rule=Host(`${DOMAIN_HELPDESK}`)"
      - "traefik.http.routers.peppermint.entrypoints=websecure"
      - "traefik.http.routers.peppermint.tls=true"
      - "traefik.http.routers.peppermint.tls.certresolver=mon-solveur"
      - "traefik.http.routers.peppermint.middlewares=crowdsec-waf@file"
      - "traefik.http.services.peppermint.loadbalancer.server.port=3000"
      - "traefik.docker.network=proxy"

networks:
  proxy:
    external: true
  peppermint-internal: 
    driver: bridge

6. Téléphonie IP (Asterisk)

Configuré en network_mode: host pour la gestion directe des flux RTP et éviter les problématiques de NAT.

Table de Résolution SIP

Composant Problématique Solution
Réseau Externe Pare-feu Cloud/Host Ouverture 5060/UDP & 10000-20000/UDP
Réseau Interne NAT Docker Bridge Passage en network_mode: host
Protocole SIP ICE / DTLS Désactivation explicite (pjsip.conf)
Codec Confusion de flux Forçage en allow=alaw

7. Sauvegardes (Rclone)

Script automatisé pour l’archivage vers un stockage cloud distant.

1
2
3
4
5
6
7
8
9
#!/bin/bash
SOURCE_DIR="/home/user/lab"
DEST_REMOTE="remote:backup_folder"
DATE=$(date +%Y-%m-%d)
FILENAME="backup_lab_$DATE.tar.gz"

tar --warning=no-file-changed -czf /tmp/$FILENAME -C /home/user lab
rclone copy /tmp/$FILENAME $DEST_REMOTE -P
rm /tmp/$FILENAME

🦖