Aufbau einer Docker Private Registry

Und wieder ein kleiner Artikel über Docker & Co. – es sieht so aus, als ließe mich das Thema noch nicht los. Zugegebenermaßen ist es ja auch spannend und bietet momentan sehr viel Raum für Experimente. Denn nachdem man die ersten Docker-Images gebaut hat, stellt sich unweigerlich die Frage, wie sich diese transportieren und auf dem Zielsystem installieren lassen.

Wie immer gibt es mehrere Optionen. Die einfachste lautet, Docker Hub zu nutzen. Dort bietet Docker selbst die Infrastruktur, um Docker-Images zu verwalten, d.h. Images hochzuladen und auf beliebigen Servern zu verwenden. Und nicht zuletzt ist Docker Hub auch der Ort, an dem bereits laut eigenen Angaben über 100000 Images vorliegen, die allesamt zur freien Verwendung zur Verfügung stehen. Das Ganze hat nur einen Nachteil – mit dem kostenlosen Account lässt sich nur ein privates Repository anlegen, alle anderen müssen öffentlich verfügbar sein. Zwar ist das kleinste Kontingent, in dem immerhin fünf private Repositories inbegriffen sind, auch nicht teuer, aber auch dann liegen die Daten nicht mehr auf eigenen Servern und noch dazu in einem anderen Land, vielleicht sollen die Images auch innerhalb eines geschlossenen Netzwerkes liegen usw., insgesamt gibt es durchaus Gründe, die den Aufbau einer privaten Registry rechtfertigen. Meiner war gar nicht so kompliziert, ich wollte letztlich wissen, wie es geht, und zeigen, dass es geht.

Docker bietet dafür bereits ein fertiges Docker-Image an und stellt zudem eine Beschreibung bereit, wie eine private Registry zum Laufen gebracht werden kann. Der Code steht als Open Source unter der Apache Lizenz 2.0 zur Verfügung,

An dieser Stelle soll nicht verschwiegen werden, dass es weitere Anbieter von privaten Registries für Docker-Images gibt. Beispielsweise lassen sich Images bei Tutum anlegen, in der Beta-Phase ist dies bis dato kostenlos, und mit Hilfe der Tutum-Infrastruktur deployen. Da Tutum jedoch inzwischen (zu) Docker gehört, dürfte es über kurz oder lang eine Verschmelzung der Registry-Server geben. Das ist natürlich nur eine Vermutung, aber Docker wird hier sicherlich alle Möglichkeiten zur Optimierung und Konsolidierung nutzen. Des Weiteren bietet Google im Rahmen der Google Container Engine eine Container Registry an, die sich zwar auch außerhalb der Google Infrastruktur nutzen lässt, aber aufgrund anderer Authentifizierungsmechanismen nur innerhalb dieser wirklich praktisch ist. Ein weiterer kommerzieller Anbieter ist Quay, der wiederum zu CoreOS gehört.

Aber zurück zur Installation einer eigenen privaten Registry. Zunächst ein paar Gedanken zur Vorbereitung. Beim Namen der Registry habe ich mich am Vorbild von Docker Hub orientiert, also soll der Server-Name hub.kuerbis.org lauten. Da die eigene Registry nicht nur von localhost aus erreichbar sein soll, muss ein SSL-Zertifikat vorliegen, ansonsten müsste der jeweilige Docker-Host, der die Registry verwendet, mit der Option “–insecure-registry myregistry.example.com:5000” gestartet werden. Das ist jedoch keine gute Idee, denn laut Docker schließen sich Authentifizierung per Basis-Authentication und die “insecure registry”-Option aus. Die günstigsten SSL-Zertifikate lassen sich für wenige Dollar pro Jahr erstehen, beispielsweise bei SSLs.com. Das sieht zwar jetzt nach Werbung aus, aber rein persönlich bin ich mit dem Anbieter sehr zufrieden und kann es wiederum nicht wirklich nachvollziehen, weshalb es hierzulande nichts Vergleichbares zu geben scheint. Allerdings geht es auch noch günstiger, dazu seien die Begriffe StartSSL und WoSign genannt, die SSL-Zertifikate kostenlos anbieten. In jedem Fall sollte die Erstellung des Schlüssels nicht auf dem Server des Zertifikat-Anbieters erfolgen, sondern auf einem eigenen Server. Ansonsten hätte der Anbieter auch den privaten Schlüssel, was im Allgemeinen keine gute Idee ist, auch wenn man sehr vertrauensvoll ist. Eine Schritt-für-Schritt-Anleitung zur Erstellung eines CSR, oder auch “Antrag zur Signierung eines Zertifikats” findet sich z.B. auf den Hilfe-Seiten von Ubuntu. Noch eine Anmerkung – zwar begrüße ich die Initiative von Let’s Encrypt sehr, auch hier werden kostenlose Zertifikate bereit gestellt. Die bisherige Variante zur Zertifikatsgenerierung wird hingegen als einfach dargestellt, erscheint mir aber extrem umständlich und setzt zudem die Verwendung der Let’s-Encrypt-Software voraus, die wiederum die Web-Server-Einstellungen auf dem Server ändert… Hier bleibt zu hoffen, dass in einer späteren Fassung auch normale CSR verarbeitet werden können. Wenn nun der DNS noch entsprechend konfiguriert wurde, dass der entsprechende Server für die Registry auch unter dem gewählten Namen erreichbar ist, kann es auch schon los gehen.

