目次
はじめに
プライベート CA が ACME プロトコルを喋れれば、ACME クライアントを使った証明書の発行や更新を気軽に試せて便利です。
もちろん正規の証明書は得られませんが、その代わり、インタネットにポートを露出することなく HTTP-01 チャレンジや TLS-ALPN-01 チャレンジが行えます。公開認証局のリソース負荷やレート制限、CT ログなども気にする必要はなくなりますし、上位の DNS で応答をコントロールすれば、実在しない TLD の証明書も発行できます。
このエントリでは、気軽に実現できる方法のうち、実際に試した次の三パタンを紹介します。
ACME クライアントを試す目的では、ふたつめの Caddy 案が個人的にはいちばん手軽で小回りが効く印象でした。
Smallstep の step-ca を使う
素直にドキュメントに従えば動いてくれます。コンテナ環境で動かしたい場合もチュートリアルがあるので安心です。なお、初期状態では ACME プロトコルでの証明書発行機能は無効化されているため、利用には明示的な宣言が必要です(三つめのリンクを参照)。
- Installing open source step-ca — Smallstep — Build Big
- Docker TLS private certificate authority in a container — Smallstep — Build Big
- ACME challenge and client connections with step-ca — Smallstep — Build Big
初期設定から起動まで
生の Docker ではなく Docker Compose で制御したかったので、最小限の Compose ファイルを作りました。設定はすべて /home/step
の下にできるので、永続化はここだけ考えればよさそうです。
version: "3"
services:
ca:
image: smallstep/step-ca:0.15.6
restart: always
ports:
- "9000:9000"
volumes:
- "ca-data:/home/step"
volumes:
ca-data:
CA としての設定は CLI ベースで、環境変数などでの流し込みはできなそうでした。このため、必要な設定ファイル群がボリューム内に置かれた状態にするところまでは、手で操作していきます。
~/step-ca$ docker-compose run --rm ca sh
Creating network "step-ca_default" with the default driver
Creating volume "step-ca_ca-data" with default driver
~ $ step ca init
? What would you like to name your new PKI? (e.g. Smallstep): EXAMPLE.COM
? What DNS names or IP addresses would you like to add to your new CA? (e.g. ca.smallstep.com[,1.1.1.1,etc.]): ca.example.com
? What address will your new CA listen at? (e.g. :443): :9000
? What would you like to name the first provisioner for your new CA? (e.g. you@smallstep.com): jwk
? What do you want your password to be? [leave empty and we'll generate one]:
? Password: b)_.u<[R7fd(uT_zit1s1MNd9<H(JlVr
Generating root certificate...
all done!
Generating intermediate certificate...
all done!
? Root certificate: /home/step/certs/root_ca.crt
? Root private key: /home/step/secrets/root_ca_key
? Root fingerprint: ef17bfc5dffda4d23257330b9e5b6aa75727c9663b32f59666c679ce3ec489c7
? Intermediate certificate: /home/step/certs/intermediate_ca.crt
? Intermediate private key: /home/step/secrets/intermediate_ca_key
? Database folder: /home/step/db
? Default configuration: /home/step/config/defaults.json
? Certificate Authority configuration: /home/step/config/ca.json
Your PKI is ready to go. To generate certificates for individual services see 'step help ca'.
FEEDBACK
The step utility is not instrumented for usage statistics. It does not
phone home. But your feedback is extremely valuable. Any information you
can provide regarding how you’re using `step` helps. Please send us a
sentence or two, good or bad: feedback@smallstep.com or join
https://github.com/smallstep/certificates/discussions.
~ $ echo 'b)_.u<[R7fd(uT_zit1s1MNd9<H(JlVr' > /home/step/secrets/password
~ $ step ca provisioner add acme --type ACME
Success! Your `step-ca` config has been updated. To pick up the new configuration SIGHUP (kill -1 <pid>) or restart the step-ca process.
~ $ exit
この段階で、必要なすべてのファイルができています。CA のルート証明書も自己署名証明書として作られ、中間証明書も生成されます。
あとは起動するだけです。
~/step-ca$ docker-compose up -d
Creating step-ca_ca_1 ... done
これで、以下の URL が ACME ディレクトリ URL として使えるようになります。
https://ca.example.com:9000/acme/acme/directory
ただし、このエンドポイントの HTTPS それ自体に、この CA のルート証明書(自己署名証明書)で署名された中間証明書が使われているので、このままではだいたいの(きちんとした)ACME クライアントではエンドポイントへのアクセスの時点で失敗します。
このため、CA のルート証明書をクライアント側で明示的に信用させるか、またはクライアントが(暗黙的に)参照している証明書ストアに登録する必要があります(システムレベルで登録することは推奨しません)。今回の場合、初期設定で生成された CA のルート証明書がコンテナ内の /home/step/certs/root_ca.crt
に保存されているので、これを docker cp
などで取り出して使います。
docker cp step-ca_ca_1:/home/step/certs/root_ca.crt .
ほかにも、コンテナ内には秘密鍵や中間証明書類も次のパスで生成されているので、必要に応じて利用できます。
/home/step/certs/root_ca.crt
/home/step/secrets/root_ca_key
/home/step/certs/intermediate_ca.crt
/home/step/secrets/intermediate_ca_key
このうち秘密鍵は、パスフレーズ(step ca init
中に入力または生成したパスワード)で保護されています。のっぴきならない事情があってパスフレーズを取り除きたい場合は、ssh-keygen
や openssl
コマンドで取り除けます。
ssh-keygen -p -m pem -f <鍵ファイル>
openssl ec -in <鍵ファイル> -out <鍵ファイル>
既存証明書の利用
CA で利用されるルート証明書として、初期化時に自動生成されるモノではなく、別途用意したモノを利用したい場合にも対応可能です。ドキュメントではいくつかの方法が紹介されています。
例えば、上記ドキュメントの最も簡単な手順(step ca init
に --root
と --key
を渡す)に従う場合は、今回であれば次のようになります。ここでは、Compose ファイルは前述のモノのまま変更せず、一時的にルート証明書と秘密鍵を適当なパスにマウントして利用しています。
~/step-ca$ docker-compose run --rm -v $PWD/root_ca.crt:/tmp/root_ca.crt -v $PWD/root_ca_key:/tmp/root_ca_key ca sh
Creating network "step-ca_default" with the default driver
Creating volume "step-ca_ca-data" with default driver
~ $ step ca init --root=/tmp/root_ca.crt --key=/tmp/root_ca_key
? What would you like to name your new PKI? (e.g. Smallstep): EXAMPLE.COM
? What DNS names or IP addresses would you like to add to your new CA? (e.g. ca.smallstep.com[,1.1.1.1,etc.]): ca.example.com
? What address will your new CA listen at? (e.g. :443): :9000
? What would you like to name the first provisioner for your new CA? (e.g. you@smallstep.com): jwk
? What do you want your password to be? [leave empty and we'll generate one]:
? Password: 6~24(,i{um'3.RFd`2ol'k$,L2'2T>K8
...
秘密鍵にパスフレーズが設定されている場合は、途中で聴かれます。後続のパスワードファイルの作成や ACME プロトコルの追加方法は通常通りなので省略しています。この方法では、中間証明書は再生成されます。
中間証明書を含めて既存のものを使いたい場合は、先のドキュメントの 2 番めの手順に従って、特に工夫せずに step ca init
してから、関連ファイルを置き換えるかマウントしてしまうのがラクそうです。
動作確認
例えば Caddy の Automatic HTTPS を使う場合であれば、Caddyfile の ca
と ca_root
で、ACME ディレクトリ URL と CA のルート証明書を指定できます。
www.example.com {
respond "Hello World!!"
tls {
ca https://ca.example.com:9000/acme/acme/directory
ca_root /etc/caddy/root.crt
}
}
Caddy では、DNS-01 チャレンジ用の設定がされていない場合、既定で HTTP-01 チャレンジか TLS-ALPN-01 チャレンジのどちらかがランダムで採用され、失敗時は他方にフォールバックします。いずれにせよ、上位の DNS でドメイン名の実 IP アドレスを適切にごまかしてやれば、任意のドメインの証明書が発行できます。
例えば、上位の DNS でエントリを以下のように登録しておきます。
- Caddy から見た
ca.example.com
がプライベート CA を向くように - プライベート CA から見た
www.example.com
が Caddy を向くように
そのうえで上記の Caddyfile を利用すれば、実在しないドメイン www.example.com
の証明書を取得できます。次のログは、TLS-ALPN-01 チャレンジが実行された様子です。
caddy_1 | {"level":"info","ts":1612702530.6507704,"logger":"tls.issuance.acme.acme_client","msg":"trying to solve challenge","identifier":"www.example.com","challenge_type":"tls-alpn-01","ca":"https://ca.example.com:9000/acme/acme/directory"}
caddy_1 | {"level":"info","ts":1612702530.6692169,"logger":"tls","msg":"served key authentication certificate","server_name":"www.example.com","challenge":"tls-alpn-01","remote":"192.168.160.1:53614","distributed":false}
caddy_1 | {"level":"info","ts":1612702530.9263132,"logger":"tls.issuance.acme.acme_client","msg":"validations succeeded; finalizing order","order":"https://ca.example.com:9000/acme/acme/order/xZCKSlnqGMB0V2YPWPUKNYEWiLvxHDHo"}
caddy_1 | {"level":"info","ts":1612702530.934317,"logger":"tls.issuance.acme.acme_client","msg":"successfully downloaded available certificate chains","count":1,"first_url":"https://ca.example.com:9000/acme/acme/certificate/ATdtrb5V2GuwyMuBKcd8j5XMIgAFZCoO"}
caddy_1 | {"level":"info","ts":1612702530.9358096,"logger":"tls.obtain","msg":"certificate obtained successfully","identifier":"www.example.com"}
Certbot でも、Certbot が動作する環境(コンテナ内など)の証明書ストアにルート CA の証明書を登録しておけば、証明書が発行できます。
# certbot certonly --standalone --server https://ca.example.com:9000/acme/acme/directory -d www.example.com -m admin@example.com --agree-tos
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator standalone, Installer None
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for www.example.com
Waiting for verification...
Cleaning up challenges
IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at:
/etc/letsencrypt/live/www.example.com/fullchain.pem
Your key file has been saved at:
/etc/letsencrypt/live/www.example.com/privkey.pem
Your cert will expire on 2021-02-14. To obtain a new or tweaked
version of this certificate in the future, simply run certbot
again. To non-interactively renew *all* of your certificates, run
"certbot renew"
Caddy の組み込みの acme_server を使う
Caddy は HTTP サーバやリバースプロキシとしての用途が一般的ですが、組み込みで ACME プロトコルで会話できる CA の機能を持っています。正確には、前述の Smallstep の step-ca そのものが内包されていて、Caddy で自己署名証明書を使う場合にデフォルトでこの CA 機能が利用されています。
この Caddy の組み込み CA 機能は、明示的に Caddyfile や API で指定すれば、外部からも利用できる状態になります。
初期設定から起動まで
Caddyfile に acme_server
を追加すれば機能します。隅から隅まで自己署名証明書で完結させる場合は、例えばこれだけで充分です。
ca.example.com:9000 {
tls internal
acme_server
}
あとはこれを読むように Caddy を起動させれば、ACME プロトコルに対応した CA として利用できます。Compose ファイルではこのようになります。
version: "3"
services:
caddy:
image: caddy:2.3.0
ports:
- 9000:9000
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy-data:/data
- caddy-config:/config
volumes:
caddy-data:
caddy-config:
この場合、ACME エンドポイントは次の URL です。
https://ca.example.com:9000/acme/local/directory
生成された証明書類はコンテナ内では次のパスにあります。
/data/caddy/pki/authorities/local/root.crt
/data/caddy/pki/authorities/local/root.key
/data/caddy/pki/authorities/local/intermediate.crt
/data/caddy/pki/authorities/local/intermediate.key
この方法では、ACME エンドポイントの HTTPS にもこの CA のルート証明書で署名された中間証明書が使われます。先の step-ca の例と同様、クライアント側でルート証明書を明示して信用させる工夫が必要になるでしょう。
なお、tls internal
ではなくまっとうに Let’s Encrypt で証明書を発行するように構成(方法は割愛しますが Caddy の通常のお作法通りです)すれば、ACME エンドポイントの HTTPS には正規の証明書が使われるようになるため、クライアント側は何も気にせずに証明書を発行できるようになります(CA としてのルート証明書とは別物なので、当たり前ですが発行される証明書は自己署名ルート証明書に連なります)。
既存証明書の利用
CA としてのルート証明書に既存のものを使わせたい場合は、上記の 4 ファイルを上記通りのパスとファイル名でバインドマウントする方法がいちばん手軽そうです。この場合、Caddyfile は先の例と同じで問題ありません。
...
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- ./certs/ca.example.com/root_ca.crt:/data/caddy/pki/authorities/local/root.crt
- ./certs/ca.example.com/root_ca_key:/data/caddy/pki/authorities/local/root.key
- ./certs/ca.example.com/intermediate_ca.crt:/data/caddy/pki/authorities/local/intermediate.crt
- ./certs/ca.example.com/intermediate_ca_key:/data/caddy/pki/authorities/local/intermediate.key
- caddy-data:/data
- caddy-config:/config
...
別のパスの証明書を使わせたい場合は、Caddyfile では表現できない部分なので、JSON フォーマットで流し込む必要があります。具体的には、pki
アプリケーションとして次のように記述できます。
...
"pki": {
"certificate_authorities": {
"local": {
"root": {
"certificate": "/etc/caddy/certs/root.crt",
"private_key": "/etc/caddy/certs/root.key"
},
"intermediate": {
"certificate": "/etc/caddy/certs/intermediate.crt",
"private_key": "/etc/caddy/certs/intermediate.key"
}
}
}
}
...
この設定は、管理用 API を使って渡すほかに、起動時(caddy run
時)に --config <JSON ファイルのパス>
を指定しても渡せます。
動作確認
動き出してしまえば、最初の step-ca の例とまったく同じ使い心地です。省略。
Let’s Encrypt の Boulder または Pebble を使う
Let’s Encrypt で実際に動いている CA の実装が Boulder で、それのテスト用の簡易版が Pebble です。どちらも GitHub で公開されています。
- letsencrypt/boulder: An ACME-based certificate authority, written in Go.
- letsencrypt/pebble: A miniature version of Boulder, Pebble is a small RFC 8555 ACME test server not suited for a production certificate authority.
Boulder は実際に Let’s Encrypt で使われているだけあって、別途 RDBMS を必要とする重厚なモノです。一方 Pebble は、ACME クライアントの開発やテストで使うことを目的に提供されている簡易版です。自動テストへの組み込みも想定されており、テスト用のコマンドやライブラリ、フェイク DNS 機能などがいろいろ詰まった Challenge Test Server も併せて提供されています。
ここでは、Pebble をミニマム構成で動かします。
初期設定から起動まで
テスト用だけあって、少し変わった実装です。
例えば、ACME エンドポイントのポートは 14000
番がデフォルトで、HTTP-01 チャレンジの検証では発行先の 5002
番ポートを見に行きます。TLS-ALPN-01 チャレンジでは 5001
番です。
設定は JSON で変えられるので、ファイルとして用意し読ませることにします。
{
"pebble": {
"listenAddress": "0.0.0.0:14000",
"managementListenAddress": "0.0.0.0:15000",
"certificate": "/test/certs/localhost/cert.pem",
"privateKey": "/test/certs/localhost/key.pem",
"httpPort": 80,
"tlsPort": 443
}
}
Compose ファイルは次のような内容です。
version: '3'
services:
pebble:
image: letsencrypt/pebble:v2.3.1
command: pebble -config /test/config/pebble-config.json
ports:
- 14000:14000
- 15000:15000
volumes:
- ./config.json:/test/config/pebble-config.json
起動すると、次のエンドポイントで利用できます。
https://pebble:14000/dir
なお、このエンドポイントの HTTPS で使われる証明書(と鍵)は 公開 されており、これを署名している CA の証明書(と鍵)も 公開 されています。クライアント側ではこの証明書を信用する必要がありますが、鍵が公開されていて偽装が容易なため、テスト用の一時的な環境以外では信用させない方が安全です。
また、HTTPS の証明書が localhost
と 127.0.0.1
と pebble
用のため、URL がこのいずれかでないとそれはそれでクライアント側の検証でハネられます。別ホストや別コンテナで動かす場合は、hosts ファイルや DNS で工夫が必要です。
HTTPS の証明書ではなく、CA としてのルート証明書と中間証明書は、起動するたびに再生成されます。管理用ポート(デフォルトは 15000
番)から HTTP でダウンロードできるようになっています。
https://localhost:15000/roots/0
https://localhost:15000/root-keys/0
https://localhost:15000/intermediates/0
https://localhost:15000/intermediate-keys/0
既存証明書の利用
前述の通り、CA としての証明書や鍵は起動毎に代わる仕様です。根本的に Pebble はテスト目的の実装であり、そもそも永続的な利用を想定していないため、自前のルート証明書に置き換える手段は公式には用意されていなさそうです。
動作確認
動いてしまえば、使い方は最初の step-ca のそれと変わりません。
ただし、設定ファイルで httpPort
や tlsPort
を変えていないと、HTTP-01 チャレンジや TLS-ALPN-01 チャレンジの検証でアクセスする先が 5002
番や 5001
番ポートになるので、その点は注意です。
# certbot certonly --standalone --server https://pebble:14000/dir -d www.example.com -m admin@example.com --agree-tos
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator standalone, Installer None
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for www.example.com
Waiting for verification...
Cleaning up challenges
IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at:
/etc/letsencrypt/live/www.example.com/fullchain.pem
Your key file has been saved at:
/etc/letsencrypt/live/www.example.com/privkey.pem
Your cert will expire on 2026-02-13. To obtain a new or tweaked
version of this certificate in the future, simply run certbot
again. To non-interactively renew *all* of your certificates, run
"certbot renew"
おわりに
ACME プロトコルで会話できるプライベート CA の立て方を 3 パタン紹介しました。個人的には、ACME クライアントを気軽に試す目的では、Caddy の組み込みの機能が設定要らずで簡単な印象です。
先の Caddy の Azure DNS 用モジュール の作成の際も、Let’s Encrypt にクエリを飛ばしすぎるのが(ステージング環境といえども)ためらわれたので、こうしたプライベート CA がほんのりと役立ちました。
お楽しみください。