Aller au contenu
Cloud medium

Chapitre 4 — Frontends Nginx HTTPS bout en bout sur OUTSCALE

17 min de lecture

logo 3ds outscale

Ce chapitre déploie les 3 frontends Nginx du capstone : une VM par AZ (eu-west-2a, b, c), placée dans le subnet privé de chaque Net, accédée uniquement via le bastion (SSH) et via les futurs LBU (HTTPS 443/tcp passthrough). Chaque frontend présente un certificat distinct signé par une CA interne générée au playbook — c'est le SSL bout en bout qui rend pertinent le passthrough du Chapitre 5 : le LBU ne déchiffre rien, le client final voit le certificat du frontend qui sert effectivement la requête. Public visé : intermédiaire à avancé, avec les Chapitres 1, 2 et 3 appliqués sur le compte cible. À la sortie de ce chapitre, vous pouvez curl -k https://<ip-privée-d-un-nginx>/ depuis le bastion et voir la page de démo de l'AZ correspondante.

  • Coder un stack Terraform 20-app/ qui crée 3 frontends + 3 SG distincts (un par Net — contrainte OUTSCALE).
  • Distinguer SG-to-SG intra-Net (bastion → frontend Net-A) et CIDR cross-Net (bastion → frontend Net-B/C via peering).
  • Gérer une CA interne reproductible côté localhost avec community.crypto, sans dépendance Internet.
  • Émettre un certificat serveur par AZ signé par la CA, sans qu'aucune clé privée serveur ne reste sur le poste opérateur après le push.
  • Utiliser le ProxyJump bastion dans Ansible via group_vars/tier_private.yml — pas d'IP en dur dans la config SSH.
  • Diagnostiquer les pièges classiques : SG scoped au Net, cycle Terraform sur la recréation de SG attaché, ControlMaster SSH saturé.
  • Les Chapitres 1, 2 et 3 appliqués (72 + 5 ressources actives).
  • Bastion durci avec AllowTcpForwarding yes (Chapitre 3) — le ProxyJump en dépend.
  • Collection Ansible : ansible-galaxy collection install community.crypto.
  • L'inventaire dynamique osc_vm configuré.

Chaque frontend vit dans un Net distinct avec sa propre AZ. Trois raisons structurelles :

  • Reliability — perte d'AZ tolérée. La perte de eu-west-2a n'affecte que nginx-a ; les deux autres restent up. Le LBU au Chapitre 5 prendra cette AZ comme défaillante et arrêtera de lui envoyer du trafic.
  • Performance — latence locale. Le LBU de Net-A discute avec nginx-a sans traverser de peering — le RTT est minimal. Le peering inter-Net devient le repli, pas le chemin nominal.
  • Cost / Sustainability — pas de NAT cross-AZ inutile. Une page servie depuis Net-A ne génère aucun trafic NAT-egress dans Net-B ou Net-C. Le facturable reste local.

Le prix à payer : 3 VMs à patcher au lieu d'1, 3 certificats à renouveler. Au Chapitre 5, le for_each sur les 3 LBU absorbe la complexité — c'est pourquoi on l'a structuré ainsi dès le Chapitre 1.

Les frontends n'ont aucune IP publique. Le seul ingress autorisé sur eux :

  • 22/tcp depuis le subnet public de Net-A (où vit le bastion).
  • 443/tcp depuis les 3 subnets publics (où vivront les LBU au Chapitre 5).

Aucune route 0.0.0.0/0 vers IGW depuis le subnet privé. La sortie internet (pour apt update au playbook) passe par le NAT Service de chaque Net créé au Chapitre 1. Surface d'attaque entrante minimale — un attaquant qui scanne eu-west-2 ne voit jamais les frontends, seulement les 3 IPs publiques des LBU (Chapitre 5) et l'EIP du bastion.

Sur OUTSCALE, un Security Group appartient à un Net (mentionné dans net_id). On ne peut pas attacher un SG de Net-A à une VM de Net-B. C'est la première contrainte qui surprend quand on vient d'AWS où VPC peering laisse cette flexibilité (avec PeeringConnections SG cross-VPC).

