【docker-compose】docker-registryを導入する  ～https接続編～

# はじめに

https接続でdocker-registryに接続します。
リバースプロキシを経由するパターン、しないパターンをそれぞれ紹介します。
docker-composeを使用します。

前編はこちら 
https://qiita.com/not75743/items/2358229fc6f83718e7bf

# 対象読者

- docker-registryにhttps接続でpush,pullしたい人
- docker-registryにリバースプロキシ(nginx)経由でpush,pullしたい人
- 自己証明書を使用してdocker-registryにhttps接続したい人

# 前提
- https接続のための証明書が取得済みであること。 
- docker,docker-composeが使える環境であること

# 環境

**client**

```bash:
$ cat /etc/redhat-release
CentOS Linux release 7.9.2009 (Core)
$ docker --version
Docker version 20.10.3, build 48d30b5
```

**host**

- AWSのEC2
- 使ったAMIはamzn2-ami-hvm-2.0.20210126.0-x86_64-gp2

```bash:
$ docker --version
Docker version 20.10.4, build d3cb89e
$ docker-compose --version
docker-compose version 1.27.4, build 40524192
```

# 構成図

![docker-compose_reverseproxy_one.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1464314/184e346c-4159-6145-963e-6a7c37bfe615.png)


# ① リバースプロキシ(nginx)を経由するパターン

## ディレクトリ構成
```bash:ディレクトリ構成
.
|-- cert
|   |-- fullchain.pem
|   `-- privkey.pem
|-- data
|-- docker-compose.yaml
|-- registry
|   `-- config.yml
`-- reverseproxy
    |-- htpasswd
    `-- nginx.conf
```

## docker-compose.yaml

以下を使用しました。

```yaml:docker-compose.yaml
version: "3.9"
services:
  reverse-proxy:
    image: nginx:1.20
    ports:
      - 5043:443
    volumes:
      - ./cert/fullchain.pem:/etc/nginx/conf.d/fullchain.pem:ro
      - ./cert/privkey.pem:/etc/nginx/conf.d/privkey.pem:ro
      - ./reverseproxy/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./reverseproxy/.htpasswd:/etc/nginx/htpasswd:ro
  registry:
    image: registry:2
    volumes:
      - ./data:/var/lib/registry
      - ./registry/config.yml:/etc/docker/registry/config.yml
```

直接のアクセスを防ぐため、registryのポートは記載していません。
コンフィグや証明書については適切なパスを指定しましょう。

## nginx.conf

以下を使用しました。
ドキュメントに記載のコメントも残しています。

```bash:nginx.conf
events {
    worker_connections  1024;
}

http {

  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 is unset
  ## since nginx is auth-ing before proxying.
  map $upstream_http_docker_distribution_api_version $docker_distribution_api_version {
    '' 'registry/2.0';
  }

  server {
    listen 443 ssl;
    server_name example.test;

    # SSL
    ssl_certificate /etc/nginx/conf.d/fullchain.pem;
    ssl_certificate_key /etc/nginx/conf.d/privkey.pem;

    # Recommendations from https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
    ssl_protocols TLSv1.1 TLSv1.2;
    ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;

    # 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/moby/moby/issues/1486)
    chunked_transfer_encoding on;

    location / {
      # 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/htpasswd; # ※②

      ## If $docker_distribution_api_version is empty, the header is not added.
      ## See the map directive above where this variable is defined.
      add_header 'Docker-Distribution-Api-Version' $docker_distribution_api_version always;

      proxy_pass                          http://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;
    }
  }
}
```

以下でポイントについて説明します。

## ① client_max_body_size 0

詳しくは[ここ](http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size)を参照  
  
HTTPボディの大きさを制限するパラメータです。
docker-registryには大きめのイメージをpushする場合もあり、このパラメータが設定されている場合、引っかかる場合があります。  
引っかかった場合は以下の様に出力されます。

**クライアント側**