Zunächst werden Verzeichnisse für die Daten der Registry und die SSL-Zertifikate erstellt. Die einfachste Variante nutzt für die Daten das Filesystem des Hosts, jedoch werden auch andere Storage-Backends bereit gestellt, mehr dazu in der genannten Dokumentation.

sudo mkdir -p /srv/docker/registry/data

sudo mkdir -p /srv/docker/registry/certs

Anschließend werden die Zertifikatsdateien, d.h. Key und Zertifikat kopiert:

sudo cp server.key /srv/docker/registry/certs/domain.key
sudo cp server.crt /srv/docker/registry/certs/domain.crt

Die Dateinamen sind hier willkürlich, sie lassen sich beim Start des Docker-Containers als Parameter übergeben.

Zuletzt wird das Registry-Image genutzt:

sudo docker run -d -p 5000:5000 --restart=always --name registry -v /srv/docker/registry/data:/var/lib/registry \
-v /srv/docker/registry/certs:/certs \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
registry:2

Damit ist die erste, einfachste Variante der privaten Registry bereits lauffähig. Aber Vorsicht – noch ist hier keine Authentifizierung vonnöten, so dass, vorausgesetzt, der Server-Name und Port-Nummer seien bekannt, jeder der eigentlich als privaten Registry gedachten Instanz bedienen kann. Daher ist dies nur das Beispiel für einen ersten Test, denn hier wird Port 5000 nach außen hin freigegeben, was in der späteren Variante weg fällt. Insbesondere sollte nach den ersten Tests auch das Daten-Verzeichnis /srv/docker/registry/data/ geleert werden.

Die private Registry kann nun zunächst getestet werden, bzw. handelt es sich dabei um die grundlegenden Kommandos. Zunächst wird ein Docker-Image mit einem Tag versehen, der auf den Registry-Server verweist. Im Beispiel liegt ein Image namens geschkenet/nginx vor, was ins Repository gespeichert werden soll. Daher zunächst taggen:

sudo docker tag geschkenet/nginx hub.kuerbis.org:5000/nginx

Anschließend per push-Befehl übertragen:

sudo docker push hub.kuerbis.org:5000/nginx
The push refers to a repository [hub.kuerbis.org:5000/nginx] (len: 1)
a5593e607a04: Image successfully pushed
8d6d1b6c6945: Image successfully pushed
2d60559d9813: Image successfully pushed
6fbd81034cd9: Image successfully pushed
60d73b197f58: Image successfully pushed
6edd29023095: Image successfully pushed
b5021012937d: Image successfully pushed
f4fddc471ec2: Image successfully pushed
latest: digest: sha256:f0e2d56a0d4863fc6d7bb8e00c59fd991181d9b6bf8ef05fa794231773fd0056 size: 13036

Das Image kann nun von jedem Server aus verwendet werden, es lässt sich holen mittels:

sudo docker pull hub.kuerbis.org:5000/nginx
Using default tag: latest
latest: Pulling from nginx
f4fddc471ec2: Already exists
b5021012937d: Already exists
6edd29023095: Already exists
60d73b197f58: Already exists
6fbd81034cd9: Already exists
2d60559d9813: Already exists
8d6d1b6c6945: Already exists
a5593e607a04: Already exists
Digest: sha256:f0e2d56a0d4863fc6d7bb8e00c59fd991181d9b6bf8ef05fa794231773fd0056
Status: Downloaded newer image for hub.kuerbis.org:5000/nginx:latest

Das sieht doch bereits sehr gut aus! Natürlich lassen sich auch andere Tags vergeben wie “latest”, falls man mehrere Versionen eines Images vorhalten möchte. Das Image liegt nun lokal auf dem Docker-Host vor und kann wie üblich verwendet werden, zur Prüfung einfach docker images aufrufen.

