OpenBSD acme-client で Let's Encrypt 証明書を取得する
Tags: openbsd, tls, security, letsencrypt
Published: 2023-09-09 14:03:50

## はじめに

### OpenBSD の acme-client

[`acme-client`](https://man.openbsd.org/acme-client.1) は [OpenBSD](https://www.openbsd.org/) で標準の自動証明書管理環境 (Automatic Certificate Management Environment, ACME) のためのクライアントです。このソフトウェアは OS インストール時にインストールされます。


[acme-client.conf](https://man.openbsd.org/acme-client.conf.5) で設定しますが、これは難しくありません。そしてそれによって [Let's Encrypt](https://letsencrypt.org/) や他の認証局から証明書を取得できるようになります。


基本的な使い方はこのような感じです:

```console
# # 設定
# nvim /etc/acme-client.conf

# # 証明書の取得または (リニューアルのための) 更新
# acme-client <domain>

# # 証明書の無効化 (失効させる)
# acme-client -r <domain>
```

### CertBot について

余談ですが、有名な他の選択肢がありますね。そうです、[CertBot](https://certbot.eff.org/) です。[電子フロンティア財団 (Electronic Frontier Foundation, EFF)](https://www.eff.org/) から提供されている、非常によく使われているクライアントです。`pkg_add certbot` の実行でインストールすることは可能です。しかしながら、このソフトウェアを OpenBSD 環境下で使うと、証明書リニューアルの時に [パーミッションの問題が発生することがあります (英語)](https://github.com/OpenSMTPD/OpenSMTPD/issues/785) 。


### 環境

- OS: OpenBSD [7.0](https://www.openbsd.org/70.html)
- ACME クライアント: OpenBSD acme-client
- 証明書の認証局: Let's Encrypt
- Web サーバー: OpenBSD [httpd](https://man.openbsd.org/httpd.8)

## 使い方

### `acme-client.conf` の設定

OpenBSD から公式に提供されている設定ファイルがあります。それを見てみましょう。

```console
$ cat /etc/examples/acme-client.conf
```

わかりやすい例が書かれています:

```conf
#
# $OpenBSD: acme-client.conf,v 1.4 2020/09/17 09:13:06 florian Exp $
#
authority letsencrypt {
	api url "https://acme-v02.api.letsencrypt.org/directory"
	account key "/etc/acme/letsencrypt-privkey.pem"
}

authority letsencrypt-staging {
	api url "https://acme-staging-v02.api.letsencrypt.org/directory"
	account key "/etc/acme/letsencrypt-staging-privkey.pem"
}

authority buypass {
	api url "https://api.buypass.com/acme/directory"
	account key "/etc/acme/buypass-privkey.pem"
	contact "mailto:me@example.com"
}

authority buypass-test {
	api url "https://api.test4.buypass.no/acme/directory"
	account key "/etc/acme/buypass-test-privkey.pem"
	contact "mailto:me@example.com"
}

domain example.com {
	alternative names { secure.example.com }
	domain key "/etc/ssl/private/example.com.key"
	domain full chain certificate "/etc/ssl/example.com.fullchain.pem"
	sign with letsencrypt
}
```

コピーして使いましょう:

```console
$ # 初期状態では以下を実行しても何も出ない (余談)
$ ls /etc/acme-client*

$ doas cp -p /etc/examples/acme-client.conf /etc/
```

設定ファイルを編集しましょう:

```console
$ doas nvim /etc/acme-client.conf
```

例えば以下のような内容です:

```diff
- domain example.com {
- 	alternative names { secure.example.com }
- 	domain key "/etc/ssl/private/example.com.key"
- 	domain full chain certificate "/etc/ssl/example.com.fullchain.pem"
- 	sign with letsencrypt
- }
+ domain your.domain {    
+ 	#alternative names { secure.example.com }    
+ 	domain key "/etc/ssl/private/your.domain.key"    
+ 	domain full chain certificate "/etc/ssl/your.domain.fullchain.pem"    
+ 	sign with letsencrypt    
+ }
```

さあこれで `your.domain` について "sign with letsencrypt (Let's Encrypt でサインする)" 準備が整いました。


### OpenBSD httpd (Web サーバー) を起動する

`httpd` が egress をリッスンしていることが必要です:

```apache
server "your.domain" {
	listen on egress port 80
	# ...
}
```

ちなみに [`httpd.conf`](https://man.openbsd.org/httpd.conf.5) のサンプルファイルも `/etc/examples/` で見付けられます。


もちろんこのデーモンを起動させておく必要があります:

```console
$ # httpd が有効化されていない場合:
$ doas rcctl start -f httpd
$ # 有効化されている場合:
$ doas rcctl restart httpd
```

### 証明書をリクエストする

以下の一行のコマンドを実行するだけです。そうすればうれしいことに証明書を取得できます:

```console
$ doas acme-client -v your.domain
```

ここで `-v` は verbose 出力のためのもので、省略可能です。

他の使い方もあります。更新は次のように行います:

```console
$ # 上と同じコマンド
$ doas acme-client your.domain
```

さらに証明書を失効させることもできます:

```console
$ doas acme-client -r your.domain
```

## 補足

### 定期的な更新が必要

Let's Encrypt 証明書の有効期間は 90 日間しか無いことを憶えておきましょう。定期的に更新する必要があります。


対応策の一つに [`cron`](https://man.openbsd.org/cron) の使用が考えられます。

### Let's Encrypt には制限 (Rate Limits) が存在する

Let's Encrypt にはいくつかのレート制限があることに注意してください。

一例ですが、

- "Certificates per Registered Domain" (登録ドメインごとの証明書)。1 週間あたり 50 件までです。

また他の例ですが、

- "Duplicate Certificate" (証明書の重複)。同じ証明書を更新できるのは 1 週間に最大 4 回 (合計で 5 回) までです。

これらに関わる公式ドキュメントは [こちら (英語)](https://letsencrypt.org/docs/rate-limits/) です。

### サイド・ストーリー: 歴史と経緯

acme-client は初めて登場したのは [6.1](https://www.openbsd.org/61.html) のリリースでした。


[6.5](https://www.openbsd.org/65.html) あるいはそれ以前 (ただし 6.1 以降) では、証明書取得のために指定しなければならないコマンドオプションの数が、現在よりも多かったです:

```console
$ doas acme-client -ADv your.domain
```

しかし [6.6](https://www.openbsd.org/66.html) で acme-client が v02 Let's Encrypt API とやり取りできるようになりました。それによって `-A` および `-D` オプションは不要になったのです。ありがたい改善です。

### トラブルシューティング: チャレンジが失敗した場合

以下のようなエラーが出て、失敗に終わり悲しい思いをすることがあるかもしれません:

```console
acme-client: /etc/ssl/private/your.domain: generated RSA domain key
acme-client: https://acme-v02.api.letsencrypt.org/directory: directories
acme-client: acme-v02.api.letsencrypt.org: DNS: xxx.xxx.xxx.xxx
acme-client: dochngreq: https://acme-v02.api.letsencrypt.org/acme/authz-v3/84303399960
acme-client: challenge, token: (...), uri: https://acme-v02.api.letsencrypt.org/acme/chall-v3/84303399960/MQGXJQ, status: 0
acme-client: /var/www/acme/(...): created
acme-client: https://acme-v02.api.letsencrypt.org/acme/chall-v3/84303399960/MQGXJQ: challenge
acme-client: order.status 0
acme-client: dochngreq: https://acme-v02.api.letsencrypt.org/acme/authz-v3/84303399960
acme-client: challenge, token: (...), uri: https://acme-v02.api.letsencrypt.org/acme/chall-v3/84303399960/MQGXJQ, status: -1
acme-client: order.status -1
acme-client: dochngreq: https://acme-v02.api.letsencrypt.org/acme/authz-v3/84303399960
acme-client: Invalid response from http://your.domain/.well-known/acme-challenge/(...) [2606:4700:3031::6815:3ae6]: \\"\\u003c!DOCTYPE html\\u003e\\\\n\\u003chtml\\u003e\\\\n\\u003chead\\u003e\\\\n\\u003cmeta charset=\\\\\\"utf-8\\\\\\"\\u003e\\\\n\\u003ctitle\\u003e404 Not Found\\u003c/title\\u003e\\\\n\\u003cstyle type=\\\\\\"text/css\\\\\\"\\u003e\\u003c!--\\\\nbody { background-\\"
acme-client: bad exit: netproc(87417): 1
```

これは 404 つまり ページが見付かりません エラーです。別のケースもあります。403 つまり アクセスが拒否されました エラーです。


これらはあなたの Web サーバーが "不正なレスポンス (bad response)" を返したことによるものです。チャレンジに対するレスポンスを正しくマッピングしてあげましょう:

```diff
  server "your.domain" {
  	listen on egress port 80
+ 	# acme-client
+ 	location "/.well-known/acme-challenge/*" {
+ 		root "/acme"
+ 		request strip 2
+ 	}
  	# ...
  }
```

その後に httpd を再起動します:

```console
$ # デーモンを有効化していない場合:
$ doas rcctl -f restart httpd
$ # 有効化している場合:
$ doas rcctl restart httpd
```

これで直るかもしれません。

```console
# acme-client -v your.domain
```

以下のようなメッセージに変われば成功です:

```console
acme-client: https://acme-v02.api.letsencrypt.org/directory: directories
acme-client: acme-v02.api.letsencrypt.org: DNS: xxx.xxx.xxx.xxx
acme-client: dochngreq: https://acme-v02.api.letsencrypt.org/acme/authz-v3/84304202350
acme-client: challenge, token: (...), uri: https://acme-v02.api.letsencrypt.org/acme/chall-v3/84304202350/NVmlRA, status: 0
acme-client: /var/www/acme/(...): created
acme-client: https://acme-v02.api.letsencrypt.org/acme/chall-v3/84304202350/NVmlRA: challenge
acme-client: order.status 0
acme-client: dochngreq: https://acme-v02.api.letsencrypt.org/acme/authz-v3/84304202350
acme-client: challenge, token: (...), uri: https://acme-v02.api.letsencrypt.org/acme/chall-v3/84304202350/NVmlRA, status: 2
acme-client: order.status 1
acme-client: https://acme-v02.api.letsencrypt.org/acme/finalize/58118741/68735636600: certificate
acme-client: order.status 3
acme-client: https://acme-v02.api.letsencrypt.org/acme/cert/(...): certificate
acme-client: /etc/ssl/your.domain: created
```

## おわりに

Let's Encrypt の証明書を取得できました ! あとはそれを `httpd.conf` で設定するだけです。

```diff
  server "your.domain" {
  	listen on * port 80
  	# acme-client
  	location "/.well-known/acme-challenge/*" {
  		root "/acme"
  		request strip 2
  	}
+ 	block return 301 "https://$SERVER_NAME$REQUEST_URI"
  }
+ server "your.domain" {
+ 	listen on * tls port 443
+ 	tls {
+ 		certificate	"/etc/ssl/your.domain.fullchain.pem"
+ 		key		"/etc/ssl/private/your.domain.key"
+ 	}
+ 	# acme-client
+ 	location "/.well-known/acme-challenge/*" {
+ 		root "/acme"
+ 		request strip 2
+ 	}
+ 	# ...
+ }
```

ここで、私は `egress` を `*` に変更しました。これは `lo` にも適用したかったからです。必須の変更ではありません。


暗号化による TLS 接続すなわち HTTPS 通信の基盤が完成しました。


Happy connecting securely🕊