Compnet

仕事とか遊びとか、日々折々

2019-05-09(木)

ついでに FTPs (明示的な TLS) 化

Posted by Nakane, R. in technical   

前の記事で今更事に FTP サーバーを構築しました。 そのついでといってはなんですが、TLS 化を施しました。

TLS 化には「明示的 TLS」と「暗黙的 TLS」の二通りがあり、たいていの場合はその両方を実現するのが普通です。 「明示的 TLS」は平文で通信を開始してその通信の中でアプリケーションのコマンド (たいていは STARTTLS コマンド、FTP では AUTH TLS コマンド) を実行することで TLS を使った暗号通信に切り替える方法です。 「明示的 TLS」では暗号化されていない平文で通信を開始することもあり、アプリケーションが使う通信ポートは TLS 化していない通信と同じものが使えます。 TLS 化していない通信ポート (FTP では TCP の 21 番ポート) で通信を開始して、切り替えるコマンドが実行されればそれ以降を TLS で暗号化し、切り替えるコマンドが実行されない限りはずっと平文のままで通信を続けます。

これに対して「暗示的 TLS」では最初から TLS を使った暗号通信を行います。 ただしこの場合は平文と同じ通信ポートが使えません。 FTP の「暗示的 TLS」では通常 TCP の 990 番ポートを使います。

今回使用した Pure-FTPd では「明示的 TLS」だけしか実現できません。 Pure-FPTd の TLS 対応はプログラムをコンパイルするときのスイッチで切り替わり、Debian の標準リポジトリで提供されている Pure-FTPd では TLS に関して --with-tls スイッチだけが指定されています。 これによって Pure-FTPd で「明示的 TLS」が使用可能になりますが、このスイッチだけでは「暗示的 TLS」は使えるようにはなりません。 「暗示的 TLS」を使用可能にするには --with-implicittls スイッチを付けてコンパイルする必要がありますが、Debian の標準リポジトリで提供されている Pure-FTPd はそのようにはなっていません。

どうしてもというのであれば、Debian の標準リポジトリからインストールした Pure-FTPd とは別に --with-implicittls スイッチを付けてソース コードから Pure-FTPd をコンパイルして、これら二つのプロセスを起動すれば、何とかできるでしょう。 ここまでして TLS 化をするつもりにはなれないので、ここでは「明示的 TLS」だけが使えるようにします。

サーバー証明書を用意

FTP サーバー アプリケーションを TLS 化するにはサーバー証明書が必要です。 よくあるのは自己署名証明書 (俗に言うオレオレ証明書) を作ってこれを使う方法ですが、今回は無料の公開用認証局である Let's Encrypt の証明書を使うことにします。 Let's Encrypt が発行する証明書は「DV (Domain Validation) 証明書」といって、ドメインの運営者がそのドメインを所有していることだけを証明します。 OV (Organization Validation) や EV (Extended Validation) のように、所有者の実在や、所有者によるドメインの占有的運用の保証を証明するものではありませんが、サーバーを運営する組織や個人の信頼性を要するのでなければ、つまりほどんどの公開サーバーでは DV 証明書で十分ことがたります。

ほとんどの認証局で証明書の発行に書類やメールなどの文書での申請が要求されます。 しかし Let's Encrypt では文書の交換の煩雑さと労力を削減するため、ACME (Automatic Certificate Management Environment) というプロトコルを使って証明書の発行を行います。 ACME プロトコルは、何らかのプログラムでこれを実装しなければ使うことができません。 「Go言語でLet's EncryptのACMEを理解する」のような強者もいますが、そんな力量も余裕もないのでここでは Let's Encrypt が推奨している Certbot を使います。

Certbot も Debian の標準リポジトリで提供されているのでそれを以下のようにインストールします。

sudo apt -y install certbot

Certbot の使い方を述べ始めるとそれだけで記事が書けてしまうので、ここでは省略します。 とりあえずは Apache や Nginx などの Web サーバー アプリケーションが動いていないことにして、以下のコマンドでサーバー証明書を取得します。

sudo certbot certonly --agree-tos --email admin@example.jp --non-interactive --standalone --domains $(hostname -f)

なお上のコマンドの --email オプションで指定しているメール アドレスに有効期限切れ等の案内が届くので、自身のメール アドレスを指定しなくてはなりません。

上のコマンドを実行すると /etc/letsencrypt/live ディレクトリにホスト名の FQDN でディレクトリが作られそこに cert.pem、chain.pem、fullchain.pem、privkey.pem の四つのファイルが作られます。 それぞれ、サーバー証明書、中間証明書、サーバー証明書と中間証明書をひとつにまとめた証明書、秘密鍵に相当します。

