<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Security on Guillaume Delré</title><link>https://guillaumedelre.github.io/fr/tags/security/</link><description>Recent content in Security on Guillaume Delré</description><generator>Hugo</generator><language>fr-FR</language><lastBuildDate>Thu, 14 May 2026 15:00:00 +0000</lastBuildDate><atom:link href="https://guillaumedelre.github.io/fr/tags/security/index.xml" rel="self" type="application/rss+xml"/><item><title>Ce qui survit au build</title><link>https://guillaumedelre.github.io/fr/2026/05/14/ce-qui-survit-au-build/</link><pubDate>Thu, 14 May 2026 15:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2026/05/14/ce-qui-survit-au-build/</guid><description>Part 2 of 8 in &amp;quot;Symfony vers le Cloud : Douze Facteurs, Treize Services&amp;quot;: Comment des fichiers .env committés alimentent un build qui grave les credentials dans les layers Docker — et ce qu&amp;#39;il faut pour vider le fichier jusqu&amp;#39;à quatre lignes.</description><category>symfony-to-the-cloud</category><content:encoded><![CDATA[<p>À un moment de l&rsquo;audit de migration cloud, quelqu&rsquo;un a lancé ça :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker run --rm &lt;image&gt; php -r <span style="color:#e6db74">&#34;var_dump(require &#39;.env.local.php&#39;);&#34;</span>
</span></span></code></pre></div><p>La sortie montrait tout ce que <code>composer dump-env prod</code> avait compilé dans l&rsquo;image au moment du build. Ce qui voulait dire tout ce qui se trouvait dans le fichier <code>.env</code> quand l&rsquo;image avait été construite. Ce qui voulait dire, entre autres, ça :</p>
<pre tabindex="0"><code class="language-dotenv" data-lang="dotenv">INFLUXDB_INIT_ADMIN_TOKEN=&lt;influxdb-admin-token&gt;
GF_SECURITY_ADMIN_USER=admin
GF_SECURITY_ADMIN_PASSWORD=admin123
BLACKFIRE_CLIENT_ID=&lt;blackfire-client-id&gt;
BLACKFIRE_CLIENT_TOKEN=&lt;blackfire-client-token&gt;
BLACKFIRE_SERVER_ID=&lt;blackfire-server-id&gt;
BLACKFIRE_SERVER_TOKEN=&lt;blackfire-server-token&gt;
NGROK_AUTHTOKEN=replace-me-optionnal
</code></pre><p>Vingt-cinq variables au total. Chaque credential accumulé dans le <code>.env</code> racine sur trois ans, désormais permanent dans un layer d&rsquo;image.</p>
<h2 id="comment-dump-env-fonctionne">Comment <code>dump-env</code> fonctionne</h2>
<p><code>composer dump-env prod</code> est une optimisation Symfony légitime. Au lieu de parser les fichiers <code>.env</code> à chaque requête, le runtime charge un tableau PHP pré-compilé depuis <code>.env.local.php</code>. Plus rapide et plus simple.</p>
<p>Le problème, c&rsquo;est ce qu&rsquo;il lit. Le Dockerfile copie le dépôt dans l&rsquo;image avec <code>COPY . ./</code>, <code>.env</code> inclus. Ensuite <code>dump-env prod</code> lit ce fichier et compile chaque variable dans <code>.env.local.php</code>. L&rsquo;image est livrée avec une capture figée des credentials qui se trouvaient dans <code>.env</code> au moment du build.</p>
<p>Les layers Docker sont des archives immuables. Même si une étape ultérieure supprimait <code>.env</code> du système de fichiers du container, le layer qui le contient existerait toujours dans l&rsquo;image. <code>docker save &lt;image&gt;</code> produit une archive tar de chaque layer ; extraire un fichier spécifique de n&rsquo;importe quel point de l&rsquo;historique de build est une opération simple. Les credentials sont invisibles à l&rsquo;exécution. Ils ne sont pas partis.</p>
<p>Le <a href="https://12factor.net/build-release-run" target="_blank" rel="noopener noreferrer">Facteur V</a>
 est explicite là-dessus : un artefact de build doit être agnostique à l&rsquo;environnement, la config arrivant à l&rsquo;étape de release depuis l&rsquo;extérieur. Dès que des credentials sont compilés dedans, l&rsquo;image n&rsquo;est plus portable. On ne peut plus la promouvoir entre environnements. On builde deux fois en espérant que le deuxième se comporte comme le premier.</p>
