← Accueil

Construire ce site comme un service en production

Terraform, OIDC et une CSP stricte

· Lire l’étude de cas

Comment je déploie mon portfolio sur AWS sans aucune clé stockée, et pourquoi sa Content Security Policy n’a besoin d’aucune exception.

L’idée

Un portfolio, c’est simple : quelques pages statiques. Je voulais héberger le mien comme je le ferais pour un service au travail, pour que l’infrastructure, le déploiement et la sécurité fassent partie de ce qu’il montre. Le site est construit avec Astro, stocké dans un bucket S3 privé et servi par CloudFront. Chaque ressource AWS est décrite en Terraform, et GitLab CI le construit et le déploie.

Trois choix portent l’essentiel : une Content Security Policy stricte, un déploiement sans identifiant longue durée, et un état Terraform stocké et verrouillé à distance. Cet article reprend chacun d’eux, puis ce qui a mal tourné.

Une Content Security Policy sans exception

Une CSP stricte est facile à écrire et difficile à tenir, car la plupart des sites reposent sur des scripts ou styles inline qu’elle bloquerait. Je suis donc parti de l’autre bout : j’ai construit le site pour que rien d’inline n’existe, puis j’ai écrit la politique.

Deux réglages d’Astro font l’essentiel du travail. inlineStylesheets: 'never' force chaque feuille de style dans un fichier, et assetsInlineLimit: 0 fait de même pour les scripts, donc le build n’intègre jamais de code inline. La politique peut alors partir de default-src 'none' et autoriser chaque type de ressource depuis ma propre origine uniquement.

La politique servie par CloudFront
default-src 'none'; script-src 'self'; style-src 'self';
img-src 'self' data:; font-src 'self'; connect-src 'self';
manifest-src 'self'; base-uri 'none'; form-action 'none';
frame-ancestors 'none'; upgrade-insecure-requests

Le bouton de mode sombre était le seul endroit où j’étais tenté d’ajouter un script inline, pour appliquer le thème enregistré avant l’affichage de la page et éviter un flash. À la place, c’est un petit fichier externe chargé de façon synchrone dans le head. C’est un script ordinaire de script-src 'self', et le flash a disparu.

Le bloc JSON-LD que j’ai ajouté pour les moteurs de recherche est un bloc de données, pas du code exécutable, donc la politique ne s’y applique pas. Résultat : Mozilla Observatory note les en-têtes A+ (130 sur 100, les 12 tests réussis), et il n’y a aucun 'unsafe-inline' nulle part.

Déployer sans clé AWS

La méthode classique pour déployer de la CI vers AWS est de créer un utilisateur IAM, générer une clé d’accès et la stocker comme variable de CI. Cette clé n’expire jamais, et quiconque la vole obtient les mêmes droits que le pipeline.

Avec OIDC, GitLab donne à chaque job un jeton signé de courte durée qui indique de quel projet et de quelle branche il vient. AWS vérifie le jeton avec une politique de confiance et remet au job des identifiants temporaires. Rien de secret n’est stocké dans GitLab : seulement l’ARN du rôle, le nom du bucket et l’identifiant de la distribution, qui sont des identifiants, pas des secrets.

L’essentiel est la politique de confiance. Elle n’accepte qu’un seul projet sur une seule branche :

Condition de la politique de confiance (Terraform)
condition {
  test     = "StringEquals"
  variable = "${local.gitlab_host}:sub"
  values   = ["project_path:${var.gitlab_project_path}:ref_type:branch:ref:${var.gitlab_branch}"]
}

Une merge request ou une autre branche produit une autre valeur de sub et est refusée. Le rôle lui-même ne peut lister et écrire des objets que dans ce bucket et invalider cette distribution, et les identifiants qu’il fournit durent au plus une heure.

En pratique, une merge request lance toutes les vérifications mais ne peut pas déployer, et seule main peut assumer le rôle.

Un état Terraform digne de confiance

Terraform garde un fichier d’état qui relie le code aux ressources réelles. Conservé sur un seul ordinateur, il est fragile : si on perd le fichier, Terraform oublie ce qu’il a créé. L’état vit maintenant dans un bucket S3 versionné et chiffré avec un verrouillage natif (use_lockfile, disponible depuis Terraform 1.10), donc deux exécutions ne peuvent jamais l’écrire en même temps, et aucune table DynamoDB n’est nécessaire.

Le bucket qui contient l’état ne peut pas être créé par la configuration qui l’utilise, donc une seconde configuration Terraform minuscule, appelée bootstrap, le crée une fois pour toutes.

La CI n’applique jamais rien. Elle vérifie le formatage, valide le code et lance Checkov à chaque pipeline, et je lance terraform apply moi-même après avoir lu le plan. Chaque règle Checkov que j’ignore est documentée avec sa raison (coût, ou inadéquation à un site statique), et toute nouvelle alerte fait échouer le build.

Ce qui a mal tourné en chemin

Les sous-pages renvoyaient des erreurs. L’objet racine par défaut de CloudFront ne s’applique qu’à l’URL racine, donc /projects/this-website/ demandait à S3 un dossier qui n’existe pas. Une minuscule CloudFront Function réécrit maintenant ces requêtes vers index.html.

Le vérificateur de liens bloquait son propre déploiement. Chaque page déclare une URL canonique sur le vrai domaine, et le vérificateur la suivait. Pour une nouvelle page, cette URL n’existe pas avant le déploiement, qui attend le vérificateur. Je lui dis maintenant d’ignorer mon propre domaine ; les liens internes sont toujours vérifiés dans le résultat du build.

Limites de région. Mon organisation AWS n’autorise que eu-north-1, hors services globaux. Les certificats CloudFront doivent se trouver en us-east-1, donc le certificat est la seule chose créée là-bas.

Checkov passait sur mon ordinateur et échouait en CI. Mon fichier de variables local n’est pas dans Git, donc la CI voyait d’autres valeurs par défaut. Un petit ci.tfvars avec des valeurs non secrètes rend les deux exécutions identiques.

La suite

Il reste l’idée de citer une recommandation quand j’en aurai une. La liste complète des décisions, avec le schéma d’architecture, se trouve dans l’étude de cas.

Contact

M’écrireLinkedIn

Basé à Lyon, France

Dernière mise à jour

Télécharger le CV (en anglais)Contact