Conséquence pour le capstone : on a besoin de 3 SG distincts, un par Net, avec les mêmes règles. La compaction for_each rend ça lisible :

resource "outscale_security_group" "nginx_frontends" {
for_each = local.nets # 3 entrées : a, b, c
description = "SG frontends Nginx ${var.project} - ${each.key}"
security_group_name = "${var.project}-nginx-frontends-${each.key}"
net_id = each.value.net_id
# tags...
}
resource "outscale_vm" "nginx_frontend" {
for_each = local.frontends
subnet_id = each.value.subnet_id
security_group_ids = [outscale_security_group.nginx_frontends[each.key].security_group_id]
# ...
}

Une clé partagée (each.key = a, b, c) entre les SG et les VMs permet d'écrire une référence symétrique. Si on renommait les clés, les 6 ressources se renommeraient ensemble et garderaient la cohérence.

Au Chapitre 3, le bastion utilise une règle SG-to-SG propre : son SG en source, le SG du frontend en destination. Élégant, indépendant des CIDRs, idéal pour une infra mouvante.

Problème : sur OUTSCALE, un SG-to-SG ne traverse pas un peering. La règle « SG-source = bastion-ssh (Net-A) → SG-cible = nginx-frontends-b (Net-B) » est rejetée par l'API. Le seul moyen pour que le bastion (Net-A) atteigne les frontends de Net-B et Net-C est de pointer un CIDR.

Le compromis adopté pour le capstone : autoriser 22/tcp depuis le CIDR du subnet public de Net-A (où vit le bastion). C'est un /24 stable, géré par le stack 00-nets, donc pas de fuite quand le bastion est recréé.

locals {
bastion_subnet_cidr = local.subnets["a_public"].cidr # 10.10.0.0/24
}
resource "outscale_security_group_rule" "ssh_from_bastion_subnet" {
for_each = local.nets # une règle par Net
flow = "Inbound"
security_group_id = outscale_security_group.nginx_frontends[each.key].security_group_id
from_port_range = 22
to_port_range = 22
ip_protocol = "tcp"
ip_range = local.bastion_subnet_cidr
}

