Il y a quelques années, pour l'un de mes employeurs (un gros CDN français pour ne pas le citer), une problématique s'est posée suite à une utilisation abusive des plateformes FTP. En l'occurrence, il y avait plusieurs serveurs FTP :
- sous Windows :IIS, Filezilla Server et CoreFTP (SFTP)
- sous Linux, plusieurs proftpd différents
Suite à l'utilisation abusive de la plateforme (historique) et l'impossibilité de remonter les traces, il a été demandé de sécuriser l'ensemble. J'en ai profité pour faire pas mal de modifications :
- avoir un seul point d'entrée (un même pool de VIP) peu importe le service
- avoir une authentification centralisée que ca soit en FTP, FTP avec SSL ou SFTP (à l'époque avec un Active Directory)
- avoir une seule et même arborescence pour tous les NAS utilisés, peu importe l'origine et réduire le nombre de comptes clients
- enregistrer toute l'activité des serveurs (accès, commandes exécutées)
- bannir les comportements anormaux
J'avais nommé ce genre de plate-forme FTPx. Je cherchais un nouveau sujet de billet. Je me suis dis que j'allais dépoussiérer cette plateforme (la refaire pour le billet donc) en lui rajoutant une petite couche complémentaire (portsentry, honeypot, ...).
Avant propos sur la plateforme FTP
L'architecture va se baser exclusivement sur du gratuit. Du coup, je remplace l'Active Directory par un plus basique MySQL. La plateforme montée rapidement se base sur les machines suivantes :
- fw : firewalling et loadbalancing
- ftpx01 & ftpx02 : serveurs SFTP + FTP(S)
- mysql : serveur MySQL
Bien sûr, on pourrait redondé la partie fw ou mysql mais ce n'est pas le sujet de ce billet (je pourrais toujours rédiger un billet dédié s'il y a de la demande pour). Au niveau accès, de l'extérieur, un utilisateur qui fait du SFTP ne pourra pas faire de SSH. De plus, il sera restreint à son arborescence et ne pourra remonté ailleurs.
Au niveau NAS, je ne ferais que la partie cliente, de manière rapide pour montrer la logique. Donc un serveur NFS et un serveur CIFS sont supposés exister.
On suppose à chaque fois partir d'une Debian légère et plutôt vierge. On ignore l'installation du serveur MySQL qui n'a rien de bien particulier.
Au niveau réseau, on notera les réseaux ainsi :
- x.x.x.y : IP publique se finissant en y (on suppose une /24 de manière abusive)
- z.z.a.y : IP pour le routage vers fw (on travaille sur une /24 encore)
- z.z.b.y : IP de service entre les serveurs FTP et MySQL
- z.z.c.y : IP de service entre les serveurs FTP et les NAS
Du coup, on se retrouve avec le plan d'adressage suivant :
- fw
- public : x.x.x.1/24
- honeypot : x.x.x.2/24
- vip : x.x.x.3/24
- vers ftpx01 & ftpx02 : z.z.a.1/24
- ftpx01 & ftpx02
- vers fw : z.z.a.11/24 et z.z.a.12/24
- vers mysql : z.z.b.11/24 et z.z.b.12/24
- vers les nas : z.z.c.11/24 et z.z.c.12/24
- mysql
- vers ftpx01 & ftpx02 : z.z.b.21/24
- nfs & cifs
- vers ftpx01 & ftpx02 : z.z.c.31/24 & z.z.c.32/24
Création de fw
On commence par installer les premiers packages.
apt-get -y install bind9 libnetfilter-conntrack3 ldirectord apticron ntp iptables module-assistant xtables-addons-common honeyd fail2ban portsentry
On implémente le module complémentaire d'iptables.
module-assistant --verbose --text-mode auto-install xtables-addons
Serveur DNS récursif
On s'installe un serveur DNS récursif en local pour diverses raisons (dont des histoires de performance).
cat << EOF > /etc/bind/named.conf.options options { directory "/var/cache/bind"; query-source address * port *; forwarders { 208.67.222.222; 208.67.220.220; }; auth-nxdomain no; # conform to RFC1035 listen-on-v6 { none; }; listen-on { 127.0.0.1; }; allow-transfer { none; }; allow-query { any; }; allow-recursion { any; }; version none; }; EOF /etc/init.d/bind9 restart echo "nameserver 127.0.0.1" > /etc/resolv.conf
Optimisations système
On s'applique à faire quelques optimisations qui seront bien pratique pour la suite.
cat << EOF > /etc/security/limits.conf * - nofile 65536 EOF cat << EOF >> /etc/profile ulimit -n 65536 EOF cat << EOF > /etc/sysctl.conf net.ipv4.conf.default.rp_filter = 1 net.ipv4.conf.default.arp_filter = 1 net.ipv4.conf.all.rp_filter = 1 net.ipv4.conf.all.arp_filter = 1 net.core.rmem_default = 4194304 net.core.rmem_max = 4194304 net.core.wmem_default = 4194304 net.core.wmem_max = 4194304 net.ipv4.tcp_rmem = 4096 87380 4194304 net.ipv4.tcp_wmem = 4096 65536 4194304 net.ipv4.tcp_mem = 4096 65536 4194304 net.ipv4.tcp_low_latency = 0 net.core.netdev_max_backlog = 30000 fs.file-max = 65536 kernel.shmmax = 8000000000 kernel.shmall = 8000000000 net.ipv4.tcp_abort_on_overflow = 1 net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_fin_timeout = 1 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_tw_recycle = 1 net.ipv4.ip_local_port_range = 1024 65535 vm.min_free_kbytes = 65536 net.ipv4.conf.all.arp_ignore = 1 net.ipv4.conf.lo.arp_ignore = 1 net.ipv4.conf.eth0.arp_ignore = 1 net.ipv4.conf.eth1.arp_ignore = 1 net.ipv4.conf.all.arp_announce = 2 net.ipv4.conf.lo.arp_announce = 2 net.ipv4.conf.eth0.arp_announce = 2 net.ipv4.conf.eth1.arp_announce = 2 net.ipv4.tcp_orphan_retries = 0 net.ipv4.tcp_timestamps = 0 net.ipv4.tcp_sack = 1 net.ipv4.tcp_window_scaling = 1 net.ipv4.tcp_keepalive_intvl = 1 net.ipv4.tcp_keepalive_probes = 1 net.ipv4.ip_forward = 1 net.ipv4.conf.default.proxy_arp = 1 net.ipv4.conf.all.proxy_arp = 1 kernel.sysrq = 1 net.ipv4.conf.default.send_redirects = 1 net.ipv4.conf.all.send_redirects = 1 kernel.core_uses_pid=1 kernel.core_pattern=1 vm.dirty_background_ratio = 20 vm.dirty_ratio = 40 vm.swappiness = 1 vm.dirty_writeback_centisecs = 1500 net.ipv4.tcp_max_syn_backlog = 65536 net.core.optmem_max = 40960 net.ipv4.tcp_max_tw_buckets = 360000 net.ipv4.tcp_reordering = 5 net.ipv4.icmp_ignore_bogus_error_responses = 1 net.ipv4.tcp_no_metrics_save = 1 net.ipv4.tcp_max_orphans = 262144 net.ipv4.tcp_rfc1337 = 0 net.core.somaxconn=65536 net.ipv4.tcp_moderate_rcvbuf=1 net.ipv4.tcp_ecn=0 net.ipv4.ip_no_pmtu_disc=0 net.ipv4.tcp_slow_start_after_idle=0 net.netfilter.nf_conntrack_acct=1 net.ipv4.icmp_echo_ignore_broadcasts=1 EOF sysctl -p
Fail2ban & honeypot
L'idée est d'utiliser le honeypot pour bannir les mauvaises personnes. Pour se faire, vu qu'on doit appliquer les règles au niveau de fw, aussi bien pour du INPUT que du FORWARD et sur plusieurs IP, on va le faire via fail2ban.
cat << EOF > /etc/rc.local farpd x.x.x.2 -i eth0 /usr/local/bin/rules.sh start exit 0 EOF
farpd x.x.x.2 -i eth0 wget http://www.alunos.di.uminho.pt/~a43175/code/perl/customPie.pm -O /etc/honeypot/customPie.pm wget http://www.alunos.di.uminho.pt/~a43175/code/perl/buildPie.pl -O /etc/honeypot/buildPie.pl cat << EOF > /etc/default/honeyd RUN="yes" INTERFACE="eth0" NETWORK=x.x.x.2 OPTIONS="--disable-webserver" EOF
cat << EOF > /etc/honeypot/honeyd.conf create win2k set win2k personality "Microsoft Windows 2000 SP2" set win2k default tcp action block set win2k default udp action block set win2k default icmp action block set win2k uptime 3567 set win2k droprate in 13 add win2k tcp port 23 "sh /usr/share/honeyd/scripts/unix/linux/suse8.0/telnetd.sh $ipsrc $sport $ipdst $dport" add win2k tcp port 21 "sh /usr/share/honeyd/scripts/win32/win2k/msftp.sh $ipsrc $sport $ipdst $dport" add win2k tcp port 25 "sh /usr/share/honeyd/scripts/win32/win2k/exchange-smtp.sh $ipsrc $sport $ipdst $dport" #add win2k tcp port 80 "sh /usr/share/honeyd/scripts/win32/win2k/iis.sh $ipsrc $sport $ipdst $dport" add win2k tcp port 110 "sh /usr/share/honeyd/scripts/win32/win2k/exchange-pop3.sh $ipsrc $sport $ipdst $dport" add win2k tcp port 143 "sh /usr/share/honeyd/scripts/win32/win2k/exchange-imap.sh $ipsrc $sport $ipdst $dport" add win2k tcp port 389 "sh /usr/share/honeyd/scripts/win32/win2k/ldap.sh $ipsrc $sport $ipdst $dport" add win2k tcp port 5901 "sh /usr/share/honeyd/scripts/win32/win2k/vnc.sh $ipsrc $sport $ipdst $dport" add win2k udp port 161 "perl /usr/share/honeyd/scripts/unix/general/snmp/fake-snmp.pl public private --config=/usr/share/honeyd/scripts/unix/general/snmp" # This will redirect incomming windows-filesharing back to the source add win2k udp port 137 proxy $ipsrc:137 add win2k udp port 138 proxy $ipsrc:138 add win2k udp port 445 proxy $ipsrc:445 add win2k tcp port 137 proxy $ipsrc:137 add win2k tcp port 138 proxy $ipsrc:138 add win2k tcp port 139 proxy $ipsrc:139 add win2k tcp port 445 proxy $ipsrc:445 bind x.x.x.2 win2k EOF
/etc/init.d/honeyd restart
cat << EOF > /etc/fail2ban/filter.d/honeyd.conf [Definition] failregex = .* S <HOST> .*$ ignoreregex = EOF
cat << EOF > /etc/fail2ban/action.d/banhost.conf [Definition] actionstart = actionstop = actioncheck = actionban = /usr/local/bin/banip.sh <ip> actionunban = /usr/local/bin/unbanip.sh <ip> EOF
cat << EOF > /etc/fail2ban/jail.conf [DEFAULT] ignoreip = 127.0.0.1 x.x.x.1 bantime = 86400 maxretry = 3 backend = polling destemail = root@localhost banaction = iptables-multiport mta = sendmail protocol = tcp action_ = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s] action_mw = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s] %(mta)s-whois[name=%(__name__)s, dest="%(destemail)s", protocol="%(protocol)s] action_mwl = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s] %(mta)s-whois-lines[name=%(__name__)s, dest="%(destemail)s", logpath=%(logpath)s] action = %(action_)s [ssh] enabled = true port = ssh filter = sshd logpath = /var/log/auth.log maxretry = 6 [honeyd] enabled = trueinitctl list filter = honeyd port = all logpath = /var/log/honeypot/honeyd.log maxretry = 1 banaction = banhost EOF
/etc/init.d/fail2ban restart
Toute personne tentant d'ouvrir un port sur l'honeypot sera automatiquement banni de la plateforme à coup de TARPIT pour la partie TCP et de DROP pour tout le reste.
Portsentry
Comment lutter contre un scan de port ? Grâce à portsentry bien sûr !
cat << EOF > /etc/default/portsentry TCP_MODE="atcp" UDP_MODE="audp" EOF
cat << EOF > /etc/portsentry/portsentry.conf TCP_PORTS="1,7,9,11,15,20,21,2370,79,109,110,111,119,138,139,143,512,513,514,515,540,635,1080,1524,2000,2001,4000,4001,5742,6000,6001,6667,12345,12346,20034,27665,30303,32771,32772,32773,32774,31337,40421,40425,49724,54320" UDP_PORTS="1,7,9,66,67,68,69,111,137,138,161,162,474,513,517,518,635,640,641,666,700,2049,31335,27444,34555,32770,32771,32772,32773,32774,31337,54321" ADVANCED_PORTS_TCP="65536" ADVANCED_PORTS_UDP="65536" ADVANCED_EXCLUDE_TCP="80" ADVANCED_EXCLUDE_UDP="" IGNORE_FILE="/etc/portsentry/portsentry.ignore" HISTORY_FILE="/var/lib/portsentry/portsentry.history" BLOCKED_FILE="/var/lib/portsentry/portsentry.blocked" RESOLVE_HOST = "0" BLOCK_UDP="2" BLOCK_TCP="2" KILL_ROUTE="/sbin/route add -host $TARGET$ reject" KILL_HOSTS_DENY="ALL: $TARGET$ : DENY" KILL_RUN_CMD_FIRST = "0" KILL_RUN_CMD="/usr/local/bin/banip.sh $TARGET$" SCAN_TRIGGER="0" EOF
cat << EOF > /etc/portsentry/portsentry.ignore.static 208.67.222.222 208.67.220.220 x.x.x.254 EOF
Encore une fois, on va utiliser un script qui va nous bannir correctement l'IP sur l'ensemble de la plateforme.
Toute personne tentant un scan de port sera automatiquement banni de la plateforme à coup de TARPIT pour la partie TCP et de DROP pour tout le reste.
Scripting
On va utiliser quelques scripts selon la tâche à exécuter :
- rules.sh va définir les règles par défaut
- banip.sh va bannir une IP donnée
- unbanip.sh va débannir une IP donnée
cat << EOF > /usr/local/bin/rules.sh #!/bin/bash
start() { echo "Routing" iptables -t nat -A POSTROUTING -o eth2 -j MASQUERADE iptables -t nat -A PREROUTING -d x.x.x.3 -m tcp -p tcp --dport 21 -j DNAT --to-destination z.z.a.11 iptables -t nat -A PREROUTING -d x.x.x.3 -m tcp -p tcp --dport 22 -j DNAT --to-destination z.z.a.11 iptables -t mangle -A PREROUTING -i eth0 -p tcp -s 0.0.0.0/0 -d x.x.x.3/32 --dport ftp -j MARK --set-mark 1 iptables -t mangle -A PREROUTING -i eth0 -p tcp -s 0.0.0.0/0 -d x.x.x.3/32 --dport 55000: -j MARK --set-mark 1 for chain in INPUT FORWARD; do echo "Block DOS - $chain - Ping of Death" iptables -A $chain -p ICMP --icmp-type echo-request -m length --length 60:65535 -j ACCEPT; echo "Block DOS - $chain - Teardrop" iptables -A $chain -p UDP -f -j DROP; echo "Block DDOS - $chain - SYN-flood" iptables -A $chain -p TCP ! --syn -m state --state NEW -j TARPIT; iptables -A $chain -p TCP ! --syn -m state --state NEW -j DROP; echo "Block DDOS - $chain - Smurf" iptables -A $chain -m pkttype --pkt-type broadcast -j DROP; iptables -A $chain -p ICMP --icmp-type echo-request -m pkttype --pkt-type broadcast -j DROP; iptables -A $chain -p ICMP --icmp-type echo-request -m limit --limit 3/s -j ACCEPT; echo "Block DDOS - $chain - UDP-flood (Pepsi)" iptables -A $chain -p UDP --dport 7 -j DROP; iptables -A $chain -p UDP --dport 19 -j DROP; echo "Block DDOS - $chain - SMBnuke" iptables -A $chain -p UDP --dport 135:139 -j DROP; iptables -A $chain -p TCP --dport 135:139 -j TARPIT; iptables -A $chain -p TCP --dport 135:139 -j DROP; echo "Block DDOS - $chain - Connection-flood" iptables -A $chain -p TCP --syn -m connlimit --connlimit-above 3 -j TARPIT; iptables -A $chain -p TCP --syn -m connlimit --connlimit-above 3 -j DROP; echo "Block DDOS - $chain - Fraggle" iptables -A $chain -p UDP -m pkttype --pkt-type broadcast -j DROP; iptables -A $chain -p UDP -m limit --limit 3/s -j ACCEPT; echo "Block DDOS - $chain - Jolt" iptables -A $chain -p ICMP -f -j DROP; done /etc/init.d/portsentry start } stop() { /etc/init.d/portsentry stop iptables -F iptables -X iptables -F -t nat iptables -X -t nat iptables -F -t mangle iptables -X -t mangle } case "$1" in start) start ;; stop) stop ;; restart|reload) stop start ;; *) echo "$0 <start|stop|restart|reload>" exit 1 ;; esac exit 0 EOF
chmod +x /usr/local/bin/rules.sh
cat << EOF > /usr/local/bin/banip.sh #!/bin/bash if [ $# -ne 1 ]; then echo "usage: $0 IP"; exit 1; fi
/sbin/iptables -I INPUT -s $1 -m tcp -p tcp -j TARPIT /sbin/iptables -I INPUT -s $1 -j DROP /sbin/iptables -I FORWARD -s $1 -m tcp -p tcp -j TARPIT /sbin/iptables -I FORWARD -s $1 -j DROP /sbin/iptables -I INPUT -s $1 -m limit --limit 1/minute --limit-burst 3 -j LOG --log-level debug --log-prefix 'Portsentry: tarpiting: ' exit 0 EOF
chmod +x /usr/local/bin/banip.sh
cat << EOF > /usr/local/bin/unbanip.sh #!/bin/bash if [ $# -ne 1 ]; then echo "usage: $0 IP"; exit 1; fi for chain in INPUT FORWARD; do for id in `iptables -L $chain -n --line-numbers | grep $1 | awk '{ print $1 }'`; do iptables -D $chain $id; done done exit 0 EOF
chmod +x /usr/local/bin/unbanip.sh
Load balancing
On enchaine avec le load balancing. On déclare l'IP de la VIP en alias.
cat << EOF >> /etc/network/interfaces auto eth0:0 iface eth0:0 inet static address x.x.x.3 netmask 255.255.255.255 EOF
Puis on configure le load balancing en lui-même. Attention, le serveur FTP va fonctionner en mode passif, donc on prévoir l'ouverture des ports dynamiques et non prévisibles au niveau du load balancing.
echo CONFIG_FILE=/etc/ldirectord.cf >> /etc/default/ldirectord
cat << EOF > /etc/ldirectord.cf checktimeout=1 negotiatetimeout=1 checkinterval=5 autoreload=yes logfile="l0" quiescent=yes virtual=1 real=z.z.a.11 gate real=z.z.a.12:21 gate service=ftp scheduler=lc protocol=fwm persistent=5 checktype=negotiate virtual=x.x.x.3:22 real=z.z.a.11:22 gate real=z.z.a.12:22 gate service=ftp scheduler=lc protocol=tcp persistent=5 checktype=negotiate EOF
/etc/init.d/ldirectord restart
Création d'un serveur ftpx
On commence par installer les premiers packages.
apt-get -y install bind9 iptables fail2ban libpam-mysql mysql-client libnss-mysql-bg nscd pure-ftpd
SERVEUR DNS RÉCURSIF
On s'installe un serveur DNS récursif en local pour diverses raisons (dont des histoires de performance).
cat << EOF > /etc/bind/named.conf.options options { directory "/var/cache/bind"; query-source address * port *; forwarders { 208.67.222.222; 208.67.220.220; }; auth-nxdomain no; # conform to RFC1035 listen-on-v6 { none; }; listen-on { 127.0.0.1; }; allow-transfer { none; }; allow-query { any; }; allow-recursion { any; }; version none; }; EOF /etc/init.d/bind9 restart echo "nameserver 127.0.0.1" > /etc/resolv.conf
OPTIMISATIONS SYSTÈME
On s'applique à faire quelques optimisations qui seront bien pratique pour la suite.
cat << EOF > /etc/security/limits.conf * - nofile 65536 EOF cat << EOF >> /etc/profile ulimit -n 65536 EOF cat << EOF > /etc/sysctl.conf net.ipv4.conf.default.rp_filter = 1 net.ipv4.conf.default.arp_filter = 1 net.ipv4.conf.all.rp_filter = 1 net.ipv4.conf.all.arp_filter = 1 net.core.rmem_default = 4194304 net.core.rmem_max = 4194304 net.core.wmem_default = 4194304 net.core.wmem_max = 4194304 net.ipv4.tcp_rmem = 4096 87380 4194304 net.ipv4.tcp_wmem = 4096 65536 4194304 net.ipv4.tcp_mem = 4096 65536 4194304 net.ipv4.tcp_low_latency = 0 net.core.netdev_max_backlog = 30000 fs.file-max = 65536 kernel.shmmax = 8000000000 kernel.shmall = 8000000000 net.ipv4.tcp_abort_on_overflow = 1 net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_fin_timeout = 1 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_tw_recycle = 1 net.ipv4.ip_local_port_range = 1024 65535 vm.min_free_kbytes = 65536 net.ipv4.conf.all.arp_ignore = 1 net.ipv4.conf.lo.arp_ignore = 1 net.ipv4.conf.eth0.arp_ignore = 1 net.ipv4.conf.eth1.arp_ignore = 1 net.ipv4.conf.all.arp_announce = 2 net.ipv4.conf.lo.arp_announce = 2 net.ipv4.conf.eth0.arp_announce = 2 net.ipv4.conf.eth1.arp_announce = 2 net.ipv4.tcp_orphan_retries = 0 net.ipv4.tcp_timestamps = 0 net.ipv4.tcp_sack = 1 net.ipv4.tcp_window_scaling = 1 net.ipv4.tcp_keepalive_intvl = 1 net.ipv4.tcp_keepalive_probes = 1 net.ipv4.ip_forward = 1 net.ipv4.conf.default.proxy_arp = 1 net.ipv4.conf.all.proxy_arp = 1 kernel.sysrq = 1 net.ipv4.conf.default.send_redirects = 1 net.ipv4.conf.all.send_redirects = 1 kernel.core_uses_pid=1 kernel.core_pattern=1 vm.dirty_background_ratio = 20 vm.dirty_ratio = 40 vm.swappiness = 1 vm.dirty_writeback_centisecs = 1500 net.ipv4.tcp_max_syn_backlog = 65536 net.core.optmem_max = 40960 net.ipv4.tcp_max_tw_buckets = 360000 net.ipv4.tcp_reordering = 5 net.ipv4.icmp_ignore_bogus_error_responses = 1 net.ipv4.tcp_no_metrics_save = 1 net.ipv4.tcp_max_orphans = 262144 net.ipv4.tcp_rfc1337 = 0 net.core.somaxconn=65536 net.ipv4.tcp_moderate_rcvbuf=1 net.ipv4.tcp_ecn=0 net.ipv4.ip_no_pmtu_disc=0 net.ipv4.tcp_slow_start_after_idle=0 net.netfilter.nf_conntrack_acct=1 net.ipv4.icmp_echo_ignore_broadcasts=1 EOF sysctl -p
Rajout d'un point d'entrée pour syslog-ng
Pensez simplement à rajouter la ligne suivante dans less sources de syslog-ng (/etc/syslog-ng/syslog-ng.conf) pour logguer tout ce que font vos utilisateurs).
unix-stream("/chroot/log" max-connections(2048));
Configuration de PAM & NSS pour utiliser MySQL
On suppose le serveur MySQL pré-installé. On y configure un compte pour PAM et la table d'utilisateurs qui va bien.
cat << EOF | mysql -u root -h z.z.b.21 -p CREATE DATABASE pam; GRANT SELECT ON pam.* TO 'pam'@'%' IDENTIFIED BY 'pampass'; GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP ON pam.* TO 'pamadm'@'%' IDENTIFIED BY 'pamadmpass'; FLUSH PRIVILEGES; USE pam; CREATE TABLE `accounts` ( `id` INT NOT NULL auto_increment primary key, `username` VARCHAR( 30 ) NOT NULL , `login` VARCHAR( 30 ) NOT NULL , `pass` VARCHAR( 50 ) NOT NULL , UNIQUE (`username`) ) ENGINE = MYISAM ; quit; EOF
On poursuit avec la modification de nsswitch.conf. Pour cela on remplace les lignes passwd et shadow (mais pas group) :
passwd: compat files mysql shadow: compat files mysql
Puis on prépare l'accès MySQL de nsswitch.
cat << EOF > /etc/libnss-mysql.cfg getpwnam SELECT login,'x',id+'2000','2000',username,CONCAT('/opt/ftpx/',login,login),'/bin/false' \ FROM accounts \ WHERE login='%1$s' \ LIMIT 1 getpwuid SELECT login,'x',id+'2000','2000',username,CONCAT('/opt/ftpx/',login,login),'/bin/false' \ FROM accounts \ WHERE id='%1$u'-2000 \ LIMIT 1 getspnam SELECT login,pass,'','','','','','','' \ FROM accounts \ WHERE login='%1$s' \ LIMIT 1 getpwent SELECT login,'x',id+'2000','2000',username,CONCAT('/opt/ftpx/',login,login),'/bin/false' \ FROM accounts getspent SELECT login,pass,'','','','','','','' \ FROM accounts getgrnam SELECT name,password,gid \ FROM groups \ WHERE name='%1$s' \ LIMIT 1 getgrgid SELECT name,password,gid \ FROM groups \ WHERE gid='%1$u' \ LIMIT 1 getgrent SELECT name,password,gid \ FROM groups memsbygid SELECT username \ FROM grouplist \ WHERE gid='%1$u' gidsbymem SELECT gid \ FROM grouplist \ WHERE username='%1$s' host z.z.b.21 database pam username pam password pampass port 3306 EOF
cat << EOF > /etc/libnss-mysql-root.cfg host z.z.b.21 database accounts username pamadm password pamadmpass port 3306 EOF
On ne définit en soit que les utilisateurs à qui on n'impose un groupe bien précis 'sftponly'.
groupadd -g 2000 sftponly
Reste à finir la configuration de PAM.
cat << EOF > /etc/pam_mysql.conf users.host=z.z.b.21 users.db_user=pamadm users.db_passwd=pamadmpass users.database=pam users.table=accounts users.user_column=login users.password_column=pass users.password_crypt=2 verbose=1 EOF
echo auth required pam_mysql.so config_file=/etc/pam_mysql.conf >> /etc/pam.d/common-auth echo account required pam_mysql.so config_file=/etc/pam_mysql.conf >> /etc/pam.d/common-account echo session required pam_mysql.so config_file=/etc/pam_mysql.conf >> /etc/pam.d/common-session echo password required pam_mysql.so config_file=/etc/pam_mysql.conf >> /etc/pam.d/common-password
Restrictions pour SSH
Maintenant que l'on peut s'authentifier avec des données en base, il faut restreindre la partie SSH. Le chroot de base ne me convient pas. En effet, il nécessite que le groupe d'utilisateurs ait accès en lecture au dossier parent. Or, je souhaite leur restreindre un maximum la visibilité sur leurs petits voisins. Pour cela, il faut patcher openssh en conséquence.
apt-get source openssh-server apt-get build-dep openssh-server cd openssh-5.9p1 cat << EOF > patch.diff --- session.c.orig 2012-09-26 15:34:02.119243513 +0200 +++ session.c 2012-09-26 15:34:24.951244387 +0200 @@ -1457,7 +1457,7 @@ if (stat(component, &st) != 0) fatal("%s: stat(\"%s\"): %s", __func__, component, strerror(errno)); - if (st.st_uid != 0 || (st.st_mode & 022) != 0) + if (st.st_uid != 0 || (st.st_mode & 077) != 0) fatal("bad ownership or modes for chroot " "directory %s\"%s\"", cp == NULL ? "" : "component ", component); EOF patch < patch.diff dpkg-buildpackage -rfakeroot -sa -b cd .. dpkg -i openssh-server*deb rm -fR openssh*
On peut maintenant finaliser la configuration d'openssh.
sed "s^Subsystem sftp /usr/lib/openssh/sftp-server^^" /etc/ssh/sshd_config mkdir /opt/sftpd chmod 700 /opt/sftpd cat << EOF >> /etc/ssh/sshd_config Subsystem sftp internal-sftp -f AUTH -l VERBOSE Match Group sftponly ChrootDirectory /opt/sftp/%u/%u ForceCommand internal-sftp AllowTcpForwarding no GatewayPorts no X11Forwarding no EOF /etc/init.d/ssh restart
Mise en place de la partie FTP
On va utiliser pure-ftpd. Pourquoi ? Pour une raison parfaitement objective indépendante de toute pollution extérieure : juste "j'aime bien". Au delà de ce point, il reste un très bon produit.
cat << EOF > /etc/default/pure-ftpd-common STANDALONE_OR_INETD=standalone VIRTUALCHROOT=true UPLOADUID= UPLOADGID= EOF
echo yes > /etc/pure-ftpd/auth/65unix echo yes > /etc/pure-ftpd/auth/70pam
echo 1 > AllowUserFXP echo stats:/var/log/pure-ftpd/transfer.log > AltLog echo 21 > Bind echo yes > ChrootEveryone echo 1 > CustomerProof echo 1 > DisplayDotFiles echo 1 > DontResolve echo UTF-8 > FSCharset echo 100 > MaxClientsNumber echo 10 > MaxClientsPerIP echo 99 > MaxDiskUsage echo 5 > MaxIdleTime echo 2000 > MinUID echo yes > NoAnonymous echo 1 > NoTruncate echo yes > PAMAuthentication echo 55000 56000 > PassivePortRange echo /etc/pure-ftpd/pureftpd.pdb > PureDB echo ftp > SyslogFacility echo 1 > TLS echo 117 007 > Umask echo yes > UnixAuthentication echo 1 > VerboseLog
mkdir -p /etc/ssl/private openssl req -x509 -nodes -newkey rsa:2048 -keyout /etc/ssl/private/pure-ftpd.pem -out /etc/ssl/private/pure-ftpd.pem chmod 600 /etc/ssl/private/pure-ftpd.pem
/etc/init.d/pure-ftpd restart
Montage des arborescences réelles et virtuelles
Comme à l'habitude, vous monter vos différents points d'accès NFS et CIFS. On suppose qu'il s'agit de sous dossier de /mnt.
cat << EOF > /etc/init.d/masquerade #!/bin/sh case "$1" in start) echo "Start 'masquarade'..." echo " mount remote fs" mount -t cifs -a mount -t nfs -a echo " enslave remote fs" mount --make-slave /mnt/cifs01 mount --make-slave /mnt/cifs02 mount --make-slave /mnt/cifs03 mount --make-slave /mnt/nfs01 mount --make-slave /mnt/nfs02 mount --make-slave /mnt/nfs03 echo " bind user mounts/chroot" /usr/local/bin/masquarade.sh add > /var/log/masquarade.log 2> /var/log/masquarade.err [ $? -ne 0 ] & echo " failed to bind all users mounts/chroots." ;; stop) echo "Stop 'masquarade'..." echo " unbind users mounts/chroot" /usr/local/bin/masquarade.sh del > /var/log/masquarade.log 2> /var/log/masquarade.err [ $? -ne 0 ] & echo " failed to unbind all users mounts/chroots." echo " free remote fs" umount /mnt/cifs01 umount /mnt/cifs02 umount /mnt/cifs03 umount /mnt/nfs01 umount /mnt/nfs02 umount /mnt/nfs03 ;; restart) $0 stop sleep 5 $0 start ;; reload) $0 restart ;; force-reload) $0 restart ;; *) echo "Usage: $0 {start|stop|restart|force-reload}" >&2 exit 3 ;; esac exit 0 EOF chmod +x /etc/init.d/masquerade update-rc.d masquerade defaults
cat << EOF > /usr/local/bin/masquarade.sh #!/bin/bash CONF="/usr/local/etc/mounts" DATE=`date +%s` # USAGE function usage() { echo "Usage: $0 <help> <add|del> [[<mount>] ...]"; echo " add - to add one or several mount"; echo " del - to remove one or several mount"; echo " mount - mount name from config file"; echo " help - this usage"; echo ""; echo "If no mount point is provided, all mount points from config file will be treated"; echo "Configuration: $CONF"; echo ""; } # ADD A PMOUNT POINT function addMount() { touch /tmp/masquarade.$DATE if [ $# -ne 0 ] then echo "[ Traitement de points particuliers ]" echo " Preparation du listing: " for point in $* do if [ `grep -ve '^#' $CONF | grep -ve '^\s*$' | grep -F $point | wc -l` -eq 0 ] then echo " Aucun point $PBIND existant dans la configuration" continue fi grep -ve '^#' $CONF | grep -ve '^\s*$' | grep -F $point >> /tmp/masquarade.$DATE done else echo "[ Utilisation du fichier complet ]" echo " Preparation du listing: " grep -ve '^#' $CONF | grep -ve '^\s*$' >> /tmp/masquarade.$DATE fi cat /tmp/masquarade.$DATE | while read line do # extract data for mount point PTYPE=`echo $line | awk -F';' '{ print $1 }'` # NOT USED PMOUNT=`echo $line | awk -F';' '{ print $3 }'` PBIND=`echo $line | awk -F';' '{ print $2 }'` PRIGHTS=`echo $line | awk -F';' '{ print $4 }'` echo -n "( + ) $PBIND: " # check the source if [ ! -d $PMOUNT ] then echo "$PMOUNT n'existe pas"; continue fi # create the binding mkdir -p $PBIND #echo "mount -vvv -o bind $PMOUNT $PBIND" &> /tmp/pouet.log; #mount -vvv -o bind $PMOUNT $PBIND &>> /tmp/pouet.log; mount -o bind $PMOUNT $PBIND 2> /dev/null; if [ $? -ne 0 ] then echo " impossible de monter $PBIND" continue fi if [ $PRIGHTS == "ro" ] then mount -o remount,ro $PBIND 2> /dev/null if [ $? -ne 0 ] then echo " impossible de passer $PBIND en lecture seule" continue fi fi echo -n "$PBIND mounted ($PRIGHTS) " echo "ok" done if [ $# -ne 0 ] then for point in $* do account=${point#/opt/sftp/} account=${account%/*} echo -n "* $account: " if [ `mount | grep -F /opt/sftp/$account/dev | wc -l` -gt 0 ] then echo "already exists" else mkdir -p /opt/sftp/$account/$account/dev/ 2> /dev/null mount -o bind /chroot/ /opt/sftp/$account/$account/dev/ 2> /dev/null chmod 700 /opt/sftp/$account 2> /dev/null chown root:root /opt/sftp/$account 2> /dev/null chmod 500 /opt/sftp/$account/$account/dev 2> /dev/null fi echo "ok" done else for account in `ls /opt/sftp`; do echo -n "* $account: " if [ `mount | grep -F /opt/sftp/$account/$account/dev | wc -l` -gt 0 ] then echo "already exists" else mkdir -p /opt/sftp/$account/$account/dev/ 2> /dev/null mount -o bind /chroot/ /opt/sftp/$account/$account/dev/ 2> /dev/null chmod 700 /opt/sftp/$account 2> /dev/null chown root:root /opt/sftp/$account 2> /dev/null chmod 500 /opt/sftp/$account/$account/dev 2> /dev/null fi echo "ok" done fi rm -f /tmp/masquarade.$DATE return } # DELETE A PMOUNT POINT function delMount() { if [ $# -ne 0 ] then echo "[ Traitement de points particuliers ]" echo " Preparation du listing: " for point in $* do if [ `grep -ve '^#' $CONF | grep -ve '^\s*$' | grep -F $point | wc -l` -eq 0 ] then echo " Aucun point $PBIND existant dans la configuration" continue fi grep -ve '^#' $CONF | grep -ve '^\s*$' | grep -F $point >> /tmp/masquarade.$DATE done else echo "[ Utilisation du fichier complet ]" echo " Preparation du listing: " grep -ve '^#' $CONF | grep -ve '^\s*$' >> /tmp/masquarade.$DATE echo " Retrait des logs" for account in `ls /opt/sftp` do umount -f /opt/sftp/$account/$account/dev/ 2> /dev/null #rmdir /opt/sftp/$account/$account/dev/ 2> /dev/null done fi tac /tmp/masquarade.$DATE | while read line do PBIND=`echo $line | awk -F';' '{ print $2 }'` PRIGHTS=`echo $line | awk -F';' '{ print $4 }'` echo -n "( - ) $PBIND: " if [ `grep $PBIND /proc/mounts | grep -v grep | wc -l` -eq 0 ] then echo "$PBIND n'est pas/plus monte" else umount -f $PBIND 2> /dev/null if [ $? -ne 0 ] then echo "Impossible de demonter $PBIND" continue fi fi echo "ok" done rm /tmp/masquarade.$DATE } if [ $# -eq 0 ] then usage exit 0 fi if [ ! -f $CONF ] then echo "Fichier $CONF manquant !" echo "Syntaxe du fichier: PTYPE;PMOUNT;PBIND;PRIGHTS" exit 1 fi action=0; pos=0; points=""; for arg in `echo $*`; do ((pos++)) case "$arg" in help) usage; exit 0; ;; add) action=1; if [ $# -eq 1 ]; then addMount; fi ;; del) action=-1; if [ $# -eq 1 ]; then delMount; fi ;; *) points="${points} $arg"; if [ $pos -eq $# ]; then if [ $action -eq 0 ]; then usage; exit 0; else if [ $action -gt 0 ]; then addMount $points; else delMount $points; fi fi exit 0; fi ;; esac done exit 0 EOF chmod +x /usr/local/bin/masquarade.sh
/etc/init.d/masquerade start
L'idée est que chaque utilisateur est isolé et chrooté dans son coin. Il ne remonte pas l'arborescence et n'a même pas les droits en lecture chez ses voisins. De plus, chaque action peut être loguée puisque transmise à syslog-ng.
VIP
Pour que le load balancing fonctionne avec la bonne IP dans le paquet réseau, on pense à définir un alias.
cat << EOF >> /etc/network/interfaces auto lo:0 iface lo:0 inet static address x.x.x.3 netmask 255.255.255.255 EOF
Création d'un utilisateur
Avec votre client MySQL préféré (CLI, phpMyAdmin, autre...), il vous suffit alors de rajouter une entrée en respectant les points suivants :
- username correspond au nom de l'utilisateur (par ex, "Nom Prénom")
- login correspond à son identifiant
- pass est le mot de passe encrypté via la fonction PASSWORD() de MySQL
- id est laissé vide pour être automatiquement incrémenté.
Conclusion
Avec cette base de plateforme, vous avez de quoi avoir un dépôt de fichiers multi utilisateur en FTP (avec ou sans SSL) et en SFTP avec un seule et unique point d'authentification, et pas mal de sécurisation au niveau des accès et remontées d'arborescence. Si vous avez des questions ou remarque, comme à l'habitude, n'hésitez pas.
Merci à Guillaume Vaillant qui m'a ressorti mes archives.