Proftpd + MySQL + Quotas HOWTO

En una red informática compuesta por más de un equipo es interesante habilitar algún servicio de transferencia de ficheros para compartir información entre ordenadores. Existen varios protocolos usables para dicha tarea (NFS, Samba, HTTP...) pero solamente uno se ha creado específicamente con este fin: FTP (file transfer protocol).

Generalmente cuando tratemos de poner a funcionar un servicio cliente/servidor, debemos hacer mayor hincapié en la parte del servidor y configurarla de forma adecuada al uso que se le pueda dar en un futuro. Al principio es probable que el número de usuarios sea pequeño y no merezca la pena configurar tasas de transferencia o quotas en disco, pero a medida que se ofrece un servicio, las exigencias van cambiando y se hacen cada vez más necesarias. Usaremos Proftpd como servidor con dos particularidades. La primera de ellas es el uso de MySQL para autentificación de usuarios virtuales, esto quiere decir que no habrá cuentas del sistema (con o sin shell) para el acceso ftp, con la consiguiente limpieza de /etc/passwd ;). La segunda particularidad es el uso de cuotas de disco para limitar el espacio que otorgamos a cada usuario.

Antes de empezar decir que la instalación y configuración del servidor se ha realizado en una Debian 3.1, siendo extrapolable a cualquier otra distribución con suma facilidad. Ahora si, comencemos.

El primer paso es la instalación del software necesario, una vez instalado miramos si Proftpd tiene los módulos necesarios para seguir con la configuración. Si tiramos de código fuente hemos de tener en cuenta los parámetros de configure (./configure --with-modules=mod_sql:mod_sql_mysql:mod_quotatab:mod_quotatab_sql --with-includes=/usr/include/mysql):
gprs:~# apt-get install proftpd-mysql mysql-server
...
gprs:~# proftpd -l
Compiled-in modules:
  mod_core.c
  mod_xfer.c
  mod_auth_unix.c
  mod_auth_file.c
  mod_auth.c
  mod_ls.c
  mod_log.c
  mod_site.c
  mod_auth_pam.c
  mod_quotatab.c
  mod_sql.c
  mod_sql_mysql.c
  mod_quotatab_sql.c
  mod_ratio.c
  mod_tls.c
  mod_rewrite.c
  mod_radius.c
  mod_wrap.c
  mod_quotatab_file.c
  mod_delay.c
  mod_readme.c
  mod_ifsession.c
  mod_cap.c
Una vez instalado en el sistema tendremos que crear un grupo/usuario que será el dueño en cuanto a permisos de los archivos que se transfieran a cada directorio. Lo explico mejor, cuando un usuario se conecte al servidor ftp tendrá su login, su contraseña, su directorio raíz... como si fuera un usuario real del sistema; sin embargo no tiene uid, por lo que sus archivos quedarían sin propietario. Pues para eso sirve este usuario que creamos a continuación. Os estareis preguntando, según lo que explicas... ¿no podría un usuario machacar los archivos de otro?. Podría, pero como cada uno está enjaulado en su directorio raíz (DefaultRoot ~/), no es capaz de entrar en el directorio de otro usuario. Espero que se haya entendido:
gprs:~# groupadd -g 5500 ftpgroup
gprs:~# adduser -u 5500 -s /bin/false -d /bin/null -c "proftpd user" -g ftpgroup ftpuser
El siguiente paso es crear la base de datos y las tablas donde guardaremos usuarios virtuales, límites de cuotas, logs... en MySQL. También vamos a crear un usuario MySQL que será el único que tenga acceso a esta estructura:
gprs:~# mysql -uroot -ppassword
grant usage on *.* to 'proftpd'@'localhost' identified by 'password'
create database ftpdb;
grant select, insert, update on ftpdb.* to proftpd@localhost identified by 'password';
use ftpdb;

#
# Table structure for table `ftpgroup`
#
CREATE TABLE `ftpgroup` (
  `groupname` varchar(16) NOT NULL default '',
  `gid` smallint(6) NOT NULL default '5500',
  `members` varchar(16) NOT NULL default '',
  KEY `groupname` (`groupname`)
) TYPE=MyISAM COMMENT='ProFTP group table';
INSERT INTO `ftpgroup` VALUES ('ftpgroup', 5500, 'ftpuser');