Debian の標準リポジトリで提供される Pure-FTPd では、サーバー証明書、中間証明書、秘密鍵のファイルを一つにまとめた上で /etc/ssl/private ディレクトリに置かなくてはなりません。 そのファイル名も pure-ftpd.pem に決められています。 そこで以下のコマンドで取得した Let's Encrypt の証明書を Pure-PDFd 用の証明書ファイルにします。

sudo cat /etc/letsencrypt/live/$(hostname -f)/{privkey,fullchain}.pem | sudo tee /etc/ssl/private/pure-ftpd.pem >/dev/null

また TLS の DH 鍵交換で使うパラメーターを以下のコマンドで作成します。 このパラメーターは DH 鍵交換ではつかう素数のセットです。 最近では少なくとも 2048 bit 以上の桁数の素数が推奨されているので、ここではもう少し多きい 3072 bit でパラメーターを作ります。

sudo openssl dhparam -out /etc/ssl/private/pure-ftpd-dhparams.pem 3072

Pure-FTPd の設定

サーバー証明書と DH 鍵交換のパラメーターを用意したら Pure-FPTd の設定を修正します。

Pure-FPTd が TLS を使うようにするには TLS パラメーターを 1 にします。 前の記事にも書いたように Pure-FTPd のパラメーターの設定は、/etc/pure-ftpd/conf ディレクトリに 設定パラメーターの名前でファイルを作成してそこに値を記述することになっているので、ここでもそのようにします。

ところで TLS のプロトコルに含まれる SSLv3 以下のバージョンは脆弱性があるためにすでに廃止されており、使用は非推奨とされています。 TLSv1.0、TLSv1.1 も同様に近いうちに廃止が予定されているので、特別な理由がない限りは使わない方がいいでしょう。 Pure-FTPd の TLS 化にあたってもこれらのプロトコルを使用しないように TLSCipherSuite パラメーターで指定します。 今回 Pure-FTPd を構築した Debian 8 jessie の場合は、Pure-FTPd が使っている OpenSSL のライブラリで SSLv2 以下のバージョンは既に削除されています。 このため、SSLv3、TLSv1.0、TLSv1.1 を使用しないように指定します。 加えて 128 bit の鍵を TLS で使わないようにします。

(
  PARAMS=(
    "TLS:1"
    "TLSCipherSuite:HIGH:-SSLv3:-TLSv1:-TLSv1.1"
  )
  for PARAM in "${PARAMS[@]}"; do
    echo "${PARAM#*:}" | sudo tee "/etc/pure-ftpd/conf/${PARAM%%:*}" >/dev/null
  done
)

上のコマンドではサーバー証明書や DH 鍵交換のパラメーターのファイルを指定していません。 これらは Debian の標準リポジトリの Pure-FTPd ではそれぞれ決まっており、それに合うように上の手順でサーバー証明書や DH 鍵交換のパラメーターを用意したので指定しなくても大丈夫です (指定しようと思えばできないことはありませんが、Debian 的には美しくありません)。

Pure-FTPd に TLS の設定を追加したら、以下のコマンドで Pure-FTPd を再起動して設定を読み込ませます。

systemctl -q is-active pure-ftpd && sudo systemctl restart pure-ftpd || sudo systemctl start pure-ftpd

ところで TLSCipherSuite パラメーターに指定した値は、実を言えばセキュリティをあまり深く追求して決めたものではありません。 ここに指定できる値は以下のコマンドで表示される字句と、man openssl ciphers コマンドで表示される man ページの CIPHER STRINGS セクションに記された字句です。 きちんとしたセキュリティを確保したいときは、これらの字句を組みあわせて TLSCipherSuite パラメーターに指定してください。

