[NetBox] Caddyを使ってLet's Encryptの証明書でHTTPSアクセスできるようにNetBoxをデプロイする

本記事は「[エーピーコミュニケーションズ Advent Calendar 2021](https://qiita.com/advent-calendar/2021/ap-com)」の5日目のエントリとなります。

[IPAM/DCIMツールであるNetBoxをDockerで簡単にデプロイできるnetbox-docker](https://github.com/netbox-community/netbox-docker)は、最小構成ではHTTPアクセスの設定でデプロイされます。
社内検証環境など閉じたネットワーク内であれば特に問題ないですが、クラウドサービスのVMにデプロイしてインターネット経由でアクセスしたい構成など、HTTPSで通信を暗号化したいケースもあると思います。
本記事では、簡単な設定追加でnetbox-dockerをHTTPSアクセスの構成にできるので、そのやり方をまとめました。

NetBox単体のデプロイは[公式のREADME](https://github.com/netbox-community/netbox-docker/blob/release/README.md)や下記の過去記事(少し古いですが)を参照してください。

https://zaki-hmkc.hatenablog.com/entry/2021/01/11/171626

※ netbox-dockerのリポジトリをクローンして`docker-compose.override.yml`を編集してデプロイ、という流れは同じです。

# HTTPSで使用する追加のミドルウェア

CaddyというGo言語で実装されたOSSのwebサーバーを使用します。

https://caddyserver.com/

このサーバーの大きな特徴は、Let's Encryptなどの証明書作成を行うACMEクライアントが内蔵していることです。証明書作成時に使用するメールアドレスの1行を設定ファイルへ記載することでLet's Encryptの証明書発行から設定までが自動で行われ、HTTPSのwebサーバーを簡単にデプロイできます。
証明書作成のデフォルト設定はLet's Encryptになっていますが、ACMEに対応していれば任意のサービスで証明書作成を利用できます。

この機能と、リバースproxyを使用し、NetBoxへのHTTPSアクセスをCaddyが中継する構成でNetBoxのHTTPSアクセスを実現します。

# 構成図

Caddyを使用したHTTPS構成の場合はクライアントからのwebアクセスはCaddyが受け付けて、Caddyからリバースproxyを使ってNetBoxへコンテナ間通信させる構成となります。

![NetBox - TLS-Page-6 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256859/b560393a-85f3-9cef-b6de-5da13fa20349.png)

従来(最小構成)の構成ではNetBox自体が(8080/TCPで)Listenしているので、これをDockerのpublish設定でホストOSからアクセスできるようにしますが、Caddyが外部からのアクセスをListenする構成になるため、NetBoxのpublish設定は不要になります。

また、大前提ですが、設定するドメイン名は名前解決できるように事前にDNSの設定を行っておきます。
本記事では`netbox.example.org`としています。

# 設定

Caddy本体の設定と、CaddyをデプロイするDocker Composeファイルの2つ行います。

## Caddy本体

`tls`についての設定は必要ありません。
ACMEで証明書作成時に使用するメールアドレスのみ、`email`ディレクティブに記載します。
ここでは`user@mail.example.org`としています。

また、設定のFQDNの部分は前述のDNSの設定の通り、証明書取得に使用するドメイン名を指定します。
`reverse_proxy`の設定は、Caddyサーバーから見たNetBoxのアドレスですが、Composeファイルで`netbox`と定義されているので、`netbox:8080`と記述することでコンテナ間通信できます。

必要なファイルは以下の通りです。

```conf:Caddyfile
{
    email user@mail.example.org
}

netbox.example.org {
    reverse_proxy netbox:8080
    encode gzip zstd
    
    log {
      level error
    }
}
```

この内容のファイルを、`docker-compose.yml`と同じディレクトリに作成します。
(Composeファイル内にパス指定するので別にどこでも良いけど、単にわかりやすく、という意味で)

## Docker Compose

[NetBoxのドキュメントにもTLSについての簡単な記載があります](https://github.com/netbox-community/netbox-docker/wiki/TLS)が、この内容だとボリュームを使っていないため、コンテナの削除によって作成した証明書なども削除されます。
Caddy内にコンテンツも保持しておらず再デプロイ時に証明書は自動生成されるので機能的には問題ないですが、コンテナ生成のたびに証明書作成が行われると[Let's Encrypt側の制限でエラー](https://letsencrypt.org/docs/rate-limits/)になる場合もあるため、作成済み証明書などを継続して使用するために自動生成されるファイル類をボリューム領域に保存する設定も追加します。
(ボリュームを削除しない限りは証明書などのデータは保持されます)

https://github.com/docker-library/docs/tree/master/caddy

ストレージ関連の設定は以下2点です。

- 証明書などのコンテナデプロイ後に生成されるファイルのボリューム設定
- `Caddyfile`のバインドマウント

```yaml:docker-compose.override.yml
services:
  tls:
    image: caddy:2-alpine
    depends_on:
      - netbox
    volumes:
      - caddy_data:/data
      - caddy_config:/config
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
    ports:
      - 80:80
      - 443:443
volumes:
  caddy_data:
  caddy_config:
```

80/TCPのListenはNetBoxのサービスには必要ないですが、HTTPアクセスされた場合にHTTPSへリダイレクトしたい場合に80/TCPも指定しておけばデフォルトで機能します。
ACMEのチャレンジにおいては、80/TCPの通信を許可していない場合は[TLS-ALPN challenge](https://caddyserver.com/docs/automatic-https#tls-alpn-challenge)が自動で選択されるため、問題無く証明書は作成されます。

また、前述の通りCaddyでリモートからのアクセスをListenするので、NetBox用の(従来設定してた以下の内容の)ポート設定は不要です。

```yaml:不要になる設定
  netbox:
    ports:
      - 8000:8080
```

# デプロイ

準備ができたら普通にDocker Composeをデプロイします。

```console
$ docker compose up -d
```

Caddyで証明書作成が完了し443/TCPでListenするようになるまでしばらく待機すれば、ブラウザを使って設定したFQDNでNetBoxへHTTPSアクセスできるようになります。

# 環境

以下の環境で確認しました。

- EC2 (Amazon Linux 2)
- Docker 20.10.7 (EC2/Amazon Linux 2の標準リポジトリでインストールできるパッケージ)
- Docker Compose v2.0.1
- NetBox v3.0.4
- netbox-docker 1.4.1

ちなみにNetBoxのDockerコンテナはamd64版しか提供されてませんが、Caddyはarm64版もあるので、より多くの環境で利用できます。
(NetBoxをarm64でデプロイするには[自前でイメージをビルドする必要がある](https://zaki-hmkc.hatenablog.com/entry/2021/09/30/231854))

# 証明書ファイルを別途用意する場合

ACMEで作成する証明書ではなく、閉域網のため自己署名の証明書を使いたいとか、すでに作成済みの証明書がある場合など、Dockerのバインドマウントを使ってCaddyで参照させてデプロイできます。
以下は`netbox01.example`というドメインの場合の例です。

例えば以下のように自己署名の証明書を作成しておき、

```
$ openssl req -newkey rsa:4096 -nodes -sha256 -keyout server.key -x509 -days 365 -out server.crt -subj '/CN=netbox01.example/'
```

Caddy本体の設定は、以下の通り、`tls`の設定を追加し、使用する証明書のパスを指定します。
このパスにコンテナ起動時に証明書ファイルをホストOSから渡すようにします。

```conf:Caddyfile
netbox01.example {
    reverse_proxy netbox:8080
    encode gzip zstd
    
    tls /etc/ssl/private/server.crt /etc/ssl/private/server.key

    log {
      level error
    }
}
```

`tls`の詳しい設定は[Caddyのドキュメント](https://caddyserver.com/docs/caddyfile/directives/tls)を参照してください。

Composeファイルは以下の通り。
ホストOS上で持っている作成済み証明書ファイルを、Caddyfileで指定したパスへバインドする設定を追加します。

```yaml
services:
  tls:
    image: caddy:2-alpine
    depends_on:
      - netbox
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./server.crt:/etc/ssl/private/server.crt:ro,z
      - ./server.key:/etc/ssl/private/server.key:ro,z
    ports:
      - 80:80
      - 443:443
```

# まとめ

本記事では、NetBoxのデプロイ時にCaddyを使って簡単にLet's Encryptの証明書取得からHTTPSサーバー構築まで自動で実施する内容を紹介しました。
今回はNetBoxが対象でしたが、同じ構成で任意のwebサーバーをHTTPS化できますので、いろんなパターンに応用できるると思います。

# 参考

- Caddy
    - [Caddy - The Ultimate Server with Automatic HTTPS](https://caddyserver.com/)
    - [tls (Caddyfile directive) — Caddy Documentation](https://caddyserver.com/docs/caddyfile/directives/tls)
    - [Automatic HTTPS — Caddy Documentation](https://caddyserver.com/docs/automatic-https)
    - [Caddy - Official Image | Docker Hub](https://hub.docker.com/_/caddy)
- NetBox
    - [netbox-community/netbox-docker: 🐳 Docker Image of NetBox](https://github.com/netbox-community/netbox-docker)
    - [TLS · netbox-community/netbox-docker Wiki](https://github.com/netbox-community/netbox-docker/wiki/TLS)
- ACME
    - [ACME クライアント実装 - Let's Encrypt - フリーな SSL/TLS 証明書](https://letsencrypt.org/ja/docs/client-options/)