Let's EncryptでHTTPSを終端させたいだけならNginxよりCaddyを使うと楽だった件

みなさん！Webサーバ立ててますか？HTTPSしてますか？

弊社でも、よく自社向けの開発サーバをDockerで立てており、開発サービスは生HTTPで運用、そのフロントにHTTPS終端用のNginxをたてて、その証明書はメンテ＆費用フリーの[Let's Encrypt](https://letsencrypt.org/ja/)で取得するようにしています。

```
インターネット → (HTTPS) → [ Nginx → (HTTP) → 開発サービス ]
```

こういうDockerでLet's EncryptでHTTPSしたいだけなら、同一のDockerネットワークに特定条件のDockerコンテナが生えるたびにNginxファイルを動的生成してLet's Encryptの[ACME](https://ja.wikipedia.org/wiki/Automated_Certificate_Management_Environment)も通してなど全部やってくれる、下記を組み合わせるのが定番なのですが、

- [nginx-proxy](https://github.com/nginx-proxy/nginx-proxy)
- [acme-companion](https://github.com/nginx-proxy/acme-companion)

上記は全般的に仕組みがやや煩雑で、LEのアップデートの煽りをくらったり、謎のエラーで死んでたり、コンテナが生きてないと通らなかったり、使わないDNSエントリでACMEが通らなかったり、何かしら転び始めるとちょうど忙しい時期に `Let's Encrypt certificate expiration notice for domain "your.example.com"` みたいなメールが来て対応を余儀なくされることがよくあります。

## そこで [Caddy](https://caddyserver.com/)！

そういうことで、もっとシンプルなソリューションはないかと考えた結果、Webサーバ（リバースプロキシ）として、世界シェアナンバーワンのNginxではなく、[Caddy](https://caddyserver.com/)を使えばいいという結論に達しました。

```
インターネット → (HTTPS) → [ Caddy → (HTTP) → 開発サービス ]

※ Caddy　は localhost のポートにリバースプロキシ
※ 開発サービスは localhost 上にポート公開
```

### Caddyとは？

CaddyはWebサーバの1種です。 2015年からリリースされているかなり若いサーバです。

ただし、[公式](https://caddyserver.com/docs/)が書いているように、Webサーバ以外としても動作可能なようです。

> Most people use Caddy as a web server or proxy, but at its core, Caddy is a server of servers. With the requisite modules, it can take on the role of any long-running process!

（詳しくは [Wikipedia](https://ja.wikipedia.org/wiki/Caddy) にまとまっています）

## Caddyのいいところ

- **Let's Encrypt にネイティブ対応**している
    - これがCaddyの最強ポイント
    - certbot など別途準備が一切不要
    - DNSでAレコード設定してれば勝手に証明書取得〜設定全部完了する
        - そのバーチャルドメインへの初回アクセス時、必要に応じて証明書を取得しに行く
        - ワイルドカードドメインについては別途設定が必要です
- [Go](https://go.dev/)で書かれている
    - 基本的にメモリ安全かつ並列性に優れ、モジュールポータビリティが高いことが期待できる
    - おそらくパフォーマンスは既存ウェブサーバに勝たないと思われるが、そんなピーキー用途は少ないと思うので問題なし
- 設定ファイルにJSON/YAML/Tomlなど使える
    - ただまあ普通は Caddyfile で書く
- 安全なデフォルト設定
    - 何も気をつけないでも HTTP/2.0 & TLS 1.3 が有効になる
    - 自動的に http が https にリダイレクトされる
- 便利なデフォルト設定
    - リバースプロキシに下記のヘッダを自動でつけてくれる
        - `X-Forwarded-For`
        - `X-Forwarded-Proto`
        - `X-Forwarded-Host`
- Caddy自身のWebAPI経由で設定を書き換えることができる
- 再起動無しで設定を根本からリロードできる
    - Nginxでも `kill -s HUP` でできます
    - [Caddy (現Caddy2) の場合はシグナル送付ではリロードできない](https://github.com/caddyserver/caddy/issues/3967)ので `caddy reload` コマンドでリロードリクエストを送ることができます

正直普通に開発サーバ用にHTTPS終端を立てる用途ならCaddyはおすすめです。


## Caddyの設定ファイルはすごくシンプル
　
たとえば、 http://localhost:8080 にHTTPSをつなぎたいサービスがある場合、下記のファイルを `/etc/caddy/Caddyfile` に置いて `systemctl reload caddy` (Linuxの場合) すればOKです。

```Caddyfile
{
    email your_mail_address_here@example.com
}

your.example.com {
    reverse_proxy localhost:8080
}

// ちなみに設定ファイルが間違っている場合、今の設定のままサーバが動き続けます。
```

まだ設定がCaddyにロードされた段階では証明書は取得されませんが、[On-Demand TLS](https://caddyserver.com/docs/automatic-https#on-demand-tls)という仕組みで、 https://your.example.com への初回アクセス発生時にオンデマンドで証明書が取得＆設定されます。

> 追記 (2023/05/30):
> [`tls { on_demand }` ディレクティブの設定が必要です。](https://caddyserver.com/docs/caddyfile/directives/tls#on_demand)

この仕組みのおかげで、特にDNSが設定されていないドメインについて、バーチャルドメイン設定が設定ファイルに書いてあっても特に害にはなりません。（使ってないドメインについてDNS設定だけが抜けるのは開発サーバあるあるです）

ちなみに、下記の設定だと、バーチャルドメインの指定すら不要になります。アクセスされたドメインで証明書を勝手に取得しにいきます。

```
https:// {
    tls {
        on_demand
    }
    reverse_proxy localhost:8080
}
```

（ただし、HTTP上はリクエスト側でバーチャルドメインは任意に指定でき、[Rate Limits](https://letsencrypt.org/docs/rate-limits/)上限に達するまでACMEをさせる攻撃が成立してしまうので実際に上記を[やってはいけません](https://caddy.community/t/serving-tens-of-thousands-of-domains-over-https-with-caddy/11179)）


## Caddyのインストール方法について

詳しくは下記のURLに書いてあるのですが、CaddyはGoで書かれているおかげか、Windows含めいろんなプラットフォームに対応しています。

https://caddyserver.com/docs/install

ただ、若干マイナーなせいかLinuxのディストリビューション標準のリポジトリに入っていないので、`apt` / `dnf` コマンド一発というわけにはいきません。加えて、私が手元のAmazon Linux2（ややメンテされてない）でインストールしようとした時も、インストール手順に従ってもインストールできませんでした。

そういうことで、若干インストールの面倒さを考慮すると、パッケージで入れずに [Docker](https://hub.docker.com/_/caddy) で立ててしまうのは一つの手だと思います。

下記のような `docker-compose.yml` で立ちます。

```yaml:docker-compose.yml
version: "3"

services:
  caddy_server:
    image: caddy
    # ローカルホストにそのままプロキシさせたいのでホストネットワークモード
    network_mode: host
    volumes:
      # Dockerはinodeでボリュームマウントするので、
      # 設定ファイルはディレクトリごとマウントしたほうが設定ファイル書き換え時に無難
      - ./caddy:/etc/caddy:ro
      # 証明書やACME情報等の保存先
      - ./data:/data
      # 今の設定状態（保存する必要がある場合は永続化）
      - ./config:/config
```

```caddyfile:./caddy/Caddyfile
{
    email your_mail_address_here@example.com
}

your.example.com {
    reverse_proxy localhost:8080
}
```

設定ファイルリロードは `docker-compose exec -w /etc/caddy caddy_server caddy reload` で出来ます。 (`-w /etc/caddy` は設定ファイルのあるディレクトリの指定)


## Caddyのちょっと面倒なところ

これはしょうがないのですが、またディレクティブを覚え直ししなきゃいけないのが面倒です。[案外IPアドレスのホワイトリストなど指定が初見時に面倒だったりします。](https://caddy.community/t/only-allow-certain-ips-to-access-the-server-in-reverse-proxy/11213)

また、ディレクティブについても書いた順ではなく、暗黙に持っている優先度順で適用されるのが、微妙に罠っぽいです。 

たとえば、下記は思ったとおりに動きません。 `redir` が `handle` より優先されます。

```Caddyfile:Caddyfile
your.example.com {
  handle /images/* {
    redir https://imageserver.example.com{uri}
  }
  # /images/ 以外は別サイトにリダイレクト
  redir https://anothersite.example.com{uri}
}

// https://your.example.com/images/foo.png
//   => https://anothersite.example.com/images/foo.png にリダイレクトされる
```

これは [route](https://caddyserver.com/docs/caddyfile/directives/route) ディレクティブを使って回避できます。名前からはあんまり想像できないですが、書いた順にディレクティブを評価するように強制できます。

```Caddyfile:Caddyfile
your.example.com {
  route {
    handle /images/* {
      redir https://imageserver.example.com{uri}
    }
    # /images/ 以外は別サイトにリダイレクト
    redir https://anothersite.example.com{uri}
  }
}

// https://your.example.com/images/foo.png
//   => https://imageserver.example.com/images/foo.png にリダイレクトされる
```

## Caddy は LocalTunnel (ngrok) の代わりにも使える

[SirTunnel](https://github.com/anderspitman/SirTunnel) という [ngrok](https://ngrok.com/) みたいに localhost のWebサーバを外部に向けてHTTPS公開できるシンプルなトンネルプロキシツールがあります。

https://github.com/anderspitman/SirTunnel

ツールの実装として単純にポートフォワーディング(`-R 9001:localhost:8080`)を行うSSHコマンドを叩いてサーバ上でPythonスクリプト(`sirtunnel.py sub1.example.com 9001`)を動かしているだけです。

```console
$ ssh -tR 9001:localhost:8080 example.com sirtunnel.py sub1.example.com 9001
```

このスクリプトの中で、ポートフォワーディング先にHTTPSからのリクエストを流し込むようにするCaddyへの構成の変更を [CaddyのAPI](https://caddyserver.com/docs/api) 経由で行っているようです。

非常にシンプルですね。これをヒントに色々 Caddy で面白いプロキシサーバを作れそうな感じもします。


## まとめ

特にピーキーな用途でなければ [Caddy](https://caddyserver.com/) は十分に実用的なHTTPS終端になります。

日常使っている感想としては、本当に設定が楽で、面倒な証明書更新に苦しまなくなりました。みなさんも使いましょう！