This commit is contained in:
@@ -98,7 +98,7 @@ resource "cloudflare_zero_trust_access_application" "cloudflare_aws_app_ssh_infr
|
||||
require = [
|
||||
{
|
||||
device_posture = {
|
||||
integration_uid = var.cloudflare_gateway_posture_id
|
||||
integration_uid = cloudflare_zero_trust_device_posture_rule.gateway_connected.id
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -210,7 +210,7 @@ resource "cloudflare_zero_trust_access_application" "cloudflare_gcp_app_ssh_infr
|
||||
require = [
|
||||
{
|
||||
device_posture = {
|
||||
integration_uid = var.cloudflare_gateway_posture_id
|
||||
integration_uid = cloudflare_zero_trust_device_posture_rule.gateway_connected.id
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -24,10 +24,10 @@ resource "cloudflare_zero_trust_access_application" "cloudflare_aws_app_ssh_brow
|
||||
uri = var.cloudflare_aws_subdomain_ssh
|
||||
}]
|
||||
|
||||
allowed_idps = [
|
||||
cloudflare_zero_trust_access_identity_provider.gmail.id,
|
||||
# Google proposé seulement si la brique IdP Google est activée
|
||||
allowed_idps = concat(local.google_idp_ids, [
|
||||
cloudflare_zero_trust_access_identity_provider.authentik_oidc.id,
|
||||
]
|
||||
])
|
||||
auto_redirect_to_identity = false
|
||||
allow_authenticate_via_warp = false
|
||||
|
||||
@@ -63,10 +63,10 @@ resource "cloudflare_zero_trust_access_application" "cloudflare_gcp_app_ssh_brow
|
||||
uri = var.cloudflare_gcp_subdomain_ssh
|
||||
}]
|
||||
|
||||
allowed_idps = [
|
||||
cloudflare_zero_trust_access_identity_provider.gmail.id,
|
||||
# Google proposé seulement si la brique IdP Google est activée
|
||||
allowed_idps = concat(local.google_idp_ids, [
|
||||
cloudflare_zero_trust_access_identity_provider.authentik_oidc.id,
|
||||
]
|
||||
])
|
||||
auto_redirect_to_identity = false
|
||||
allow_authenticate_via_warp = false
|
||||
|
||||
|
||||
@@ -24,10 +24,10 @@ resource "cloudflare_zero_trust_access_application" "cloudflare_aws_app_vnc_brow
|
||||
uri = var.cloudflare_aws_subdomain_vnc
|
||||
}]
|
||||
|
||||
allowed_idps = [
|
||||
cloudflare_zero_trust_access_identity_provider.gmail.id,
|
||||
# Google proposé seulement si la brique IdP Google est activée
|
||||
allowed_idps = concat(local.google_idp_ids, [
|
||||
cloudflare_zero_trust_access_identity_provider.authentik_oidc.id,
|
||||
]
|
||||
])
|
||||
auto_redirect_to_identity = false
|
||||
allow_authenticate_via_warp = false
|
||||
|
||||
@@ -58,10 +58,10 @@ resource "cloudflare_zero_trust_access_application" "cloudflare_gcp_app_vnc_brow
|
||||
uri = var.cloudflare_gcp_subdomain_vnc
|
||||
}]
|
||||
|
||||
allowed_idps = [
|
||||
cloudflare_zero_trust_access_identity_provider.gmail.id,
|
||||
# Google proposé seulement si la brique IdP Google est activée
|
||||
allowed_idps = concat(local.google_idp_ids, [
|
||||
cloudflare_zero_trust_access_identity_provider.authentik_oidc.id,
|
||||
]
|
||||
])
|
||||
auto_redirect_to_identity = false
|
||||
allow_authenticate_via_warp = false
|
||||
|
||||
|
||||
@@ -100,11 +100,13 @@ locals {
|
||||
(contains(local.other_countries, code) ? null : code)
|
||||
])
|
||||
|
||||
# OS posture checks
|
||||
# OS posture checks — MIGRÉ : ces règles sont désormais des ressources
|
||||
# Terraform (Team_Resources-Devices-Device_posture.tf), plus aucun ID
|
||||
# à recopier depuis le dashboard.
|
||||
os_posture_checks = [
|
||||
var.cloudflare_linux_posture_id,
|
||||
var.cloudflare_macos_posture_id,
|
||||
var.cloudflare_windows_posture_id
|
||||
cloudflare_zero_trust_device_posture_rule.linux_kernel_version.id,
|
||||
cloudflare_zero_trust_device_posture_rule.macos_os_version.id,
|
||||
cloudflare_zero_trust_device_posture_rule.windows_os_version.id,
|
||||
]
|
||||
|
||||
policy_groups = {
|
||||
@@ -595,10 +597,11 @@ resource "cloudflare_zero_trust_access_policy" "policies" {
|
||||
|
||||
# Require conditions
|
||||
require = concat(
|
||||
# Device posture (always required if specified)
|
||||
# Device posture (always required if specified) — MIGRÉ : la posture
|
||||
# "connecté via Gateway" est gérée par Terraform, plus d'ID manuel
|
||||
try(each.value.require_posture, false) ? [{
|
||||
device_posture = {
|
||||
integration_uid = var.cloudflare_gateway_posture_id
|
||||
integration_uid = cloudflare_zero_trust_device_posture_rule.gateway_connected.id
|
||||
}
|
||||
}] : [],
|
||||
# MFA requirement
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# =============================================================================
|
||||
# CLOUDFLARE : Access Controls : Service Auth : mTLS Certificates
|
||||
# =============================================================================
|
||||
# BRIQUE mTLS : en plus de l'identité (IdP), exiger un CERTIFICAT CLIENT
|
||||
# installé sur le poste pour accéder aux applications sensibles. Même un
|
||||
# compte compromis ne suffit plus : il faut aussi la machine.
|
||||
#
|
||||
# 1. Une autorité de certification (CA) est déposée dans Vault (secret/mtls,
|
||||
# clé ca_pem — le certificat PUBLIC de la CA, pas sa clé privée !)
|
||||
# 2. Cloudflare vérifie que le client présente un certificat signé par
|
||||
# cette CA sur les hostnames associés
|
||||
# 3. La politique réutilisable ci-dessous ("Allow valid client certificate")
|
||||
# peut être ajoutée aux applications à durcir
|
||||
#
|
||||
# La brique est inerte tant que mtls_enabled = false.
|
||||
# ACTIONS EXTERNES (voir ACTIONS_EXTERNES.md) : générer la CA, distribuer
|
||||
# les certificats clients sur les postes, pousser le PEM dans Vault.
|
||||
# =============================================================================
|
||||
|
||||
data "vault_generic_secret" "mtls" {
|
||||
count = var.mtls_enabled ? 1 : 0
|
||||
path = var.vault_mtls_path
|
||||
}
|
||||
|
||||
#======================================================
|
||||
# La CA de confiance, associée aux hostnames à protéger
|
||||
#======================================================
|
||||
resource "cloudflare_zero_trust_access_mtls_certificate" "home_ca" {
|
||||
count = var.mtls_enabled ? 1 : 0
|
||||
account_id = local.cloudflare_account_id
|
||||
|
||||
name = "Home mTLS CA"
|
||||
certificate = data.vault_generic_secret.mtls[0].data["ca_pem"]
|
||||
|
||||
associated_hostnames = [
|
||||
"home.${var.cloudflare_zone}",
|
||||
]
|
||||
}
|
||||
|
||||
#======================================================
|
||||
# Politique réutilisable : certificat client valide exigé
|
||||
#======================================================
|
||||
resource "cloudflare_zero_trust_access_policy" "allow_valid_client_certificate" {
|
||||
count = var.mtls_enabled ? 1 : 0
|
||||
account_id = local.cloudflare_account_id
|
||||
|
||||
name = "Allow valid client certificate"
|
||||
decision = "non_identity"
|
||||
session_duration = "24h"
|
||||
|
||||
include = [{
|
||||
certificate = {}
|
||||
}]
|
||||
}
|
||||
@@ -2,16 +2,29 @@
|
||||
# CLOUDFLARE : Integrations : Identity Providers
|
||||
# =============================================================================
|
||||
|
||||
#======================================================
|
||||
# BRIQUE IdP n°3 : Google (OAuth)
|
||||
# Secrets lus depuis Vault : secret/google
|
||||
# - client_id_cloudflare : Client ID OAuth (Google Cloud Console)
|
||||
# - secret_cloudflare : Client secret OAuth
|
||||
#
|
||||
# CORRECTIF : l'ancienne version contenait des placeholders en dur
|
||||
# ("<google_client_id>") -> le bouton Google de la page de login échouait
|
||||
# systématiquement. Les identifiants viennent désormais de Vault, et la
|
||||
# brique est inerte (bouton absent) tant que google_idp_enabled = false.
|
||||
# ACTIONS EXTERNES : créer le client OAuth Google + le secret Vault,
|
||||
# voir ACTIONS_EXTERNES.md.
|
||||
#======================================================
|
||||
resource "cloudflare_zero_trust_access_identity_provider" "gmail" {
|
||||
count = var.google_idp_enabled ? 1 : 0
|
||||
|
||||
account_id = local.cloudflare_account_id
|
||||
name = "Gmail"
|
||||
type = "google"
|
||||
zone_id = local.cloudflare_zone_id
|
||||
|
||||
config = {
|
||||
client_id = "<google_client_id>"
|
||||
client_secret = "<google_secret>"
|
||||
client_id = local.google_oidc_client_id_cloudflare
|
||||
client_secret = local.google_oidc_secret_cloudflare
|
||||
email_claim_name = "email"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,195 @@ Inspiré de [macharpe/terraform-cloudflare-zero-trust-demo](https://github.com/m
|
||||
|
||||
> 📋 **Avant tout apply : lire [ACTIONS_EXTERNES.md](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
|
||||
|
||||
```mermaid
|
||||
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 » :
|
||||
|
||||
```mermaid
|
||||
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)
|
||||
|
||||
```mermaid
|
||||
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 :
|
||||
|
||||
```mermaid
|
||||
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 :
|
||||
|
||||
```mermaid
|
||||
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) :
|
||||
|
||||
```mermaid
|
||||
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`).
|
||||
@@ -20,14 +209,15 @@ Les fichiers suivent la navigation du dashboard Cloudflare Zero Trust (`Section-
|
||||
| `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é — deux briques IdP interchangeables
|
||||
## 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` |
|
||||
|
||||
Les deux alimentent les mêmes concepts : rule groups, politiques réutilisables, enrôlement WARP.
|
||||
Toutes alimentent les mêmes concepts : rule groups, politiques réutilisables, enrôlement WARP.
|
||||
|
||||
## Accès SSH — deux approches comparées
|
||||
|
||||
@@ -64,9 +254,11 @@ Les deux alimentent les mêmes concepts : rule groups, politiques réutilisables
|
||||
|
||||
| Fichier | Contenu |
|
||||
|---|---|
|
||||
| `Insights-Digital_Experience-Tests.tf` | Tests DEX (HTTP + traceroute) exécutés par les postes WARP |
|
||||
| `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
|
||||
|
||||
@@ -117,11 +309,12 @@ terraform plan
|
||||
|
||||
| Workflow | Déclencheur | Rôle |
|
||||
|---|---|---|
|
||||
| `validate.yml` | pull request | fmt + validate + **plan** |
|
||||
| `validate.yml` | pull request | fmt + validate + **terraform test** + tflint/trivy + **plan** |
|
||||
| `cicd.yml` | push sur `main` | plan + **apply** |
|
||||
| `action-update-lists.yml` | cron quotidien | met à jour la liste Pi-hole et ouvre une PR |
|
||||
| `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 : `VAULT_TOKEN`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`.
|
||||
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`).
|
||||
|
||||
# Buy me a coffe
|
||||
<a href='https://ko-fi.com/R5R2KNI3N' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi4.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
|
||||
|
||||
@@ -73,6 +73,24 @@ resource "cloudflare_zero_trust_device_posture_rule" "disk_encrypted" {
|
||||
}
|
||||
}
|
||||
|
||||
#======================================================
|
||||
# Le trafic du poste passe par Gateway
|
||||
# (remplace l'ancien ID manuel cloudflare_gateway_posture_id)
|
||||
#======================================================
|
||||
resource "cloudflare_zero_trust_device_posture_rule" "gateway_connected" {
|
||||
account_id = local.cloudflare_account_id
|
||||
|
||||
name = "TF - Connecte via Gateway"
|
||||
description = "Le trafic du poste transite par Cloudflare Gateway"
|
||||
type = "gateway"
|
||||
|
||||
match = [
|
||||
{ platform = "windows" },
|
||||
{ platform = "mac" },
|
||||
{ platform = "linux" },
|
||||
]
|
||||
}
|
||||
|
||||
#======================================================
|
||||
# Version d'OS minimale (exemple Windows 11 23H2)
|
||||
#======================================================
|
||||
@@ -94,6 +112,50 @@ resource "cloudflare_zero_trust_device_posture_rule" "windows_os_version" {
|
||||
}
|
||||
}
|
||||
|
||||
#======================================================
|
||||
# Version d'OS minimale macOS
|
||||
# (remplace l'ancien ID manuel cloudflare_macos_posture_id)
|
||||
#======================================================
|
||||
resource "cloudflare_zero_trust_device_posture_rule" "macos_os_version" {
|
||||
account_id = local.cloudflare_account_id
|
||||
|
||||
name = "TF - macOS a jour"
|
||||
description = "Version minimale de macOS exigee"
|
||||
type = "os_version"
|
||||
schedule = "5m"
|
||||
|
||||
match = [
|
||||
{ platform = "mac" },
|
||||
]
|
||||
|
||||
input = {
|
||||
version = "14.0.0"
|
||||
operator = ">="
|
||||
}
|
||||
}
|
||||
|
||||
#======================================================
|
||||
# Version minimale du noyau Linux
|
||||
# (remplace l'ancien ID manuel cloudflare_linux_posture_id)
|
||||
#======================================================
|
||||
resource "cloudflare_zero_trust_device_posture_rule" "linux_kernel_version" {
|
||||
account_id = local.cloudflare_account_id
|
||||
|
||||
name = "TF - Noyau Linux a jour"
|
||||
description = "Version minimale du noyau Linux exigee"
|
||||
type = "os_version"
|
||||
schedule = "5m"
|
||||
|
||||
match = [
|
||||
{ platform = "linux" },
|
||||
]
|
||||
|
||||
input = {
|
||||
version = "5.15.0"
|
||||
operator = ">="
|
||||
}
|
||||
}
|
||||
|
||||
#======================================================
|
||||
# Rule group Access s'appuyant sur ces postures Terraform
|
||||
# (équivalent "new generation" de latest_os_version_requirements_rule_group)
|
||||
|
||||
@@ -17,6 +17,9 @@ locals {
|
||||
|
||||
# HTTP (L7) Policies - Remote Browser Isolation
|
||||
webmail_isolate = 25300 # Isolate personal webmail in a remote browser (RBI)
|
||||
|
||||
# HTTP (L7) Policies - Do Not Inspect (évaluées avant l'inspection)
|
||||
dni_privacy = 12500 # Ne pas déchiffrer banque/santé (confidentialité)
|
||||
}
|
||||
|
||||
# Common rule settings for block policies
|
||||
@@ -40,6 +43,25 @@ locals {
|
||||
# Note: Dashboard Do-Not-Inspect policies (13000-15000) evaluated before Terraform HTTP policies
|
||||
#==========================================================
|
||||
|
||||
# Do Not Inspect - Confidentialité (Precedence: 12500)
|
||||
# Bonne pratique : NE PAS déchiffrer le trafic vers les sites bancaires
|
||||
# et de santé (obligations de confidentialité + certains sites cassent
|
||||
# avec l'inspection TLS : certificate pinning). action = "off" est le
|
||||
# "Do Not Inspect" du dashboard ; seuls les champs http.conn.* (basés
|
||||
# sur le SNI, sans déchiffrement) sont utilisables ici.
|
||||
# NOTE : vérifier les IDs de catégories dans le dashboard (Gateway >
|
||||
# politiques > sélecteur Content Categories) et ajuster si besoin.
|
||||
do_not_inspect_privacy = {
|
||||
name = "HTTP - Do Not Inspect: Banque & Sante"
|
||||
description = "Ne pas dechiffrer le trafic vers les sites bancaires et de sante (confidentialite)"
|
||||
enabled = true
|
||||
action = "off"
|
||||
precedence = local.precedence_http.dni_privacy
|
||||
filters = ["http"]
|
||||
traffic = "any(http.conn.content_category[*] in {113 156})"
|
||||
notification_enabled = false
|
||||
}
|
||||
|
||||
# AI Application Governance (Precedence: 24000-24100)
|
||||
redirect_ai_to_claude = {
|
||||
name = "HTTP - Redirect: Redirect users to claude.ai"
|
||||
|
||||
@@ -67,7 +67,7 @@ locals {
|
||||
filters = ["l4"]
|
||||
traffic = "net.dst.ip == ${var.gcp_windows_vm_internal_ip} and net.dst.port == ${var.cloudflare_domain_controller_rdp_port} and net.protocol == \"tcp\""
|
||||
identity = "any(identity.saml_attributes[*] == \"groups=${var.okta_itadmin_saml_group_name}\") or any(identity.saml_attributes[*] == \"groups=${var.okta_infra_admin_saml_group_name}\")"
|
||||
device_posture = "any(device_posture.checks.passed[*] == \"${var.cloudflare_macos_posture_id}\") or any(device_posture.checks.passed[*] == \"${var.cloudflare_windows_posture_id}\") or any(device_posture.checks.passed[*] == \"${var.cloudflare_linux_posture_id}\")"
|
||||
device_posture = "any(device_posture.checks.passed[*] == \"${cloudflare_zero_trust_device_posture_rule.macos_os_version.id}\") or any(device_posture.checks.passed[*] == \"${cloudflare_zero_trust_device_posture_rule.windows_os_version.id}\") or any(device_posture.checks.passed[*] == \"${cloudflare_zero_trust_device_posture_rule.linux_kernel_version.id}\")"
|
||||
notification_enabled = false
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# =============================================================================
|
||||
# CLOUDFLARE : Workers : Deny Page (page de refus d'accès personnalisée)
|
||||
# =============================================================================
|
||||
# BRIQUE DENY PAGE : toutes les applications Access du repo pointent leur
|
||||
# custom_deny_url vers https://denied.tips-of-mine.org/ — mais ce hostname
|
||||
# n'existait pas jusqu'ici (idée reprise du projet macharpe). Cette brique
|
||||
# comble le trou avec un Worker Cloudflare qui sert une vraie page de refus
|
||||
# (HTML dans workers/deny-page.js), sans aucun serveur à héberger.
|
||||
#
|
||||
# Chaîne : DNS denied.<zone> (proxifié, IP factice) -> route Workers -> Worker
|
||||
# =============================================================================
|
||||
|
||||
#======================================================
|
||||
# Le script Worker
|
||||
#======================================================
|
||||
resource "cloudflare_workers_script" "deny_page" {
|
||||
account_id = local.cloudflare_account_id
|
||||
script_name = "deny-page"
|
||||
content = file("${path.module}/workers/deny-page.js")
|
||||
}
|
||||
|
||||
#======================================================
|
||||
# DNS : denied.<zone> — enregistrement proxifié vers une adresse réservée
|
||||
# (100:: = IPv6 discard) : le trafic n'atteint jamais cette IP, le Worker
|
||||
# intercepte la requête sur l'edge Cloudflare.
|
||||
#======================================================
|
||||
resource "cloudflare_dns_record" "deny_page" {
|
||||
zone_id = local.cloudflare_zone_id
|
||||
name = "denied"
|
||||
type = "AAAA"
|
||||
content = "100::"
|
||||
ttl = 1
|
||||
proxied = true
|
||||
comment = "Managed by Terraform - page de refus Access (Worker deny-page)"
|
||||
}
|
||||
|
||||
#======================================================
|
||||
# La route qui lie le hostname au Worker
|
||||
#======================================================
|
||||
resource "cloudflare_workers_route" "deny_page" {
|
||||
zone_id = local.cloudflare_zone_id
|
||||
pattern = "denied.${var.cloudflare_zone}/*"
|
||||
script = cloudflare_workers_script.deny_page.script_name
|
||||
}
|
||||
@@ -19,6 +19,15 @@ data "vault_generic_secret" "azure" {
|
||||
path = var.vault_azure_path
|
||||
}
|
||||
|
||||
# Secrets Google OAuth — lus uniquement si la brique IdP Google est activée.
|
||||
# ACTION EXTERNE : créer un client OAuth dans Google Cloud Console, déposer
|
||||
# le secret "secret/google" dans Vault (clés client_id_cloudflare /
|
||||
# secret_cloudflare), puis google_idp_enabled = true dans le tfvars.
|
||||
data "vault_generic_secret" "google" {
|
||||
count = var.google_idp_enabled ? 1 : 0
|
||||
path = var.vault_google_path
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# LOCALS
|
||||
# =============================================================================
|
||||
@@ -42,6 +51,14 @@ locals {
|
||||
azure_oidc_secret_cloudflare = var.azure_idp_enabled ? data.vault_generic_secret.azure[0].data["secret_cloudflare"] : ""
|
||||
azure_directory_id = var.azure_idp_enabled ? data.vault_generic_secret.azure[0].data["directory_id"] : ""
|
||||
|
||||
# Secrets Google OAuth depuis Vault (brique IdP Google, optionnelle)
|
||||
google_oidc_client_id_cloudflare = var.google_idp_enabled ? data.vault_generic_secret.google[0].data["client_id_cloudflare"] : ""
|
||||
google_oidc_secret_cloudflare = var.google_idp_enabled ? data.vault_generic_secret.google[0].data["secret_cloudflare"] : ""
|
||||
|
||||
# Liste d'IdP Google utilisable dans les allowed_idps des applications
|
||||
# (vide si la brique est désactivée -> le bouton Google disparaît)
|
||||
google_idp_ids = var.google_idp_enabled ? [cloudflare_zero_trust_access_identity_provider.gmail[0].id] : []
|
||||
|
||||
# Construction des ingress rules pour toutes les applications
|
||||
# CORRECTIF : le hostname est construit avec le NOM de la zone
|
||||
# (var.cloudflare_zone = "tips-of-mine.org") et non plus avec l'ID de zone.
|
||||
|
||||
+23
-2
@@ -1,8 +1,11 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
cloudflare = {
|
||||
# Cap < 6.0.0 : on accepte les évolutions de la v5 mais jamais un saut
|
||||
# de version majeure automatique (c'est un provider v5 non épinglé qui
|
||||
# avait déclenché le crash cty avec Terraform 1.7.5).
|
||||
source = "cloudflare/cloudflare"
|
||||
version = ">= 5.12.0"
|
||||
version = ">= 5.12.0, < 6.0.0"
|
||||
}
|
||||
vault = {
|
||||
source = "hashicorp/vault"
|
||||
@@ -39,5 +42,23 @@ provider "vault" {
|
||||
# (Let's Encrypt, CA interne importée sur le runner CI, etc.).
|
||||
# En dépannage uniquement : vault_skip_tls_verify = true dans le tfvars.
|
||||
skip_tls_verify = var.vault_skip_tls_verify
|
||||
token = var.vault_token
|
||||
|
||||
# Deux modes d'authentification :
|
||||
# - AppRole (recommandé pour la CI) : renseigner vault_role_id +
|
||||
# vault_secret_id (secrets Gitea VAULT_ROLE_ID / VAULT_SECRET_ID) ->
|
||||
# la CI obtient un token COURT à chaque run, plus de token statique
|
||||
# longue durée qui traîne. Mise en place : voir ACTIONS_EXTERNES.md.
|
||||
# - Token statique (mode historique, toujours fonctionnel) : vault_token.
|
||||
token = var.vault_role_id != "" ? null : var.vault_token
|
||||
|
||||
dynamic "auth_login" {
|
||||
for_each = var.vault_role_id != "" ? [1] : []
|
||||
content {
|
||||
path = "auth/approle/login"
|
||||
parameters = {
|
||||
role_id = var.vault_role_id
|
||||
secret_id = var.vault_secret_id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,5 +3,13 @@ terraform {
|
||||
bucket = "tips-of-mine-terraform-bucket-backend"
|
||||
key = "backend/cloudflare-tunnel-org/terraform.tfstate"
|
||||
region = "eu-north-1"
|
||||
|
||||
# Verrou d'état NATIF S3 (Terraform >= 1.10) : un fichier .tflock est créé
|
||||
# à côté du state pendant chaque opération -> deux applies simultanés
|
||||
# deviennent impossibles. 100 % gratuit : aucune table DynamoDB, aucun
|
||||
# service supplémentaire, juste une écriture conditionnelle dans le bucket
|
||||
# existant. Si ton fournisseur S3 ne supporte pas les écritures
|
||||
# conditionnelles (erreur au lock), retirer simplement cette ligne.
|
||||
use_lockfile = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
# =============================================================================
|
||||
# TESTS TERRAFORM NATIFS (terraform test)
|
||||
# =============================================================================
|
||||
# Exécutés par la CI (validate.yml) avec des providers MOCKÉS : aucun appel
|
||||
# réel à Cloudflare, Vault, ou autre. On vérifie la logique de construction
|
||||
# (locals, for_each) — notamment le bug historique "hostname construit avec
|
||||
# le zone ID au lieu du nom de domaine", qui ne peut plus revenir.
|
||||
# =============================================================================
|
||||
|
||||
mock_provider "cloudflare" {}
|
||||
mock_provider "tls" {}
|
||||
mock_provider "http" {}
|
||||
|
||||
mock_provider "vault" {
|
||||
# Les data sources vault_generic_secret renvoient ces valeurs factices
|
||||
# (les clés couvrent secret/cloudflare ET secret/authentik)
|
||||
mock_data "vault_generic_secret" {
|
||||
defaults = {
|
||||
data = {
|
||||
api_token = "mock-api-token"
|
||||
account_id = "mock-account-id"
|
||||
zone_id_org = "0123456789abcdef0123456789abcdef"
|
||||
email = "[email protected]"
|
||||
client_id_cloudflare = "mock-client-id"
|
||||
secret_cloudflare = "mock-secret"
|
||||
directory_id = "mock-directory-id"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variables {
|
||||
vault_token = "mock-token"
|
||||
}
|
||||
|
||||
run "hostnames_utilisent_le_nom_de_zone" {
|
||||
command = plan
|
||||
|
||||
# Les URLs publiques sont construites avec le NOM de la zone…
|
||||
assert {
|
||||
condition = output.applications_urls["homeassistant"] == "https://home.tips-of-mine.org"
|
||||
error_message = "L'URL publique de homeassistant doit être https://home.tips-of-mine.org (nom de zone, pas zone ID)"
|
||||
}
|
||||
|
||||
# …et les règles d'ingress du tunnel aussi (le bug historique utilisait
|
||||
# le zone ID hexadécimal ici)
|
||||
assert {
|
||||
condition = contains(
|
||||
[for r in cloudflare_zero_trust_tunnel_cloudflared_config.home_tunnel_config.config.ingress : try(r.hostname, "")],
|
||||
"home.tips-of-mine.org"
|
||||
)
|
||||
error_message = "L'ingress du tunnel doit contenir le hostname home.tips-of-mine.org"
|
||||
}
|
||||
|
||||
# Aucun hostname d'ingress ne doit contenir le zone ID mocké
|
||||
assert {
|
||||
condition = length([
|
||||
for r in cloudflare_zero_trust_tunnel_cloudflared_config.home_tunnel_config.config.ingress :
|
||||
r if can(regex("0123456789abcdef", try(r.hostname, "")))
|
||||
]) == 0
|
||||
error_message = "Un hostname d'ingress contient le zone ID : régression du bug zone ID vs nom de domaine"
|
||||
}
|
||||
|
||||
# La règle catch-all 404 doit rester la dernière règle d'ingress
|
||||
assert {
|
||||
condition = element(cloudflare_zero_trust_tunnel_cloudflared_config.home_tunnel_config.config.ingress, length(cloudflare_zero_trust_tunnel_cloudflared_config.home_tunnel_config.config.ingress) - 1).service == "http_status:404"
|
||||
error_message = "La dernière règle d'ingress doit être le catch-all http_status:404"
|
||||
}
|
||||
|
||||
# Chaque application du tfvars a bien son enregistrement DNS
|
||||
assert {
|
||||
condition = length(cloudflare_dns_record.applications) == length(var.applications)
|
||||
error_message = "Chaque application doit avoir exactement un enregistrement DNS"
|
||||
}
|
||||
}
|
||||
+11
-6
@@ -122,6 +122,14 @@ remote_sites = {
|
||||
}
|
||||
}
|
||||
|
||||
# BRIQUE IdP GOOGLE : passer à true APRÈS avoir créé le client OAuth Google
|
||||
# et le secret Vault "secret/google" (client_id_cloudflare / secret_cloudflare)
|
||||
google_idp_enabled = false
|
||||
|
||||
# BRIQUE mTLS : passer à true APRÈS avoir déposé la CA dans Vault
|
||||
# ("secret/mtls", clé ca_pem)
|
||||
mtls_enabled = false
|
||||
|
||||
# BRIQUE DEX : passer à true APRÈS avoir ajouté la permission
|
||||
# "Digital Experience Monitoring : Edit" au token API Cloudflare (403 sinon)
|
||||
dex_enabled = false
|
||||
@@ -198,12 +206,9 @@ cloudflare_domain_controller_rdp_port = 3389
|
||||
#cloudflare_azure_admin_rule_group_id = "5f253130-a400-4215-44444444"
|
||||
|
||||
# Device Posture - Sensitive: manually retrieved from Cloudflare dashboard
|
||||
cloudflare_gateway_posture_id = "4d8d7499-38c3-4bf0-55555555"
|
||||
cloudflare_macos_posture_id = "6d64ff80-1308-4462-66666666"
|
||||
#cloudflare_ios_posture_id = "56454654-1245-8564-77777777"
|
||||
cloudflare_windows_posture_id = "67b05735-3b9b-4bcc-88888888"
|
||||
cloudflare_linux_posture_id = "ed5639c7-3305-4a91-9999999"
|
||||
cloudflare_device_os = "mac" # Options: "linux", "windows", "mac"
|
||||
# MIGRÉ : les IDs de posture recopiés du dashboard ont été supprimés — les
|
||||
# postures sont gérées par Terraform (Team_Resources-Devices-Device_posture.tf)
|
||||
cloudflare_device_os = "mac" # Options: "linux", "windows", "mac"
|
||||
|
||||
#cloudflare_tunnel_warp_connector_azure_id = ""
|
||||
#cloudflare_tunnel_warp_connector_gcp_id = ""
|
||||
|
||||
+52
-24
@@ -9,9 +9,24 @@ variable "vault_url" {
|
||||
}
|
||||
|
||||
variable "vault_token" {
|
||||
description = "Token d'authentification Vault (fourni par CI/CD)"
|
||||
description = "Token d'authentification Vault statique (mode historique — inutile si AppRole configuré)"
|
||||
type = string
|
||||
sensitive = true
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "vault_role_id" {
|
||||
description = "Role ID AppRole pour l'authentification Vault de la CI (vide = auth par token statique)"
|
||||
type = string
|
||||
sensitive = true
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "vault_secret_id" {
|
||||
description = "Secret ID AppRole pour l'authentification Vault de la CI"
|
||||
type = string
|
||||
sensitive = true
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "vault_cloudflare_path" {
|
||||
@@ -32,6 +47,18 @@ variable "vault_azure_path" {
|
||||
default = "secret/azure"
|
||||
}
|
||||
|
||||
variable "vault_google_path" {
|
||||
description = "Chemin vers les secrets Google OAuth dans Vault (brique IdP Google)"
|
||||
type = string
|
||||
default = "secret/google"
|
||||
}
|
||||
|
||||
variable "vault_mtls_path" {
|
||||
description = "Chemin vers le certificat CA mTLS dans Vault (brique mTLS)"
|
||||
type = string
|
||||
default = "secret/mtls"
|
||||
}
|
||||
|
||||
variable "vault_skip_tls_verify" {
|
||||
description = "Désactiver la vérification TLS vers Vault (dépannage uniquement — laisser à false)"
|
||||
type = bool
|
||||
@@ -215,29 +242,10 @@ variable "dns_proxied" {
|
||||
# sensitive = true
|
||||
#}
|
||||
|
||||
variable "cloudflare_gateway_posture_id" {
|
||||
description = "Gateway posture ID in Cloudflare"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "cloudflare_macos_posture_id" {
|
||||
description = "Latest macOS version posture ID in Cloudflare"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "cloudflare_windows_posture_id" {
|
||||
description = "Latest Windows version posture ID in Cloudflare"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "cloudflare_linux_posture_id" {
|
||||
description = "Latest Linux Kernel version posture ID in Cloudflare"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
# MIGRÉ : les 4 variables cloudflare_*_posture_id (IDs recopiés à la main
|
||||
# depuis le dashboard) ont été supprimées. Les vérifications de posture sont
|
||||
# désormais des ressources Terraform dans
|
||||
# Team_Resources-Devices-Device_posture.tf, référencées par leur attribut .id.
|
||||
|
||||
variable "cloudflare_device_os" {
|
||||
description = "This is the OS you are running on your own client machine"
|
||||
@@ -709,6 +717,26 @@ variable "cloudflare_tunnel_warp_connector_site_b_id" {
|
||||
default = ""
|
||||
}
|
||||
|
||||
#======================================================
|
||||
# BRIQUE IdP GOOGLE
|
||||
#======================================================
|
||||
|
||||
variable "google_idp_enabled" {
|
||||
description = "Activer la brique IdP Google (nécessite le secret Vault secret/google — voir ACTIONS_EXTERNES.md)"
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
#======================================================
|
||||
# BRIQUE mTLS
|
||||
#======================================================
|
||||
|
||||
variable "mtls_enabled" {
|
||||
description = "Activer la brique mTLS (nécessite le secret Vault secret/mtls avec la clé ca_pem)"
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
#======================================================
|
||||
# BRIQUE DEX
|
||||
#======================================================
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Page de refus d'accès personnalisée — servie sur denied.tips-of-mine.org
|
||||
* Toutes les applications Access du repo pointent leur custom_deny_url ici.
|
||||
* Déployée par Terraform : Workers-Deny_Page.tf
|
||||
*/
|
||||
addEventListener('fetch', (event) => {
|
||||
event.respondWith(handleRequest(event.request));
|
||||
});
|
||||
|
||||
async function handleRequest(request) {
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Accès refusé — Tips-Of-Mine</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
body {
|
||||
margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center;
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
background: #f5f5f5; color: #1a1a1a;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body { background: #16181d; color: #e8e8e8; }
|
||||
.card { background: #1f232b !important; box-shadow: 0 4px 24px rgba(0,0,0,.4) !important; }
|
||||
}
|
||||
.card {
|
||||
background: #fff; border-radius: 12px; box-shadow: 0 4px 24px rgba(0,0,0,.08);
|
||||
max-width: 520px; margin: 1rem; padding: 2.5rem; text-align: center;
|
||||
}
|
||||
.icon { font-size: 3rem; }
|
||||
h1 { font-size: 1.4rem; margin: .75rem 0 .25rem; }
|
||||
p { line-height: 1.55; opacity: .85; }
|
||||
.hint {
|
||||
text-align: left; font-size: .9rem; border-left: 3px solid #f6821f;
|
||||
padding: .5rem .9rem; margin-top: 1.25rem; opacity: .9;
|
||||
}
|
||||
a { color: #f6821f; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="card">
|
||||
<div class="icon">🚫</div>
|
||||
<h1>Accès refusé</h1>
|
||||
<p>Votre identité a bien été vérifiée, mais elle ne remplit pas
|
||||
les conditions d'accès de cette application (groupe, posture du poste, MFA ou localisation).</p>
|
||||
<div class="hint">
|
||||
<strong>Que faire ?</strong><br>
|
||||
• Vérifiez que vous êtes connecté avec le bon compte ;<br>
|
||||
• Vérifiez que le client WARP est actif et votre poste conforme ;<br>
|
||||
• Si le besoin est légitime, contactez l'administrateur :
|
||||
<a href="mailto:[email protected]">[email protected]</a>.
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
return new Response(html, {
|
||||
status: 403,
|
||||
headers: { 'content-type': 'text/html; charset=utf-8' },
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user