FTPサーバーの構築(proftpd)

1.はじめに

自宅サーバーが出来たらFTP、WWWサーバーも立ち上げたくなりますね。
それではFTPサーバーを立ち上げましょう!

2.インストール

インストールにはportsを利用します
# cd /usr/ports/ftp/proftpd
# make install clean
初めての場合はオプション選択画面になります
あえて設定し直したい時は「make config」で設定しましょう

3.起動方法

proftpdの起動方法には2種類有ります。
■デーモンで常駐するタイプ
■inetdでアクセスが有る度に起動するタイプ

それぞれの利点は以下のとおりです
■inetdで立ち上げる場合
・confファイルを書き換えた場合、立ち上げ直さなくても次に接続された時から反映される
・接続のない時にはRAMを消費しない
・IPが動的の場合PASVで帰すアドレスが変っても対処できる
■デーモンで立ち上げる場合
・立ち上がりが早い
・複数接続された場合おそらくRAMの消費量が少ないと思う
・VirtualHostが使える

だと思います。

そこで、個人で立ち上げる場合は上記の内容からinetdの方が都合がいいのではと思い
私はinetdで起動させています。

3.設定

inetdの場合を基本に解説します。

まずはinetdで起動するのでinetdのファイルに記述します。
/etc/inetd.conf を編集する
#ftp    stream  tcp     nowait  root    /usr/libexec/ftpd               ftpd -l
ftp     stream  tcp     nowait  root    /usr/local/sbin/proftpd      proftpd
デフォルトで上記1行目が有る場合はコメントアウトして下さい。
2行目がproftpdを立ち上げる為の行です。

次にproftpdの本体とも言えるproftpd.confを設定します。
サンプルが置いてあると思うのでそれを参照するのもいいでしょう!
/usr/local/etc/proftpd.conf を編集する
# ログイン時に表示されるメッセージ
ServerIdent                     on "Personal FTP Server"
# サーバーの名前
ServerName                      "server.hogehoge.org"
# 立ち上げ方法(デーモンの場合はstandalone)
ServerType                      inetd
# バーチャルホストで一致しない場合はデフォルトで受けるか、とりあえずON
DefaultServer                   on
# 制御用ポート番号
Port                            21
# 書き込まれた場合のパーミッション、002なら書かれたファイルは775になる
Umask                           002
# 時計はGMTではないのでFALSE
TimesGMT                        FALSE
# 最大接続数(スタンドアローンだけかも)
MaxInstances                    30
# デーモンを走らせるユーザー名
User                            nobody
# 同じくグループ名
Group                           nogroup
# デフォルトのルートディレクトリー(~はhomeを示す)
DefaultRoot                     ~/public_html
# LOGの場所
systemlog                       /var/log/proftpd.log
# アイドルの許す時間(秒)
TimeoutIdle                     300
# 接続からログイン終了までの許す時間(秒)
TimeoutLogin                    60
# データ転送しない時間がどれくらい許すか(秒)
TimeoutNoTransfer               600
# LOGのフォーマット
LogFormat default "%t %a %l %u %r %f"
# IDENTを使うか(自分が閉じるくらいなのでoffしておきましょう)
IdentLookups                    off
# サーバードメイン名(PASV接続先指示に使います)
MasqueradeAddress server.hogehoge.org
# PASVモードで使用可能なポート番号範囲
PassivePorts 7030 7050
# とりあえず今はOFF、TLS出来るようになったらONかな?
AuthPAM                         off
#なぜか1.210からscoreboardが無いとエラーしますので指定しておきます
ScoreboardFile /var/run/proftpd.scoreboard
#ftps用
LoadModule           mod_tls.c
#パスワードファイルを設定する
AuthUserFile            /usr/local/etc/proftpd/ftpd.passwd

