Whitelister un domaine dans le Dev Container¶
Vous travaillez dans le Dev Container Kirexo et une commande échoue avec Connection refused, Could not resolve host ou un timeout suspect. C'est probablement le pare-feu interne qui bloque le domaine. Cette recette explique comment l'ajouter à la whitelist.
Prérequis¶
- Vous êtes dans le Dev Container (cf. le tutoriel PHPStorm).
- Vous avez identifié le domaine bloqué — il apparaît typiquement dans le message d'erreur de la commande qui a échoué.
Comment savoir que c'est le pare-feu
Le pare-feu rejette les connexions sortantes hors whitelist avec icmp-admin-prohibited (cf. .devcontainer/init-firewall.sh). En pratique vous voyez Connection refused, Could not resolve host ou un timeout. Pour confirmer, testez depuis le conteneur : curl -v https://example.com doit échouer rapidement.
Étape 1 — Identifier le bon domaine¶
Listez les domaines déjà whitelistés pour vérifier que le vôtre n'est pas couvert par un parent :
La cible affiche un tableau Origine | Domaine (baseline = versionné, local = override personnel), le total, et — depuis l'intérieur du devcontainer — le nombre d'IPs actuellement résolues dans l'ipset allowed-domains. Cf. référence.
Le préfixe /domain/ matche le domaine et tous ses sous-domaines. Si symfony.com est whitelisté, alors ux.symfony.com, live.symfony.com, flex.symfony.com le sont aussi — pas la peine de les ajouter individuellement.
Lecture directe des fichiers (sans Castor)
Si Castor est indisponible (PHP cassé, fixtures perdues), les fichiers texte se lisent directement :
Étape 2 — Ajouter le domaine à la baseline¶
La baseline est désormais stockée dans un fichier dédié .devcontainer/baseline-domains.txt (source de vérité unique pour la whitelist partagée par toute l'équipe). Éditez ce fichier et ajoutez votre domaine en fin de liste, une ligne par domaine :
Format du fichier :
- une ligne = un FQDN exact (sans schéma, sans chemin, sans wildcard) ;
- les lignes vides sont ignorées ;
- un
#en début de ligne marque un commentaire — utile pour grouper les domaines par thème comme dans le fichier livré (« Plateforme Anthropic », « Écosystème PHP/Symfony »…).
L'ordre n'a pas d'importance — init-firewall.sh lit le fichier ligne par ligne et construit la directive ipset=…/allowed-domains en concaténant la baseline et l'override local (cf. étape suivante).
Claude Code n'a pas le droit d'éditer ce fichier
.devcontainer/baseline-domains.txt est verrouillé dans .claude/settings.json — un Claude Code en bypassPermissions ne peut pas le modifier. Si Claude a besoin d'un domaine partagé pour une tâche, il doit vous demander d'ajouter la ligne vous-même. Pour un domaine personnel (pas partagé par l'équipe), utiliser plutôt l'override local décrit plus bas.
Étape 3 — Recharger le pare-feu¶
Relancez le script de configuration du pare-feu via Castor :
La cible wrap sudo /app/.devcontainer/init-firewall.sh. Le sudo ne demande pas de mot de passe : un fichier /etc/sudoers.d/init-firewall autorise l'utilisateur app à lancer uniquement ce script en privilégié (cf. Dockerfile, stage frankenphp_dev). Tout autre sudo … reste interdit. La cible refuse de tourner depuis l'hôte (cf. référence).
Le script :
- flushe les règles
iptablesexistantes, - pose immédiatement les policies par défaut
OUTPUT/INPUT/FORWARD DROP— fermé avant de construire les exceptions, - recrée l'
ipset allowed-domains, - recharge la baseline depuis
.devcontainer/baseline-domains.txtet, si présent, concatène l'override local.devcontainer/whitelist.local.txt(voir section suivante), - relance
dnsmasqavec la nouvelle ligneipset=…, - revérifie les smoke-tests :
example.comdoit être bloqué (KO bloquant attendu — preuve que la policyOUTPUT DROPest effective) et quatre domaines critiques (gitlab.com,symfony.com,packagist.org,registry.npmjs.org) sont testés en non bloquant (un avertissement est émis si l'un d'eux est injoignable — drift de whitelist ou panne externe — sans empêcher le pare-feu d'être actif), - vérifie en bloquant la résolution intra-stack (
database,redis,rabbitmq,typesense,gotenberg,mailer) — un échec ici signale un DNAT cassé ou un service Compose à l'arrêt.
Si toutes les lignes « OK : … » s'affichent, le pare-feu est rechargé. Si vous voyez « AVERTISSEMENT : ... inaccessible », le pare-feu est actif mais une partie de la whitelist externe est injoignable — voir Debugger le pare-feu.
Appel direct possible en debug
Si Castor lui-même est cassé (PHP en panne, dépendances absentes), l'appel direct sudo /app/.devcontainer/init-firewall.sh reste valide — la cible Castor n'est qu'un wrapper.
Rejouer pare-feu + FrankenPHP en bloc
init-firewall.sh ne touche qu'au pare-feu. Si vous voulez aussi vous assurer que FrankenPHP tourne (par exemple après un kill manuel), lancez plutôt /app/.devcontainer/post-start.sh : il rejoue les deux étapes et reste idempotent (FrankenPHP ne sera pas relancé s'il tourne déjà). Cf. référence du Dev Container.
Étape 4 — Vérifier¶
Testez la connexion vers le domaine que vous venez d'ajouter :
Vous devez obtenir une réponse HTTP — plus de Connection refused. Si vous obtenez encore une erreur :
- la résolution DNS doit faire apparaître votre domaine dans
dnsmasqla première fois qu'il est interrogé. Si ce n'est pas le cas, vérifiez que vous avez bien collé le nom de domaine (pas une URL avechttps://devant) ; - relancez le script — un caractère oublié dans la ligne
ipset=peut rendre le parsing partiel.
Alternative : whitelist locale (override personnel)¶
Si le domaine que vous voulez ajouter est personnel (un service interne à votre boîte, un mirror Composer privé, un domaine que vous testez ponctuellement), ne touchez pas au script versionné. init-firewall.sh lit aussi, si présent, un fichier .devcontainer/whitelist.local.txt dont les domaines sont concaténés à la baseline avant la génération de la directive ipset=….
Un modèle versionné .devcontainer/whitelist.local.txt.dist documente le format et propose des exemples commentés. Pour partir d'une base propre :
Le fichier copié reste gitignored — chaque dev a le sien.
Caractéristiques :
- Format : une ligne = un domaine. Les lignes vides sont ignorées ; un
#en début de ligne marque un commentaire. - Gitignored : le fichier n'est pas suivi par git — votre override ne sera jamais commité par mégarde.
- Validation côté shell : chaque ligne est confrontée à la regex FQDN (mêmes règles que
FirewallWhitelist::DOMAIN_REGEXcôté PHP). Une ligne invalide (URL avec schéma, wildcard*.foo, IP littérale, espace…) est skipée avec un avertissement plutôt que d'avorter le pare-feu. - Hors-périmètre Claude Code : Claude n'a pas le droit de modifier ce fichier (verrouillé dans
.claude/settings.json). Si Claude a besoin d'un domaine supplémentaire pour une tâche, il doit vous demander d'ajouter la ligne vous-même.
Méthode recommandée — cible Castor¶
castor devcontainer:whitelist-add <domain> valide le format en amont, dédoublonne contre la baseline (refuse si le domaine ou un de ses parents est déjà couvert) et contre les entrées locales existantes, append au fichier puis relance le pare-feu en une seule commande :
# Ajouter raw.githubusercontent.com à titre personnel
castor devcontainer:whitelist-add raw.githubusercontent.com
Le wrapper refuse explicitement les formes invalides avant écriture :
| Entrée | Pourquoi refusée |
|---|---|
https://foo.com |
Schéma collé — écrire foo.com. |
*.foo.com |
Wildcard inutile — foo.com couvre déjà tous les sous-domaines via /domain/ dnsmasq. |
1.2.3.4 |
IPv4 littérale — dnsmasq ne résout pas une IP, l'entrée ipset=/1.2.3.4/... resterait morte. La regex FQDN partagée avec PHP acceptait techniquement cette forme (chaque octet matche [a-z0-9]+), le wrapper la filtre désormais en amont (cf. firewall_is_valid_domain dans .devcontainer/init-firewall.lib.sh). |
foo |
Single-label sans TLD — refusé par la regex FQDN. |
foo.com/api ou foo.com:8080 |
Chemin / port — le pare-feu filtre au niveau IP/port, le path n'est pas vu. |
foo.com (espaces autour) |
Trimmé automatiquement, accepté. |
Pour retirer une entrée :
whitelist-remove ne touche pas à la baseline — il édite uniquement whitelist.local.txt et préserve les commentaires et lignes vides existants. Pour retirer un domaine de la baseline versionnée, éditez .devcontainer/baseline-domains.txt à la main puis commitez.
Méthode manuelle — édition directe¶
Si vous voulez commiter (côté machine) un bloc complet bien commenté (plusieurs domaines liés à un même besoin, par exemple), éditez directement le fichier puis rechargez le pare-feu :
# Ajouter raw.githubusercontent.com à titre personnel (sans modifier le script versionné)
echo "raw.githubusercontent.com" >> .devcontainer/whitelist.local.txt
# Recharger le pare-feu pour prendre en compte l'ajout
castor devcontainer:firewall-reload
Pour distinguer baseline et override : si plusieurs développeur·se·s ont besoin du même domaine, ajoutez-le à .devcontainer/baseline-domains.txt et commitez. Si c'est un besoin personnel ou ponctuel, utilisez whitelist.local.txt (via la cible Castor ou directement).
Étape 5 — Persister entre rebuilds¶
À ce stade, le pare-feu est rechargé pour la session courante. Mais à la prochaine reconstruction du devcontainer (ou au prochain postStartCommand), init-firewall.sh est relancé : si votre modification de la baseline n'est pas commitée, elle est perdue. (À l'inverse, whitelist.local.txt survit aux rebuilds tant que vous ne le supprimez pas — il vit côté hôte dans le bind-mount.)
Commitez la baseline :
git add .devcontainer/baseline-domains.txt
git commit -m "Ajoute votre-domaine.example à la whitelist devcontainer"
Voir aussi¶
- Référence du Dev Container — liste complète des domaines whitelistés par défaut.
- Choix d'isolation du Dev Container — pourquoi un pare-feu interne plutôt que se reposer sur le sandbox Claude Code.
- Debugger le pare-feu du devcontainer — quand une commande échoue suspectement et qu'on veut vérifier si c'est le pare-feu.