[Из песочницы] Настройка связки proftpd + {mysql/postgresql} с хранением паролей в md5+salt
Давно мне не давал покоя тот факт, что пароли пользователей у меня в БД хранятся в открытом виде. Также очень неудобно было вручную добавлять/удалять пользователей и менять им пароли.
В результате, настроил связку proftpd + mod_sql + mod_sql_passwd c хранением паролей в виде md5 + salt, а также написал три скрипта для добавления, удаления и смены пароля пользователей.
Ввиду того, что на разных серверах у меня используется где-то PostgreSQL, а где-то MySQL, выкладываю описание настроек для обеих СУБД.
Сервера работают под управлением ОС ALT Linux Sisyphus.
Шаг 1. Установка всего необходимого
Для работы нам понадобится сам пакет proftpd, модули к нему proftpd-mod_sql, proftpd-mod_sql_passwd и proftpd-mod_sql_mysql/postgres и модули Perl.
# apt-get update
# apt-get install proftpd proftpd_mod_sql proftpd-mod_sql_passwd proftpd-mod_sql_mysql proftpd-mod_sql_postgres
# apt-get install perl-Crypt-PasswdMD5 perl-Config-Simple perl-DBI perl-DBD-Pg perl-DBD-mysql
Шаг 2. Подготовка базы данных
В данной статье подключаться к БД мы будем на хост DBHOST с пользователем DBUSER, паролем DBPASSWD и базе DBNAME
CREATE TABLE users (
id integer NOT NULL,
username character varying(20),
password character varying(50),
salt character varying(50),
groupname character varying(24),
uid integer,
gid integer,
homedir character varying(70),
shell character varying(20),
last_login character varying(30),
login_count integer,
last_error_login character varying(30),
login_error_count integer
);
ALTER TABLE users OWNER TO DBUSER;
CREATE SEQUENCE users_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER TABLE users_id_seq OWNER TO DBUSER;
CREATE TABLE log_failed_logins (
id integer DEFAULT nextval('log_failed_logins_id_seq'::regclass) NOT NULL,
datetime character varying(30),
user_name character varying(64),
client_name character varying(127),
client_ip character varying(15)
);
ALTER TABLE log_failed_logins OWNER TO DBUSER;
CREATE SEQUENCE log_failed_logins_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER TABLE log_failed_logins_id_seq OWNER TO DBUSER;
CREATE TABLE xfer_errors (
id integer DEFAULT nextval('xfer_errors_id_seq'::regclass) NOT NULL,
datetime character varying(30),
user_name character varying(64),
file_and_path text,
client_name character varying(127),
client_ip character varying(15),
client_command character varying(5)
);
ALTER TABLE xfer_errors OWNER TO DBUSER;
CREATE SEQUENCE xfer_errors_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER TABLE xfer_errors_id_seq OWNER TO DBUSER;
CREATE TABLE xfer_table (
id integer DEFAULT nextval('xfer_table_id_seq'::regclass) NOT NULL,
datetime character varying(30),
user_name character varying(64),
file_and_path text,
bytes integer,
client_name character varying(127),
client_ip character varying(15),
client_command character varying(5),
send_time character varying(9)
);
ALTER TABLE xfer_table OWNER TO DBUSER;
CREATE SEQUENCE xfer_table_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER TABLE xfer_table_id_seq OWNER TO DBUSER;
DROP TABLE IF EXISTS `log_failed_logins`;
CREATE TABLE `log_failed_logins` (
`unic_id` int(32) NOT NULL AUTO_INCREMENT,
`datetime` varchar(30) NOT NULL,
`user_name` varchar(64) NOT NULL,
`client_name` varchar(127) NOT NULL,
`client_IP` varchar(15) NOT NULL,
PRIMARY KEY (`unic_id`)
) ENGINE=MyISAM AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(20) NOT NULL,
`password` varchar(50) DEFAULT NULL,
`salt` varchar(50) DEFAULT NULL,
`groupname` varchar(24) NOT NULL,
`uid` int(10) unsigned NOT NULL,
`gid` int(10) unsigned NOT NULL,
`homedir` varchar(70) NOT NULL,
`shell` varchar(20) DEFAULT NULL,
`last_login` varchar(30) DEFAULT NULL,
`login_count` int(10) DEFAULT NULL,
`last_error_login` varchar(30) DEFAULT NULL,
`login_error_count` int(10) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `xfer_errors`;
CREATE TABLE `xfer_errors` (
`unic_id` int(32) NOT NULL AUTO_INCREMENT,
`datetime` varchar(30) NOT NULL,
`user_name` varchar(64) NOT NULL,
`file_and_path` tinytext NOT NULL,
`client_name` varchar(127) NOT NULL,
`client_IP` varchar(15) NOT NULL,
`client_command` varchar(5) NOT NULL,
PRIMARY KEY (`unic_id`)
) ENGINE=MyISAM AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `xfer_table`;
CREATE TABLE `xfer_table` (
`unic_id` int(32) NOT NULL AUTO_INCREMENT,
`datetime` varchar(30) NOT NULL,
`user_name` varchar(64) NOT NULL,
`file_and_path` tinytext NOT NULL,
`bytes` int(15) NOT NULL DEFAULT '0',
`client_name` varchar(127) NOT NULL,
`client_IP` varchar(15) NOT NULL,
`client_command` varchar(5) NOT NULL,
`send_time` varchar(9) NOT NULL DEFAULT '0',
PRIMARY KEY (`unic_id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
Шаг 3. Конфигурация proftpd
UseReverseDNS on
AuthOrder mod_sql.c
AuthPAM off # отключаем авторизацию через PAM
LoadModule mod_sql.c
LoadModule mod_sql_mysql.c (при использовании mysql)
LoadModule mod_sql_postgres.c (при использовании postgres)
LoadModule mod_sql_passwd.c
SQLPasswordEngine on
SQLPasswordEncoding hex
SQLPasswordOptions HashEncodeSalt
SQLAuthTypes Crypt
SQLAuthenticate users
SQLConnectInfo DBUSER@DBHOST:DBPORT DBNAME DBPASSWD
SQLUserInfo users username password uid gid homedir shell
SQLMinUserUID 50
SQLMinUserGID 50
RequireValidShell off
SQLNamedQuery get-user-salt SELECT "salt FROM users WHERE username = '%U'"
SQLPasswordUserSalt sql:/get-user-salt Append
Далее конфиг для postgres и mysql различается синтаксисом SQL запросов:
SQLLog PASS counter_login
SQLNamedQuery counter_login UPDATE "\
last_login=date_trunc ( 'seconds' ,\
timestamp without time zone 'now' ),\
login_count=login_count+1 WHERE \
username='%u'" users
SQLLog ERR_PASS counter_err
SQLNamedQuery counter_err UPDATE "\
last_error_login=date_trunc ( 'seconds' ,\
timestamp without time zone\'now' ), \
login_error_count=login_error_count+1 WHERE \
username='%U'" users
SQLLog ERR_PASS log_fails
SQLNamedQuery log_fails INSERT "nextval('log_failed_logins_id_seq'::regclass), date_trunc ('seconds',timestamp without time zone 'now'),'%U','%h','%a'" log_failed_logins
SQLLog DELE,RETR,STOR log_story_transfer
SQLNamedQuery log_story_transfer INSERT "nextval('xfer_table_id_seq'::regclass), date_trunc ('seconds',timestamp without time zone 'now'),'%u', \
'%f','%b','%h','%a','%m', '%T'" xfer_table
SQLLOG ERR_RETR,ERR_STOR,ERR_DELE,ERR_RMD,ERR_RNTO \
log_err_modify
SQLNamedQuery log_err_modify INSERT "nextval('xfer_errors_id_seq'::regclass), date_trunc ('seconds',timestamp without time zone 'now'),'%u', \
'%f','%h','%a','%m'" xfer_errors
SQLNamedQuery get-user-salt SELECT "salt FROM users WHERE username = '%U'"
SQLPasswordUserSalt sql:/get-user-salt Append
SQLLog PASS counter_login
SQLNamedQuery counter_login UPDATE "\
last_login=now(),\
login_count=login_count+1 WHERE \
username='%u'" users
SQLLog ERR_PASS counter_err
SQLNamedQuery counter_err UPDATE "\
last_error_login=now(), \
login_error_count=login_error_count+1 WHERE \
username='%U'" users
SQLLog ERR_PASS log_fails
SQLNamedQuery log_fails INSERT "'', now(),'%U','%h','%a'" log_failed_logins
SQLLog DELE,RETR,STOR log_story_transfer
SQLNamedQuery log_story_transfer INSERT "'', now(),'%u', \
'%f', '%b', '%h', '%a', '%m', '%T'" xfer_table
SQLLOG ERR_RETR,ERR_STOR,ERR_DELE,ERR_RMD,ERR_RNTO log_err_modify
SQLNamedQuery log_err_modify INSERT "'', now(), '%u', '%f', '%h', '%a', '%m'" xfer_errors
Шаг 4. Скрипты для управления
Для управления пользователями в БД я написал 3 простых скрипта на perl: ftpadduser, ftpdeluser и ftppasswd + единый конфиг к ним, чтобы не прописывать в каждом из них одни и те же переменные:
# SQL Configuration
# sql_type can be "mysql" or "postgres" ONLY!
sql_type mysql
sql_host DBHOST
sql_user DBUSER
sql_passwd DBPASSWD
sql_db DBNAME
# FTP Settings
# default FTP directory
ftp_dir /home/ftp
# Пользователь и группа в системе, кому будет принадлежать каталог нового пользователя
ftp_groupname ftpadm
ftp_uid 507
ftp_gid 507
ftp_shell /dev/null
#!/usr/bin/perl -wl
use strict;
use Crypt::PasswdMD5 qw(unix_md5_crypt);
use DBI;
use DBD::mysql;
use Config::Simple;
my $cfg = new Config::Simple('/etc/proftpd_sql.conf');
my %Config = $cfg->vars();
my $dsn;
if ($Config{sql_type} eq "mysql") { $dsn = "dbi:mysql:dbname=$Config{sql_db};host=$Config{sql_host};"; }
elsif ($Config{sql_type} eq "postgres") { $dsn = "dbi:Pg:dbname=$Config{sql_db};host=$Config{sql_host};"; }
else { die("Incorrect \$sql_type in config"); }
if (@ARGV < 2) { print "Usage: $0 ftpuser password."; exit; }
my $ftpuser = $ARGV[0];
my $dbh = DBI->connect($dsn, $Config{sql_user},$Config{sql_passwd}, {AutoCommit => 1});
my $ftp_user_exists = usercheck($ARGV[0]);
if ($ftp_user_exists!=0) { print "$0: user $ARGV[0] already exits."; exit; }
elsif (-e "$Config{ftp_dir}/$ftpuser" and -d "$Config{ftp_dir}/$ftpuser") { print "$0: directory $Config{ftp_dir}/$ftpuser/ exists, check the path."; exit; }
else {
mkdir("$Config{ftp_dir}/$ftpuser");
chown $Config{ftp_uid},$Config{ftp_gid}, "$Config{ftp_dir}/$ftpuser";
chmod 0700, "$Config{ftp_dir}/$ftpuser";
my $ftppass = $ARGV[1];
my $salt = gensalt(8);
my $encrypted = unix_md5_crypt($ftppass, $salt);
my $dbh_sql;
if ($Config{sql_type} eq "mysql") {
$dbh_sql = "INSERT INTO users SET username='$ftpuser', password='$encrypted', salt='$salt', groupname='$Config{ftp_groupname}', uid='$Config{ftp_uid}', gid='$Config{ftp_gid}', homedir='$Config{ftp_dir}/$ftpuser', shell='$Config{ftp_shell}', login_count=0, login_error_count=0";
}
elsif ($Config{sql_type} eq "postgres") {
$dbh_sql = "INSERT INTO users ";
$dbh_sql .= "(id, username, password, salt, groupname, uid, gid, homedir, shell, last_login, login_count, last_error_login, login_error_count) ";
$dbh_sql .= "VALUES (nextval('users_id_seq'::regclass), '$ftpuser', '$encrypted', '$salt', '$Config{ftp_groupname}', '$Config{ftp_uid}', '$Config{ftp_gid}', ";
$dbh_sql .= " '$Config{ftp_dir}/$ftpuser', '$Config{ftp_shell}', NULL, 0, NULL, 0);";
}
$dbh->do($dbh_sql);
print "FTP user $ARGV[0] added.";
}
$dbh->disconnect;
sub usercheck {
my $sth;
my $ftpuser = shift;
my $req = "select id from users where username='$ftpuser'";
$sth = $dbh->prepare($req);
if (!$sth) {
my $tmp=$dbh->errstr;
print "$tmp.\n$req failed.";
}
elsif (!$sth->execute) {
my $tmp=$sth->errstr;
print "$tmp.\n$req failed.";
}
elsif ($sth->rows()!=1) { return 0; }
else {
my $ref = $sth->fetchrow_arrayref;
return $$ref[0];
}
$sth->finish;
}
sub gensalt {
my $count = shift;
my @salt = ( '.', '/', 0 .. 9, 'A' .. 'Z', 'a' .. 'z' );
my $salt;
for (1..$count) {
$salt .= (@salt)[rand @salt];
}
return $salt;
}
#!/usr/bin/perl -w
use strict;
use Crypt::PasswdMD5 qw(unix_md5_crypt);
use DBI;
use DBD::mysql;
use Config::Simple;
my $cfg = new Config::Simple('/etc/proftpd_sql.conf');
my %Config = $cfg->vars();
my $dsn;
if ($Config{sql_type} eq "mysql") { $dsn = "dbi:mysql:dbname=$Config{sql_db};host=$Config{sql_host};"; }
elsif ($Config{sql_type} eq "postgres") { $dsn = "dbi:Pg:dbname=$Config{sql_db};host=$Config{sql_host};"; }
else { die("Incorrect \$sql_type in config"); }
my $rec = 0;
my $cleanlogs = 0;
my $ftpuser = $ARGV[0];
if (@ARGV < 1) { print "Usage: $0 ftpuser\n"; exit; }
my $dbh = DBI->connect($dsn, $Config{sql_user},$Config{sql_passwd}, {AutoCommit => 1});
my $ftp_user_exists = usercheck($ARGV[0]);
if ($ftp_user_exists==0) { print "$0: user $ARGV[0] not found."; exit; }
if (-d "$Config{ftp_dir}/$ftpuser") {
print "Do you want to remove user's home directory recursively? (Yes/No): ";
my $ans1 = ;
if ($ans1 eq "Yes\n" or $ans1 eq "Y\n") { $rec = 1; }
}
print "Do you want to cleanup database user activity logs? (Yes/No): ";
my $ans2 = ;
if ($ans2 eq "Yes\n" or $ans2 eq "Y\n") { $cleanlogs = 1; }
$dbh->do("DELETE FROM users WHERE id=$ftp_user_exists");
if ($cleanlogs == 1) {
$dbh->do("DELETE FROM log_failed_logins WHERE user_name='$ftpuser'");
$dbh->do("DELETE FROM xfer_errors WHERE user_name='$ftpuser'");
$dbh->do("DELETE FROM xfer_table WHERE user_name='$ftpuser'");
}
print "FTP user $ARGV[0] deleted, ";
if ($rec == 1) { system("rm -rf $Config{ftp_dir}/$ftpuser"); print "with homedir.\n"; }
else { print "homedir kept.\n"; }
$dbh->disconnect;
sub usercheck {
my $sth;
my $ftpuser = shift;
my $req = "select id from users where username='$ftpuser'";
$sth = $dbh->prepare($req);
if (!$sth) {
my $tmp=$dbh->errstr;
print "$tmp.\n$req failed.";
}
elsif (!$sth->execute) {
my $tmp=$sth->errstr;
print "$tmp.\n$req failed.";
}
elsif ($sth->rows()!=1) { return 0; }
else {
my $ref = $sth->fetchrow_arrayref;
return $$ref[0];
}
$sth->finish;
}
#!/usr/bin/perl -wl
use strict;
use Crypt::PasswdMD5 qw(unix_md5_crypt);
use DBI;
use DBD::mysql;
use Config::Simple;
my $cfg = new Config::Simple('/etc/proftpd_sql.conf');
my %Config = $cfg->vars();
my $dsn;
if ($Config{sql_type} eq "mysql") { $dsn = "dbi:mysql:dbname=$Config{sql_db};host=$Config{sql_host};"; }
elsif ($Config{sql_type} eq "postgres") { $dsn = "dbi:Pg:dbname=$Config{sql_db};host=$Config{sql_host};"; }
else { die("Incorrect \$sql_type in config"); }
if (@ARGV < 2) { print "Usage: $0 ftpuser password."; exit; }
my $ftppass = $ARGV[1];
my $dbh = DBI->connect($dsn, $Config{sql_user},$Config{sql_passwd}, {AutoCommit => 1});
my $ftp_uid = usercheck($ARGV[0]);
if ($ftp_uid==0) { print "$0: user $ARGV[0] not found."; }
else {
my $salt = gensalt(8);
my $encrypted = unix_md5_crypt($ftppass, $salt);
$dbh->do("UPDATE users SET password='$encrypted',salt='$salt' where id=$ftp_uid");
print "FTP password for user $ARGV[0] changed.";
}
$dbh->disconnect;
sub usercheck {
my $sth;
my $ftpuser = shift;
my $req = "select id from users where username='$ftpuser'";
$sth = $dbh->prepare($req);
if (!$sth) {
my $tmp=$dbh->errstr;
print "$tmp.\n$req failed.";
}
elsif (!$sth->execute) {
my $tmp=$sth->errstr;
print "$tmp.\n$req failed.";
}
elsif ($sth->rows()!=1) { return 0; }
else {
my $ref = $sth->fetchrow_arrayref;
return $$ref[0];
}
$sth->finish;
}
sub gensalt {
my $count = shift;
my @salt = ( '.', '/', 0 .. 9, 'A' .. 'Z', 'a' .. 'z' );
my $salt;
for (1..$count) {
$salt .= (@salt)[rand @salt];
}
return $salt;
}
В результате я получил FTP сервер с хранением паролей пользователей БД в шифрованном виде, управление пользователями и ведение логов в БД.