オンプレにLDAPで認証できるWebコンソール付きのレジストリサーバーを立てました

# はじめに
色々とあって社内にDocker Hubのようなレジストリサーバーがあったほうがいいよね、ってことになりました。
社内にはすでにLDAPが稼働していて、httpsにしてLDAP認証できたらいいよね、と思いました。

# 数日溶かしました
調べたところ以下のような方式がありそうでした。
* docker_auth を使う
* ngx_auth_mod を使う

いずれの方法もなんだか思いの外ガチャガチャしないといけなさそうでめんどくさそう。

調べているうちにBasic認証であればLDAPを認証基盤として使うことができ、かなり可能性を感じました。
が、そんな古臭い方式より◯◯のほうがいいんだぜ！っていう内容がたくさん出てくるのですが、それらの内容もゴリゴリすぎてやってられないなぁ…と思ったり。
registryの管理APIを直に叩くのはめんどくさすぎるのでWebコンソールが欲しいし、WebコンソールがLDAPに対応しているぜ！っていうサイトもいくつか見たのですが、設定内容がゴリゴリでそっ閉じを繰り返し、気づけば数時間経っていました。
オンプレにレジストリを建てることが時代錯誤になっているためか出てくる情報がいずれも古く調べている自分も生きた化石なのだろうとか考えていました。

でも、今求められているから。
もっと手軽なものを求めているんだよぉぉ…。


結果、諦めて次の構成にすることにしました。

[認証用のプロキシ]-[Webコンソール]-[レジストリ]

古の技術を前面に使うことで無理やりWebコンソールに認証機能を付与するという…。
Basic認証にアレルギーがなければ社内向けのシステムは全部これでいいじゃん、って思いました。
神(or 紙)構成です。

# 使用技術
* docker (+compose)
* Apache
* LDAP (既存)

社内のサーバー群はrootless docker + LDAPで運用されているのですが、レジストリサーバー運用者が誰でも同一のコンテナを操作するためにrootful dockerを使用しました。

# 構築手順
## ログインしてレジストリサーバー用のディレクトリを作る
* サーバーにrootでログインする
* `mkdir local-registry`
* `cd local-registry`
## 自己署名証明書を作る
* `mkdir certs`
* `cd certs`
* `openssl genrsa 4096 > local-registry.key`
* `vi san.txt`
    * `subjectAltName=DNS:local-registry.example.com`
* `openssl req -new -key local-registry.key > local-registry.csr`
    * それっぽい内容を入れる

```bash:自己署名証明書を作るためにガチャガチャする
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:JP
State or Province Name (full name) [Some-State]:Osaka
Locality Name (eg, city) []:Osaka
Organization Name (eg, company) [Internet Widgits Pty Ltd]:HOGEHOGE
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
```

* `openssl x509 -days 3650 -req -sha256 -signkey local-registry.key  -extfile san.txt < local-registry.csr > local-registry.crt`
* `cd ..`

## apache用の設定ファイルを準備する
* `mkdir httpd_config`
* `vi httpd_config/httpd.conf`
```config:httpd.conf
LoadModule headers_module modules/mod_headers.so
LoadModule mpm_event_module modules/mod_mpm_event.so

LoadModule authn_file_module modules/mod_authn_file.so
LoadModule authn_core_module modules/mod_authn_core.so
LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
LoadModule authz_user_module modules/mod_authz_user.so
LoadModule authz_core_module modules/mod_authz_core.so
LoadModule auth_basic_module modules/mod_auth_basic.so
LoadModule access_compat_module modules/mod_access_compat.so

LoadModule authnz_ldap_module modules/mod_authnz_ldap.so
LoadModule ldap_module modules/mod_ldap.so

LoadModule log_config_module modules/mod_log_config.so

LoadModule ssl_module modules/mod_ssl.so

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so

LoadModule unixd_module modules/mod_unixd.so

<IfModule ssl_module>
    SSLRandomSeed startup builtin
    SSLRandomSeed connect builtin
</IfModule>

<IfModule unixd_module>
    User daemon
    Group daemon
</IfModule>

ServerAdmin you@example.com

ErrorLog /proc/self/fd/2

LogLevel warn

<IfModule log_config_module>
    LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
    LogFormat "%h %l %u %t \"%r\" %>s %b" common

    <IfModule logio_module>
      LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio
    </IfModule>

    CustomLog /proc/self/fd/1 common
</IfModule>

ServerRoot "/usr/local/apache2"

Listen 443

<Directory />
    AllowOverride none
    Require all denied
</Directory>

<VirtualHost *:443>

  ServerName local-registry.example.com

  SSLEngine on
  SSLCertificateFile /usr/local/apache2/certs/local-registry.crt
  SSLCertificateKeyFile /usr/local/apache2/certs/local-registry.key

  SSLCompression off

  SSLProtocol all -SSLv2 -SSLv3 -TLSv1

  SSLCipherSuite EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH
  SSLHonorCipherOrder on

  Header always set "Docker-Distribution-Api-Version" "registry/2.0"
  Header onsuccess set "Docker-Distribution-Api-Version" "registry/2.0"
  RequestHeader set X-Forwarded-Proto "https"

  ProxyRequests     off
  ProxyPreserveHost on

  # no proxy for /error/ (Apache HTTPd errors messages)
  ProxyPass /error/ !

  ProxyPass        / http://registry-ui:81/
  ProxyPassReverse / http://registry-ui:81/

  <Location />
    AuthName "LDAP Authentication"
    AuthType basic
    AuthBasicProvider ldap
    AuthLDAPURL ldap://<LDAP server's ip address>/cn=accounts,dc=example,dc=com?uid
    AuthLDAPBindDN "uid=admin,cn=users,cn=accounts,dc=example,dc=com"
    AuthLDAPBindPassword "<LDAP Administrator password>"


    # Read access to authentified users
    <Limit GET HEAD>
      Require valid-user
    </Limit>

    # Write access to docker-deployer only
    <Limit POST PUT DELETE PATCH>
      Require ldap-group cn=registry-operator,cn=groups,cn=accounts,dc=example,dc=com
    </Limit>

  </Location>

</VirtualHost>
```

