Files
cloudron-box/setup/start/cloudron-firewall.sh
Girish Ramakrishnan 83d7535d84 turn: add outbound ratelimit
coturn will send 401 when receiving UDP packets with forged source IP.
this can cause a flood of 401s at the victim. the primary concern appears
to be that these packets are quite large compared to handshake packets
below.

TCP is also affected but effects are minimal because they will get
discarded at the connection handshake level.

UDP/TLS (DTLS) has similar handshake mechanism of TCP and effects are
minimal.

https://forum.cloudron.io/topic/13855/reflection-attack-via-stun-turn
https://github.com/coturn/coturn/pull/1588
2025-06-04 14:15:45 +02:00

202 lines
11 KiB
Bash
Executable File

#!/bin/bash
set -eu -o pipefail
echo "==> Setting up firewall"
has_ipv6=$(cat /proc/net/if_inet6 >/dev/null 2>&1 && echo "yes" || echo "no")
# wait for 120 seconds for xtables lock, checking every 1 second
readonly iptables="iptables --wait 120"
readonly ip6tables="ip6tables --wait 120"
function ipxtables() {
$iptables "$@"
[[ "${has_ipv6}" == "yes" ]] && $ip6tables "$@"
}
ipxtables -t filter -N CLOUDRON || true
ipxtables -t filter -F CLOUDRON # empty any existing rules
# first setup any user IP block lists . remove all references in iptables before destroying them
echo "==> Creating ipset cloudron_blocklist"
$iptables -t filter -D DOCKER-USER -m set --match-set cloudron_blocklist src -j DROP || true
sleep 1 # without this there is a race that iptables is still referencing the ipset
ipset destroy cloudron_blocklist || true
ipset create cloudron_blocklist hash:net maxelem 262144 || true # if you change the size, change network.js size check
echo "==> Creating ipset cloudron_blocklist6"
$ip6tables -D FORWARD -m set --match-set cloudron_blocklist6 src -j DROP || true
sleep 1 # without this there is a race that iptables is still referencing the ipset
ipset destroy cloudron_blocklist6 || true
ipset create cloudron_blocklist6 hash:net family inet6 maxelem 262144 || true # if you change the size, change network.js size check
/home/yellowtent/box/src/scripts/setblocklist.sh
$iptables -t filter -A CLOUDRON -m set --match-set cloudron_blocklist src -j DROP
$iptables -t filter -I DOCKER-USER 1 -m set --match-set cloudron_blocklist src -j DROP # the DOCKER-USER chain is not cleared on docker restart
$ip6tables -t filter -A CLOUDRON -m set --match-set cloudron_blocklist6 src -j DROP
$ip6tables -I FORWARD 1 -m set --match-set cloudron_blocklist6 src -j DROP # there is no DOCKER-USER chain in ip6tables, bug?
# allow related and establisted connections
echo "==> Opening standard ports"
ipxtables -t filter -A CLOUDRON -m state --state RELATED,ESTABLISHED -j ACCEPT
ipxtables -t filter -A CLOUDRON -p tcp -m tcp -m multiport --dports 22,80,202,443 -j ACCEPT # 202 is the alternate ssh port
# whitelist any user ports. we used to use --dports but it has a 15 port limit (XT_MULTI_PORTS)
echo "==> Opening up user specified ports"
ports_json="/home/yellowtent/platformdata/firewall/ports.json"
if allowed_tcp_ports=$(node -e "console.log(JSON.parse(fs.readFileSync('${ports_json}', 'utf8')).allowed_tcp_ports.join(' '))" 2>/dev/null); then
for p in $allowed_tcp_ports; do
ipxtables -A CLOUDRON -p tcp -m tcp --dport "${p}" -j ACCEPT
done
fi
if allowed_udp_ports=$(node -e "console.log(JSON.parse(fs.readFileSync('${ports_json}', 'utf8')).allowed_udp_ports.join(' '))" 2>/dev/null); then
for p in $allowed_udp_ports; do
ipxtables -A CLOUDRON -p udp -m udp --dport "${p}" -j ACCEPT
done
fi
# LDAP user directory allow list
echo "==> Configuring LDAP allow list"
if ! ipset list cloudron_ldap_allowlist >/dev/null 2>&1; then
echo "==> Creating the cloudron_ldap_allowlist ipset"
ipset create cloudron_ldap_allowlist hash:net
fi
ipset flush cloudron_ldap_allowlist
if ! ipset list cloudron_ldap_allowlist6 >/dev/null 2>&1; then
echo "==> Creating the cloudron_ldap_allowlist6 ipset"
ipset create cloudron_ldap_allowlist6 hash:net family inet6
fi
ipset flush cloudron_ldap_allowlist6
ldap_allowlist="/home/yellowtent/platformdata/firewall/ldap_allowlist.txt"
# delete any existing redirect rule
$iptables -t nat -D PREROUTING -p tcp --dport 636 -j REDIRECT --to-ports 3004 2>/dev/null || true
$ip6tables -t nat -D PREROUTING -p tcp --dport 636 -j REDIRECT --to-ports 3004 >/dev/null || true
if [[ -f "${ldap_allowlist}" ]]; then
# without the -n block, any last line without a new line won't be read it!
while read -r line || [[ -n "$line" ]]; do
[[ -z "${line}" ]] && continue # ignore empty lines
[[ "$line" =~ ^#.*$ ]] && continue # ignore lines starting with #
if [[ "$line" == *":"* ]]; then
ipset add -! cloudron_ldap_allowlist6 "${line}" # the -! ignore duplicates
else
ipset add -! cloudron_ldap_allowlist "${line}" # the -! ignore duplicates
fi
done < "${ldap_allowlist}"
# ldap server we expose 3004 and also redirect from standard ldaps port 636
$iptables -t nat -I PREROUTING -p tcp --dport 636 -j REDIRECT --to-ports 3004
$iptables -t filter -A CLOUDRON -m set --match-set cloudron_ldap_allowlist src -p tcp --dport 3004 -j ACCEPT
$ip6tables -t nat -I PREROUTING -p tcp --dport 636 -j REDIRECT --to-ports 3004
$ip6tables -t filter -A CLOUDRON -m set --match-set cloudron_ldap_allowlist6 src -p tcp --dport 3004 -j ACCEPT
fi
# turn and stun service (constants.TURN_PORT, constants.TURN_TLS_PORT, constants.TURN_UDP_PORT_START, constants.TURN_UDP_PORT_END)
echo "==> Opening ports for TURN and STUN"
ipxtables -t filter -A CLOUDRON -p tcp -m multiport --dports 3478,5349 -j ACCEPT
ipxtables -t filter -A CLOUDRON -p udp -m multiport --dports 3478,5349 -j ACCEPT
ipxtables -t filter -A CLOUDRON -p udp -m multiport --dports 50000:50100 -j ACCEPT
# ICMPv6 is very fundamental to IPv6 connectivity unlike ICMPv4
echo "==> Allow ICMP"
$iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-request -j ACCEPT
$iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-reply -j ACCEPT
$ip6tables -t filter -A CLOUDRON -p ipv6-icmp -j ACCEPT
# NDP (546) and RA (547) portion of ICMPv6
$ip6tables -t filter -A CLOUDRON -p udp --sport 547 --dport 546 -j ACCEPT
ipxtables -t filter -A CLOUDRON -p udp --sport 53 -j ACCEPT
# for ldap,dockerproxy server (ipv4 only) to accept connections from apps. for connecting to addons and mail container ports, docker already has rules
$iptables -t filter -A CLOUDRON -p tcp -s 172.18.0.0/16 -d 172.18.0.1 -m multiport --dports 3002,3003 -j ACCEPT
$iptables -t filter -A CLOUDRON -p udp -s 172.18.0.0/16 --dport 53 -j ACCEPT # dns responses from docker (127.0.0.11)
ipxtables -t filter -A CLOUDRON -i lo -j ACCEPT # required for localhost connections (mysql)
# log dropped incoming. keep this at the end of all the rules
ipxtables -t filter -A CLOUDRON -m limit --limit 2/min -j LOG --log-prefix "Packet dropped: " --log-level 7
ipxtables -t filter -A CLOUDRON -j DROP
# prepend our chain to the filter table
echo "==> Adding cloudron filter chain"
$iptables -t filter -C INPUT -j CLOUDRON 2>/dev/null || $iptables -t filter -I INPUT -j CLOUDRON
$ip6tables -t filter -C INPUT -j CLOUDRON 2>/dev/null || $ip6tables -t filter -I INPUT -j CLOUDRON
# masquerading rules for container ports to be accessible using public IP from other containers
echo "==> Adding cloudron postrouting chain"
ipxtables -t nat -N CLOUDRON_POSTROUTING || true
ipxtables -t nat -F CLOUDRON_POSTROUTING # empty any existing rules
$iptables -t nat -A CLOUDRON_POSTROUTING -s 172.18.0.0/16 -d 172.18.0.0/16 -j MASQUERADE
$ip6tables -t nat -A CLOUDRON_POSTROUTING -s fd00:c107:d509::/64 -d fd00:c107:d509::/64 -j MASQUERADE
$iptables -t nat -C POSTROUTING -j CLOUDRON_POSTROUTING 2>/dev/null || $iptables -t nat -I POSTROUTING -j CLOUDRON_POSTROUTING
$ip6tables -t nat -C POSTROUTING -j CLOUDRON_POSTROUTING 2>/dev/null || $ip6tables -t nat -I POSTROUTING -j CLOUDRON_POSTROUTING
# Setup rate limit chain (the recent info is at /proc/net/xt_recent)
echo "==> Setup rate limit chain"
ipxtables -t filter -N CLOUDRON_RATELIMIT || true
ipxtables -t filter -F CLOUDRON_RATELIMIT # empty any existing rules
# log dropped incoming. keep this at the end of all the rules
echo "==> Setup logging"
ipxtables -t filter -N CLOUDRON_RATELIMIT_LOG || true
ipxtables -t filter -F CLOUDRON_RATELIMIT_LOG # empty any existing rules
ipxtables -t filter -A CLOUDRON_RATELIMIT_LOG -m limit --limit 2/min -j LOG --log-prefix "IPTables RateLimit: " --log-level 7
ipxtables -t filter -A CLOUDRON_RATELIMIT_LOG -j DROP
# http https . 5000 requests per second per IP
for port in 80 443; do
ipxtables -A CLOUDRON_RATELIMIT -p tcp --syn --dport ${port} -m connlimit --connlimit-above 5000 -j CLOUDRON_RATELIMIT_LOG
done
# ssh and sftp. 5 connections per 10 seconds per IP
for port in 22 202 222; do
ipxtables -A CLOUDRON_RATELIMIT -p tcp --dport ${port} -m state --state NEW -m recent --set --name "public-${port}"
ipxtables -A CLOUDRON_RATELIMIT -p tcp --dport ${port} -m state --state NEW -m recent --update --name "public-${port}" --seconds 10 --hitcount 5 -j CLOUDRON_RATELIMIT_LOG
done
# ldaps
for port in 636 3004; do
ipxtables -A CLOUDRON_RATELIMIT -p tcp --syn --dport ${port} -m connlimit --connlimit-above 5000 -j CLOUDRON_RATELIMIT_LOG
done
# docker translates (dnat) 25, 587, 993, 4190 in the PREROUTING step . 50 connections per second per app
for port in 2525 4190 9993; do
$iptables -A CLOUDRON_RATELIMIT -p tcp --syn ! -s 172.18.0.0/16 -d 172.18.0.0/16 --dport ${port} -m connlimit --connlimit-above 50 -j CLOUDRON_RATELIMIT_LOG
done
# msa, ldap, imap, sieve, pop3 . 500 connections per second per app
for port in 2525 3002 4190 9993 9995; do
$iptables -A CLOUDRON_RATELIMIT -p tcp --syn -s 172.18.0.0/16 -d 172.18.0.0/16 --dport ${port} -m connlimit --connlimit-above 500 -j CLOUDRON_RATELIMIT_LOG
done
# cloudron docker network: mysql postgresql redis mongodb. 5000 connections per second per app
for port in 3306 5432 6379 27017; do
$iptables -A CLOUDRON_RATELIMIT -p tcp --syn -s 172.18.0.0/16 -d 172.18.0.0/16 --dport ${port} -m connlimit --connlimit-above 5000 -j CLOUDRON_RATELIMIT_LOG
done
# Add the rate limit chain to input chain
$iptables -t filter -C INPUT -j CLOUDRON_RATELIMIT 2>/dev/null || $iptables -t filter -I INPUT 1 -j CLOUDRON_RATELIMIT
$ip6tables -t filter -C INPUT -j CLOUDRON_RATELIMIT 2>/dev/null || $ip6tables -t filter -I INPUT 1 -j CLOUDRON_RATELIMIT
# Workaround issue where Docker insists on adding itself first in FORWARD table
ipxtables -D FORWARD -j CLOUDRON_RATELIMIT || true
ipxtables -I FORWARD 1 -j CLOUDRON_RATELIMIT
echo "==> Adding cloudron output chain"
ipxtables -t filter -N CLOUDRON_OUTPUT || true
ipxtables -t filter -F CLOUDRON_OUTPUT # empty any existing rules
# turn - mitigate reflection/amplification attack with UDP . The 5349 port is DTLS/UDP and not affected. https://github.com/coturn/coturn/pull/1588
# hashlimit-above is applied _after_ allowing a hashlimit-burst
sudo iptables -t filter -A CLOUDRON_OUTPUT -p udp --sport 3478 -m hashlimit --hashlimit-name turn-401 --hashlimit-above 10/second --hashlimit-burst 30 --hashlimit-mode dstip -j CLOUDRON_RATELIMIT_LOG
# Add the ouput rate limit chain to output chain
$iptables -t filter -C OUTPUT -j CLOUDRON_OUTPUT 2>/dev/null || $iptables -t filter -I OUTPUT 1 -j CLOUDRON_OUTPUT
$ip6tables -t filter -C INPUT -j CLOUDRON_OUTPUT 2>/dev/null || $ip6tables -t filter -I OUTPUT 1 -j CLOUDRON_OUTPUT