#
# Table structure for table `ftpquotalimits`
#
CREATE TABLE `ftpquotalimits` (
  `name` varchar(30) default NULL,
  `quota_type` enum('user','group','class','all') NOT NULL default 'user',
  `per_session` enum('false','true') NOT NULL default 'false',
  `limit_type` enum('soft','hard') NOT NULL default 'soft',
  `bytes_in_avail` float NOT NULL default '0',
  `bytes_out_avail` float NOT NULL default '0',
  `bytes_xfer_avail` float NOT NULL default '0',
  `files_in_avail` int(10) unsigned NOT NULL default '0',
  `files_out_avail` int(10) unsigned NOT NULL default '0',
  `files_xfer_avail` int(10) unsigned NOT NULL default '0'
) TYPE=MyISAM;

#
# Table structure for table `ftpquotatallies`
#
CREATE TABLE `ftpquotatallies` (
  `name` varchar(30) NOT NULL default '',
  `quota_type` enum('user','group','class','all') NOT NULL default 'user',
  `bytes_in_used` float NOT NULL default '0',
  `bytes_out_used` float NOT NULL default '0',
  `bytes_xfer_used` float NOT NULL default '0',
  `files_in_used` int(10) unsigned NOT NULL default '0',
  `files_out_used` int(10) unsigned NOT NULL default '0',
  `files_xfer_used` int(10) unsigned NOT NULL default '0'
) TYPE=MyISAM;
#
# Table structure for table `ftpuser`
#
CREATE TABLE `ftpuser` (
  `id` int(10) unsigned NOT NULL auto_increment,
  `userid` varchar(32) NOT NULL default '',
  `passwd` varchar(32) NOT NULL default '',
  `uid` smallint(6) NOT NULL default '5500',
  `gid` smallint(6) NOT NULL default '5500',
  `homedir` varchar(255) NOT NULL default '',
  `shell` varchar(16) NOT NULL default '/sbin/nologin',
  `count` int(11) NOT NULL default '0',
  `accessed` datetime NOT NULL default '0000-00-00 00:00:00',
  `modified` datetime NOT NULL default '0000-00-00 00:00:00',
  PRIMARY KEY  (`id`)
) TYPE=MyISAM COMMENT='ProFTP user table'
INSERT INTO `ftpuser` VALUES (1, 'testaccount', 'ftppasswd', 5500, 5500, '/home/testdomain.com', '/sbin/nologin',0,'','');
Algunas aclaraciones al respecto. La tabla ftpgroup lista los usuarios de cada grupo, como solo habrá un grupo (inicialmente) no necesita más registros para comenzar a usar el servicio. La tabla ftuser guarda los usuarios, estadísticas y es la usada por Proftpd para comparar los datos de autentificación. Usando la directiva 'asdfasf' haremos que Proftpd cree el directorio raíz del usuario si este no existe. La tabla ftpquotalimits define los límites de cuota en disco y ftpquotatalities va guardando los datos de cada usuario para totalizar y comprobar que el límite no sobrepasa lo acordado en ftpquotalimits. Si el valor de algún campo de límite es '0' automáticamente se definirá como ilimitado.

Hemos creado un usuario testaccount para probar el servicio. Agreguemos ahora una cuota para ese usuario de 15Mb:
INSERT INTO ftpquotalimits VALUES("testaccount","user","true","hard","15728640","0","0","0","0","0");
Antes de probar que todo funciona debemos decirle a Proftpd que debe tener en cuenta todos estos datos, el archivo principal de configuración se encuentra (en Debian) en /etc/proftpd.conf y quedará de la siguiente forma:
# Configuraciones generales
ServerName                      "Nuestro MegaFTP Server"
ServerType                      Standalone
ServerAdmin                     root@localhost

# Ocultamos todo lo posible a usuarios externos
ServerIdent                     on "Bienvenido a MegaFTP Server..."
DeferWelcome                    on
DefaultServer                   on

# Permitimos resumes, configuramos puerto y demás
AllowStoreRestart               on
Port                            21
Umask                           022
MaxInstances                    30
User                            nobody
Group                           nogroup

# Enjaulamos a nuestros usuarios (chroot)
DefaultRoot ~
AllowOverwrite                on

