Je me suis mis en tête de rationaliser un peu l'accès aux services que j'auto-héberge et que je mets à disposition de mes proches. Ça passe notamment par une gestion unifiée des comptes utilisateurs et de l'identification sur chaque application. Devoir retenir un couple login/mot de passe pour chacun semble être au delà des capacités moyennes, car bien sûr une minorité utilise un gestionnaire de mots de passe. Mais bref. 🙄
J'ai jeté mon dévolu sur Authentik qui est - évidemment - open-source et auto-hébergeable. La mise en place initiale a été un peu complexe, notamment par le fait que je n'y connaissais pas grand chose en protocole d'authentification unifiée, et aussi par la variété d'implémentation et de manière d'aborder l'authentification externe par chacun des services : pour certains c'est quelques champs de config à remplir et tout marche tout seul, pour d'autres c'est une véritable plaie. Sans compter que certains services bloquent le login local quand un SSO est configuré, alors que d'autres combinent très bien les deux.
Encore un truc qui, parce qu'il simplifie l'usage chez l'utilisateur final, rend l'administration encore plus complexe qu'elle ne l'est déjà...
Après de nombreuses heures, j'ai réussi à configurer l'authentification par SSO sur la plupart des services (mais avec parfois des compromis) :
- FreshRSS
- Lychee
- Hedgedoc
- Gitlab
- Nextcloud
- RocketChat
- Nginx
- Apache (via https://github.com/OpenIDC/mod_auth_openidc)
Pour ce dernier, j'ai buté sur une erreur quand j'ai voulu protéger l'accès à mon instance de myMPD. Celui-ci renvoyait constamment une erreur, alors qu'une configuration similaire fonctionnait pour un autre service, et que les requêtes visibles dans la console du navigateur étaient cohérentes.
D'où l'introduction de l'outil qui m'a sorti du pétrin et qui mérite cet article : mitmproxy (et son pote mitmweb).
Je suis arrivé à la conclusion que les requêtes visibles depuis le navigateur, n'étaient peut-être exactement égales à celles arrivant sur l'application. Il suffisait de mettre un petit proxy au milieu pour le vérifier, et si nécessaire, altérer les requêtes pour voir l'effet produit.
Sur myMPD, ma stack Docker Compose est grosso-modo la suivante :
services:
app:
image: ghcr.io/jcorporation/mympd/mympd
ports:
- 8004:8080
volumes:
- ./data/workdir:/var/lib/mympd/
- ./data/cachedir:/var/cache/mympd/
environment:
MYMPD_HTTP_PORT: 8080
Et j'ai un vhost Apache qui sert de reverse-proxy pour servir du HTTPS, à peu près comme ceci :
<VirtualHost *:443>
ServerName mympd.domain.tld
DocumentRoot /var/www
# Config pour Authentik (tronquée ici)
OIDCProviderMetadataURL ...
OIDCClientID ...
OIDCClientSecret ...
# ...
<Proxy *>
Deny from all
AuthType openid-connect
Require valid-user
Satisfy any
</Proxy>
ProxyRequests Off
ProxyPreserveHost on
ProxyVia On
ProxyPass / http://duncan:8004/
ProxyPassReverse / http://duncan:8004/
RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule /(.*) ws://duncan:8004/$1 [P,L]
RewriteCond %{HTTP:Upgrade} !=websocket [NC]
</VirtualHost>
(oui, myMPD tourne sur Duncan mais Apache est sur Usul)
Pour intercepter les requêtes, il suffit d'adapter un tout petit peu la stack comme ceci :
services:
app:
image: ghcr.io/jcorporation/mympd/mympd
# ports:
# - 8004:8004
volumes:
- ./data/workdir:/var/lib/mympd/
- ./data/cachedir:/var/cache/mympd/
environment:
MYMPD_HTTP_PORT: 8004
debug-proxy:
image: mitmproxy/mitmproxy
command: mitmweb --web-host 0.0.0.0 --mode reverse:http://app:8080@8004 --set keep_host_header=true
ports:
- 8004:8080
- 8014:8081
C'est-à-dire :
- Ne plus exposer le port
8004
du containerapp
(myMPD) - Ajouter un container
mitmproxy/mitmproxy
exposant son port8080
sur le port d'origine de l'application cible (donc8004
). Ici on prend la dernière version disponible, c'est-à-dire celle de la série 11.x, qui dispose notamment des features dont je vais avoir besoin. - Exposer aussi le port
8081
de mitmproxy pour accéder à l'interface web de debug, à savoir mitmweb (ici via le port8014
de l'hôte). - Lancer la commande mitmweb avec la configuration pour forwarder les requêtes depuis le port 8080 vers l'application cible sur le port 8004 en mode reverse-proxy :
--mode reverse:http://app:8080@8004
- Conserver intact le header
Host
reçu au lieu de le remplacer par le host cible :--set keep_host_header=true
⚠️ Le dernier point peut être très important pour certains applicatifs qui refusent de répondre si le host ne correspond pas à celui défini dans leur configuration. Dans mon cas, la requête provient déjà d'un reverse-proxy (à savoir Apache sur Usul) qui conserve bien le host "externe" (ProxyPreserveHost on
), à savoir mympd.domain.tld
dans mon exemple, et je ne veux pas que mitmweb le remplace par app
.
Ceci fait, on relance la stack modifiée avec un petit docker compose up -d
et on ouvre le navigateur à l'adresse https://mympd.domain.tld d'un côté, et http://{IP du serveur}:8014 de l'autre (ou via du local port forwarding par SSH si le port n'est pas accessible directement). L'IP est obligatoire car - au moins par défaut - mitmproxy n'autorise pas les connexions via un nom d'hôte par sécurité.
Normalement à ce stade, il devrait y avoir l'interface de l'applicatif cible qui s'ouvre sans erreur sur le premier onglet (myMPD dans mon cas), et l'interface de mitmweb sur l'autre.
Et en cliquant sur une requête on peut voir sa trace complète : headers, contenu, timings.
Assez bluffant !
Il est à présent assez simple de consulter les requêtes envoyées à l'applicatif, et surtout de vérifier si leas headers critiques sont bien tels qu'attendus : Host
, Forwarded-For
, etc.
Dans mon cas, j'ai surtout pu voir qu'il y avait beaucoup de headers ajoutés par le module OpenID d'Apache, et en supprimant simplement ces headers (inutiles pour l'applicatif dans ce cas) le comportement redevenait normal.
J'ai pu avoir la confirmation qu'il s'agissait en réalité d'une limitation due à une configuration sur le nombre maximum de headers pris en compte par Mongoose, utilisé par myMPD. En augmentant cette limite, le problème disparaissait aussi.
Mais j'ai pu découvrir un bel outil pour mes futures analyses HTTP.
☝️ Ah oui, dernier point : quand vous avez fini, n'oubliez pas de remettre le container en commentaire dans votre docker-compose.yml
, de décommenter le port de l'applicatif, et de relancer la stack avec docker compose up -d --remove-orphans
pour supprimer aussi le container mitmproxy désormais inutile.