Das war simpel, jedoch liegt die Registry sehr ungeschützt vor, und das soll verhindert werden. Zwar bietet das Registry-Image auch selbst die Möglichkeit, Basic-Authentication einzurichten, aber die bessere Variante erschien mir die Verwendung eines Nginx-Proxies. Diese Art der Konfiguration eröffnet weitere Möglichkeiten, so dient Nginx als Authentifizierungsschicht mit all seinen Modulen, etwa könnte eine Single-Sign-On-Lösung (SSO) realisiert werden. Ich habe zunächst eine einfache Variante realisiert, bei der die Zugangsdaten in einer Datei vorliegen.

Zunächst wird der Registry-Container gestoppt und gelöscht:

sudo docker stop registry

sudo docker rm registry

Anschließend neu gestartet, diesmal jedoch ohne die Portfreigabe nach aussen, vielmehr findet die Verknüpfung von Nginx-Proxy und Registry-Container über einen Link statt, denn selbstverständlich wird Nginx auch innerhalb eines Containers betrieben.

sudo docker run -d --restart=always --name registry -v /srv/docker/registry/data:/var/lib/registry \
-v /srv/docker/registry/certs:/certs \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
registry:2

Für Nginx nehme ich mein vor kurzem erstelltes und sehr kleines Image, natürlich ist das keine zwingende Voraussetzung, die bereits zitierte Anleitung verwendet das offizielle Nginx-Image dafür.

Auch Nginx muss die Zertifikatsdateien kennen, daher werden sie ihm zugänglich gemacht. Als Speicherort für alles, was der Nginx-Container benötigt, verwende ich /srv/docker/nginx.

sudo cp server.key /srv/docker/nginx/conf.d/hub_kuerbis_org.key
sudo cp server.crt /srv/docker/nginx/conf.d/hub_kuerbis_org_bundle.crt

Als nächstes werden die Zugänge konfiguriert. Dazu dient klassischerweise das dem Apache-httpd-Server beiliegende Kommando htpasswd. Falls es nicht vorliegt, kann z.B. folgendes Shellskript verwendet werden, siehe dazu auch den FAQ-Eintrag bei Nginx. Das Skript:

#!/bin/sh

user=$1
passwd=$2
printf "$1:$(openssl passwd -1 $2)\n" 

Die Erzeugung der Passwort-Datei:

./htpasswd.sh user supersicherespasswort >> htpasswd

Die erzeugte Datei wird ebenfalls in das conf.d-Verzeichnis kopiert.

Zuletzt die eigentliche Konfiguration für den virtuellen Host bzw. Proxy. Inspiriert von der Konfiguration unter Ubuntu Linux werden die einzelnen Sites im Verzeichnis /srv/docker/nginx/sites-enabled konfiguriert. Beim Start liest Nginx die darin befindlichen Dateien ein. Für die Registry sieht die Nginx-Konfiguration wie folgt us:

upstream docker-registry {
  server registry:5000;
}

## Set a variable to help us decide if we need to add the
## 'Docker-Distribution-Api-Version' header.
## The registry always sets this header.
## In the case of nginx performing auth, the header will be unset
## since nginx is auth-ing before proxying.
map $upstream_http_docker_distribution_api_version $docker_distribution_api_version {
  'registry/2.0' '';
  default registry/2.0;
}

server {
        listen 80 ssl;
        server_name hub.kuerbis.org;

        root /var/www/hub.kuerbis.org/htdocs;

        ssl on;
        ssl_session_timeout 5m;

        add_header Front-End-Https on;

        # disable any limits to avoid HTTP 413 for large image uploads
        client_max_body_size 0;

        # required to avoid HTTP 411: see Issue #1486
        # (https://github.com/docker/docker/issues/1486)
        chunked_transfer_encoding on;

        ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_prefer_server_ciphers on;
        ssl_session_cache shared:SSL:10m;
        add_header X-Content-Type-Options nosniff;
        ssl_session_tickets off; # Requires nginx >= 1.5.9
        ssl_stapling on; # Requires nginx >= 1.3.7
        ssl_stapling_verify off; # was: on # Requires nginx => 1.3.7
        
        # Nginx main config includes only .conf files in conf.d directory
        ssl_certificate /etc/nginx/conf.d/hub_kuerbis_org_bundle.crt;
        ssl_certificate_key /etc/nginx/conf.d/hub_kuerbis_org.key;

        # Remember this setting for 365 days
        add_header Strict-Transport-Security max-age=31536000;
        
        location /v2/ {
                # Do not allow connections from docker 1.5 and earlier
                # docker pre-1.6.0 did not properly set the user agent on ping, catch
                # "Go *" user agents
                if ( $http_user_agent ~ "^(docker\/1\.(3|4|5(?!\.[0-9]-dev))|Go ).*$" ) {
                        return 404;
                }

                # To add basic authentication to v2 use auth_basic setting.
                auth_basic "Registry realm";
                auth_basic_user_file /etc/nginx/conf.d/htpasswd;
        
                ## If $docker_distribution_api_version is empty, the header will not be
                ## added.
                ## See the map directive above where this variable is defined.
                add_header 'Docker-Distribution-Api-Version' $docker_distribution_api_version always;

                proxy_pass                          https://docker-registry;
                proxy_set_header  Host              $http_host;   # required for docker client's sake
                #proxy_set_header  X-Real-IP         $remote_addr; # pass on real client's IP
                #proxy_set_header  X-Forwarded-For   $proxy_add_x_forwarded_for;
                #proxy_set_header  X-Forwarded-Proto $scheme;
                proxy_read_timeout                  900;

        }
}

