HDE Advent Calendar 2015

HDE Advent Calendar Day 21: GoでS3連携しつつLet's Encryptする話

クラウドプロダクト開発部のたなべです。Goを社内布教していたら牧さんと一緒に働ける日が来てしまった一年でした。

昨年は「Day 21 GoとAWSと私」という題で大混乱にあった Go向けのAWSライブラリについて書きました。あれから一年、どうなったかはもうご存知の通りです。 この業界の1年の速さに驚くばかりです。

今回は題の通り、Let's Encrypt(を支えているACMEプロトコル)を あれこれやっていい感じに活用できないかと足掻いてみた記録です。

ACMEプロトコルについて

基本的事項については Go言語でLet's EncryptのACMEを理解するを参照してください。

ACMEプロトコルはまだworking draftであり、実装を進めつつ仕様を詰めていく段階にあります。 仕様を読んでもよくわからない箇所は事実上の参照実装(Go〜)である letsencrypt/boulderのコードを読むほかありません。

クライアント実装はxenolf/legoを読むのがオススメです。

Let's encryptの理想と現状

DV証明書が無償でかつ自動で発行できるので、業務で活用するなら、テスト環境や社内向けサービスにぴったりです。

弊社ではほぼすべてのリソースをAWS上で運用しています。また、提供しているサービスもhttpだけではなく、smtpも提供しています。 そのため証明書はELB、nginx、Postfix、自前のsmtp実装など、httpに限らず使用しています。

現在はテスト環境であっても1000円程度のDV証明書を購入し、使用しています。 Let's Encrypt(以下LE)を使ってこれらを自動発行できる!と期待していました。 しかし、現状ではドメインの所有を確認するための認可プロセスがhttpをベースにしているため、http以外での利用は 難しい状況です。また、httpであってもロードバランサと組み合せている場合は特別な考慮が必要です。

仕様では3つの認可プロセス(チャレンジ)が定義されています。

  • http-01: key authorizationを所定のパスへ配置 (/.well-known/acme-challenge/{token})
  • tls-sni-01: TLSのSNI (Server Name Indicator) 拡張の値を key authorization を元に所定のアルゴリズムで生成し応答する
  • dns-01: key authorizationをbase64エンコードした値を所定のTXTリソースレコード(_acme_challenge.{domain})へ配置

残念ながら、DNS-01は参照実装はあるものの、LEのプロダクション環境ではまだ有効化されていません。 ステージングでは有効になっていますが、古い仕様のままであるため、現行では動作していません。手元では古い仕様で送っても動作しませんでした。

DNSサーバの操作はAPIでできることが前提(Route53など)ですが、DNS-01が動くとサーバレスで証明書が発行できます。 個人的にはdns-01がきてからが真の自動化の始まりだと思っています。

http-01に対するS3を利用したワークアラウンド

DNS-01がくるのはおそらく来年の早い段階だと思わます。しかし、http-01でもできることがあります。 それはHTTPリダイレクトを利用した認可プロセスの中央化です。アイデアはこうです。

  1. 予めnginxなどアプリケーション側で /.well-known/acme-challenge/ 以下へのリクエストをS3のバケットへリダイレクトさせる

    /.well-known/acme-challenge/{token} -> https://s3-{region}.amazonaws.com/{bucket}/.well-known/acme-challenge/{token}

  2. チャレンジに必要な答えをS3へPutし、回答する

  3. LEのサーバは最終的にS3へリダイレクトされ、回答をGETする
  4. Profit!

こうすることで、予めnginx等にlocation設定を仕込んでおけば、あとは実際のサーバを必要とせず、任意のタイミングで チャレンジに応答することができます。

細かい話をすると、単に設定しまうといつでもリダイレクトが発生してしまうので気持ち悪いかもしれません。 チャンレジ時のみリダイレクトが発生するようにnginxを仕込むのもありだと思います。

このアイデア

https://community.letsencrypt.org/t/http-authentication-follows-http-redirect/3110

で議論されていたものをベースにしています。

さて、あとはコードを書けばよいだけですね。

