Fallback authentification nginx

technique 16 févr. 2022

Supposons que vous ayez une instance zoneminder, et que vous souhaitiez y accéder via zmninja (Android/iOS), avec, en façade de ce serveur, un serveur Nginx.

Pour reprendre cette description de la situation, un petit schéma:

Supposons que je dispose de 4 caméras, positionnées tout autour de mon logement. L'objectif est d'enregistrer le flux de ces caméras en continu, tout en enregistrant les éventuels événements pouvant survenir.

Chaque caméra a son port 554 ouvert, permettant de récupérer le flux vidéo associé via le protocole RTSP. Ce flux va traité par l'instance de Zone Minder tournant sur une machine dédiée grâce à la fonction "MoCord", pour MOtion detection and reCORD, correspondant à un mode d'enregistrement continu associé à une détection de mouvement dans l'image.

Jusqu'ici, il s'agissait d'expliquer la partie gauche du schéma. Cependant, lorsque l'on souhaite pouvoir surveiller l'état des caméras depuis son téléphone, il est nécessaire d'exposer l'API de ZoneMinder sur internet via Nginx en mode reverse proxy.

Spoiler: Zone Minder est développé en PHP. Si vous voulez que votre machine fasse tourner un mineur de cryptomonnaies tout en se faisant chiffrer par un random-ransomware qui passait par là, ne rien faire pour protéger cette entrée serveur est une bonne idée.

Malheureusement, je ne suis pas assez altruiste pour proposer une station de minage si facilement. J'ai donc décidé d'imposer une authentification de type "Basic", ce qui suffit, dans le cas d'un login fort, couplé à une surveillance des logs pour contrer les tentatives de bruteforce, à se protéger des requêtes malveillantes.

Le problème ici est que l'application ZoneMinder sur mobile est codée de telle sorte à ce qu'elle envoie l'authentification via l'url de temps en temps (sur certaines requêtes, dans certains écrans, pour une raison inconnue) lorsque l'on coche dans le menu Dev. Settings l'option Append basic auth token in images. Le reste du temps, l'authentification est passée de manière classique via l'header Authenticate. Cela entraîne de nombreux problèmes d'affichage, dont l'impossibilité totale de visualiser les événements survenus via le menu dédié.

[ILLUSTRATION MENU K C]

La solution que j'ai trouvé est de mixer une authentification par header Authenticate, et par validation par un petit programme maison. La configuration associée est la suivante:

location /x/ {
	internal;

	proxy_pass			http://127.0.0.1:1234/auth;
	proxy_pass_request_body		off;
	proxy_set_header		X-Original-URI $request_uri;
}

location /y {
	proxy_pass          http://192.168.1.100:80/zm;

	proxy_set_header    Host $http_host;
	proxy_redirect      off;

	satisfy             any;

	auth_basic          "Authenticate";
	auth_basic_user_file /path/to/htpasswd;

	auth_request        /x/;
}

Nginx est configuré ici de telle sorte que lorsqu'il reçoit une requête sur /y, il tente deux méthodes d'authentification (satifisfy any;):

  • Une méthode classique d'authentication via auth_basic "Authenticate"
  • Une méthode déportée, en interrogeant /x et en lui passant l'authentification, à la recherche d'un code 200 (auth_request /x/;)

Si l'une de ces deux méthodes fonctionne, la requête est autorisée, et est forwardée (proxy_pass http://192.168.1.100:80/zm;) à la machine hébergeant le service zone minder.

Je ne m'attarderai pas sur la première méthode, très classique, utilisant l'header Authenticate pour passer l'authentification sous forme Base64(username:password).

Cependant, la deuxième méthode, moins courante, a pour principe de récupérer l'URI envoyée par l'application ZoneMinder et de la faire suivre à mon programme maison via un header X-Original-URI (proxy_set_header X-Original-URI $request_uri;)

Le programme va alors analyser ce header, extraire la partie relative à l'authentification dans l'URI (plus spécifiquement le paramètre nommé basicauth), et vérifier que basicauth=Base64(username:password) corresponde bien à l'authentification attendue.

Ce programme écrit en Go, très simple, est le suivant:

package main

import (
	"encoding/base64"
	"fmt"
	"net/http"
	"net/url"
	"os"
)

const(
	username = "XXX"
	password = "YYY"
	headerName = "X-Original-Uri"
	basicAuthGetParam = "basicauth"
)

type handler struct {
	basicAuthBase64 string
}

func (h *handler) ServeHTTP(rw http.ResponseWriter,
	request *http.Request) {
	if originalUri, err := url.ParseRequestURI(request.Header.Get(headerName)); err == nil {
		if originalUri.Query().Get(basicAuthGetParam) == h.basicAuthBase64 {
			fmt.Println("OK")
			rw.WriteHeader(http.StatusOK)
		} else {
			fmt.Println("UNAUTHORIZED")
			rw.WriteHeader(http.StatusUnauthorized)
		}
	} else {
		fmt.Println("MISSING HEADER")
		rw.WriteHeader(http.StatusUnauthorized)
	}
}

func main() {
	if err := http.ListenAndServe(":8080",
		&handler{
			basicAuthBase64: base64.StdEncoding.EncodeToString([]byte(username + ":" + password)),
		},
	); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

En lançant ce petit serveur sur la machine faisant tourner nginx, on peut ainsi valider à la fois l'header Authenticate, mais également le paramètre basicauth de l'URL.

Cela conclut cette présentation d'une authentification fallback sur Nginx.

Mots clés

SOARES Lucas

30 ans, développeur C/Go/Kotlin/C++/Java (quel enfer)/Python