# Para MySQL
SQLAuthTypes    Plaintext
SQLConnectInfo  ftpdb@localhost proftpd password
SQLUserInfo     ftpuser userid passwd uid gid homedir shell
SQLGroupInfo    ftpgroup groupname gid members
SQLMinID        500
SQLHomedirOnDemand on
SQLLog PASS updatecount
SQLNamedQuery updatecount UPDATE "count=count+1, accessed=now() WHERE userid='%u'" ftpuser
SQLLog  STOR,DELE modified
SQLNamedQuery modified UPDATE "modified=now() WHERE userid='%u'" ftpuser

# Para las cuotas en disco
QuotaEngine on
QuotaDirectoryTally on
QuotaDisplayUnits Mb
QuotaShowQuotas on
SQLNamedQuery get-quota-limit SELECT "name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail FROM ftpquotalimits WHERE name = '%{0}' AND quota_type = '%{1}'"

SQLNamedQuery get-quota-tally SELECT "name, quota_type, bytes_in_used, bytes_out_used, bytes_xfer_used, files_in_used, files_out_used, files_xfer_used FROM ftpquotatallies WHERE name = '%{0}' AND quota_type = '%{1}'"

SQLNamedQuery update-quota-tally UPDATE "bytes_in_used = bytes_in_used + %{0}, bytes_out_used = bytes_out_used + %{1}, bytes_xfer_used = bytes_xfer_used + %{2}, files_in_used = files_in_used + %{3}, files_out_used = files_out_used + %{4}, files_xfer_used = files_xfer_used + %{5} WHERE name = '%{6}' AND quota_type = '%{7}'" ftpquotatallies

SQLNamedQuery insert-quota-tally INSERT "%{0}, %{1}, %{2}, %{3}, %{4}, %{5}, %{6}, %{7}" ftpquotatallies

QuotaLimitTable sql:/get-quota-limit
QuotaTallyTable sql:/get-quota-tally/update-quota-tally/insert-quota-tally

# No permitimos login a root y no hace falta tener shell
RootLogin off
RequireValidShell off
Arrancamos Proftpd, o lo reiniciamos en caso de tenerlo ya funcionando y ya estamos listos para probar la nueva configuración. Debemos tener a la vista siempre los logs por si hubiera algún error sintáctico a la hora de escribir la configuración (a veces pasa):
gprs:~# /etc/init.d/proftpd restart
Restarting ProFTPD ftp daemon.proftpd.
..proftpd.
 done.
gprs:~# tail -f /var/log/syslog
Feb 16 13:45:25 gprs proftpd[1184]: gprs - ProFTPD killed (signal 15) 
Feb 16 13:45:25 gprs proftpd[1184]: gprs - ProFTPD 1.2.10 standalone mode SHUTDOWN
Feb 16 13:45:27 gprs proftpd[2678]: gprs - ProFTPD 1.2.10 (stable) (built do mrt 22 18:28:32 CET 2001) standalone mode STARTUP 
gprs:~# 
//Entramos con algún cliente de ftp a nuestro servidor
gprs:~# tail -f /var/log/mysql/mysql.log
050216 14:19:02      39 Connect     proftpd@localhost on ftpdb
                     39 Query       SELECT userid, passwd, uid, gid, homedir, shell FROM ftpuser WHERE (userid='testaccount') LIMIT 1
                     39 Query       SELECT groupname FROM ftpgroup WHERE (gid = 5500) LIMIT 1
                     39 Query       SELECT groupname, gid, members FROM ftpgroup WHERE (groupname = 'ftpgroup')
                     39 Query       SELECT groupname, gid, members FROM ftpgroup WHERE (members = 'testaccount' OR members LIKE 'testaccount,%' OR members LIKE '%,testaccount' OR members LIKE '%,testaccount,%')
gprs:~# 
Funciona, vemos como conecta con MySQL mirando los campos que le hemos indicado en la configuración. Si probamos a subir un archivo de más de 15Mb veremos como justo después de hacer la transferencia lo borra porque excede la cuota.

Ya tenemos todo listo para dar cuentas a nuestros amigos y dejar que usen el servicio sin preocuparnos demasiado por el espacio en disco. Eso sí, recomendaría redactar una política de uso en el motd y mirar de vez en cuando las estadísticas de cada usuario.

About the author

Óscar
has doubledaddy super powers, father of Hugo and Nico, husband of Marta, *nix user, Djangonaut and open source passionate.