パッシブFTPをTLS/SSLで暗号化する設定のトラブルシューティング(vsftpd, OpenSSL, GnuTLS)

## このトラブルシューティングについて
OpenSSLで作成した自己署名証明書を使用し、vsftpdでFTPSをテストしていてハマったので。結果からいうと暗号化スイートをopenssl、vsftpdの設定ファイルで設定する必要がありました。ついでにhttpdの設定もなぜか触ることに。

### 事の発端
まず初めに疑ったのは、Implicit モードがデフォルトで使用している990 番ポートをlftpのプログラムかftpコマンドが使用しようとしているのではないか？ということ。Expicitモードで21番ポートをファイアウォールで開けてても、そこではなく990番が空いていないためコントロールコネクションが確立していないのかもしれないと思いました。

> We’ll need to open port 21 for FTPS login and commands. Port 990 is default for TLS, but WordPress appears to use port 21 exclusively ......
> 「TLSで990がデフォルトになっていてもFTPでのログインとコマンドは21番ポートを使用する必要がありますが、（例えば)WordPressは21番をブロックしてしまうようで......」

__参考サイト__

https://nullexit.org/blog/index.php/2021/01/09/setting-up-vsftpd-for-ftps-transactions-in-wordpress/

つまり、疑問点はImplicit（990ポートが使用される）とExplicit モードのどちらのモードをftp-userユーザーの接続に於いてどのように選択しているか、ということです。

## 環境
Amazon Linux 2023
vsftpd: version __3.0.5__
httpd __2.4.58__
openssl __3.0.8__

### 確認すること
__【確認その1】sudo firewall-cmd --permanent --list-allでサーバーの以下のポートへのインバウンドアクセスが許可されていることを確認する。__


下記の実行結果は、`ufw`（Not `firewalld` in RHEL）での設定結果。

```
To                         Action      From
--                         ------      ----
21/tcp                     ALLOW       Anywhere
65000:65535/tcp            ALLOW       Anywhere

```

とくに65000〜65535はパッシブモードで接続するときのポート番号の値の範囲。


以下の出力は必須かどうかわからない（手元の環境では出力されなかった）。

```
To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere
```

上記をチェックした上で、lftpコマンドをデバッグオプション (-dフラグ)を付けて実行すると、パスワードを入力した直後に下記のエラーメッセージが。