C'est moins fin qu'un SG-to-SG (n'importe quelle VM qui apparaîtrait dans le subnet public de Net-A pourrait théoriquement SSH sur les frontends), mais le subnet public ne contient que le bastion dans ce capstone. La règle reste défendable côté audit.

Pourquoi SSL bout en bout et pas terminé sur le LBU

Section intitulée « Pourquoi SSL bout en bout et pas terminé sur le LBU »

C'est une décision d'architecture, pas un détail technique. Trois critères :

AspectSSL terminé sur le LBUSSL bout en bout (passthrough)
Confidentialité du flux LBU↔frontendEn clair (HTTP) ou re-chiffré (HTTPS)Chiffré nativement
Visibilité du LBU sur le payloadTotale (header injection, WAF L7, sticky cookie)Aucune (TCP transparent)
Coût opérationnel TLSCentralisé sur le LBU (1 cert)Distribué (1 cert / frontend)
Surface attaque sur le LBUCert privé + clé sur le LBULBU agnostique du contenu
Conformité (HDS, secteur santé)Déchiffrement intermédiaire = parfois litigieuxChemin TLS continu, audit simple

Pour le capstone qui vise une posture proche de SecNumCloud / HDS, le passthrough est l'option qui simplifie l'audit conformité — aucune machine intermédiaire ne voit jamais le payload en clair. Le coût opérationnel est absorbé par Ansible (le playbook gère 3 certs aussi facilement qu'1).

  • Répertoireterraform/
    • Répertoire20-app/
      • provider.tf
      • variables.tf
      • locals.tf
      • security-group.tf
      • nginx-frontends.tf
      • outputs.tf

Total : 3 SG + 3 SG rule SSH + 3 SG rule HTTPS × 3 CIDRs (= 9) + 3 VMs = 18 ressources. Le 9 du HTTPS vient du produit cartésien (3 SG × 3 CIDRs publics) — chaque SG accepte le 443 depuis n'importe lequel des 3 subnets publics, parce qu'un LBU peut router cross-Net via peering :

locals {
https_rules = merge([
for net_key, _ in local.nets : {
for cidr in local.public_cidrs :
"${net_key}_from_${replace(cidr, "/", "_")}" => {
net_key = net_key
cidr = cidr
}
}
]...)
}
resource "outscale_security_group_rule" "https_from_public_subnets" {
for_each = local.https_rules
flow = "Inbound"
security_group_id = outscale_security_group.nginx_frontends[each.value.net_key].security_group_id
from_port_range = 443
to_port_range = 443
ip_protocol = "tcp"
ip_range = each.value.cidr
}

C'est dense, mais ça scale : passer à 4 Nets = 4 SG, 16 règles HTTPS. Le code est inchangé.

Le bastion expose une EIP fixe, mais l'IP elle-même peut changer entre projets (ou entre régions). On résout dynamiquement via les hostvars de l'inventaire osc_vm. Un seul fichier dans group_vars/:

ansible/group_vars/tier_private.yml
ansible_ssh_common_args: >-
-o ProxyJump=outscale@{{ hostvars["capstone-bastion-a"].public_ip }}
-o StrictHostKeyChecking=accept-new
-o UserKnownHostsFile=/dev/null

Toutes les VMs taguées tier=private (les 3 frontends, plus tard les backends et la BDD) héritent automatiquement du ProxyJump. Si on régénère le bastion avec une nouvelle EIP, rien à changer côté playbook — la résolution se refait au prochain run.

La CA interne capstone — community.crypto côté localhost

Section intitulée « La CA interne capstone — community.crypto côté localhost »

Le playbook gère deux niveaux de PKI :

  • CA interne (localhost) — la racine. Une seule fois dans la vie du projet. Idempotente — relancer ne re-génère pas la CA.
  • Cert serveur par frontend (localhost) — généré, signé par la CA, copié sur la VM avec mode: 0600 et no_log: true.
- name: CA interne capstone (localhost)
hosts: localhost
connection: local
tasks:
- community.crypto.openssl_privatekey:
path: "{{ ca_dir }}/ca.key"
size: 4096
mode: "0600"
- community.crypto.x509_certificate:
path: "{{ ca_dir }}/ca.crt"
privatekey_path: "{{ ca_dir }}/ca.key"
provider: selfsigned
selfsigned_not_after: "+3650d"
ownca_create_authority_key_identifier: true
mode: "0644"

Le répertoire .ca/ est gitignored — la clé privée de la CA ne quitte jamais le poste opérateur. En production, elle vivrait dans un coffre-fort (HashiCorp Vault, hashicorp/cosign, OUTSCALE OOS chiffré avec clé client) — pour le capstone, elle reste locale et c'est documenté.

- name: Frontends Nginx HTTPS
hosts: role_nginx_frontend
become: true
tasks:
- community.crypto.openssl_privatekey:
path: "{{ ca_dir }}/{{ inventory_hostname }}.key"
size: 2048
delegate_to: localhost
become: false
- community.crypto.openssl_csr:
path: "{{ ca_dir }}/{{ inventory_hostname }}.csr"
privatekey_path: "{{ ca_dir }}/{{ inventory_hostname }}.key"
common_name: "{{ inventory_hostname }}"
subject_alt_name:
- "DNS:{{ inventory_hostname }}"
- "IP:{{ ansible_default_ipv4.address }}"
delegate_to: localhost
become: false
- community.crypto.x509_certificate:
path: "{{ ca_dir }}/{{ inventory_hostname }}.crt"
csr_path: "{{ ca_dir }}/{{ inventory_hostname }}.csr"
ownca_path: "{{ ca_dir }}/ca.crt"
ownca_privatekey_path: "{{ ca_dir }}/ca.key"
provider: ownca
ownca_not_after: "+365d"
delegate_to: localhost
become: false
# Push sur la VM avec mode 0600 + no_log
- ansible.builtin.copy:
src: "{{ ca_dir }}/{{ inventory_hostname }}.key"
dest: /etc/nginx/ssl/server.key
mode: "0600"
no_log: true
notify: Restart nginx

delegate_to: localhost exécute la tâche côté poste opérateur mais avec le contexte de la VM cible (les variables inventory_hostname, ansible_default_ipv4.address restent celles de la VM). C'est le pattern idiomatique pour une PKI Ansible-driven : on génère localement avec la CA puis on pousse.

  1. Vérifier que le bastion est durci et joignable.

    Fenêtre de terminal
    ssh outscale@$(terraform -chdir=terraform/10-bastion output -raw bastion_public_ip) 'whoami'
  2. Apply Terraform 20-app/.

    Fenêtre de terminal
    cd terraform/20-app/
    terraform init
    terraform plan # → 18 ressources à créer
    terraform apply

    Plan attendu : 18 to add. Compter ~3 minutes (les 3 VMs montent en parallèle).

  3. Vérifier l'inventaire dynamique depuis ansible/.

    Fenêtre de terminal
    cd ../ansible/
    rm -rf .osc_vm_cache
    ansible-inventory --graph
    # → role_nginx_frontend doit contenir capstone-nginx-{a,b,c}
  4. Lancer le playbook.

    Fenêtre de terminal
    ansible-galaxy collection install community.crypto
    ansible-playbook playbooks/deploy-nginx-frontends.yml

    Compter ~2 minutes (install Nginx via apt = la phase la plus longue, ~30 s par hôte).

  5. Tester depuis le bastion.

    Fenêtre de terminal
    ssh outscale@<bastion_public_ip> '
    for ip in $(cd /home/outscale; echo "10.10.1.X 10.11.1.X 10.12.1.X"); do
    echo "=== $ip ==="
    curl -sk https://$ip/healthz
    curl -sk https://$ip/ | grep "Frontend Nginx"
    done'

    Sortie attendue : ok\n × 3 + 3 lignes <h1>Frontend Nginx — capstone OUTSCALE</h1>. Sur ce capstone, les IPs résolues sont 10.10.1.229, 10.11.1.92, 10.12.1.59.

SymptômeCauseSolution
The SecurityGroupId 'sg-XXX' doesn't exist in this NetSG attaché à une VM d'un autre NetUtiliser for_each = local.nets pour créer un SG par Net
The Security Group is the unique Security Group of the following VMs (au destroy/rename)Refactor SG en for_each avec VM existanteDétruire la VM d'abord, state rm du SG, ré-apply
Connection timed out during banner exchange (Ansible)ControlMaster SSH saturé / bastion MaxStartupsrm -f ~/.ansible/cp/* + ControlPersist=600s
Permission denied (publickey) sur les frontends via ProxyJumpKeypair capstone-bastion pas réutiliséeVérifier var.frontend_keypair_name = "capstone-bastion"
curl: (35) error:14094410:SSL routines:ssl3_read_bytes:sslv3 alert handshake failureLe client teste TLS 1.0/1.1 (banni dans la conf Nginx)Tester avec --tlsv1.2 ; sur clients legacy, ajuster ssl_protocols
nginx -t OK mais le site retourne 502 / déconnecteTLS-passthrough du LBU pas encore en placeNormal au Chapitre 4 — le LBU arrive au Chapitre 5
  • 3 SG nginx-frontends, un par Net — contrainte OUTSCALE (SG scoped au Net).
  • SG-to-SG ne traverse pas un peering — fallback CIDR du subnet bastion (10.10.0.0/24 stable).
  • 3 frontends en subnets privés, sortie Internet uniquement via NAT (apt updates au playbook).
  • CA interne capstone-internal-ca générée par community.crypto, idempotente, gitignored.
  • Cert serveur signé localement puis poussé en mode 0600 + no_log: true — la clé privée ne traîne pas dans les logs.
  • ControlPersist=600s + ServerAliveInterval=20 dans ansible.cfg quand on enchaîne des plays via ProxyJump.
  • Le passthrough TLS au Chapitre 5 dépend de cette PKI — sans certs valides côté frontend, le LBU ne peut pas faire son rôle.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn