Aller au contenu

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 :

castor devcontainer:whitelist-list

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 :

grep -v '^\s*#' /app/.devcontainer/baseline-domains.txt | grep -v '^\s*$'
grep -v '^\s*#' /app/.devcontainer/whitelist.local.txt 2>/dev/null | grep -v '^\s*$'

É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 :

# .devcontainer/baseline-domains.txt
# ...
symfony.com
jolicode.com
votre-domaine.example

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 :

castor devcontainer:firewall-reload

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 :

  1. flushe les règles iptables existantes,
  2. pose immédiatement les policies par défaut OUTPUT/INPUT/FORWARD DROP — fermé avant de construire les exceptions,
  3. recrée l'ipset allowed-domains,
  4. recharge la baseline depuis .devcontainer/baseline-domains.txt et, si présent, concatène l'override local .devcontainer/whitelist.local.txt (voir section suivante),
  5. relance dnsmasq avec la nouvelle ligne ipset=…,
  6. revérifie les smoke-tests : example.com doit être bloqué (KO bloquant attendu — preuve que la policy OUTPUT DROP est 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),
  7. 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 :

curl -v https://votre-domaine.example

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 dnsmasq la 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 avec https:// 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 :

cp .devcontainer/whitelist.local.txt.dist .devcontainer/whitelist.local.txt

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_REGEX cô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 :

castor devcontainer:whitelist-remove raw.githubusercontent.com

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