Les goélands lents

Idées, textes et images entre ciel, terre et mer

social: [ mastodon logo @ololduck@vit.am | github logo github | sourcehut logo sourcehut | radicle logo radicle ]

lang: [ en | fr ]

Héberger son propre registry de containers

2026-06-21

buildah login, buildah push, et pourtant authentication required. Voici pourquoi.

Je maintiens un petit projet qui me tient à coeur, SearchHub, depuis quelques temps maintenant. C’est un moteur de recherche local qui indexe mes marque-pages et me permet d’interroger aussi quelques moteurs externes (crates.io, Wikipedia, etc).

Je construis une image OCI à chaque release pour pouvoir déployer facilement, et jusque là je buildais en local, via un Containerfile, et je taguais l’image dans mon registry local.

J’ai un serveur qui me sert pas mal de services (dont ce blog), et je me suis dit: pourquoi ne pas y héberger mon propre registry de containers? Comme ça, je peux pusher mes images dessus et les tirer depuis n’importe quelle machine sans devoir rebuild.

Le setup🔗

J’ai un serveur sous Debian, avec Docker d’installé pour quelques services. Le registre officiel registry:2 se lance en une ligne:

sudo docker run -d --name registry -p 127.0.0.1:5050:5000 registry:2

Le port 5050 est en écoute locale seulement, il faut un reverse proxy pour y accéder depuis l’extérieur. Un petit bloc nginx plus tard:

server {
    server_name oci.vit.am;

    client_max_body_size 0;

    location / {
        proxy_pass http://127.0.0.1:5050;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 900;
    }
}

Et un Certbot pour le TLS. Jusque là, tout va bien.

L’authentification🔗

Je ne veux pas que n’importe qui puisse pusher sur mon registry. Par contre, je veux que tout le monde puisse tirer les images (c’est public). Je protège donc les écritures avec auth_basic:

location / {
    limit_except GET HEAD {
        auth_basic "Registry Login";
        auth_basic_user_file /etc/registry/auth/htpasswd;
    }

    proxy_pass http://127.0.0.1:5050;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_read_timeout 900;
}

Cette config dit: «les méthodes GET et HEAD sont libres d’accès, tout le reste nécessite un mot de passe.» Pratique: les pulls (GET) sont anonymes, les pushes (POST, PUT) sont authentifiés.

Je crée un utilisateur avec htpasswd, je reload nginx, je teste:

$ curl -s -o /dev/null -w "%{http_code}" https://oci.vit.am/v2/
200

$ curl -s -o /dev/null -w "%{http_code}" -X POST https://oci.vit.am/v2/search-hub/blobs/uploads/
401

Parfait: les lectures passent, les écritures demandent une auth. Je connecte buildah:

$ buildah login oci.vit.am
Username: paul
Password:
Login Succeeded!

Je pousse:

$ buildah push oci.vit.am/search-hub:latest
Error: writing blob: initiating layer upload to /v2/search-hub/blobs/uploads/
        in oci.vit.am: authentication required

Hein?

Le problème🔗

Pourtant je suis bien loggé:

$ buildah login --get-login oci.vit.am
paul

Je vérifie avec curl que mes identifiants fonctionnent:

$ curl -s -o /dev/null -w "%{http_code}" \
  -u "paul:mon-mot-de-passe" \
  -X POST https://oci.vit.am/v2/search-hub/blobs/uploads/
202

Les identifiants sont bons. C’est buildah qui ne les envoie pas.

Un petit tour avec --log-level debug m’en dit plus:

DEBU GET https://oci.vit.am/v2/ → status 200
DEBU POST https://oci.vit.am/v2/search-hub/blobs/uploads/ → status 401
     WWW-Authenticate: Basic realm="Registry Login"

Buildah commence par GET /v2/, ce qui est normal: c’est le «ping» du protocole Distribution, qui permet de découvrir comment s’authentifier.

Le problème, c’est que nginx répond 200 sans en-tête WWW-Authenticate. Buildah en déduit que le registry n’a pas d’auth, et ne se fatigue pas à envoyer des identifiants pour la suite. Quand la requête POST tombe sur le auth_basic de nginx et reçoit un 401, buildah ne retente pas avec les identifiants: il considère que l’auth n’est pas disponible et abandonne.

La solution🔗

Il y a deux options:

  1. Protéger aussi les lectures. On vire le limit_except et on met auth_basic sur tout le bloc location. L’inconvénient c’est que tout le monde doit se logguer pour pull.

  2. Ajouter l’en-tête WWW-Authenticate même sur les réponses GET. Comme ça, buildah voit qu’il y a une auth possible, et envoie ses identifiants pour les requêtes qui en ont besoin.

J’ai choisi la deuxième. Le bloc /v2/ reçoit un add_header qui annonce la présence de l’auth Basic:

location /v2/ {
    add_header WWW-Authenticate 'Basic realm="Registry Login"' always;

    limit_except GET HEAD {
        auth_basic "Registry Login";
        auth_basic_user_file /etc/registry/auth/htpasswd;
    }

    proxy_pass http://127.0.0.1:5050;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_read_timeout 900;
}

location / {
    limit_except GET HEAD {
        auth_basic "Registry Login";
        auth_basic_user_file /etc/registry/auth/htpasswd;
    }

    proxy_pass http://127.0.0.1:5050;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_read_timeout 900;
}

Maintenant, le ping /v2/ répond:

200 + WWW-Authenticate: Basic realm="Registry Login"

Buildah voit l’en-tête, sait qu’il doit s’authentifier, et envoie ses identifiants pour les requêtes suivantes. Les pulls anonymes (GET, HEAD) ne sont pas bloqués par nginx, ils passent toujours sans auth. Les écritures (POST, PUT) sont protégées par le auth_basic.

Et cette fois:

$ buildah push oci.vit.am/search-hub:latest
Getting image source signatures
Copying blob sha256:523040b3fc983556b6486323e681ebfb7d35fe6685a3b1b5b4c9745417b5ed6f
Copying blob sha256:f4f8b983b714f130e2cff99176baa26352db6a55d3622e10ada40f2b4720a4eb

Ça marche.

Pourquoi buildah se comporte comme ça?🔗

C’est un comportement du protocole Docker Registry V2. Le client (buildah, podman, docker) commence par un ping sur /v2/:

Notre config nginx avec limit_except faisait répondre 200 sans WWW-Authenticate, ce qui plaçait buildah dans le troisième cas. Une fois décidé qu’il n’y a pas d’auth, il n’envoie plus d’identifiants, même quand la requête suivante échoue avec 401.

La leçon: si vous mettez un auth_basic conditionnel devant un registry, assurez-vous que le ping /v2/ reçoive bien l’en-tête WWW-Authenticate. Sinon, les clients conformes au protocole risquent de ne jamais envoyer leurs identifiants.