$ openssl ciphers -s | sed -e 's/:/\n/g'
ECDHE-ECDSA-AES256-GCM-SHA384
ECDHE-RSA-AES256-GCM-SHA384
DHE-RSA-AES256-GCM-SHA384
ECDHE-ECDSA-CHACHA20-POLY1305
ECDHE-RSA-CHACHA20-POLY1305
DHE-RSA-CHACHA20-POLY1305
ECDHE-ECDSA-AES128-GCM-SHA256
ECDHE-RSA-AES128-GCM-SHA256
DHE-RSA-AES128-GCM-SHA256
ECDHE-ECDSA-AES256-SHA384
ECDHE-RSA-AES256-SHA384
DHE-RSA-AES256-SHA256
ECDHE-ECDSA-AES128-SHA256
ECDHE-RSA-AES128-SHA256
DHE-RSA-AES128-SHA256
ECDHE-ECDSA-AES256-SHA
ECDHE-RSA-AES256-SHA
DHE-RSA-AES256-SHA
ECDHE-ECDSA-AES128-SHA
ECDHE-RSA-AES128-SHA
DHE-RSA-AES128-SHA
AES256-GCM-SHA384
AES128-GCM-SHA256
AES256-SHA256
AES128-SHA256
AES256-SHA
AES128-SHA

サーバー証明書の自動更新

Let's Encrypt で取得したサーバー証明書は、有効期限が 90 日と短く設定されています。 有効期限を短くしている理由については Let's Encrypt の Web サイトの「Why ninety-day lifetimes for certificates?」で述べられており、そこではサーバー証明書を自動的に更新する仕組みづくりを利用者に求めています。 実際のところ、サーバー証明書の更新を忘れると酷い目に遭うことは目に見えて明らかですし、更新する際に費用の支払いが生じることもないので、自動的にこれを更新する仕組みを作るのが正しい方向性でしょう。

Let's Encrypt のサーバー証明書の更新は、これを取得したときと同じく certbot コマンドで行います。 certbot に renew サブコマンドをつけて certbot renew のように実行すると、Let's Encrypt で取得したすべてのサーバー証明書に対して更新を試みます。 このとき certbot は有効期限に近づいた証明書だけを更新し、有効期限までにまだ余裕のある証明書の更新は行いません。 つまりサーバー証明書をいくつ取得していようと、またそれらをいつ取得していようとも、何も考えずに certbot renew コマンドを実行すれば証明書ごとに有効期限をみて更新の有無を判定して、必要に応じた更新が行えます。

certbot の設定にもよりますが更新の有無の判定は、有効期限まで残り 30 日を切ったかどうかで判断されます。 このため certbot renew コマンドを月に一度 (たとえば毎月 1日) に実行するのでは有効期限切れの問題が起こるので、週一でこれを実行するようにします。 これを実現するために、以下のようにして /etc/cron.d/weekly ディレクトリに certbot renew コマンドを実行するスクリプトを作成します。

cat <<'__EOT__'| sudo tee /etc/cron.weekly/letsencript >/dev/null
#!/bin/bash
LANG=C certbot renew --non-interactive
__EOT__

sudo chmod a+x /etc/cron.weekly/letsencript

これで、週一の頻度でサーバー証明書の有効期限を確認し、それが切れる前に更新されるようにできました。 しかし、これだけでは /etc/letsencrypt/live ディレクトリにあるサーバー証明書が更新されるに過ぎません。 Pure-FTPd が利用する /etc/ssl/private/pure-ftpd.pem ファイルのサーバー証明書はまったく更新されないままです。

続いては、/etc/letsencrypt/live ディレクトリの証明書が更新に合わせて、/etc/ssl/private/pure-ftpd.pem ファイルを更新するようにします。 それには、certbot renew コマンドで証明書が更新されたときに /etc/letsencrypt/renewal-hooks/deploy ディレクトリにあるスクリプトが実行されるという仕組みを使います。 以下のように/etc/letsencrypt/renewal-hooks/deploy ディレクトリにスクリプトを作成して、サーバー証明書の更新に合わせて /etc/ssl/private/pure-ftpd.pem ファイルを更新するようにします。 また、このスクリプトでは /etc/ssl/private/pure-ftpd.pem ファイルの更新に合わせて Pure-FTPd を再起動させて新しい証明書を使わせるようにしています。

cat <<'__EOT__'| sudo tee /etc/letsencrypt/renewal-hooks/deploy/20-ftpd >/dev/null
#!/bin/bash
DAEMON=pure-ftpd
TARGET_DOMAIN=$(hostname -f)

for DOMAIN in ${RENEWED_DOMAINS}; do
  [ "${DOMAIN}" != "${TARGET_DOMAIN}" ] && contineu
  cat /etc/letsencrypt/live/${TARGET_DOMAIN}/{privkey,fullchain}.pem | tee /etc/ssl/private/pure-ftpd.pem >/dev/null
  [ systemctl -q is-active "${DAEMON}" ] && systemctl -q restart "${DAEMON}"
done
__EOT__

sudo chmod a+x /etc/letsencrypt/renewal-hooks/deploy/20-ftpd

Comments