```bash:イメージpush時のログ確認
$ docker push example.test:5043/centos:test
The push refers to repository [example.test/centos]
174f56854903: Pushing [==================================================>]  203.9MB/203.9MB
error parsing HTTP 413 response body: invalid character '<' looking for beginning of value: "<html>\r\n<head><title>413 Request Entity Too Large</title></head>\r\n<body>\r\n<center><h1>413 Request Entity Too Large</h1></center>\r\n<hr><center>nginx/1.20.0</center>\r\n</body>\r\n</html>\r\n"
```

**docker-compose側**

```bash:docker-compose.yaml
registry_1       | time="2021-05-30T05:38:32.00570856Z" level=error msg="response completed with error" err.code="blob unknown" err.detail=sha256:2d473b07cdd5f0912cd6f1a703352c82b512407db6b05b43f2553732b55df3bc err.message="blob unknown to registry" go.version=go1.11.2 http.request.host="example.test" http.request.id=d62a7937-e2bf-4b68-9e62-96f9fe7721f9 http.request.method=HEAD http.request.remoteaddr=<クライアントIP> http.request.uri="/v2/centos/blobs/sha256:2d473b07cdd5f0912cd6f1a703352c82b512407db6b05b43f2553732b55df3bc" http.request.useragent="docker/20.10.3 go/go1.13.15 git-commit/46229ca kernel/3.10.0-1160.15.2.el7.x86_64 os/linux arch/amd64 UpstreamClient(Docker-Client/20.10.3 \(linux\))" http.response.contenttype="application/json; charset=utf-8" http.response.duration=1.422727ms http.response.status=404 http.response.written=157 vars.digest="sha256:2d473b07cdd5f0912cd6f1a703352c82b512407db6b05b43f2553732b55df3bc" vars.name=centos
reverse-proxy_1  | 2021/05/30 05:38:37 [error] 30#30: *9 client intended to send too large chunked body: 1048576+32768 bytes, client: <クライアントIP>, server: example.test, request: "PATCH /v2/centos/blobs/uploads/617a6324-7a93-4212-987e-a48eb501f34d?_state=k8ymp1axNemJM79Vg5G-pRgHuZW_UkkYGREuBH29q5d7Ik5hbWUiOiJjZW50b3MiLCJVVUlEIjoiNjE3YTYzMjQtN2E5My00MjEyLTk4N2UtYTQ4ZWI1MDFmMzRkIiwiT2Zmc2V0IjowLCJTdGFydGVkQXQiOiIyMDIxLTA1LTMwVDA1OjM4OjMyLjQ3NTA4Njg2MVoifQ%3D%3D HTTP/1.1", host: "example.test"
```

