Pure-FTPd の存在を知ってしまったので試さざるを得ない

TL;DR
----

- Pure-FTPd はセキュリティを第1に考えているとのこと
- Pure-FTPd ではアトミックなアップロードを実現できる
    - vsftpd, ProFTPD で実現可能か把握できていない(デフォルト設定では不可)
- アップロード成功時に後処理を実行させる事ができる
- MySQL や PostgreSQL を使ってアカウントを管理する事ができる
    - chroot 先ディレクトリをログイン時に自動で作る機能があるので、アカウント追加作業はレコード作成だけで良い


Pure-FTPd とは
----

Security First が合い言葉(?)な FTPd です。

> Pure-FTPd is a free (BSD), secure, production-quality and standard-conformant FTP server. It doesn't provide useless bells and whistles, but focuses on efficiency and ease of use. It provides simple answers to common needs, plus unique useful features for personal users as well as hosting providers.
> [Pure-FTPd - About](https://www.pureftpd.org/project/pure-ftpd)

CVE への報告数も最小だとか。


何ができるのか
----

機能リストをざっと見た感じではありますが、FTPd に望まれることは大抵できるでしょう。FTPd の運用経験が豊富なわけではありませんが…。

以下、機能リスト([Pure-FTPd - About](https://www.pureftpd.org/project/pure-ftpd))から一部を抜粋しました。

- Linux カウントを FTP アカウントとして利用できる(指定 UID 未満の利用を禁止という設定も可)
- 認証を PAM 経由で行える
- デフォルトで chroot する
- FTP アカウントは Linux アカウントではない、独立したデータベースで管理されたものにできる(バーチャルユーザ)
- LDAP サポート
- 複数の認証方式を好みの順序でチェインさせることができる
- quota サポート
- 帯域制限サポート
- ユーザ単位での細かな制限
- アップロード成功後にシェルスクリプトを実行させることができる
- ホームディレクトリを on-demand に作成することができる
- 同一ホストで複数のバーチャル FTP サーバを運用できる
- chroot 外へのシンボリックリンクをフォローする
- アップロードは atomic


CentOS7 の VM を準備
----

いくつかの動作検証のために CentOS 7 の VM を用意します。
CentOS である必要性はありません。

※ 以後のコマンド実行記録にて SELinux に関する話は曖昧にさせていただきます(Permissive だった、とだけ言っておきましょう)。

### Vagrantfile

```ruby
# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure(2) do |config|
  # config.ssh.insert_key = false
  config.vm.box = "bento/centos-7.1"

  config.vm.define 'pure-ftpd' do |c|
    c.vm.network "private_network", ip: "192.168.33.10"
    c.vm.hostname = 'pure-ftpd'
  end

  config.vm.define 'pro-ftpd' do |c|
    c.vm.network "private_network", ip: "192.168.33.11"
    c.vm.hostname = 'pro-ftpd'
  end

  config.vm.define 'vsftpd' do |c|
    c.vm.network "private_network", ip: "192.168.33.12"
    c.vm.hostname = 'vsftpd'
  end
end
```


Atomic なアップロード？
----

アップロード用に大きめの Sparse file を作成しておきます。

```shell-session
% dd if=/dev/zero of=./hoge.dat seek=1000 bs=1048576 count=1
```

### vsftpd は？

vsftpd を最小の手間でセットアップしました。

```shell-session
[vagrant@vsftpd]$ sudo yum install vsftpd
[vagrant@vsftpd]$ sudo systemctl start vsftpd
[vagrant@vsftpd]$ sudo systemctl enable vsftpd
```

アップロードします。

```shell-session
% ftp vagrant@192.168.33.12
> put ./hoge.dat
```

アップロード途中のファイルが作られる事を確認しました。

```shell-session
[vagrant@vsftpd ~]$ ll
合計 131008
-rw-r--r--. 1 vagrant vagrant 85738976 10月  9 17:41 hoge.dat
[vagrant@vsftpd ~]$ ll
合計 524224
-rw-r--r--. 1 vagrant vagrant 515505800 10月  9 17:41 hoge.dat
[vagrant@vsftpd ~]$ ll
合計 1025024
-rw-r--r--. 1 vagrant vagrant 1049624576 10月  9 17:41 hoge.dat
```

アップロード途中のファイルへ適当な出力をリダイレクトしても上書きされない事を確認しました。また、 `rm` コマンドでファイルを削除できる事を確認しました。

オプション `lock_upload_files` のデフォルト値が `YES` なので、アップロード途中のファイルは write ロックされる様です。
ざっと設定値の一覧を確認した限りでは、Atomic なアップロードには対応していない様に見えます。

- [Manpage of VSFTPD.CONF](https://security.appspot.com/vsftpd/vsftpd_conf.html)

### ProFTPD

最小の手間でセットアップしました。

```
[vagrant@pro-ftpd]$ sudo yum install epel-release
[vagrant@pro-ftpd]$ sudo yum install proftpd
[vagrant@pro-ftpd]$ sudo systemctl start proftpd
[vagrant@pro-ftpd]$ sudo systemctl enable proftpd
```

アップロードします。

```shell-session
% ftp vagrant@192.168.33.11
> put ./hoge.dat
```

アップロード途中のファイルが作られる事を確認しました。

```shell-session
[vagrant@pro-ftpd ~]$ ll
合計 524224
-rw-r--r--. 1 vagrant vagrant 290361320 10月  9 17:54 hoge.dat
[vagrant@pro-ftpd ~]$ ll
合計 1048512
-rw-r--r--. 1 vagrant vagrant 586870048 10月  9 17:54 hoge.dat
[vagrant@pro-ftpd ~]$ ll
合計 1025024
-rw-r--r--. 1 vagrant vagrant 1049624576 10月  9 17:55 hoge.dat
```

アップロード途中のファイルへ適当な出力をリダイレクトしても上書きされない事を確認しました。また、 `rm` コマンドでファイルを削除できる事を確認しました。

ざっと設定値の一覧を確認した限りでは、Atomic なアップロードには対応していない様に見えます。

- [List of Directives](http://www.proftpd.org/docs/directives/linked/by-name.html)

### Pure-FTPd

最小の手間でセットアップしました。

```shell-session
$ sudo yum install epel-release
$ sudo yum install pure-ftpd pure-ftpd-selinux
$ sudo systemctl start pure-ftpd
$ sudo systemctl enable pure-ftpd
```

アップロードします。

```shell-session
% ftp vagrant@192.168.33.10
> put ./hoge.dat
```

アップロード途中のファイルが作られる事を確認しました。

```shell-session
[vagrant@pure-ftpd ~]$ ll
合計 262080
-rw-r--r--. 1 vagrant vagrant 204178848 10月  9 17:59 hoge.dat
[vagrant@pure-ftpd ~]$ ll
合計 1048512
-rw-r--r--. 1 vagrant vagrant 611263808 10月  9 17:59 hoge.dat
[vagrant@pure-ftpd ~]$ ll
合計 1025024
-rw-r--r--. 1 vagrant vagrant 1049624576 10月  9 17:59 hoge.dat
```

Atomic なアップロードはオプションの様なので、設定を変更して再度テストします。

```shell-session
[vagrant@pure-ftpd ~]$ sudo sed -i.orig -e 's/# NoTruncate/NoTruncate/' /etc/pure-ftpd/pure-ftpd.conf
[vagrant@pure-ftpd ~]$ diff -u /etc/pure-ftpd/pure-ftpd.conf.orig /etc/pure-ftpd/pure-ftpd.conf
[vagrant@pure-ftpd ~]$ sudo systemctl stop pure-ftpd
[vagrant@pure-ftpd ~]$ sudo systemctl start pure-ftpd
[vagrant@pure-ftpd ~]$ rm ~/hoge.dat
```

改めてアップロードします。

```shell-session
% ftp vagrant@192.168.33.10
> put ./hoge.dat
```

アップロードが完了してからファイルが作られました。

```shell-session
[vagrant@pure-ftpd ~]$ ll
合計 0
[vagrant@pure-ftpd ~]$ ll
合計 1025024
-rw-r--r--. 1 vagrant vagrant 1049624576 10月  9 18:19 hoge.dat
```

Pure-FTPd でもデフォルトはアップロード途中のファイルが作られる様になっている事が分かりました。


Pure-FTPd のアップロード後処理
----

アップロード後に任意の処理を実行させるための設定 `CallUploadScript` です。

アップロード後の処理を実行するためには、 `CallUploadScript` を `yes` にした上で FTPd である `pure-ftpd` とは別のプロセスである `pure-uploadscript` と呼ばれるデーモンを稼働させる必要があります。

`pure-uploadscript` デーモンは、起動時に指定された実行ファイルの第1引数にアップロードファイルのパスを与えます。
また、以下の様な環境変数も使える様にします。

> UPLOAD_SIZE  : the size of the file, in bytes.
> UPLOAD_PERMS : the permissions, as an octal value.
> UPLOAD_UID   : the uid of the owner.
> UPLOAD_GID   : the group the file belongs to.
> UPLOAD_USER  : the name of the owner.
> UPLOAD_GROUP : the group name the file belongs to.
> UPLOAD_VUSER : the full user name, or the virtual user name. (127 chars max)

`pure-uploadscript` の詳細については `man pure-uploadscript` や [download.pureftpd.org/pub/pure-ftpd/doc/README](http://download.pureftpd.org/pub/pure-ftpd/doc/README) を参照してください。

### 後処理スクリプト

アップロード後に実行するスクリプトを書きます。
今回は、動く事の確認だけしたいので、入力をプリントするだけのスクリプトとします。

```sh
[vagrant@pure-ftpd ~]$ cat <<\SCRIPT > /home/vagrant/pure-uploadscript.sh
#!/bin/sh

cat <<EOF >> /home/vagrant/pure-uploadscript.log
\$1 = $1
UPLOAD_SIZE = $UPLOAD_SIZE
UPLOAD_PERMS = $UPLOAD_PERMS
UPLOAD_UID = $UPLOAD_UID
UPLOAD_GID = $UPLOAD_GID
UPLOAD_USER = $UPLOAD_USER
UPLOAD_GROUP = $UPLOAD_GROUP
UPLOAD_VUSER = $UPLOAD_VUSER
EOF
SCRIPT
[vagrant@pure-ftpd ~]$ chmod 0755 /home/vagrant/pure-uploadscript.sh
```

### pure-uploadscript

`pure-uploadscript` を起動します。
実行には root 権限が必要です。

```
[vagrant@pure-ftpd]$ sudo pure-uploadscript -r /home/vagrant/pure-uploadscript.sh
```

#### 補足

ドキュメントに書かれていた以下の文を、「先に `pure-ftpd` を起動して、その後で `pure-uploadscript` を動かさないとだめよ。」と読んでその順序で試みたのですが、 `systemctl start pure-ftpd` がタイムアウトしてしまいまいた。

> YOU MUST START PURE-FTPD _FIRST_ and _THEN_ START PURE-UPLOADSCRIPT. THE REVERSE ORDER WON'T WORK.

英語が読めないので、上手くいった逆順の方で動作確認しています。

### pure-ftpd

設定 `CallUploadScript` を有効にします。

```
[vagrant@pure-ftpd ~]$ sudo sed -i.notruncate -e 's/#CallUploadScript/CallUploadScript/' /etc/pure-ftpd/pure-ftpd.conf
[vagrant@pure-ftpd ~]$ sudo systemctl stop pure-ftpd
[vagrant@pure-ftpd ~]$ sudo systemctl start pure-ftpd
[vagrant@pure-ftpd ~]$ ls -l /var/run/pure-*
```

`pure-uploadscript` を利用する際は `CustomerProof` を `yes` としておく事が強く推奨されています(デフォルトの設定ファイルで有効になっていました)。
`CustomerProof` を有効にすることで、ユーザによる不用意な(誤った) `chmod` の実行を防ぎます。

### アップロード

```
% ftp vagrant@192.168.33.10
> put Vagrantfile
```

```shell-session
[vagrant@pure-ftpd]$ cat pure-uploadscript.log
$1 = /home/vagrant/Vagrantfile
UPLOAD_SIZE = 541
UPLOAD_PERMS = 644
UPLOAD_UID = 1000
UPLOAD_GID = 1000
UPLOAD_USER = vagrant
UPLOAD_GROUP = vagrant
UPLOAD_VUSER = vagrant
```


PureFTPd でバーチャルユーザ
----

PostgreSQL を使って FTP アカウントを作成してみます。

- [download.pureftpd.org/pub/pure-ftpd/doc/README.PGSQL](http://download.pureftpd.org/pub/pure-ftpd/doc/README.PGSQL)

### PostgreSQL のセットアップ

#### インストール

```shell-session
[vagrant@pure-ftpd]$ sudo yum install postgresql postgresql-server
[vagrant@pure-ftpd]$ sudo postgresql-setup initdb
[vagrant@pure-ftpd]$ sudo systemctl start postgresql
[vagrant@pure-ftpd]$ sudo systemctl enable postgresql
```

#### データベース作成

```shell-session
=# CREATE DATABASE pureftpd;
```

#### テーブル作成

```shell-session
=# \c pureftpd;
=# CREATE TABLE "users" (
(#   "User" TEXT NOT NULL,
(#   "Password" TEXT NOT NULL,
(#   "Uid" INTEGER NOT NULL default '-1',
(#   "Gid" INTEGER NOT NULL default '-1',
(#   "Dir" TEXT NOT NULL,
(#   PRIMARY KEY ("User")
(# ) WITHOUT OIDS;
```

ついでにアカウントを登録しておきます。

```shell-session
=# INSERT INTO users VALUES ('user', 'password', 1000, 1000, '/home/vagrant/user');
```

#### 接続ユーザ

```shell-session
=# CREATE USER pureftpd PASSWORD 'password';
=# GRANT SELECT ON users TO pureftpd;
```

#### 接続設定

PostgreSQL のデフォルト設定でローカル接続が制限されているため、任意のユーザ/パスワードで接続できる様にします。

```shell-session
[vagrant@pure-ftpd ~]$ sudo cp /var/lib/pgsql/data/pg_hba.conf /var/lib/pgsql/data/pg_hba.conf.orig
[vagrant@pure-ftpd ~]$ sudo vi /var/lib/pgsql/data/pg_hba.conf
[vagrant@pure-ftpd ~]$ sudo diff -u /var/lib/pgsql/data/pg_hba.conf.orig /var/lib/pgsql/data/pg_hba.conf
--- /var/lib/pgsql/data/pg_hba.conf.orig        2015-10-10 08:43:26.851201925 +0000
+++ /var/lib/pgsql/data/pg_hba.conf     2015-10-10 08:46:53.410201925 +0000
@@ -77,6 +77,7 @@
 # TYPE  DATABASE        USER            ADDRESS                 METHOD

 # "local" is for Unix domain socket connections only
+local pureftpd pureftpd password
 local   all             all                                     peer
 # IPv4 local connections:
 host    all             all             127.0.0.1/32            ident
[vagrant@pure-ftpd ~]$ sudo systemctl reload postgresql
[vagrant@pure-ftpd ~]$ psql -U pureftpd -d pureftpd
ユーザ pureftpd のパスワード:
psql (9.2.13)
"help" でヘルプを表示します.

pureftpd=> SELECT * FROM users;
 User | Password | Uid | Gid | Dir
------+----------+-----+-----+-----
(0 行)
```

これだと、 `-h localhost -p 5432` を付けたときに接続できないのでもう一つ設定を加えます。

```shell-session
[vagrant@pure-ftpd ~]$ sudo cp /var/lib/pgsql/data/pg_hba.conf /var/lib/pgsql/data/pg_hba.conf.local-pureftpd
[vagrant@pure-ftpd ~]$ sudo vi /var/lib/pgsql/data/pg_hba.conf
[vagrant@pure-ftpd ~]$ sudo diff -u /var/lib/pgsql/data/pg_hba.conf.local-pureftpd /var/lib/pgsql/data/pg_hba.conf
--- /var/lib/pgsql/data/pg_hba.conf.local-pureftpd      2015-10-10 08:49:27.841201925 +0000
+++ /var/lib/pgsql/data/pg_hba.conf     2015-10-10 08:52:27.383201925 +0000
@@ -80,8 +80,10 @@
 local pureftpd pureftpd password
 local   all             all                                     peer
 # IPv4 local connections:
+host    pureftpd        pureftpd        127.0.0.1/32            password
 host    all             all             127.0.0.1/32            ident
 # IPv6 local connections:
+host    pureftpd        pureftpd        ::1/128                 password
 host    all             all             ::1/128                 ident
 # Allow replication connections from localhost, by a user with the
 # replication privilege.
[vagrant@pure-ftpd ~]$ sudo systemctl reload postgresql
[vagrant@pure-ftpd ~]$ psql -U pureftpd -d pureftpd -h localhost -p 5432
ユーザ pureftpd のパスワード:
psql (9.2.13)
"help" でヘルプを表示します.

pureftpd=>
```

### 設定

デフォルトで作成されている PostgreSQL 用の設定ファイルを見てみましょう。

```shell-session
[vagrant@pure-ftpd ~]$ grep '^PGSQL' /etc/pure-ftpd/pureftpd-pgsql.conf
PGSQLServer     localhost
PGSQLPort       5432
PGSQLUser       postgres
PGSQLPassword   rootpw
PGSQLDatabase   pureftpd
PGSQLCrypt      cleartext
PGSQLGetPW      SELECT Password FROM users WHERE User='\L'
PGSQLGetUID     SELECT Uid FROM users WHERE User='\L'
PGSQLGetGID     SELECT Gid FROM users WHERE User='\L'
PGSQLGetDir     SELECT Dir FROM users WHERE User='\L'
```

この設定ファイルはデフォルトだと `pure-ftpd` の設定ファイルからは読み込まれていません。

```shell-session
[vagrant@pure-ftpd ~]$ grep 'PG' /etc/pure-ftpd/pure-ftpd.conf
# Postgres configuration file (see README.PGSQL)
# PGSQLConfigFile               /etc/pure-ftpd/pureftpd-pgsql.conf
```

今回のケースにあわせて設定を変更し反映させましょう。

```shell-session
[vagrant@pure-ftpd ~]$ sudo cp /etc/pure-ftpd/pureftpd-pgsql.conf /etc/pure-ftpd/pureftpd-pgsql.conf.orig
[vagrant@pure-ftpd ~]$ sudo vi /etc/pure-ftpd/pureftpd-pgsql.conf
[vagrant@pure-ftpd ~]$ sudo diff /etc/pure-ftpd/pureftpd-pgsql.conf.orig /etc/pure-ftpd/pureftpd-pgsql.conf
19c19
< PGSQLUser       postgres
---
> PGSQLUser       pureftpd
22c22
< PGSQLPassword   rootpw
---
> PGSQLPassword   password
46c46
< PGSQLGetPW      SELECT Password FROM users WHERE User='\L'
---
> PGSQLGetPW      SELECT "Password" FROM users WHERE "User"='\L'
51c51
< PGSQLGetUID     SELECT Uid FROM users WHERE User='\L'
---
> PGSQLGetUID     SELECT "Uid" FROM users WHERE "User"='\L'
56c56
< #PGSQLDefaultUID 1000
---
> PGSQLDefaultUID 1000
61c61
< PGSQLGetGID     SELECT Gid FROM users WHERE User='\L'
---
> PGSQLGetGID     SELECT "Gid" FROM users WHERE "User"='\L'
66c66
< #PGSQLDefaultGID 1000
---
> PGSQLDefaultGID 1000
71c71
< PGSQLGetDir     SELECT Dir FROM users WHERE User='\L'
---
> PGSQLGetDir     SELECT "Dir" FROM users WHERE "User"='\L'
```

PostgreSQL 認証を使える様に設定を変更します。

```shell-session
[vagrant@pure-ftpd ~]$ sudo cp /etc/pure-ftpd/pure-ftpd.conf /etc/pure-ftpd/pure-ftpd.conf.uploadscript
[vagrant@pure-ftpd ~]$ sudo vi /etc/pure-ftpd/pure-ftpd.conf
[vagrant@pure-ftpd ~]$ sudo diff /etc/pure-ftpd/pure-ftpd.conf.uploadscript /etc/pure-ftpd/pure-ftpd.conf
121c121
< # PGSQLConfigFile               /etc/pure-ftpd/pureftpd-pgsql.conf
---
> PGSQLConfigFile               /etc/pure-ftpd/pureftpd-pgsql.conf
137c137
< PAMAuthentication             yes
---
> PAMAuthentication             no
143c143
< # UnixAuthentication            yes
---
> UnixAuthentication            no
343c343
< #CreateHomeDir               yes
---
> CreateHomeDir               yes
[vagrant@pure-ftpd ~]$ sudo systemctl stop pure-ftpd
[vagrant@pure-ftpd ~]$ sudo systemctl start pure-ftpd
```

PostgreSQL と直接関係ありませんが、レコード挿入だけでログインできる様にするため、ディレクトリはログイン時に作成する様に `CreateHomeDir` を `yes` としています。

### ログイン

ログインに成功し、ホームディレクトリも自動的に作成されました。

```shell-session
% ftp user@192.168.33.10
Connected to 192.168.33.10.
220---------- Welcome to Pure-FTPd [privsep] [TLS] ----------
220-You are user number 1 of 50 allowed.
220-Local time is now 09:46. Server port: 21.
220-IPv6 connections are also welcome on this server.
220 You will be disconnected after 15 minutes of inactivity.
331 User user OK. Password required
Password:
230 OK. Current restricted directory is /
Remote system type is UNIX.
Using binary mode to transfer files.
```


まとめ
----

Pure-FTPd の特徴的なだなと感じた機能を試しました。

相変わらずのアサチャプ(浅瀬でチャプチャプ)メソッドなので、本当に完全なアトミックなアップロードが可能なのか、実は vsftpd や ProFTPD でも可能なのではないか、などは深掘りできていません。

また、アップロード後処理についても、これを利用して運用を安定させるための工夫など、検証が足りていません。

PostgreSQL 認証についても、UID, GID, パスワード暗号化などは適当です。

色々とやり残しはありますが、かなり良い感触をつかむことができたのでかなり満足しています。

実は他と比べた Pure-FTPd の致命的なデメリットがあるのかもしれませんが、個人的(直近で見えている用途的)にはかなりの高評価です。