## レジストリサーバーを設定する

* `mkdir -p registry/data`
* `mkdir registry/config`
* `vi registry_config/garbage-collection-config.yaml`

```yaml:要るのかわからないガベージコレクション用のconfig
version: 0.1
storage:
  filesystem:
    rootdirectory: /var/lib/registry
```

## 準備したもろもろでdocker composeを設定する
* `vi docker-compose.yaml`

```yaml:docker-compose.yaml
version: '3.8'

services:
  registry-ui:
    image: joxit/docker-registry-ui:main
    restart: always
    environment:
      - SINGLE_REGISTRY=true
      - REGISTRY_TITLE=My Local Registry
      - DELETE_IMAGES=true
      - SHOW_CONTENT_DIGEST=true
      - NGINX_LISTEN_PORT=81
      - NGINX_PROXY_PASS_URL=http://registry-server:5000
      - SHOW_CATALOG_NB_TAGS=true
      - CATALOG_MIN_BRANCHES=1
      - CATALOG_MAX_BRANCHES=1
      - TAGLIST_PAGE_SIZE=100
      - REGISTRY_SECURED=false
      - CATALOG_ELEMENTS_LIMIT=1000
    container_name: registry-ui

  registry-server:
    image: registry:2.8.2
    restart: always
    environment:
      REGISTRY_HTTP_HEADERS_Access-Control-Origin: '[http://local-registry.example.com]'
      REGISTRY_HTTP_HEADERS_Access-Control-Allow-Methods: '[HEAD,GET,OPTIONS,DELETE]'
      REGISTRY_HTTP_HEADERS_Access-Control-Credentials: '[true]'
      REGISTRY_HTTP_HEADERS_Access-Control-Allow-Headers: '[Authorization,Accept,Cache-Control]'
      REGISTRY_HTTP_HEADERS_Access-Control-Expose-Headers: '[Docker-Content-Digest]'
      REGISTRY_STORAGE_DELETE_ENABLED: 'true'
    volumes:
      - ./registry/data:/var/lib/registry
      - ./registry_config:/etc/registry
    container_name: registry-server

  auth-proxy:
    image: httpd:2.4
    ports:
      - 443:443
    volumes:
      - ./httpd_config:/usr/local/apache2/conf
      - ./certs:/usr/local/apache2/certs
```

* `docker compose up -d`

## 要るのか？これ
なんとなくcronにgarbage-collectionを仕込んでおく。

* `crontab -e`
```bash:crontab
0 0 * * * docker compose exec registry-server registry garbage-collect -m /etc/registry/garbage-collection-config.yaml
```

試しに動かしてみてもtagがないリポジトリが消えてないような気がする。
容量が削減されていそうなので、良しとしよう。

# 使い方
## 初期設定
* 安全でないリポジトリを許可する
    * `vi ~/.config/docker/daemon.json` | `vi /etc/docker/daemon.json`

下の内容を既存の設定にマージする。
```
{
    "insecure-registries":[ "local-registry.example.com" ]
}
```

* dockerを再起動する
    * `systemctl --user restart docker` | `systemctl restart docker`


* ログインする
    * `docker login local-registry.example.com`

LDAPアカウントでログインする、すごい。
```
Username: <Enter your LDAP account>
Password: <Enter your LDAP account's password>
Login Succeeded
```

あとは自由にpush/pullする！

* `docker pull ubuntu:latest`
* `docker tag ubuntu:latest local-registry.example.com/ubuntu:latest`
* `docker push local-registry.example.com/ubuntu:latest`
* `docker pull local-registry.example.com/ubuntu:latest`

作業したサーバーに`https`でアクセスするとWebコンソールが開けます、すごい。

# おまけ
独自の都合でプルスルー構成にすることはできませんでした。
制限の無い方は次の設定をdaemon.jsonに追加するともっと幸せになれるそうです。

```
{
  "registry-mirrors": ["https://<my-docker-mirror-host>"]
}
```

# 最後に
この内容は数年に一度誰かを喜ばせることが出来るかもしれません。
そうだと嬉しいです。