<h2 id="comment-vingt-cinq-variables-saccumulent">Comment vingt-cinq variables s&rsquo;accumulent</h2>
<p>Avant de voir comment on a réparé ça, il vaut la peine de comprendre comment on en est arrivé là.</p>
<p>Les tokens <code>BLACKFIRE_*</code> sont le cas facile à comprendre. Un membre de l&rsquo;équipe configure le profiling, a besoin de partager la configuration, et le dépôt est déjà ouvert à tout le monde. Une ligne dans <code>.env</code> est la voie de moindre résistance. Les credentials InfluxDB et Grafana suivent la même logique — outillage partagé, dépôt partagé, un commit.</p>
<p>Puis il y a les variables qui révèlent une autre dérive. Dans certains <code>.env</code> de services :</p>
<pre tabindex="0"><code class="language-dotenv" data-lang="dotenv">APP__RATINGS__SERIALS=&#39;{&#34;marque1&#34;:{&#34;fr&#34;:&#34;12345&#34;},...}&#39;  # ~40 lignes de JSON
APP__YOUTUBE__CREDENTIALS=&#39;{&#34;marque1&#34;:{&#34;client_id&#34;:&#34;xxx&#34;,&#34;refresh_token&#34;:&#34;yyy&#34;},...}&#39;
</code></pre><p>Des numéros de série pour la mesure d&rsquo;audience. Des refresh tokens YouTube par marque. Ce ne sont pas des secrets au sens des tokens Blackfire. Ce sont des données métier — le genre de valeurs qui varient entre marques et environnements, que quelqu&rsquo;un a décidé de versionner dans <code>.env</code> parce qu&rsquo;elles se comportaient comme de la configuration et que <code>.env</code> était l&rsquo;endroit où vivait la configuration.</p>
<p>Vingt-cinq variables, c&rsquo;est la somme de décisions incrémentales, dont aucune ne semblait fausse isolément. Le problème est structurel : quand <code>.env</code> est la seule réponse disponible, tout finit par y ressembler.</p>
<h2 id="où-les-choses-appartiennent-vraiment">Où les choses appartiennent vraiment</h2>
<p>Vider le fichier exigeait de répondre à une question pour chaque variable : <em>où est-ce que ça appartient vraiment ?</em></p>
<p>Les réponses ont révélé trois catégories que l&rsquo;équipe n&rsquo;avait jamais explicitement nommées :</p>
<p><strong>La config statique</strong> vit dans le code. Règles métier, logique de routing, fichiers de paramètres Symfony — tout ce qui ne varie pas entre les déploiements. Un changement exige un rebuild. Les blocs JSON de numéros de série se sont révélés ne pas être de la config statique du tout : ils étaient interrogés depuis un service Config dédié à l&rsquo;exécution. Ils n&rsquo;avaient rien à faire dans un fichier.</p>
<p><strong>La config environnementale</strong> varie entre les déploiements : hostnames, chaînes de connexion, credentials de services tiers. C&rsquo;est ce que le <a href="https://12factor.net/config" target="_blank" rel="noopener noreferrer">Facteur III</a>
 désigne par &ldquo;config dans les variables d&rsquo;environnement&rdquo; — de vraies variables au niveau OS, injectées à l&rsquo;exécution, jamais des fichiers qui voyagent avec le code. Dans Kubernetes, c&rsquo;est un ConfigMap pour les valeurs non sensibles et un Kubernetes Secret pour les credentials. Le choix retenu pour les secrets a été SOPS — les credentials sont chiffrés et committés dans git, plutôt que stockés dans un coffre-fort externe comme Azure Key Vault ou HashiCorp Vault. Un coffre-fort échange la simplicité contre l&rsquo;auditabilité : rotation automatique, logs d&rsquo;audit centralisés, accès via workload identity sans clé à protéger. SOPS échange ces capacités contre un modèle opérationnel plus simple — pas de service externe à interroger au déploiement, les secrets transitent par le processus de review normal du code, l&rsquo;historique git fait office de piste d&rsquo;audit. Les contreparties acceptées sont la rotation manuelle et la responsabilité de protéger la clé de déchiffrement elle-même. Pour la taille de l&rsquo;équipe, le compromis était délibéré.</p>
<p><strong>La config dynamique</strong> change sans déploiement : paramètres éditoriaux, seuils par marque, configuration de modération de contenu. Elle appartient à une base de données, gérée via le service Config de l&rsquo;application. Une partie de ce qui s&rsquo;était accumulé dans les <code>.env</code> de services était cette catégorie depuis le début, passant pour des valeurs par défaut statiques parce qu&rsquo;elle changeait assez rarement pour que personne ne le remarque.</p>
<p>Une fois les catégories nommées, les variables se sont triées. Le <code>.env</code> racine est arrivé à quatre lignes :</p>
<pre tabindex="0"><code class="language-dotenv" data-lang="dotenv">DOMAIN=platform.127.0.0.1.sslip.io
XDEBUG_MODE=off
SERVER_NAME=:80
APP_ENV=dev
</code></pre><p>Des valeurs par défaut sûres. Rien de sensible. <code>dump-env prod</code> compile maintenant des chaînes vides ; les vraies valeurs arrivent à l&rsquo;exécution depuis Kubernetes.</p>
<h2 id="limage-postgresql">L&rsquo;image PostgreSQL</h2>
<p>L&rsquo;image PostgreSQL utilisée en CI a un mot de passe codé en dur :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> <span style="color:#e6db74">postgres:15</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">ENV</span> POSTGRES_PASSWORD<span style="color:#f92672">=</span>admin123
</span></span></code></pre></div><p>Ça ressemble au même problème. Ce n&rsquo;en est pas un, parce que le modèle de menace est différent. La base CI est éphémère — elle existe le temps d&rsquo;un run de pipeline, ne contient pas de vraies données, tourne dans un réseau isolé. Un mot de passe codé en dur sur une base de test jetable est un risque acceptable, pas une entorse à la règle.</p>
<p>En production, la question ne se pose pas : la plateforme utilise Azure Flexible Server, un service PostgreSQL managé. Il n&rsquo;y a pas d&rsquo;image Docker. Les credentials arrivent via injection dans les charts Helm, sans jamais toucher un layer.</p>
<h2 id="ce-qui-survit-au-build-maintenant">Ce qui survit au build maintenant</h2>
<p>L&rsquo;image qui part en production contient maintenant une garantie : <code>var_dump(require '.env.local.php')</code> ne retourne que des chaînes vides et des valeurs par défaut sûres. Les credentials ne sont pas là parce qu&rsquo;ils n&rsquo;y ont jamais été mis — ils arrivent à l&rsquo;exécution, depuis l&rsquo;extérieur.</p>
<p>C&rsquo;est la frontière de responsabilité que <code>dump-env</code> avait silencieusement effacée : l&rsquo;image est l&rsquo;application, le runtime est l&rsquo;environnement. Ils ne devraient pas connaître les secrets de l&rsquo;autre.</p>
]]></content:encoded></item></channel></rss>