Proxy Squid & DNS (unbound)¶
Rôle¶
Deux services indépendants, chacun en haute disponibilité propre, hébergés sur 4 VM en zone DMZ :
- Squid : proxy sortant applicatif filtrant par whitelist de domaines — seul point de sortie Internet autorisé pour l'installation de paquets, dépôts, mises à jour.
- unbound : résolveur DNS agissant en pur forwarder vers l'Active Directory — seul chemin de résolution DNS pour les zones SOC et admin.
Les deux répondent aux mêmes zones clientes (admin + SOC) mais sont des clusters strictement séparés, sans dépendance l'un envers l'autre.
Architecture¶
Nœuds et VIP — deux clusters keepalived indépendants¶
| Cluster | Nœuds | Segment | VIP | VRID |
|---|---|---|---|---|
Squid (squid) |
INDPRXY001 (10.100.2.122→MASTER prio110), INDPRXY002 (10.100.2.123→BACKUP prio100) |
seg-proxy (10.100.10.1/29) |
10.100.10.4/29 |
20 |
DNS (dnsproxy) |
INDPRXD001 (10.100.2.124→MASTER prio110), INDPRXD002 (10.100.2.125→BACKUP prio100) |
seg-proxy-dns (10.100.10.9/29) |
10.100.10.12/29 |
21 |
Pas de HAProxy devant ces VIP
Contrairement à ce qu'on pourrait attendre par analogie avec d'autres services de l'infra, Squid et unbound ne sont pas exposés par le cluster HAProxy (10.100.2.130) : ce sont deux VIP keepalived indépendantes, propres à ce dépôt, chacune avec son propre vrrp_instance (VI_SQUID / VI_DNS) et son propre script de contrôle (killall -0 squid / killall -0 unbound).
flowchart TD
CLIENTS["Zones clientes : admin (10.100.2.0/23)<br/>+ SOC (10.100.20.0/24)"]
subgraph SQ[" Cluster Squid — seg-proxy 10.100.10.0/29 — VRID 20 "]
VIPSQ(["VIP 10.100.10.4"])
SQ1["INDPRXY001 · .122<br/>MASTER prio 110"]
SQ2["INDPRXY002 · .123<br/>BACKUP prio 100"]
VIPSQ === SQ1
SQ1 -.failover.-> SQ2
end
subgraph DN[" Cluster unbound — seg-proxy-dns 10.100.10.8/29 — VRID 21 "]
VIPDN(["VIP 10.100.10.12"])
DN1["INDPRXD001 · .124<br/>MASTER prio 110"]
DN2["INDPRXD002 · .125<br/>BACKUP prio 100"]
VIPDN === DN1
DN1 -.failover.-> DN2
end
CLIENTS -->|"3128, whitelist dstdomain"| VIPSQ
CLIENTS -->|"53, DNS"| VIPDN
VIPSQ --> INTERNET["Internet<br/>domaines whitelistés uniquement"]
VIPDN --> AD["Active Directory<br/>10.100.2.241 / .242"]
Chaque VM porte un second NIC dédié sur seg-admin (accès SSH/management), séparé du NIC exposé en DMZ — sshd n'écoute plus du tout sur l'IP DMZ. Ce NIC admin est ajouté par Terraform mais son adressage/routage est achevé à la main dans le guest (voir la section Procédure manuelle ci-dessous) :
flowchart LR
subgraph VM[" Une VM proxy, ex. INDPRXY001 "]
direction TB
SVC["NIC service — seg-proxy<br/>.122 · Squid : 3128 seul<br/>route par défaut globale"]
ADM["NIC admin — seg-admin<br/>.122 · sshd ListenAddress<br/>table de routage dédiée (100)"]
end
CLIENT["Clients admin/SOC"] -->|"3128"| SVC
MGMT["Poste admin<br/>(SNAT FortiGate grp-mgmt-nat)"] -->|"22/tcp"| ADM
SVC -->|réponse| GWSVC["Gateway seg-proxy .1"]
ADM -->|"réponse (policy-based routing,<br/>ip rule + table 100)"| GWADM["Gateway seg-admin"]
Zones desservies¶
Clients autorisés, identiques pour les deux services (défense en profondeur — la DFW restreint déjà l'accès) :
127.0.0.0/8/127.0.0.1/32(local)10.100.2.0/23(zone admin)10.100.20.0/24(zone SOC)
Squid — whitelist de domaines (dstdomain)¶
Domaines groupés par usage dans squid_allowed_domains : VMware/Broadcom, Rocky Linux, EPEL/Fedora, HashiCorp/Terraform, Ansible Galaxy, PyPI, endpoints AWS (.amazonaws.com, .aws.amazon.com), SIEM SOC (Splunk, Elastic), threat intelligence (VirusTotal, AlienVault OTX, abuse.ch), TheHive, tarballs Apache (Kafka), Docker Hub, GHCR, Windows Update, PostgreSQL PGDG, GitHub/githubusercontent.com (CDN de redirection Terraform/Galaxy), Confluent (Kafka), dépôt Zabbix.
DNS — forwarding vers l'Active Directory¶
unbound transmet toutes les requêtes (forward-zone ".") vers les deux contrôleurs AD (10.100.2.241, 10.100.2.242). domain-insecure est déclaré pour infra.indio (domaine interne sans délégation DNSSEC publique).
Provisioning Terraform¶
Un seul main.tf à la racine du dépôt (pas de sous-dossier terraform/ séparé, à la différence du dépôt haproxy) déploie les 4 VM depuis tmpl-rocky96-hardened, chacune avec deux interfaces réseau : le réseau applicatif (seg-proxy ou seg-proxy-dns selon le nœud, défini par var.nodes[].network) et le réseau admin (seg-admin, var.admin_network).
variables.tf:nodeslistename/network/gateway/admin_ip(les 4 VM),vm_cpu=2,vm_ram=2048,vm_netmask=29(segments proxy),vm_dns=[10.100.2.241, 10.100.2.242].- Providers :
hashicorp/vsphere >= 2.6.0,e-breuninger/netbox >= 5.0.0, < 6.0.0. Terraform>= 1.5.0. outputs.tf: uniquementvms(nom → IP).
Configuration Ansible¶
site.yml définit deux plays : squid (rôles squid + keepalived) et dnsproxy (rôles dnsproxy + keepalived).
- Rôle
squid: installe le paquet, déploieallowed_domains(liste desquid_allowed_domains) etsquid.conf(validé parsquid -k parse -f %s), ouvre le port3128dans firewalld (zonedrop). La config applique : ACL réseaux clients, restriction desSafe_ports/SSL_ports, ACLdstdomainsur le fichier whitelist,deny allen dernier, durcissement (via off,forwarded_for delete,httpd_suppress_version_string on). - Rôle
dnsproxy: installeunbound, déploiednsproxy.conf(validé parunbound-checkconf), ouvre le servicednsdans firewalld (zonedrop). - Rôle
keepalived(générique, réutilisé par les deux clusters viakeepalived_instance/keepalived_check_process) :vrrp_script chk_svc(killall -0 <process>), authentificationPASS, VIP/masque. group_vars/squid.ymletgroup_vars/dnsproxy.ymlportent chacun unkeepalived_auth_passchiffré en ansible-vault ($ANSIBLE_VAULT), déchiffré viavault_password_file = /root/.proxy_vault_pass(ansible.cfg, hors dépôt).
Procédure manuelle¶
Le NIC admin (seg-admin) est bien créé par Terraform (2ᵉ bloc network_interface
dans main.tf), mais tout le reste — adressage IP, routage et restriction SSH —
est appliqué à la main dans le guest : le bloc clone.customize ne gère que
le NIC applicatif (DMZ/DNS) et est de toute façon ignoré après la création
initiale (lifecycle.ignore_changes). Procédure telle qu'appliquée le
2026-07-05 sur les 4 VM (INDPRXY001/002, INDPRXD001/002), reproductible
pour toute VM future ajoutée à ce schéma multi-homed :
-
Adresser le NIC admin avec l'IP statique prévue (
admin_ipdevariables.tf,10.100.2.122-.125) : -
Assouplir
rp_filtersur le NIC admin. Sans gateway par défaut sur ce 2ᵉ NIC, le noyau rejette silencieusement (« martian source », visible dansdmesg) les paquets entrants dont la route retour ne repasse pas par la même interface — cas systématique ici puisque la source vue par le NIC admin est l'IP SNAT du FortiGate (10.15.91.14), hors du subnetseg-admin:La valeur effective étant
max(all, interface),net.ipv4.conf.all.rp_filter=1(défaut Rocky) n'oblige à changer que la valeur de l'interface admin. -
Router par stratégie (policy-based routing). Le fix
rp_filterseul ne suffit pas : sans route dédiée, la réponse (SYN-ACK) ressort par le NIC de service via la route par défaut globale, avec l'IP source du NIC admin — une combinaison invalide que l'appelant rejette (routage asymétrique). Fix définitif : -
Restreindre
sshdau NIC admin : déployer/etc/ssh/sshd_config.d/01-admin-only-listen.confavecListenAddress <ip-admin>(+127.0.0.1), puissystemctl reload sshd. Cette restriction applicative complète (ne remplace pas) la suppression côté NSX de la règlemgmt-to-dmz-ssh: la défense en profondeur repose sur les deux couches — plus aucun chemin, réseau ou applicatif, n'autorise SSH vers l'IP exposée en DMZ. -
Diagnostiquer un timeout résiduel après ces étapes :
tcpdump -i <nic-service> host <ip-admin>côté serveur pendant une tentative de connexion — si le SYN-ACK apparaît sur le NIC de service au lieu du NIC admin, l'étape 3 est incomplète ou la règle de routage a été perdue après un redémarrage réseau (vérifierip rule showetip route show table 100).
L'IP admin (pas l'IP de service) est ensuite ce qu'ansible_host référence
dans inventory.ini (voir Architecture ci-dessus) — donc ce que reçoivent
Ansible/ansible_host pour toute connexion de gestion ultérieure.
Effet de bord sur le rôle Ansible ipa_client
Ces 4 VM sont aussi enrôlées comme clients FreeIPA (voir
FreeIPA & trust AD). Le rôle ipa_client
(/root/identity) templait /etc/hosts avec {{ ansible_host }} — casse
dès qu'ansible_host pointe vers l'IP admin, puisque le FQDN doit résoudre
vers l'IP de service, pas l'IP de management utilisée pour la connexion
SSH d'Ansible. Corrigé par l'introduction d'une variable dédiée
ipa_client_ip (IP de service) découplée d'ansible_host (IP admin).
Procédure de déploiement¶
cd /root/proxy
cp terraform.tfvars.example terraform.tfvars
terraform init
terraform plan
terraform apply # crée les 4 VM (2 NIC chacune)
# configurer manuellement l'IP du NIC admin (seg-admin) dans le guest si nécessaire
cd ansible
ansible-playbook site.yml # déploie squid+keepalived puis dnsproxy+keepalived
Points d'attention¶
- Piège Squid déjà rencontré (2026-07-14) : Squid 5.5 refuse de démarrer si la whitelist contient une entrée qui est un sous-domaine d'une entrée déjà présente (« is a subdomain of… »). Un ajout de
pkg-containers.githubusercontent.com(déjà couvert par.githubusercontent.com) a cassé Squid sur les 2 nœuds simultanément, ~15 minutes de panne totale. Toujours vérifier qu'un nouveau domaine n'est pas déjà couvert par une entrée existante avant de déployer un changement desquid_allowed_domains. interface-automatic: yesobligatoire pour unbound : sans ce réglage, un socket UDP bindé sur0.0.0.0répond avec l'IP primaire de l'interface au lieu de l'IP de destination reçue (la VIP) — le client rejette la réponse (timeout côtédig), alors que le paquet a bien circulé. Le TCP n'est pas affecté (un socket TCP accepté hérite nativement de l'IP de destination exacte), ce qui rend le symptôme trompeur si le diagnostic ne teste qu'en TCP.domain-insecureobligatoire pourinfra.indio: ce domaine interne n'a pas de délégation DNSSEC publique — sans cette exception, le validateur DNSSEC (actif par défaut) rejette les réponses forwardées par l'AD en « bogus », d'où unSERVFAILsystématique.- Le NIC admin dédié (
seg-admin) n'est configuré que manuellement dans le guest : le bloc Terraformclone.customizene gère que le NIC applicatif (DMZ), et il est de toute façon ignoré après la création initiale (lifecycle.ignore_changes). Piège classique de VM multi-homed (routage asymétrique,rp_filter) si le NIC admin est mal configuré après coup — voir la section Procédure manuelle ci-dessus pour le détail complet (rp_filter en mode loose, routage par stratégie, restriction sshd, diagnostic tcpdump). keepalived_auth_passest chiffré en ansible-vault dans les deuxgroup_vars— un déploiement sansvault_password_fileaccessible (/root/.proxy_vault_pass) échoue au déchiffrement, pas silencieusement.- Contrairement au dépôt HAProxy (qui génère
ansible/inventory.iniautomatiquement depuis Terraform), ce dépôt ne montre pas de mécanisme équivalent dansmain.tf: l'inventaire (ansible/inventory.ini) semble maintenu manuellement — à vérifier avant toute modification de la liste de nœuds. - La whitelist Squid liste les endpoints AWS (
.amazonaws.com,.aws.amazon.com) : c'est le chemin de sortie utilisé par plusieurs intégrations vers AWS de l'infra (voir AWS), dont l'Elastic Agent du lab de détectionaws-s3-siem, configuré pour ne sortir que via10.100.10.4:3128. - Piège de boucle egress pour ces 4 VM elles-mêmes (2026-07-16) : ces VM ne peuvent pas utiliser leur propre VIP Squid (
10.100.10.4:3128) commeHTTP_PROXY/HTTPS_PROXYpour sortir vers Internet — elles sont elles-mêmes les backends de cette VIP, la requête boucle vers elles-mêmes/leur pair et Squid la refuse (403) ou elle timeout selon le nœud. Symptôme observé : échec du téléchargement du RPMzabbix-release(repo.zabbix.com) lors du déploiement de l'agent Zabbix sur ces 4 VM uniquement, alors que le reste de la flotte (derrière le vrai proxy Squid) fonctionnait normalement. Fix appliqué : dépôt Nexusyum proxydédié, accessible viarepo.infra.infraet déjà couvert par la règle DFW existantedmz-to-repo/soc-*-to-repo— voir Nexus. Pattern à reproduire pour tout futur logiciel tiers nécessitant un dépôt YUM externe sur ces 4 VM spécifiquement.