Aller au contenu

Publier un nouveau service sur *.savethemall.eu

Procédure complète pour exposer un service sur un nom public, protégé par Authentik. Compter une trentaine de minutes, dont l'essentiel en attente de propagation DNS.

Dans tout ce qui suit, remplacer monservice par le sous-domaine visé et monservice:8080 par le nom de service Compose et son port.

Ce que la procédure suppose acquis

  • Le conteneur tourne et répond en HTTP sur infrastructure_network.
  • Le contrôle : depuis vm-docker, docker exec caddy wget -qO- http://monservice:8080/ | head -5. Si cette commande ne rend rien, s'arrêter ici : rien de ce qui suit ne compensera un upstream muet.

L'ordre, et pourquoi il n'est pas négociable

flowchart TB
    A["1 · Record A chez OVH"]
    B["2 · Certificat acme.sh<br/>validation DNS-01"]
    C["3 · Provider + application<br/>Authentik"]
    D["4 · Bloc Caddy vm-docker"]
    E["5 · Bloc Caddy nas-caddy"]
    F["6 · Secrets, s'il y en a"]
    G["7 · decisions.md, commit"]

    A -->|"sans le record,<br/>la validation DNS-01 échoue"| B
    B -->|"sans le cert, `caddy validate`<br/>refuse et le reload COUPE<br/>les 76 autres sites"| D
    C -->|"l'app doit exister,<br/>sinon 404 au lieu de 302"| D
    D -->|"le cert doit être copié<br/>sur le NAS"| E
    E --> G
    F --> G

    B -.->|"rsync quotidien<br/>ou déclenché à la main"| E

    classDef risk stroke-width:3px
    class D risk

La seule étape qui peut casser autre chose est la 4 : le Caddy de vm-docker porte 76 vhosts, et un rechargement sur une configuration invalide les couche tous. D'où caddy validate avant tout restart, et le certificat avant le bloc.


1. Enregistrement DNS chez OVH

Toujours en premier. acme.sh valide en DNS-01 : sans l'enregistrement, l'émission du certificat échoue, et l'échec ressemble à un problème d'identifiants.

ssh -p <port-ssh> <user>@vm-docker \
  "sudo /docker-mounts/homelab-vault/00-infra/scripts/ovh-record.sh \
     add savethemall.eu A monservice <IP publique> --apply"

Contrôle

dig +short monservice.savethemall.eu @1.1.1.1
# attendu : <IP publique>

Surtout pas le résolveur local : le DNS interne répond déjà pour ce nom, par joker, et donnerait une fausse confirmation.

Et pas non plus les serveurs autoritatifs d'OVH — ns.ovh.net et dns1.ovh.net ne répondent pas aux requêtes directes depuis ce parc (mesuré le 2026-08-29 depuis vm-claude et depuis le VPS : communications error … timed out). Un résolveur public a déjà fait le travail.


2. Certificat TLS

Ajouter le domaine à la liste d'émission — c'est elle qui sera relue au prochain déploiement complet — puis émettre le certificat seul.

# 1. inscrire le domaine dans la liste
ssh -p <port-ssh> <user>@vm-docker \
  "grep -q 'monservice.savethemall.eu' /opt/docker/acme/issue-all.sh || \
   sed -i 's|^)|  monservice.savethemall.eu\n)|' /opt/docker/acme/issue-all.sh"

# 2. émettre ce seul certificat
ssh -p <port-ssh> <user>@vm-docker \
  "docker exec acme acme.sh --issue --dns dns_ovh -d monservice.savethemall.eu \
     --cert-file      /certs/monservice.savethemall.eu/cert.pem \
     --key-file       /certs/monservice.savethemall.eu/privkey.pem \
     --fullchain-file /certs/monservice.savethemall.eu/fullchain.pem \
     --reloadcmd      \"curl -sf -X POST http://127.0.0.1:2019/load \
                        -H 'Content-Type: text/caddyfile' \
                        --data-binary @/etc/caddy/Caddyfile\""

--dnssleep est obligatoire ici, et la raison n'est pas chez OVH

Sans lui, l'émission échoue systématiquement sur :

Timed out waiting for DNS.
Error checking DNS.

acme.sh vérifie son enregistrement TXT par DoH sur cloudflare-dns.com, et AdGuard résout ce nom vers 0.0.0.0 : le filtrage anti-contournement DNS de la maison bloque son propre outil d'émission. Le TXT est pourtant bien posé chez OVH, puis proprement retiré — d'où un échec qui ressemble à un problème d'identifiants ou de propagation.

Le contrôle qui tranche en une commande :

docker exec acme sh -c 'nslookup cloudflare-dns.com | tail -3'
# Address: 0.0.0.0  → c'est AdGuard, pas OVH

--dnssleep remplace la vérification par une attente fixe. C'est le contournement ; le correctif est d'autoriser cloudflare-dns.com dans AdGuard.

Contrôle

ssh -p <port-ssh> <user>@vm-docker \
  "ls -l /opt/docker/acme/certs/monservice.savethemall.eu/fullchain.pem"

Le NAS reçoit sa copie au rsync quotidien. Pour ne pas attendre, la déclencher à la main avant l'étape 5.


3. Application et fournisseur dans Authentik

Dans l'interface d'administration :

  1. Applications → Providers → Create → Proxy Provider
    • Mode : Forward auth (single application) — pas Proxy, pas Domain level.
    • External host : https://monservice.savethemall.eu
    • Flux d'autorisation : celui du parc (consentement implicite).
  2. Applications → Create
    • Slug : monservice
    • Provider : celui qui vient d'être créé
    • Icon : chercher le logo officiel avant de créer l'application. Par ordre : le dépôt GitHub du projet, le /favicon.svg du site officiel, https://cdn.simpleicons.org/<nom>, puis le catalogue https://cdn.jsdelivr.net/gh/selfhst/icons/svg/<nom>.svg.
  3. Applications → Outposts → authentik Embedded Outpost → Edit → Applications
    • Ajouter l'application à la main.