AAA - An ACME Agent for AWS environment

https://github.com/nabeken/aaa

既存のACMEクライアントを使用せずに実装しました。なぜならgo-jwxを使ってみたかったからです:)

もともとgo-jwxはACMEで使うには機能が足りていなかったのですが、 PRを送ったり、 牧さんがちょちょっと入れてくれたのもあり、 現在では問題なくACMEで使うことができます。

他には、新しめのコマンドラインフラグパーサーであるkingpinや、ファイルシステムへのアクセスを抽象化するaferoを試しに使っています。

AAA の現状ですが、S3を利用したワークアラウンドまで実装しています。

f:id:nabekent:20151221163036p:plain

ただ、本当にやりたかったことはまだできていません。READMEにあるように本当にやりたかったのは

  • AAAAWS Lambdaへデプロイし、
  • すべての情報をサーバサイド暗号化を使ってS3へ保存し、
  • S3 Event Notificationsを発火させ、
    • 証明書をELBなどへ自動インストール
    • DynamoDB上に証明書情報を保存し集中管理
  • Slackから証明書発行要求を受け、チャレンジを自動で解き、S3へ保存

などです。今できるのは、

です。また、すべての情報は今のところカレントディレクトリの aaa-agent ディレクトリに保存されます。 aferoのS3実装ができればコードをあまり変えずにS3対応できるはずです。

AAAの使いかた

※2015/12/25 追記:KMS対応など入れたので最新情報はREADMEを見てください。

READMEのままですが、日本語でも書いておきます。

AAAはデフォルトではLEのステージングに向いています。手順に自信が持てたらプロダクションのエンドポイントを使用してください。

export AAA_DIRECTORY_URL=https://acme-v01.api.letsencrypt.org/directory

メールアドレスはいずれも専用のものを使用することを推奨します。ドメインもまずは試験用のものでお試しください。

プロダクションのエンドポイントで使う場合、アカウントに対する鍵は必ず保存してください(aaa-agentディレクトリ以下)。

今回はnginxが動いている le-test.example.com 向けにS3を使ったhttp-01チャレンジに答えて証明書を発行してみます。

予め、nginxの設定に以下の location 設定を入れてください。 ({region}と{bucket}は適切なものに置換してください。)

location /.well-known/acme-challenge/ {
    return 302 https://s3-{region}.amazonaws.com/{bucket}$request_uri;
}

まずはアカウントを登録します。

aaa reg --email {email}

TOS(サービス規約)を読んで、問題なければ引数にTOSのURLを指定して同意します。

aaa reg --email {email} --agree https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf

登録できたら、次にドメインの認可作業を実施します。 ここから先はAWSリソースへアクセスするため、AWS SDKにクレデンシャルを設定しておく必要があります。 https://github.com/aws/aws-sdk-go/wiki/configuring-sdk などを参考にリージョン等を適切に設定してください。

aaa authz --email {email} --domain le-test-01.example.com --challenge s3-http-01 --s3-bucket {bucket}

なお、SANによる複数ドメイン向けの証明書を発行したい場合はここで追加分の認可作業を実施します。

aaa authz --email {email} --domain le-test-02.example.com --challenge s3-http-01 --s3-bucket {bucket}

問題なければいよいよ証明書発行です。

aaa cert --email {email} --common-name le-test-01.example.com

SANを使う場合は以下のように --domain 引数を必要な分だけ追加してください。

aaa cert --email {email} --common-name le-test-01.example.com --domain le-test-02.example.com

証明書は aaa-agent/{email}/domain/le-test-01.example.com/cert.pem に保存されています。 SANの場合はCommon Nameに指定したドメインに保存されています。

opensslで中身を確かめることもできます。

openssl x509 -in aaa-agent/{email}/domain/le-test-01.example.com/cert.pem -text

おわりに

駆け足でしたが、今回はAWSに特化することでACMEプロトコルをより便利に統合できる可能性を示せた…と思います。 まだ実装は道半ばなのでDNS-01がプロダクションに来ることを願いつつ、来年も引き続き実装していく予定です。