Dazu ein paar Anmerkungen:

  • Die Konfiguration des Proxy wird wie bei Nginx üblich durchgeführt. Dabei wird der Registry-Container unter dem Link-Namen “registry” zugänglich gemacht, so dass der Server einfach “registry:5000” lautet.
  • Nginx ist so eingerichtet, dass er auf Port 80 und 443 lauscht. Auch auf Port 80 soll SSL eingeschaltet sein, letztlich dient dieser Port nur zur internen Weiterleitung, d.h. von außen wird die Registry wie später ersichtlich auf dem Port 81 erreichbar sein. Dies ist ebenso willkürlich, im Beispiel von Docker wird etwa 5043 verwendet.
  • Das Document Root Verzeichnis ist im Beispiel eingerichtet, so dass der Nutzer, falls er sich mit dem Browser auf den Server verirren sollte, eine minimale HTML-Datei mit dem Hostnamen erhält.
  • Die restlichen Einstellungen sind weitestgehend analog zum Beispiel, so werden die Zertifikatsdateien eingebunden, ebenso wie die Passwort-Datei htpasswd.
  • Zum Schluss wird mittels proxy_pass der unter upstream eingerichtete Proxy angesprochen. Dabei wird ebenso https und nicht nur http verwendet, da der Registry-Container die Zertifikate nutzt und somit https zur Verfügung stellt. Analog zur Konfiguration aus den Docker-Quellen wäre diese Konfiguration auch ohne durchgängige Verschlüsselung möglich, so dass nur zwischen Client und Nginx-Proxy verschlüsselt würde.
  • Die Einstellungen unter proxy_set_header führten bei mir zu Fehlern, daher sind sie einfach auskommentiert.

Nun muss der Nginx-Container nur noch gestartet werden:

sudo docker run -d --name nginx --restart=always \
-v /srv/docker/nginx/sites-enabled:/etc/nginx/sites-enabled \
-v /srv/docker/nginx/conf.d:/etc/nginx/conf.d \
-v /srv/docker/nginx/www:/var/www --publish 81:80 \
--link registry:registry \
geschkenet/nginx

Das war es auch schon. Der Port 81 des Hosts wird auf den Port 80 des Nginx-Containers weiter geleitet, von dem wiederum der Registry-Container angesprochen wird. Der Link namens “registry” sorgt dafür, dass der Registry-Container unter eben diesem Hostnamen erreichbar ist.

Um die neue private Registry zu erreichen, ist zunächst ein Login notwendig:

sudo docker login hub.kuerbis.org:81
Username: geschke
Password: <wird nicht angezeigt>
Email: 
WARNING: login credentials saved in /home/geschke/.docker/config.json
Login Succeeded

Anschließend können die bereits geschilderten Kommandos docker push oder docker pull wie gewohnt ausgeführt werden.

Das war eine kurze Übersicht über den Aufbau einer privaten Registry für Docker-Images. Natürlich erhebt diese keinerlei Anspruch auf Vollständigkeit, davon abgesehen ist es auch nur ein Beispiel einer lauffähigen Konfiguration. Themen wie Load-Balancing, Skalierung oder weiter gehende Authentifizierungsmechanismen wurden nicht dargestellt, aber vielleicht beschäftige ich mich später noch damit. Die weiteren Schritte gehen in Richtung automatisiertes Deployment, was zunächst die Automatisierung der Erstellung der Docker-Images und des “Pushens” auf die Registry voraussetzt. Aber auch das bietet genug Platz für weitere Beiträge…

 

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Tags: