Une abstraction pour gérer TLS dans les applications auto-hébergées

Nous avons récemment rendu Hyvor Blogs auto-hébergeable. En travaillant sur ce projet, j'ai conçu une abstraction reposant sur certains choix : une manière de réfléchir à la gestion de TLS dans les applications auto-hébergées.

Mode TLS

Le principe fondamental consiste à introduire une configuration TLS_MODE prenant en charge les valeurs suivantes :

  • auto : l'application gère TLS

  • external : un proxy inverse termine TLS

  • manual : l'utilisateur fournit les certificats

  • disabled : TLS est désactivé ; l'application sert uniquement du HTTP

Mode TLS : auto

L'application écoute sur le port 443 et gère elle-même TLS. Elle génère un certificat à l'aide d'un service comme Let's Encrypt et le renouvelle automatiquement. Cela peut être géré à l'aide d'un serveur HTTPS automatique comme Caddy.

Je pense qu'il est logique de conserver cette option par défaut. La prise en main est plus simple. L'utilisateur n'a pas à se préoccuper de la génération des certificats TLS.

Cependant, pour utiliser Let's Encrypt (le protocole ACME), le domaine doit être public. Cette option ne couvre donc pas tous les cas d'utilisation à elle seule, mais elle facilite l'auto-hébergement de votre application.

Si cette option est sélectionnée par défaut, vous devrez également accepter le nom de domaine comme entrée ou variable d'environnement. Cela avait du sens pour Hyvor Blogs, car il est déjà conçu pour fonctionner avec plusieurs domaines (les domaines personnalisés des blogs des utilisateurs), et la définition de DOMAIN_APP était nécessaire.

Mode TLS : external

Dans ce mode, l'application s'exécute derrière un proxy inverse qui termine TLS. Cela couvre une grande variété de cas d'utilisation : le routage au sein d'un réseau privé, une solution comme Kubernetes Ingress, Cloudflare Flexible SSL, et bien plus encore.

L'application elle-même écouterait sur le port 80 ou un autre port. Le proxy inverse terminerait TLS, auquel cas il gérerait les certificats et transmettrait les requêtes au port HTTP de l'application.

Ici, une configuration TRUSTED_PROXY est très importante. Lorsqu'elle s'exécute derrière un proxy inverse, votre application peut avoir besoin de savoir si l'utilisateur final utilise HTTPS ainsi que de connaître son adresse IP. La norme consiste à utiliser respectivement les en-têtes X-Forwarded-Proto et X-Forwarded-For. Cependant, l'application ne devrait pas faire aveuglément confiance à ces en-têtes, car cela permettrait à n'importe qui de les usurper et de causer des dommages. Les utilisateurs devraient pouvoir définir des plages CIDR pour les proxies de confiance.

Mode TLS : manual

Permettre à l'utilisateur de fournir ses propres certificats. Cela offre davantage de flexibilité à votre application et couvre de nombreux cas où celle-ci s'exécute entièrement sur un réseau privé ou derrière un proxy externe (Cloudflare Strict SSL).

Dans Hyvor Blogs, nous demandons à l'utilisateur de monter les certificats dans un chemin standard du conteneur :

volumes:
- ./cert.pem:/certs/cert.pem:ro
- ./key.pem:/certs/key.pem:ro

Votre application écouterait sur le port 443 et utiliserait les certificats fournis par l'utilisateur pour gérer TLS.

Mode TLS : disabled

Cela désactiverait entièrement TLS pour toute l'application. Cette option est destinée à faciliter l'utilisation et sa mise en œuvre est facultative.

Génération d'URL dans l'application

Lors de la génération d'URL, par exemple pour les URL de redirection OAuth, nous devons d'abord savoir quel protocole utilise l'utilisateur final. C'est simple : pour les modes auto et manual, c'est toujours https:// puisque notre application gère TLS. Pour le mode external, cela dépend de l'en-tête X-Forwarded-Proto. Pour le mode disabled, c'est simplement http://.

Nous voulons également connaître le domaine de l'application. Si vous prenez en charge une configuration pour le domaine, utilisez-la. Sinon, nous pouvons simplement utiliser l'en-tête Host de la requête actuelle.

Fonctionnement dans Hyvor Blogs

Hyvor Blogs fonctionne sur FrankenPHP, un environnement d'exécution PHP basé sur Caddy. Nous utilisons donc directement Caddy pour implémenter cette logique.

Tout d'abord, il y a un script Bash qui calcule les paramètres fictifs de Caddy :

# Configure the app domain's Caddy address/TLS directive from TLS_MODE
# auto: Caddy manages a Let's Encrypt certificate automatically
# manual: the certificate/key mounted at /certs/cert.pem and /certs/key.pem are used to serve TLS
# external: TLS is terminated by a reverse proxy in front; the container only serves HTTP
# disabled: no TLS at all, no https redirect
TLS_MODE="${TLS_MODE:-auto}"
case "$TLS_MODE" in
auto)
APP_ADDRESS="$DOMAIN_APP"
APP_TLS_DIRECTIVE=""
;;
manual)
APP_ADDRESS="$DOMAIN_APP"
APP_TLS_DIRECTIVE="tls /certs/cert.pem /certs/key.pem"
;;
external)
APP_ADDRESS="$DOMAIN_APP:80"
APP_TLS_DIRECTIVE=""
;;
disabled)
APP_ADDRESS="$DOMAIN_APP:80"
APP_TLS_DIRECTIVE=""
;;
*)
echo "Invalid TLS_MODE: $TLS_MODE. Must be one of: auto, external, manual, disabled." >&2
exit 1
;;
esac

Ensuite, le Caddyfile utilise ces variables pour générer une configuration dynamique prenant en charge tous les modes TLS. Cela s'appuie sur l'excellente fonctionnalité de Caddy de substitution des variables d'environnement.

{$APP_ADDRESS} {
{$APP_TLS_DIRECTIVE}
# routing...
}

Liens et références :

Comments