L'outpost embarqué ne récupère pas l'application tout seul

C'est l'oubli le plus fréquent de cette procédure. Sans cet ajout explicite, tout est correctement configuré côté Authentik, Caddy renvoie bien vers le flux de connexion, et la connexion échoue quand même — sans message qui désigne la cause.


4. Bloc Caddy sur vm-docker

Éditer 10-stacks/caddy/Caddyfile dans le vault, puis synchroniser. Le fichier du vault n'est pas un lien symbolique : les deux copies sont réelles et divergent si on oublie.

monservice.savethemall.eu {
    import tls_cert monservice.savethemall.eu
    # Endpoints de l'outpost embarqué (sign_out, start, callback) → Authentik
    handle /outpost.goauthentik.io/* {
        reverse_proxy authentik-server:9000
    }
    handle {
        import authentik
        reverse_proxy monservice:8080
    }
}

L'ordre des deux handle n'est pas décoratif : voir forward_auth passe avant handle.

Synchronisation, obligatoire après toute édition :

ssh -p <port-ssh> <user>@vm-docker \
  "cp /docker-mounts/homelab-vault/10-stacks/caddy/Caddyfile /opt/docker/caddy/Caddyfile && \
   docker compose -f /opt/docker/caddy/docker-compose.yml restart"

Contrôle — en LAN, où le DNS pointe déjà sur vm-docker

curl -s -o /dev/null -w '%{http_code}\n' https://monservice.savethemall.eu/
# 302 vers auth.savethemall.eu → le mur est en place

curl -s -o /dev/null -w '%{http_code}\n' \
  https://monservice.savethemall.eu/outpost.goauthentik.io/zzz
# 404 → la route de l'outpost existe (302 ici = le handle est mal placé)

5. Bloc Caddy sur nas-caddy — c'est lui qui ouvre le WAN

Tant que cette étape n'est pas faite, le service répond en LAN et en tailnet, et reste invisible depuis Internet. C'est le NAS qui est l'edge.

Fichier : /volume3/docker/nas-caddy/Caddyfile sur le NAS.

monservice.savethemall.eu {
    import tls_cert monservice.savethemall.eu
    import to_vmdocker
}

Rien d'autre : l'authentification et le routage restent en aval, sur vm-docker. Le snippet (to_vmdocker) porte déjà header_up Host {host} — sans lui, l'amont recevrait Host: vm-docker.

Variante : garder le service hors du WAN

Pour un service qui doit rester interne, ajouter import lan_only au bloc plutôt que de ne pas le déclarer. Ici, remote_ip est bien le client réel : nas-caddy est l'edge, il n'y a pas de proxy en amont pour brouiller la source.

Contrôle — le vrai chemin public

curl -s -o /dev/null -w '%{http_code}\n' \
  --resolve monservice.savethemall.eu:443:<IP publique> \
  https://monservice.savethemall.eu/

Sans --resolve, la commande repasse par le DNS interne et ne teste rien du chemin WAN.


6. Secrets, s'il y en a

  • Les valeurs vivent dans /opt/docker/monservice/.env sur l'hôte, jamais dans le vault en clair, jamais dans le fichier Compose.
  • La version chiffrée va dans 00-infra/secrets/vm-docker/monservice.enc.env (SOPS + age). L'arbre est indexé par hôte : trois noms de stack existent sur deux hôtes ou plus, et sans cette qualification le second écrase le premier — sans bruit.
  • Seule la clé publique age est nécessaire pour chiffrer. Aucune clé privée n'a besoin d'exister sur les autres hôtes.
  • Tout jeton d'API installé se range dans Vaultwarden dans la foulée, sous la convention <hôte>/<stack>·<NOM_DE_VARIABLE>.

Le ciphertext poussé est permanent

Ces fichiers partent aussi sur GitHub. Le dépôt est privé, mais ce qui a été poussé y reste : une rotation de secret ne rend pas illisible le chiffré déjà publié. Rotation veut dire rotation en amont, pas simplement réécriture du fichier.

Jamais de secret dans une URL

La trace d'erreur journalise l'URL résolue, jeton compris. De même, Caddy n'expurge automatiquement que trois en-têtes — Authorization, Cookie, Set-Cookie. Un en-tête maison (X-Mon-Token) part en clair dans access.log, qui est lu par CrowdSec. Tout nouvel en-tête porteur d'un secret doit être ajouté au filtre du journal en même temps qu'il est créé.


7. Écrire ce qui vient d'être fait

  • Entrée datée dans 10-stacks/monservice/decisions.md : raison, et comment revenir en arrière.
  • Mise à jour de la table maîtresse des stacks.
  • Commit du vault et poussée sur les deux dépôts distants.

Récapitulatif

  • Enregistrement A chez OVH, vérifié sur le serveur autoritatif
  • Certificat émis, domaine ajouté à la liste d'émission
  • Fournisseur Proxy Authentik en mode forward auth (single application)
  • Application créée, avec son logo
  • Application ajoutée à la main à l'outpost embarqué
  • Bloc Caddy vm-docker, dans le vault et synchronisé sur l'hôte
  • Sonde /outpost.goauthentik.io/zzz → 404
  • Bloc Caddy nas-caddy, certificat copié sur le NAS
  • Contrôle WAN avec --resolve
  • Secrets chiffrés dans le vault, jeton rangé dans le coffre
  • decisions.md, commit, double poussée

Dernière vérification de cette page : 2026-09-08