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
|
🦖