ssh port forwardingとstone経由でLet's encryptのワイルドカード証明書を作成する

Let's encryptではワイルドカード証明書を発行してもらうこともできるが、これにはDNS認証が必須となっている。
...このためだけに動的なDNSサーバを用意するのもちょっと面倒なので、グローバルIPv4アドレスを持つVPSUDP port 53のパケットを転送させ、ローカルに立てたDNSサーバを使ってDNS認証を通してみた。
要するに、"静的なグローバルIPv4アドレスが割り当てられ、root権限を持っているsshでログインできてCコードをコンパイルできるホスト"と、"NSレコードをそのホストに向けられるドメイン"を持っていれば、PC上にDNSサーバを構築してそこにワンタイムパスワードを置くことで、Let's encryptのワイルドカード証明書を入手できる。

必要なソフトは、

の3つで、SSHとstoneを使ってポート転送をすることで、VPS側でgoのビルド環境を用意する必要が無いのがポイント。

NSレコード / DNSサーバの準備

Let's encryptのDNS認証は _acme-challenge.EXAMPLE.ORG のようなDNS名のTXTレコードを引き、そこにワンタイムパスワードを格納することによって行われる ( https://tools.ietf.org/html/draft-ietf-acme-acme-14#section-8.4 ) 。これは通常のDNS解決によってクエリされるため、他のサーバにコンテンツを移譲することができる
先のページ https://qiita.com/binzume/items/698d12779b8ad5cda423 は、まさにこの点を使用していて、普段使用しているDNSコンテンツサーバとは別の適当なDNSサーバを静的なIPv4アドレス上に配備して、必要なワンタイムパスワードをLet's encrypt側に見せている。NSレコードの書き方もこのページにある。
ただ、今回はグローバルIPv4を持つサーバにはgoのビルド環境が無いため一工夫が必要になる。

SSHとstoneを使用してDNSクエリのproxyをする

goで書かれたDNSサーバをVPS上に持っていくにはVPS上にgoの開発環境が必要になる。これはちょっと面倒なので、DNSのパケットを手元のPCにproxyしてきて、PC上にDNSサーバを立てることで代えたい。ここでは、インターネット上のUDP port 53トラフィックを手元のPCのUDP port 9953に転送することを目指す。(直接53に転送しないのは、1024未満のポートにbindするにはroot権限が要るため。)
DNSプロトコルUDP上に実装されているため、インターネットからやってきたUDPパケットを自宅のPCまで転送することができればproxyできたことになる。stoneにはUDPTCPの相互変換機能が存在する( http://www.gcd.org/blog/2007/06/121/ )ため、これとSSHTCP転送機能を組み合わせれば良い。
グローバルIPv4を持つホストでは "stone localhost:9953 0.0.0.0:53/udp" を実行して、0.0.0.0に対するポート53(DNS)のパケットをTCP localhost:9953 に転送させる。bindするポートが1024以下なのでsetuidなりなんなりしておく必要がある。
tmpdnsを動作させるPCではこの逆操作を行う: "stone localhost:9953/udp localhost:9953" を実行しておき、"ssh -R9953:localhost:9953" でグローバルIPv4を持つホストに接続し、stoneによってUDPTCP変換されたDNSプロトコルを転送する。
あとは普通にacme.shなりなんなりを使ってDNS認証を進めることで証明書が入手できる。

手法の安全性

というわけで、自分のドメインのNSレコードを編集でき、グローバルIPv4アドレスを持つホストのUDPポート53にアクセスすることさえできれば、Let's encryptは証明書を発行してくれることはわかった。
... コレ安全なんだろうか。。?
Let's encryptが使用しているプロトコル(ACME)のIDでは、

   o  Always querying the DNS using a DNSSEC-validating resolver
      (enhancing security for zones that are DNSSEC-enabled)

   o  Querying the DNS from multiple vantage points to address local
      attackers

   o  Applying mitigations against DNS off-path attackers, e.g., adding
      entropy to requests [I-D.vixie-dnsext-dns0x20] or only using TCP
...
   It is RECOMMENDED that the server perform DNS queries and make HTTP
   connections from various network perspectives, in order to make MitM
   attacks harder.
...
   Servers SHOULD perform DNS queries over TCP, which provides better
   resistance to some forgery attacks than DNS over UDP.

のようにいくつかの保護を考察しているが、今回のように実際のTXTレコードの取得はUDPパケット一発で完了してしまっている。もちろん、実際の証明書の発給は別の(TLS保護された)チャネルによって行われるので、攻撃を実施するにはこのTXTレコード応答を改竄しつつ、同時に証明書をリクエストする必要が有るなどそれなりのハードルは有る。
今回のやりかたが通用することでわかるように、少くともLet's encryptの実装ではACME手続きを実施するホストとDNS応答を行うホストは一致している必要がない -- そもそも現実的なシナリオでは両者に関係を求めずらい。
TCP onlyのDNSサーバで上手くいくのかは興味深いポイントだが、時間の都合で試せていない。

SSHポートフォワードとstoneで何でもできるのか

個人的には、この"グローバルIPv4アドレスを持った低機能なサーバにSSH接続してサービスする"形態はもっと色々な可能性があると思っていて、環境含めもっと追求していきたい。
ローカルのprivate環境をインターネットに公開するproxyシステムとしてpagekite( https://pagekite.net/ )のようなサービスがあるが、これをSSHのポートフォワードで実現するものとしてServeo( https://serveo.net/ )がある。pagekiteはサーバを公開するためには専用のクライアントが必要なのに対し、ServeoはSSHクライアントさえあれば良いためかなり手軽になっている。
...というか最初はこのstoneをつかったやり方を全く思いつかず、Azureを借りたり、Herokuやglitch(https://glitch.com/ - LinuxアプリのためにAWSインスタンスを無料で貸してくれるサービス)でできないか考えたり、VPNを張ったりしていた。。意外と"静的グローバルIPv4と簡単なアプリだけ"という構成のサーバは手軽なものが無く、例えばAzureだとContainer InstanceやAzure Functionsに静的IPが振れない等静的アドレスを要求にした途端にかなりハードルが上がってしまう。
日本の特殊な事情としてトラフィックが激烈に安いというポイントが有り、他所の国ではコンテナ化したサービスをアップロードして実行するものでも、日本ではサービスをローカルで実装し公開エンドポイントへのoutbound接続で済ませられる可能性がある。このトポロジの違いを、開発やデプロイの面で生かせないだろうか。
もっとも、SSHプロトコルは現実のワークロードに耐える程効率的でもないし、そもそも静的なグローバルIPv4アドレスの必要性自体が低下している。。Amazon LambdaやAzure Functionsに見られるように、アプリケーションロードバランサの下にAPIサービスを展開することで多くの機能を実現できるので、Webアプリケーションにとって自前のドメイン名や静的なIPアドレスが必要な局面はかなり小さくなっていると言える。今回ワイルドカード証明書を用意したのは、単にPWAのframeを自前のドメインで描画する必要があったためで、PWA本体はそのドメインから配信する必要性自体が無い。