terraform-cloudflare-tunnel-zone-org
Bibliothèque d'exemples Cloudflare Zero Trust gérés par Terraform, construite autour d'un tunnel Cloudflare « home lab » sur la zone tips-of-mine.org.
Inspiré de macharpe/terraform-cloudflare-zero-trust-demo, adapté à une stack Vault (miroir OpenBao) + Authentik + Gitea Actions, et enrichi de briques supplémentaires.
📋 Avant tout apply : lire ACTIONS_EXTERNES.md — tout ce que Terraform ne peut pas faire (secrets Vault à créer, sshd à configurer, connecteurs à installer...) y est détaillé brique par brique.
🎯 Ce que cette plateforme fait
Le principe : Zero Trust au lieu d'un VPN
Dans un modèle classique, on ouvre des ports sur sa box ou on installe un VPN : une fois « dedans », on accède à tout. Ici, rien n'est exposé : aucun port ouvert, aucune IP publique ne pointe vers le réseau domestique. À la place :
- Un connecteur cloudflared établit un tunnel sortant vers le réseau Cloudflare — c'est lui qui appelle Cloudflare, jamais l'inverse ;
- Chaque application (Home Assistant, apps web, SSH...) est publiée sur un sous-domaine public (
home.tips-of-mine.org...), mais chaque requête doit d'abord prouver qui elle est (identité via Authentik/Azure/Google), depuis quoi (posture du poste : WARP actif, disque chiffré, pare-feu...) et d'où (pays, réseau) ; - Les postes de travail embarquent l'agent WARP : tout leur trafic passe par Gateway, qui filtre DNS, HTTP et réseau (publicités, malware, mouvements latéraux...) ;
- Plusieurs sites distants (agences d'entreprise) sont raccordés au même réseau privé virtuel, même avec des plans d'adressage identiques.
Vue d'ensemble
flowchart LR
subgraph POSTES["🖥️ Postes de travail"]
NAV["Navigateur"]
WARP["Agent WARP<br/>multi-OS"]
end
subgraph EDGE["☁️ Réseau Cloudflare"]
direction TB
ACCESS["🔐 Access<br/>authentification & politiques"]
GW["🛡️ Gateway<br/>filtrage DNS / HTTP / réseau"]
DENY["📄 Worker<br/>page de refus"]
end
subgraph IDP["🪪 Fournisseurs d'identité"]
AUTHENTIK["Authentik OIDC"]
AZUREAD["Azure Entra ID"]
GOOGLE["Google OAuth"]
end
subgraph HOME["🏠 Réseau domestique - 10.0.x.x"]
CFD["connecteur cloudflared"]
HA["Home Assistant"]
WEB["Apps web internes"]
SSHSRV["Serveur SSH"]
end
subgraph SITES["🏢 Sites distants d'entreprise"]
PARIS["Site Paris<br/>10.100.0.0/16"]
LYON["Site Lyon<br/>10.100.0.0/16<br/>même CIDR !"]
end
NAV -- "https://home.tips-of-mine.org" --> ACCESS
WARP -- "tout le trafic du poste" --> GW
ACCESS <-->|"vérification d'identité"| IDP
ACCESS -- "refus ➜ explication" --> DENY
ACCESS -- "tunnel (connexion sortante)" --> CFD
CFD --> HA & WEB & SSHSRV
GW -- "réseau privé virtuel" --> SITES
PARIS -."vnet-site-paris".- GW
LYON -."vnet-site-lyon".- GW
Point clé : les flèches vers le réseau domestique et les sites passent par des tunnels initiés depuis l'intérieur. Couper cloudflared = plus aucun accès possible, il n'existe pas de « porte » à attaquer.
Parcours d'un accès utilisateur
Ce qui se passe entre « je tape l'URL » et « je vois l'application » :
sequenceDiagram
autonumber
actor U as Utilisateur
participant CF as Edge Cloudflare
participant AC as Access
participant IdP as Authentik (ou Azure/Google)
participant T as cloudflared (tunnel)
participant App as Home Assistant
U->>CF: https://home.tips-of-mine.org
CF->>AC: session Access valide ?
AC-->>U: page de login (choix de l'IdP)
U->>IdP: authentification + MFA
IdP-->>AC: identité confirmée (OIDC)
Note over AC: Évaluation des politiques :<br/>groupe autorisé ? posture du poste OK ?<br/>pays autorisé ? MFA (pas SMS) ?
alt Accès autorisé
AC-->>U: cookie de session (durée limitée)
CF->>T: requête transmise dans le tunnel
T->>App: http://10.0.4.135:8123 (réseau local)
App-->>U: l'application s'affiche
else Accès refusé
AC-->>U: redirection ➜ denied.tips-of-mine.org<br/>(page Worker : raison + contact)
end
À noter : l'application d'origine (Home Assistant...) ne voit jamais de trafic non authentifié — les scans, bots et tentatives de force brute meurent sur l'edge Cloudflare, avant le tunnel.
Cycle de vie d'un poste de travail (agent WARP)
flowchart TB
A["1 - Installation du client WARP<br/>(manuelle ou MDM)"] --> B["2 - Login with Cloudflare Zero Trust<br/>équipe : tips-of-mine"]
B --> C{"Politique d'enrôlement :<br/>email du domaine ?<br/>MFA validée ?"}
C -- non --> X["❌ Poste refusé"]
C -- oui --> D["3 - Poste enrôlé dans l'organisation"]
D --> E["Profil appareil appliqué selon l'OS<br/>(Windows / macOS / Linux)"]
D --> F["Vérifications de posture en continu :<br/>WARP actif · pare-feu · disque chiffré · OS à jour"]
D --> G["Tout le trafic passe par Gateway :<br/>filtrage DNS (pubs, malware)<br/>filtrage HTTP (catégories, IA, DLP)<br/>filtrage réseau (anti-mouvement latéral)"]
D --> H["Tests DEX : mesure de latence<br/>vers les apps depuis le poste"]
F -- "posture KO" --> I["⛔ Les applications exigeant<br/>la posture deviennent inaccessibles"]
La posture n'est pas vérifiée qu'à la connexion : un poste qui désactive son pare-feu perd l'accès aux applications qui l'exigent, même avec une session valide.
Sites distants : un réseau d'entreprise multi-agences
Le scénario simulé : une entreprise dont les agences ont été déployées sur le même template réseau (ou issues d'une fusion) — donc des plages IP identiques, normalement impossibles à interconnecter :
flowchart TB
subgraph CF["☁️ Réseau privé virtuel Cloudflare"]
VNP["vnet-site-paris"]
VNL["vnet-site-lyon"]
end
subgraph P["🏢 Site Paris - siège"]
CP["cloudflared"] --- LANP["LAN 10.100.0.0/16<br/>serveurs, imprimantes, AD"]
end
subgraph L["🏢 Site Lyon - agence"]
CL["cloudflared"] --- LANL["LAN 10.100.0.0/16<br/>⚠️ même plage que Paris"]
end
U["💻 Poste WARP<br/>warp-cli vnet ..."]
CP -- "tunnel sortant" --> VNP
CL -- "tunnel sortant" --> VNL
U -- "vnet actif = paris ➜ joint Paris" --> VNP
U -. "vnet actif = lyon ➜ joint Lyon" .-> VNL
Chaque site vit dans son réseau virtuel : les deux 10.100.0.0/16 cohabitent sans conflit, et le poste WARP choisit à quel site « se téléporter » en changeant de réseau virtuel actif. La variante WARP Connector (bidirectionnelle : le site peut aussi initier des connexions vers le reste du réseau) est prête dans la brique correspondante.
Accès SSH : deux philosophies comparées
Le repo implémente volontairement les deux pour montrer la différence :
flowchart LR
subgraph MODERNE["✅ Certificats éphémères (recommandé)"]
direction LR
U1["Utilisateur"] -- "1 - auth Access<br/>(IdP + MFA + posture)" --> CA["CA Cloudflare"]
CA -- "2 - certificat SSH<br/>valable quelques secondes" --> U1
U1 -- "3 - connexion SSH" --> S1["sshd<br/>TrustedUserCAKeys"]
end
subgraph CLASSIQUE["⚠️ Clé SSH classique (pédagogique)"]
direction LR
U2["Utilisateur"] -- "clé privée permanente<br/>(stockée, à faire tourner)" --> S2["sshd<br/>authorized_keys"]
end
| Certificats éphémères | Clé classique | |
|---|---|---|
| Durée de vie du secret | quelques secondes | des années |
| Vol de la clé | inutilisable ensuite | compromission totale |
| Rotation | automatique | manuelle, souvent oubliée |
| Révocation d'un utilisateur | retirer du groupe IdP | retirer la clé de chaque serveur |
| Traçabilité | chaque certificat est nominatif | une clé peut être partagée |
Le pipeline de filtrage Gateway
Chaque requête d'un poste WARP traverse les politiques dans cet ordre (la précédence numérique fait foi) :
flowchart LR
REQ["Requête du poste"] --> DNS["1 - Politiques DNS<br/>· blocage malware<br/>· liste pubs Pi-hole<br/>(mise à jour quotidienne)"]
DNS --> L4["2 - Politiques réseau L4<br/>· anti-mouvement latéral<br/>SSH RDP SMB WinRM DB<br/>· contrôle RDP par identité"]
L4 --> DNI["3 - Do Not Inspect<br/>banque & santé :<br/>jamais déchiffrés"]
DNI --> HTTP["4 - Politiques HTTP<br/>· gouvernance IA<br/>· blocage catégories<br/>· DLP (IBAN)<br/>· isolation RBI"]
HTTP --> OK["✅ Autorisé"]
DNS -- "domaine bloqué" --> BLOCK["⛔ Page de blocage"]
L4 -- "latéral détecté" --> BLOCK
HTTP -- "contenu interdit" --> BLOCK
L'anti-mouvement latéral mérite une explication : même à l'intérieur du réseau, une machine compromise ne peut pas rebondir en SSH/RDP/SMB vers ses voisines — seuls les clients WARP (identifiés, conformes) le peuvent. C'est le « Zero Trust » appliqué au trafic est-ouest, pas seulement à l'entrée.
Accès machine-to-machine
Les robots (CI, supervision) n'ont pas d'IdP : ils présentent un service token (paire ID/secret dans deux en-têtes HTTP) évalué par une politique dédiée non_identity. Même modèle que les humains — identité vérifiable, durée limitée, révocable — mais sans navigateur.
Les briques
Les fichiers suivent la navigation du dashboard Cloudflare Zero Trust (Section-SousSection-Nom.tf).
Cœur — tunnel & applications
| Fichier | Contenu |
|---|---|
main.tf |
Secrets Vault, locals, construction des règles d'ingress |
Networks-Connectors-Cloudflare_Tunnels.tf |
Tunnels cloudflared (home + clouds) |
Networks-Routes-cidr.tf |
Routes réseau, DNS des applications, config d'ingress du tunnel |
Networks-Routes-Virtual_Networks.tf |
Réseaux virtuels par cloud |
variables.auto.tfvars → applications |
La map des applications exposées : ajouter une entrée = DNS + ingress créés au prochain apply |
Identité — trois briques IdP interchangeables
| Brique | Fichiers | Activation |
|---|---|---|
| Authentik (OIDC) | Integrations-Identity_providers.tf, Access_Controls-Policies-Rule_Groups.tf |
toujours active |
| Azure Entra ID | Integrations-Identity_providers.tf, Access_Controls-Policies-Rule_Groups-Azure.tf |
azure_idp_enabled = true + secret Vault secret/azure |
| Google (OAuth) | Integrations-Identity_providers.tf |
google_idp_enabled = true + secret Vault secret/google |
Toutes alimentent les mêmes concepts : rule groups, politiques réutilisables, enrôlement WARP.
Accès SSH — deux approches comparées
| Brique | Fichier | Principe |
|---|---|---|
| Certificats éphémères | Access_Controls-Service_Auth-Short_Lived_Certificates.tf + apps Infrastructure/ssh |
Cloudflare signe un certificat SSH de quelques secondes à chaque connexion — aucune clé à distribuer |
| Clé SSH classique | Access_Controls-Applications-ssh_key_based.tf |
Paire de clés ED25519 générée par Terraform, publiée via le tunnel — montre les limites (clé dans le state, rotation manuelle) |
Agent WARP (postes de travail)
| Fichier | Contenu |
|---|---|
Team_Resources-Devices-Enrollment_Permissions.tf |
Qui peut enrôler son poste (app type warp, MFA, domaine mail) |
Team_Resources-Devices-Device_posture.tf |
Postures gérées par Terraform : WARP actif, pare-feu, disque chiffré, version d'OS |
Team_Resources-Devices-Device_profiles.tf |
Profils WARP par OS |
Sites distants d'entreprise
| Brique | Fichier | Principe |
|---|---|---|
| Tunnels par site | Networks-Routes-Remote_Sites.tf |
Un cloudflared + un réseau virtuel par agence — CIDR identiques entre sites (cas réel post-fusion), isolés par réseau virtuel |
| WARP Connector | Networks-Connectors-WARP_Connector.tf |
Connectivité bidirectionnelle site↔site (création manuelle du connecteur, tokens récupérés par Terraform) |
Politiques de trafic (Gateway)
| Fichier | Contenu |
|---|---|
Traffic_Policies-Firewall_Policies-DNS.tf |
Blocage malware + liste publicitaire Pi-hole (mise à jour quotidienne par action-update-lists.yml) |
Traffic_Policies-Firewall_Policies-HTTP.tf |
Gouvernance IA, filtrage contenu, RBI (webmail isolé, sous licence) |
Traffic_Policies-Firewall_Policies-Network.tf |
Anti-mouvement latéral (SSH/RDP/SMB/WinRM/DB), contrôle RDP |
Traffic_Policies-Firewall_Policies-DLP.tf |
Profil DLP IBAN + politique de blocage (sous licence) |
Observabilité & M2M
| Fichier | Contenu |
|---|---|
Insights-Digital_Experience-Tests.tf |
Tests DEX (HTTP + traceroute) exécutés par les postes WARP (dex_enabled) |
Insights-Logs-Logpush.tf |
Export des logs Gateway vers S3/R2/SIEM (logpush_enabled) |
Access_Controls-Service_Auth-Service_Tokens.tf |
Accès machine-to-machine (CI) par service token |
Access_Controls-Service_Auth-mTLS_Certificates.tf |
Certificat client exigé en plus de l'identité (mtls_enabled) |
Workers-Deny_Page.tf + workers/deny-page.js |
Page de refus d'accès personnalisée sur denied.tips-of-mine.org |
Prérequis
- Terraform >= 1.8.0 (CI : 1.15.8 — les cores 1.7.x crashent avec le provider Cloudflare v5 récent) — https://developer.hashicorp.com/terraform/install
- Un Vault (ou OpenBao) joignable avec les secrets décrits dans ACTIONS_EXTERNES.md
- Un bucket S3 pour le state (
state.tf)
Token API Cloudflare (stocké dans Vault : secret/cloudflare, clé api_token)
| Type | Item | Permission | Mandatory |
|---|---|---|---|
Account |
Cloudflare Tunnel |
Edit | Required |
Account |
Zero Trust |
Edit | Required |
Account |
Access: Apps and Policies |
Edit | Required |
Account |
Access: Service Tokens |
Edit | Required |
Account |
Digital Experience Monitoring |
Edit | For dex_enabled = true |
Account |
Zero Trust: DLP |
Edit | For dlp_enabled = true (+ abonnement DLP) |
Account |
Zero Trust: PII |
Read | Recommended |
Account |
Account Settings |
Read | Recommended |
Zone |
DNS |
Edit | Required |
Zone |
Zone |
Read | Recommended |
Démarrage manuel
git clone https://gitea.tips-of-mine.com/terraform-cloudflare-tunnel-zone-org.git
cd terraform-cloudflare-tunnel-zone-org
Utilisation du repository
init
terraform init
validation
terraform validate
plan (le token Vault est demandé, ou via TF_VAR_vault_token)
terraform plan
CI/CD (Gitea Actions)
| Workflow | Déclencheur | Rôle |
|---|---|---|
validate.yml |
pull request | fmt + validate + terraform test + tflint/trivy + plan |
cicd.yml |
push sur main |
plan + apply |
drift.yml |
cron quotidien 16h00 | détection de dérive (plan seul, échoue si l'infra a divergé du code) |
action-update-lists.yml |
cron quotidien 14h00 | met à jour la liste Pi-hole et ouvre une PR |
Secrets Gitea nécessaires : AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, et pour Vault : VAULT_ROLE_ID + VAULT_SECRET_ID (AppRole, recommandé) ou VAULT_TOKEN (token statique). Le state S3 est verrouillé nativement (use_lockfile).