スマートフォン・タブレットからインターネットサーバーオペレーション

APPW.jp
 
webm@appw-vps:~$ cat incident_report_ftps.log

appw.jp / サーバー運用ログ

OpenSSL更新でFTPSが突然死んだ日

サーバー引っ越しをひと通り終え、動作確認まで済んでいたはずのvsftpdが、 まったく別の作業をきっかけに突然FTPSでログインできなくなった。 応急処置から原因の見立て、Pure-FTPdへの乗り換え、そして乗り換え作業中に踏んだ ちょっとしたbashの落とし穴まで、一連の顛末を記録しておく。

incident発端

動作確認済みのvsftpdが、ある日ログインできなくなった

新VPSへの一連の移設作業が完了し、vsftpdもFileZilla・スマートフォンアプリの両方で FTPS接続を確認済みだった。ところが、その後に進めていたMongoDBまわりの作業―― PHPのPECL拡張をビルドするためにlibssl-devを導入したところ、 依存関係でOpenSSL本体がセキュリティパッチ版に更新された――を境に、 FileZilla・スマホアプリともFTPSでログインできなくなった。

vsftpd自体の設定は変更していない。時系列だけを見れば、OpenSSLの更新が引き金になった 可能性がかなり濃厚に見えるが、これはあくまで状況証拠であり、断定はできない。

応急処置 急遽SFTPに切り替えて運用を継続。ただし、スマートフォン・タブレットで使っている FTPアプリの一部がSFTPの鍵認証に対応しておらず、そのままでは詰んでしまう端末があった。

decision判断

vsftpdの延命ではなく、Pure-FTPdへの乗り換えを選んだ理由

  • SFTP一本化では、鍵認証に対応していないスマホ・タブレットアプリが使えなくなる
  • 原因がOpenSSL側の仕様変更だとすると、vsftpdを直しても再発する可能性が残る
  • vsftpd自体、開発がほぼ停止していると言われており、今後同様の非互換が起きても 迅速な追従は期待しにくい

乗り換え後もOpenSSLに起因する問題であれば同じ症状が出る可能性はゼロではない。 それでも、待つより動く方を選んだ。


notePure-FTPdの特徴(vsftpdとの主な違い)

  • 設定は単一confファイルではなく、/etc/pure-ftpd/conf/配下に 「1オプション1ファイル」を置く独特の方式
  • TLS証明書は秘密鍵・証明書・チェーンを1本のPEMに結合した /etc/ssl/private/pure-ftpd.pemを読む仕様
  • 1.0.45以降、TLSv1.0は標準で拒否される
  • ChrootEveryoneでユーザーを標準でホームディレクトリにchroot
  • 癖のある古いFTPクライアントとの互換性維持を重視した設計思想があり、 雑多なスマホ・タブレットアプリとの相性は期待しやすい

setupアンインストール〜インストール

vsftpdを退場させる

# 念のため現行confを退避(TLS/ポート設定の見比べ用)
sudo cp /etc/vsftpd.conf ~/vsftpd.conf.bak.$(date +%Y%m%d)

sudo systemctl stop vsftpd
sudo systemctl disable vsftpd
sudo apt purge vsftpd
sudo apt autoremove

Let's Encrypt証明書はvsftpdとは独立管理なのでそのまま残る。ufwの21番ポート許可ルールも そのまま流用できる(パッシブポート分は別途追加)。

Pure-FTPdを迎える

sudo apt update
sudo apt install pure-ftpd
sudo systemctl status pure-ftpd   # standaloneで起動しているか確認

setup設定

TLSを必須化する

echo 2 | sudo tee /etc/pure-ftpd/conf/TLS

2は制御・データ両チャンネルで平文FTPを拒否する設定 (vsftpdのforce_local_logins_ssl/force_local_data_ssl相当)。 TLSv1.0は1.0.45以降デフォルトで拒否されるため、個別設定は不要だった。

証明書はvsftpdと勝手が違う

pure-ftpdは秘密鍵と証明書を1本のPEMに結合した /etc/ssl/private/pure-ftpd.pemを既定で読む。vsftpdのように 鍵と証明書を別々のパスで指定する方式ではない。

sudo bash -c 'cat /etc/letsencrypt/live/<ドメイン>/fullchain.pem \
  /etc/letsencrypt/live/<ドメイン>/privkey.pem > /etc/ssl/private/pure-ftpd.pem'
sudo chmod 600 /etc/ssl/private/pure-ftpd.pem
要注意 certbotの証明書更新のたびにこの結合ファイルを作り直し、pure-ftpdを再起動する deploy-hookを/etc/letsencrypt/renewal-hooks/deploy/に用意しておかないと、 次回更新時にFTPSが静かに切れる。

その他の基本設定

# ホームディレクトリへのchroot
echo yes | sudo tee /etc/pure-ftpd/conf/ChrootEveryone

# パッシブポート範囲(NAT/ファイアウォール・モバイルアプリ対策)
echo "60000 60100" | sudo tee /etc/pure-ftpd/conf/PassivePortRange
sudo ufw allow 60000:60100/tcp

# 癖のあるクライアントとの互換性
echo yes | sudo tee /etc/pure-ftpd/conf/BrokenClientsCompatibility

# 匿名ログイン無効化
echo yes | sudo tee /etc/pure-ftpd/conf/NoAnonymous

sudo systemctl restart pure-ftpd

ポート21自体やIPスコープの絞り込みは、これまで通りpure-ftpd側ではなくufw側で 対応する方針を踏襲した。


gotchaハマりどころ

暗号スイート指定で event not found

暗号スイートを絞ろうとして、こう打った。

echo "HIGH:!aNULL:!MD5:!3DES" | sudo tee /etc/pure-ftpd/conf/TLSCipherSuite
-bash: !aNULL: event not found

bashのヒストリー展開機能が、ダブルクォート内の!を 「イベント参照」として解釈してしまったのが原因。ダブルクォートは この展開を止めてくれない。シングルクォートに変えれば!は リテラル扱いになり、そのまま通る。

echo 'HIGH:!aNULL:!MD5:!3DES' | sudo tee /etc/pure-ftpd/conf/TLSCipherSuite

バックスラッシュでのエスケープ(\!)は効かない点も覚えておきたい (\ごと残ってしまう)。!を含む文字列を扱うときは、 基本的にシングルクォートを使うのが安全。


result結果

接続テストは一発でOK

設定後、FileZilla・スマートフォン/タブレットアプリともにFTPS接続テストは すんなり通った。SFTP鍵認証に対応していないシンプルな機能のFTPアプリからも、 問題なくサーバーのファイルにアクセスできる状態になった。

appw.jp — サーバー運用ログ / 今後の課題: 証明書更新タイミングでの pure-ftpd.pem再生成フックの動作確認

『OpenSSL更新でFTPSが突然死んだ日 — vsftpdからPure-FTPdへ』を公開しました。