[ドキュメント](https://docs.docker.com/registry/recipes/nginx/)では `client_max_body_size 0;` として制限を無効にしていました。
デフォルトのnginx.conf設定は`1M`のため、忘れずに適用しましょう。

## ② 認証ファイルのフォーマット

詳しくは[ここ](http://nginx.org/en/docs/http/ngx_http_auth_basic_module.html)を参照 
  
nginxが参照できるパスワードフォーマットは[RFC 2307](https://datatracker.ietf.org/doc/html/rfc2307#section-5.3)に記載のあるものとのことです。その中にbcryptはなく、対応してないため注意しましょう。  
今回は以下の様にMD5でパスワードファイルを作っています。

```bash:htpasswd作成
$ htpasswd -b -c -m htpasswd test test
```
ちなみに`-m`オプションはhtpasswdのデフォルトなのであってもなくてもいいです。

## コンテナ立ち上げ

起動します。

```bash
$ docker-compose up -d
$ docker-compose ps
             Name                           Command               State               Ports
-------------------------------------------------------------------------------------------------------
registry-nginx_registry_1        /entrypoint.sh /etc/docker ...   Up      5000/tcp
registry-nginx_reverse-proxy_1   /docker-entrypoint.sh ngin ...   Up      0.0.0.0:5043->443/tcp, 80/tcp
```

OK

## イメージpush,pull

レジストリにログインします。

```bash
$ docker login example.test:5043
Username: test
Password:
WARNING! Your password will be stored unencrypted in /home/test/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded
```
イメージをpushします

```bash
$ docker image tag nginx:alpine example.test:5043/test-nginx:alpine
$ docker push example.test:5043/test-nginx:alpine
The push refers to repository [example.test:5043/test-nginx]
4689e8eca613: Pushed
3480549413ea: Pushed
3c369314e003: Pushed
4531e200ac8d: Pushed
ed3fe3f2b59f: Pushed
b2d5eeeaba3a: Pushed
alpine: digest: sha256:480395b7be07df06368c93469df41343c1e6285f5b37d387b5a17f2e8708a2a3 size: 1568
$ curl -u test:test https://example.test:5043/v2/_catalog
{"repositories":["test-nginx"]}
```
OKです。
次はpullします

```bash
### 確認のために既存のイメージを削除
$ docker rmi -f nginx:alpine
$ docker rmi -f example.test:5043/test-nginx:alpine
### pullする
$ docker pull example.test:5043/test-nginx:alpine
alpine: Pulling from test-nginx
Digest: sha256:480395b7be07df06368c93469df41343c1e6285f5b37d387b5a17f2e8708a2a3
Status: Downloaded newer image for example.test:5043/test-nginx:alpine
example.test:5043/test-nginx:alpine
```
OKです。  
では次のパターン...の前に忘れずにコンテナを落としておきましょう。

```bash
$ docker-compose down
$ docker-compose ps
Name   Command   State   Ports
------------------------------
$
```

# リバースプロキシ(nginx)を経由しないパターン

## ディレクトリ構成
ディレクトリ構成は以下

```bash:ディレクトリ構成
.
|-- cert
|   |-- fullchain.pem
|   `-- privkey.pem
|-- data
|-- docker-compose.yaml
`-- registry
    |-- htpasswd
    `-- no-nginx-config.yml
```
config.ymlは区別のために名前を変えています。

## docker-compose.yaml

```yaml:docker-compose.yaml
version: "3.9"
services:
  only-registry:
    image: registry:2
    ports:
      - 5000:5000
    volumes:
      - ./data:/var/lib/registry
      - ./registry/no-nginx-config.yml:/etc/docker/registry/config.yml
      - ./registry/htpasswd:/auth/htpasswd:ro
      - ./cert/fullchain.pem:/certs/fullchain.pem:ro
      - ./cert/privkey.pem:/certs/privkey.pem:ro
```

## no-nginx-config.yml

```yaml:no-nginx-config.yml
version: 0.1
log:
  accesslog:
    disabled: false
  level: warn
  formatter: text
  fields:
    environment: test
storage:
  cache:
    blobdescriptor: inmemory
  filesystem:
    rootdirectory: /var/lib/registry
http:
  addr: :5000
  tls: # ※①
    certificate: /certs/fullchain.pem
    key: /certs/privkey.pem
auth:
  htpasswd: # ※②
    realm: basic-realm
    path: /auth/htpasswd
```
以下でポイントについて説明します。

## ①tls

詳しくは[ここ](https://docs.docker.com/registry/configuration/#tls)を参照  

```yaml
http:
  addr: :5000
  tls:
    certificate: /certs/fullchain.pem
    key: /certs/privkey.pem
```
docker-registryに直接証明書設定を入れる場合はtlsオプションを設定します。  
秘密鍵、証明書のパスを指定します。ホストのパスとごっちゃにならないように注意しましょう

## ②認証ファイルのフォーマット
詳しくは[ここ](https://docs.docker.com/registry/configuration/#htpasswd)を参照 

以下の通りbcryptフォーマットしかサポートしていないため、上述したnginxの場合とは異なります。

>The htpasswd authentication backed allows you to configure basic authentication using an Apache htpasswd file. The only supported password format is bcrypt. Entries with other hash types are ignored.

以下の様にhtpasswdファイルを作成しましょう。

```bash
$ htpasswd -b -B -c test.htpasswd test test
```

## コンテナ立ち上げ

起動します。

```bash
$ docker-compose up -d
$ docker-compose ps
            Name                           Command               State           Ports
-----------------------------------------------------------------------------------------------
registry-only_only-registry_1   /entrypoint.sh /etc/docker ...   Up      0.0.0.0:5000->5000/tcp
```
OK

## イメージpush,pull

ログインします。

```bash
$ docker login example.test:5000
Username: test
Password:
WARNING! Your password will be stored unencrypted in /home/test/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded
```

pushします

```bash
$ docker pull nginx:alpine
$ docker image tag nginx:alpine example.test:5000/test-nginx:alpine
$ docker push example.test:5000/test-nginx:alpine
The push refers to repository [example.test:5000/test-nginx]
058eb06e0efd: Pushed
2f2df3ae0cad: Pushed
2b60f0243825: Pushed
96131b349b16: Pushed
a42a23cd7b07: Pushed
b2d5eeeaba3a: Pushed
alpine: digest: sha256:2e76f4d4e21bae1d012267cff67e032bb006a2cff5c00afa5d4ad011c1855128 size: 1568
$ curl -u test:test https://example.test:5000/v2/_catalog
{"repositories":["test-nginx"]}
```

OK。pullします。

```bash
$ docker pull example.test:5000/test-nginx:alpine
alpine: Pulling from test-nginx
540db60ca938: Already exists
0ae30075c5da: Pull complete
9da81141e74e: Pull complete
b2e41dd2ded0: Pull complete
7f40e809fb2d: Pull complete
758848c48411: Pull complete
Digest: sha256:2e76f4d4e21bae1d012267cff67e032bb006a2cff5c00afa5d4ad011c1855128
Status: Downloaded newer image for example.test:5000/test-nginx:alpine
example.test:5000/test-nginx:alpine
```
OK！
どちらのパターンでもpush,pullできていますね。  
それでは落としておきましょう

```bash
$ docker-compose down
$ docker-compose ps
Name   Command   State   Ports
------------------------------
$
```

# 自己証明書を使用する

詳しくは[ここ](https://docs.docker.com/engine/security/certificates/)を参照 

nginxを経由するしないにかかわらず、自己証明書を使用した場合は以下の様に出力され、registryへログインできません。

```bash
$ docker login example.test:5043
Authenticating with existing credentials...
Login did not succeed, error: Error response from daemon: Get https://example.test:5043/v2/: x509: certificate signed by unknown authority
```

その場合、以下の様に対処します。

```bash
### 移動
$ cd /etc/docker/certs.d/

### ディレクトリ作成
$ sudo mkdir example.test:5043

### 証明書ファイル配置
$ ll example.test\:5043/
合計 12
-rw-r--r-- 1 root root 1172  6月  6 14:00 ca.crt
-rw-r--r-- 1 root root 1155  6月  6 14:00 client.cert
-rw-r--r-- 1 root root 1679  6月  6 14:01 client.key

### 接続
$ docker login example.test:5043
Authenticating with existing credentials...
WARNING! Your password will be stored unencrypted in /home/test/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded

```
無事接続できました。
接続先FQDN + ポート番号でディレクトリを作り、そこに各ファイルを格納しています。 
各種ファイルは自己証明書作成時にできたものを持ってきましょう。

ちなみにファイル名が上記以外だとログインに失敗します。ドキュメントに従いましょう

```bash
$ sudo mv ca.crt ca.cr
$ sudo mv client.cert client.cet
$ sudo mv client.key client.ky
$ docker login example.test:5043
Authenticating with existing credentials...
Login did not succeed, error: Error response from daemon: Get https://example.test:5043/v2/: x509: certificate signed by unknown authority
```

# 終わりに

無事https接続できてよかったです。
次回はhttp APIを叩いてイメージをいろいろ見てみたいと思います。