Création d'une plateforme (S) FTP (S) sécurisée avec authentification centralisée et utilisant divers stockages

Par Faichelbaum @faichelbaum

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.