[An unexpected TLS packet was received](https://www.google.com/search?q=gnutls_record_recv%3A+An+unexpected+TLS+packet+was+received&oq=gnutls_record_recv%3A+An+unexpected+TLS+packet+was+received&gs_lcrp=EgZjaHJvbWUyBggAEEUYOdIBCTE3MzZqMGoxNagCCLACAfEFdmifo6wOgPfxBXZon6OsDoD3&sourceid=chrome&ie=UTF-8)

このようなエラーメッセージの場合、以下の投稿のトラブルシューティングにしたがい、エラーが解消できるか試してみてください。

https://qiita.com/Am_nine/items/f6084c52fda47aca71f6

<br>

__【確認その2】（任意）990番ポートを開放してnetstatでLISTENできているか確認する__

もしこれでFTPSがいけたら、Implicitモードが使用されていることがわかる。

## Explicitモードはコントロールコネクションとデータコネクションに分かれているから接続できないのでは？という疑問
**追記 (2025/5/13):**
__本節タイトルに誤謬があります。コネクションが２つに分かれているのはFTP全般でいえることです。Explicitモードにおいてネゴシエーションが平文と暗号化の２つに分かれているということが言いたかったです。__

lftpは厳密にはFTPクライアントではないらしいです。

> Despite its name lftp is not an ftp "client". It is a tool that allows you to run ftp, ftps or other commands
> 「lftpという名前ですがこれはFTPクライアントではありません。ftpやftps、その他のコマンドを実行できるようにするツールです。」

> It is important to note that lftp is a session tool. It is not staying logged in as you might think but rather issues new commands to the remote using the credentials you supplied when you started lftp. This caused me grief early on because I thought it logged in successfully when I started lftp and it was only after viewing debug/verbose output that I realized it was attempting and failing to login when I ran individual commands within it.
> 「重要なのはlftpはセッションツールであることに注意が必要だということです。誰もが思っているようにログインしたままにはならないのです。
lftpを起動するときに入力する認証情報で、リモートマシンにたいして新規にコマンドを発行しているだけです。
私も最初これには頭を抱えました。lftpを起動した時点でログインが成功していると思っていたのですが、デバッグ/バーボスの出力を表示したあとで、コマンドを個別に実行する時にログインしようとして失敗したことに気づいただけでした。」

[FTP with the SSL modules turned on ( LinuxQuestions.org )](https://www.linuxquestions.org/questions/linux-software-2/ftp-with-the-ssl-modules-turned-on-4175655134/#google_vignette)

この人が書いているログインというのは、FTPによるリモートマシンへのログインのことだと思われます。つまりコントロールコネクションがlftpによって行われるわけではなく、あくまでローカルマシンがftpコマンドを実行している。

私がlftpを使っていたのは、FTPで転送する宛先のグローバルIPアドレスを指定するためでした。その用途ではうまくいきました。暗号化されていないFTPはファイルのアップロードもlftpでできていました。

が、FTPSのExplicitモードでlftpを使用しようとすると、セッションがデータコネクションには自動では切り替わらないのかもしれません。おそらくコントロールコネクションとデータコネクションが別のセッションであり、lftp側でサーバーの設定にしたがってセッションを発行している。つまり、FTPのAUTHコマンドでコネクションを初期化したあと、別のセッションでlsとかcdでリモートのディレクトリやファイルを参照する時に、個別のコマンドレベルで失敗しているのでは。

## トラブルシューティング 
__ケース 1:__
lftp等でvsftpdサーバーにFTP接続しようとした場合に“An unexpected tls packet is recieved” というメッセージが表示される。

* Explicitモードのvsftpdあるいはサーバーの設定を見直す
* opensslを見直す
* ftpコマンドの設定を見直す

ftpコマンドについては、[このサイト](https://serverfault.com/questions/1085577/can-the-same-sever-accept-both-explicit-and-implicit-ftps) の質問の回答を参考にすると、ImplicitモードしかサポートしていないFTPクライアントはほとんどなく、ftpコマンドの設定如何（いかん）で状況が変わるとはあまり思えないため、今回は見直さないことにします。

### Step1. opensslで稼働確認
Explicit modeで確認する場合、以下のopensslコマンドを-starttlsフラグを付けて実行。(Implicit modeの場合は openssl s_client -connect localhost:990を実行)この方法は後述の参考記事に記載されています。

```
openssl s_client -connect localhost:21 -starttls ftp
```

#### 実行結果 (概略）

「Verify return code: 18 (self-signed certificate)」や「220 (vsFTPd 3.0.5)」、
``Post-Handshake New Session Ticket arrived:
SSL-Session:
    Protocol  : TLSv1.3
    Cipher    : TLS_AES_256_GCM_SHA384
    Session-ID: 00F53A0C46727.....``
のように表示される。`SSL-Session`のブロックが2か所出力され、それぞれ別の`Session-ID`が表示されている。

上記の詳細の行につづき、以下のようにサーバー証明書の詳細などが出力される。気になったのは、その下の`SSL handshake...` 以降の部分。（一部を抜粋）

``` .bash
CONNECTED(00000003) # 接続されている

...
......
......
---
Server certificate #サーバー証明書
-----BEGIN CERTIFICATE-----
MIIDMjCCA......
............
cAJYhI
-----END CERTIFICATE-----
...
......
---
SSL handshake has read 1622 bytes and written 743 bytes
Verification error: self-signed certificate # lftpの設定ファイルで、 ssl:verify-certificateの値をfalse（証明書を検証しない）にすればOK (自己署名証明書の場合)
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384 # TLSのバージョン、Cipher (暗号化スイート)
Server public key is 2048 bit # 公開鍵のサイズ
Secure Renegotiation IS NOT supported # 失敗 (Cipher (暗号化スイート) がサポートされていない

```

上の抜粋の一番下の方に``Secure Renegotiation IS NOT supported``と表示されており、これは暗号化スイートの``TLS_AES_256_GCM_SHA384``がサポートされていない可能性がある (下記の投稿を参照)ため、そのあたりを確認する。

### Step2. 暗号化スイートがサポートされているか確認する

以下の記事を参考にすると、`openssl s_client`で確認した結果、``Secure Renegotiation IS NOT supported``となっている場合、指定した暗号化スイートはサポートされていない可能性がある。

https://qiita.com/toshihirock/items/eb1337ba471b4757071a

`openssl s_client`を実行した後、Cipherの行には``TLS_AES_128_GCM_SHA256``と出力されているが、その2行下くらいで``Secure Renegotiation IS NOT supported``と表示されている。

#### 実行結果 (概略）
再度確認のため、以下の`openssl s_client`コマンドを実行。

``` .bash
# 別の暗号化スイートを指定して確認。
openssl s_client -connect example.com:443 -cipher ECDHE-RSA-AES256-GCM-SHA384
CONNECTED(00000003)
...
......
---
SSL handshake has read 5489 bytes and written 318 bytes
Verification: OK
---
New, TLSv1.3, Cipher is TLS_AES_128_GCM_SHA256 # TLSのバージョン (v1.3)、暗号化スイート
Server public key is 2048 bit
Secure Renegotiation IS NOT supported
```


``` .bash
# サーバーが特定のTLSバージョンで、指定した暗号化スイートをサポートしているか確認する
sudo openssl s_client -connect example.com:443 -cipher TLS_AES_256_GCM_SHA384 -quiet -no_tls1_3
Call to SSL_CONF_cmd(-cipher, TLS_AES_256_GCM_SHA384) failed # サポートしていない
```

また、Copilot (MicrosoftのChatgpt) にきいてみると、vsftpdは TLS v1.2を標準でサポートしているらしい。
しかし``TLS_AES_128_GCM_SHA256``はTLS v1.3に固有のものらしいので、vsftpdがこの暗号化スイートをサポートしていないのかもしれない。(おそらく ``AES_256_GCM_SHA384``も同じくTLS v1.3に固有のものと考えられるため。）

### Step3. TLS v1.2でサポートされている暗号化スイートに変えてみる

TLSのバージョンごとに、サポートされている暗号化スイートの一覧を``openssl ciphers``コマンドで調べることができるようです。
4行目までは、TLSv1.3のみでサポートされているものが出てきている。上記の説明で出てきた``TLS_AES_256_GCM_SHA384``も、一番上の行に出ています。これらはvsftpdでサポートされていない可能性があるので、今回はそれ以外の暗号化スイートを使うように修正します。

``` .bash
# サポートされている暗号化スイートの一覧を取得（一部を抜粋）
openssl ciphers -v
TLS_AES_256_GCM_SHA384         TLSv1.3 Kx=any      Au=any   Enc=AESGCM(256)            Mac=AEAD
TLS_CHACHA20_POLY1305_SHA256   TLSv1.3 Kx=any      Au=any   Enc=CHACHA20/POLY1305(256) Mac=AEAD
TLS_AES_128_GCM_SHA256         TLSv1.3 Kx=any      Au=any   Enc=AESGCM(128)            Mac=AEAD
TLS_AES_128_CCM_SHA256         TLSv1.3 Kx=any      Au=any   Enc=AESCCM(128)            Mac=AEAD
ECDHE-ECDSA-AES256-GCM-SHA384  TLSv1.2 Kx=ECDH     Au=ECDSA Enc=AESGCM(256)            Mac=AEAD
ECDHE-RSA-AES256-GCM-SHA384    TLSv1.2 Kx=ECDH     Au=RSA   Enc=AESGCM(256)            Mac=AEAD
ECDHE-ECDSA-AES128-GCM-SHA256  TLSv1.2 Kx=ECDH     Au=ECDSA Enc=AESGCM(128)            Mac=AEAD
AES256-GCM-SHA384              TLSv1.2 Kx=RSA      Au=RSA   Enc=AESGCM(256)            Mac=AEAD
AES256-CCM                     TLSv1.2 Kx=RSA      Au=RSA   Enc=AESCCM(256)            Mac=AEAD
AES128-GCM-SHA256              TLSv1.2 Kx=RSA      Au=RSA   Enc=AESGCM(128)            Mac=AEAD
AES128-CCM                     TLSv1.2 Kx=RSA      Au=RSA   Enc=AESCCM(128)            Mac=AEAD
AES256-SHA256                  TLSv1.2 Kx=RSA      Au=RSA   Enc=AES(256)               Mac=SHA256
DHE-RSA-AES256-GCM-SHA384      TLSv1.2 Kx=DH       Au=RSA   Enc=AESGCM(256)            Mac=AEAD
DHE-RSA-CHACHA20-POLY1305      TLSv1.2 Kx=DH       Au=RSA   Enc=CHACHA20/POLY1305(256) Mac=AEAD
DHE-RSA-AES256-CCM             TLSv1.2 Kx=DH       Au=RSA   Enc=AESCCM(256)            Mac=AEAD
DHE-RSA-AES128-GCM-SHA256      TLSv1.2 Kx=DH       Au=RSA   Enc=AESGCM(128)            Mac=AEAD
DHE-RSA-AES128-CCM             TLSv1.2 Kx=DH       Au=RSA   Enc=AESCCM(128)            Mac=AEAD
......
..........
```

#### vsftpd.confの設定箇所
vsftpdの設定ファイルに、暗号化スイート (Cipher) の設定を行います。
``openssl genpkey -algorithm ec`` で秘密鍵を作ったので、`ECDSA` ( Elliptic Curve Digital Signature Algorithm)を指定していますが、``-algorithm rsa`` でカギを作成した場合は、RSAに対応した暗号化スイートを指定する必要があることに注意。
また、``:`` (コロン)でつなぐことで複数の暗号化スイートを指定することも可能ですが、下記の例では簡単化のために一つor2つだけ指定しています。

``` vsftpd.conf
# ssl_ciphersを指定。

例1) ECDSAでカギを生成した場合
# AES256 & SHA384
ssl_ciphers=ECDHE-ECDSA-AES256-GCM-SHA384

例2) ECDSAでカギを生成した場合（AES128 & SHA256)
# AES128 & SHA256
ssl_ciphers=ECDHE-ECDSA-AES128-GCM-SHA256


例3) RSAの鍵交換でカギを生成した場合
# AES256 & SHA384
ssl_ciphers=DHE-RSA-AES256-GCM-SHA384
```

今回実際に設定したvsftpd.confは以下です。
``` vsftpd.conf
# ECDHの鍵交換でカギを生成した場合 (OpenSSL方式でして)
ssl_ciphers=kEECDH+AESGCM+AES128:kEECDH+AESGCM:kEECDH+AES128:kEECDH+AES:!aNULL:!eNULL:!LOW:!EXP


# 秘密鍵と証明書のパスを指定。
rsa_cert_file=/path/to/your/certificate.pem
rsa_private_key_file=/path/to/your/ecdsa_private.key

# 使用プロトコル
ssl_sslv2=NO
ssl_sslv3=NO
ssl_tlsv1=NO
ssl_tlsv11=NO
ssl_tlsv12=YES
```

下の方で秘密鍵と証明書のパスを指定していますが、Copilotによると`rsa_`というディレクティブ名でも`ECDSA`をRSAのかわりに指定してもOKなようです。``openssl genpkey -algorithm ec``のようにアルゴリズム（左記のコマンドの場合は`ECDSA`）を指定して秘密鍵を作成していても`RSA`以外の場合でも、`rsa_private_key_file`にそのパスを設定すれば大丈夫です。


#### openssl.cnfの設定箇所
opensslの設定ファイルにTLS v1.2を使用するように指定する。
    
``` /etc/pki/tls/openssl.cnf
MaxProtocol = TLSv1.2
```

#### その他
- vsftpd が GnuTLSでコンパイルされている場合、OpenSSL方式の暗号化スイート規約を優先しない可能性がある。そのためもしGnuTLSを使用している場合、OpenSSLベースのvsftpdパッケージへの切り替えを検討する。

``` .bash
# GnuTLSの情報が表示されるか確認
vsftpd -version
vsftpd: version 3.0.5 # とくになし

# libsslが表示されれば、GnuTLSを使用していない
ldd $(which vsftpd) | grep ssl
        libssl.so.3 => /lib64/libssl.so.3 (0x00007f61c44f4000)

# vsftpdのパッケージを確認
rpm -qa | grep vsftpd
vsftpd-3.0.5-1.amzn2023.0.2.x86_64 # GnuTLSではない
```

### Step4. httpdのSSL設定をおこなう
ssl.confの内容を自動で生成してくれるサイトを使用して、SSLCipherSuiteなどを設定する。以下のサイトは自分が使用するhttpdとopensslのバージョンを指定すると、設定値を自動生成してくれる。

https://ssl-config.mozilla.org/#server=apache&version=2.4.58&config=intermediate&openssl=1.3.0&guideline=5.7

``` .bash
# httpdのバージョン確認
yum list installed | grep httpd
httpd.x86_64                              2.4.58-1.amzn2023                  @amazonlinux

# opensslのバージョン確認
yum list installed | grep openssl
openssl.x86_64                            1:3.0.8-1.amzn2023.0.10            @System
```


``` /etc/httpd/conf.d/ssl.conf
/* 許可する暗号化プロトコルを設定 */
#SSLProtocol all -SSLv3
SSLProtocol             -all +TLSv1.2

/* 暗号化スイートを指定する */
#SSLCipherSuite PROFILE=SYSTEM
SSLCipherSuite          ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305

/* ランクの高い暗号化スイートを優先するようサーバーに強制 */
SSLHonorCipherOrder on
```

### 確認
`openssl s_client` コマンドでTLS接続の状態を確認します。

``` .bash
# TLSv1.2で接続の詳細を確認
sudo openssl s_client -connect localhost:443 -tls1_2 < /dev/null
CONNECTED(00000003)
...
......
..........
#
---
New, TLSv1.2, Cipher is ECDHE-RSA-AES256-GCM-SHA384 # TLSv1.2がサポートしている暗号化スイートが使用されている
Server public key is 2048 bit
Secure Renegotiation IS supported # 安全な再ネゴシエーションがサポートされている
```

__ケース 2:__
lftpで `ls` や `cd` コマンドを打っても “Not connected” というメッセージが表示されるだけで、 `pwd`コマンドを打つと何も結果が返ってこない。

lftpにログインすると、正常ならリモート側のカレントディレクトリに対してUNIX系のシェルと同じように `ls` や `cd` が実行できますが、ディレクトリのリストが返ってこないし、カレントディレクトリも移動しない状況の場合、以下を確認してください。

**Step1.** クライアント側のシステムログ
``` .bash
sudo journalctl -a --lines=all # すべての行を出力 (あるいは journalctl -af で最新のログだけを出力)
```

**Step2.** lftpをデバッグモードで起動
デバッグモードで `ls` を実行し、表示されるメッセージを確認します。

``` .bash
lftp -u hoge -e "debug 12" ftp://example.com
~> ls
```

**Step3.** Step2 で以下のメッセージが出る場合
`GNUTLS: Received record packet of unknown type 53` や `gnutls_record_recv: An unexpected TLS packet was received.` というメッセージが出ることがあります。

``` .bash
lftp -u hoge -e "debug 12" ftp://example.com
Password:
~> ls
......
..........
---- Connecting to xx.xx.xxx.xxx (xx.xx.xxx.xxx) port 21
<--- 220 (vsFTPd 3.0.5)
......
..........
<--- 200 Always in UTF8 mode.
---> USER ftp-user
<--- 331 Please specify the password.
---> PASS XXXX
GNUTLS: Received record packet of unknown type 53
**** gnutls_record_recv: An unexpected TLS packet was received. # エラーが発生
---- Closing control socket
ls: Fatal error: gnutls_record_recv: An unexpected TLS packet was received. # エラーが発生
```

<br>
上記のようなエラーメッセージの場合、以下の投稿のトラブルシューティングにしたがい、エラーが解消できるか試してみてください。

https://qiita.com/Am_nine/items/f6084c52fda47aca71f6

<br>

**Step 1.** のシステムログに異常がない場合で、**Step 3.** で書いているエラーではなく "Not connected”が直らない場合、下記の投稿で対処法を書いています。手順に従い、宛先のグローバルIPアドレスを見直し、TLSプロトコルバージョンの優先順位の設定を行ってください。

https://qiita.com/Am_nine/items/63335a3a98dd4d3507b5