#ftpsの設定
<IfModule mod_tls.c>
    TLSEngine on
    TLSLog  /var/log/proftpd-tls.log
    TLSProtocol SSLv23
    TLSCipherSuite ALL:!ADH:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP

    # Are clients required to use FTP over TLS when talking to this server?
    TLSRequired off

    # Server's certificate
    TLSRSACertificateFile /usr/local/etc/letsencrypt/live/server.hogehoge.org/cert.pem
    TLSRSACertificateKeyFile /usr/local/etc/letsencrypt/live/server.hogehoge.org/privkey.pem

    # Authenticate clients that want to use FTP over TLS?
    TLSVerifyClient off
</IfModule>

# デフォルトはローカルからしかログインできないようにする
<Limit LOGIN>
  Order Allow,Deny
  Allow from 192.168.1.
  Deny from all
</Limit>

<Global>
  <Directory /*>
    AllowOverwrite              on
    AllowStoreRestart           on
    AllowRetrieveRestart        on
  </Directory>
</Global>

# ここからは基本とは別の動作をします
# /data/tempがルート
<Anonymous /data/temp>
# その場合のUNIXユーザー
  User                          ユーザー名(UNIX)
# FTPログイン名をUNIXに変換
  UserAlias                     ログインユーザー名 ユーザー名(UNIX)
# FTPログインパスワード(暗号化された物を書きます)
  UserPassword                  ユーザー名(UNIX) 暗号化PASS
# その時のグループ
  Group                         グループ名(UNIX)
  AnonRequirePassword           on
  AuthAliasOnly                 on
  RequireValidShell             off
  HideNoAccess                  on
  LsDefaultOptions              "-o"
# 読み込み速度制限(バイト)
  RateReadBPS                   92160
# 書き込み速度制限(バイト)
  RateWriteBPS                  91260
# 最大接続数
  MaxClients                    2 "Sorry, already %m users login now."
# 1クライアントあたりの最大接続数
  MaxClientsPerHost             1 "Sorry, 1 connect 1 Client MAX."
# ファイルの持ち主をrootに見せかける
  DirFakeUser                   on root
# 同じくグループもwheelに見せかける
  DirFakeGroup                  on wheel
# パーミッションも全て644に見せかける
  DirFakeMode                   644
  AllowRetrieveRestart          on
  <Directory *>
    <Limit ALL>
      IgnoreHidden              on
    </Limit>
    <Limit WRITE>
      DenyAll
    </Limit>
  </Directory>
# incomingのディレクトリーは見えない
  <Directory incoming>
    <Limit ALL>
      DenyAll
    </Limit>
  </Directory>
# tempのディレクトリー設定
  <Directory temp>
# 送信レジューム可
      AllowStoreRestart           on
# 上書き可
      AllowOverwrite              on
# 書き込み削除可
    <Limit WRITE DELE>
      AllowAll
    </Limit>
  </Directory>
# ログイン制限
# 怪しい外国から接続を試みた物は弾きましょう!
  <Limit LOGIN>
    Order Deny,Allow
    Deny from .fr,.nl,.ca,.kr,.it,.hk
    Allow from all
  </Limit>
</Anonymous>
気をつけなければならないのは<Anonymous></Anonymous>のあいだは
グローバルで設定していてもその内容が全てに効くわけではなく、それぞれ設定しなければならない項目も
ありますので、各自設定して試して見てください。
設定ファイルの文法は上記でいいかと思いますが、それぞれの意味が数が多くく
日本語の解説はこちらの「Directive 設定のオンラインマニュアル」にありますので
あとは見ながら設定して下さい。
また、上記リンク先が無くなる可能性もあるので無断でひかえを取らさせていただいております。
それと先程のscoreboardファイルを生成しておきます
# touch /var/run/proftpd.scoreboard

そしてユーザーを作成する
# ftpasswd --passwd --file=/usr/local/etc/proftpd/ftpd.passwd --name=ログインユーザー名 --uid=UNIXユーザー番号 --gid=UNIXグループ番号 --home=/usr/home/ftpdata --shell=/sbin/nologin

4.起動

設定が完了したら起動して見ましょう。
inetdの場合はinetdを再起動する事でアクセスすれば自動で立ち上がります。
(91はプロセス番号ですので各自プロセス番号に合わせてHUPして下さい)
# ps -ax | grep inetd
   91  ??  Is     0:00.52 /usr/sbin/inetd -wW
# kill -HUP 91
デーモンで走らせる場合は
/etc/rc.conf
proftpd_enable="YES"
を追記して
# /usr/local/etc/rc.d/proftpd start
で走ります

もし
error opening scoreboard: No such file or directory
このようなエラーが出た場合は
/var/run/proftpd.scoreboard
というファイルを作成して下さい(パーミッションは644)
とあるバージョンからなぜかこのようなエラーが出ることがあります
それにしても、自動で作成してくれてもいいと思うのは私だけでしょうか?

またinetdエラーする事があります。
inetd[23031]: unknown rpc/udp6 or rpc/tcp6
この場合は
/etc/netconfig
udp6 tpi_clts v inet6 udp - -
tcp6 tpi_cots_ord v inet6 tcp - -
を有効にすることで起動する。

5.おまけ

・暗号化パスワードの生成方法
以下の内容をファイルにしてCGI実行可能なディレクトリーにセーブし、ブラウザからアクセスすれば
暗号化パスワードを生成できます。
crypt.cgi を作成する
#!/usr/bin/perl

#
# パスワード生成ツール "crypt.cgi" is Free.
# by www.rescue.ne.jp
#

$buffer = $ENV{'QUERY_STRING'};

@pairs = split(/&/,$buffer);
foreach $pair (@pairs) {

    ($name, $value) = split(/=/, $pair);
    $value =~ tr/+/ /;
    $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
    $FORM{$name} = $value;
}

if ($buffer eq '') {

        print "Content-type: text/html\n\n";
        print "<html><head><title>暗号文字化</title></head>\n";
        print "<body>\n";
        print "<h1>暗号文字化</h1>\n";
        print "<form action=\"crypt.cgi\">\n";
        print "パスワード <input type=text name=\"plain\" size=30> <input type=submit value=\"暗号化\"><input type=reset value=\"RESET\">\n";
        print "</form><p>\n";
        print "<ul>\n";
        print "<li>半角英数字(_を含む)のみ使えます.\n";
        print "</ul><p>\n";
        print "</body></html>\n";
        exit;
}

if ($FORM{'plain'} eq '' || $FORM{'plain'} =~ /\W/) { &error('パスワードの入力が無いか、文字列に半角英数字以外の文字が含まれています.'); }

@char = ('a'..'z','A'..'Z','0'..'9');
srand(time|$$);
foreach (0..7) {
        {
                local(@temp);
                push(@temp,splice(@char,rand(@char),1)) while @char;
                @char = @temp;
        }
        $keisu = $char[($_)] . $keisu;
}

$now = time;
($p1, $p2) = unpack("C2",$keisu);
$wk = $now / (60*60*24*7) + $p1 + $p2 - 8;
@saltset = ('a'..'z','A'..'Z','0'..'9','.','/');
$nsalt = $saltset[$wk % 64] . $saltset[$now % 64];
$pwd = crypt($FORM{'plain'}, $nsalt);

if ($pwd =~ /^\$1\$/) { $salt = 3; } else { $salt = 0; }
if (crypt($FORM{'plain'}, substr($pwd,$salt,2)) eq $pwd) { $verify = 'OK'; } else { $verify = 'NG'; }

print "Content-type: text/html\n\n";
print "<html><head><title>暗号文字化</title></head>\n";
print "<BODY>\n";
print "<h2>実行結果</h2>\n";
print "<blockquote>\n";
print "<form>\n";
print "パスワード       <input size=30 value=\"$FORM{'plain'}\"><br>\n";
print "暗号化されたパスワード <input size=30 value=\"$pwd\"><p>\n";
print "[照合試験] $pwd → $FORM{'plain'} = $verify<p>\n";
if ($verify ne 'OK') { print "照合に失敗しました。このサーバではこのツールはご利用できないと思われます。<p>\n"; }
print "</form></blockquote>\n";
print "</body></html>\n";
exit;

sub error {

        print "Content-type: text/html\n\n";
        print "<html><head><title>暗号文字化</title></head>\n";
        print "<body>\n";
        print "<h3>$_[0]</h3>\n";
        print "</body></html>\n";
        exit;
}

サンプルはこちら
ですが、なぜかMD5が上手く照合できない…

友人がrubyで作ったので、そちらなら照合OKでした。
crypt.rb
#!/usr/bin/env ruby
# -*- mode:ruby; coding:shift_jis -*-
require "cgi"

cgi = CGI.new("html3")
passwd1 = cgi["passwd1"].to_s
passwd2 = cgi["passwd2"].to_s

cgi.out() do
        cgi.head{ "\n<META HTTP-EQUIV=\"Content-type\" CONTENT=\"text/html; charset=Shift_JIS\">\n" } +
        cgi.body() do
                if (passwd1 == "") or (passwd2 == "") then
                "<h1>暗号文字化</h1>\n" +
                cgi.form() do
                        "<table border=0>\n" +
                        "<tr><td>パスワード</td><td>" + cgi.password_field("passwd1") + "</td></tr>\n" +
                                "<tr><td>もう一回</td><td>" + cgi.password_field("passwd2") + "</td></tr>\n" +
                                "</table>\n" +
                                cgi.submit("暗号化") +
                                cgi.reset("リセット")
                end +
                "<li>半角英数字(_を含む)のみ使えます.\n"
                elsif (passwd1 != passwd2) or (passwd1 =~ /\W/) then
                "<h1>パスワードが異なっているか、半角英数字以外の文字が使われています!</h1>" +
                cgi.form() do
                                cgi.hidden("passwd1", "") + cgi.hidden("passwd2", "") + cgi.submit("元に戻る")
                end
                else
                        srand(Time.now.to_i)
                        salt = []
                        (0..7).each { salt << rand(64) }
                        salt = salt.pack("C*").tr("\x00-\x3f","A-Za-z0-9./")
                        des = passwd1.crypt(salt[0,2])
                        md5 = passwd1.crypt("$1$#{salt}$")
                        deschk = ((passwd1.crypt(des[0,2]) == des) and (des[0,2] == salt[0,2]) ? "OK" : "NG")
                        md5chk = ((passwd1.crypt(md5[0,12]) == md5) and (md5[0,12] == "$1$#{salt}$") ? "OK" : "NG")
                        "<h1>実行結果</h1>\n" +
                        "<table border=0>\n" +
                        "<tr><td>暗号化されたパスワード(DES)</td>" +
                        "<td>" + cgi.text_field("des", des, 50) + "</td>" +
                        "<td>照合結果:#{deschk}</td></tr>\n" +
                        "<tr><td>暗号化されたパスワード(MD5)</td>" +
                        "<td>" + cgi.text_field("md5", md5, 50) + "</td>" +
                        "<td>照合結果:#{md5chk}</td></tr>\n" +
                        "</table>\n" +
                        if (deschk == "NG") or (md5chk == "NG") then
                                "<h3><font color=red>照合に失敗した項目があります。" +
                                "このサーバではこのツールはご利用できないと思われます。</font></h3>\n" + cgi.br
                        else
                                "<h3><font color=blue>いずれも照合に成功しました!</font></h3>\n" + cgi.br
                        end +
                        cgi.form() do
                                cgi.hidden("passwd1", "") + cgi.hidden("passwd2", "") + cgi.submit("元に戻る")
                        end
                end
        end
end