176 lines
6.5 KiB
Plaintext
176 lines
6.5 KiB
Plaintext
#!/usr/sbin/nft -f
|
|
# ==============================================================
|
|
# nftables.conf — server-mv
|
|
#
|
|
# Substitui a parte de firewall do rc.local antigo (iptables/ip6tables,
|
|
# antes espalhada entre rc.local e test.sh). Regras funcionalmente
|
|
# equivalentes, com os bugs já corrigidos:
|
|
# - sem "-j DROP" solto no meio da chain (o catch-all é só a policy)
|
|
# - sem duplicação de regra de porta única + multiport
|
|
# - anti-spoof de 192.168.0.0/16 restrito à interface WAN
|
|
# - chain PORTSCAN removida (nunca era populada, sempre morta)
|
|
#
|
|
# Vantagens sobre iptables/ip6tables para este caso:
|
|
# - um ruleset só cobre IPv4 e IPv6 (table inet), então não existe
|
|
# mais o risco de esquecer de espelhar uma regra do v4 pro v6
|
|
# - "flush ruleset" no topo faz o carregamento ser atômico: se o
|
|
# arquivo tiver erro de sintaxe, o `nft -f` falha ANTES de aplicar
|
|
# qualquer coisa — você nunca fica com um firewall pela metade
|
|
# (iptables -F seguido de regras que podem falhar no meio é o
|
|
# oposto disso)
|
|
# - meters (rate-limit por IP de origem) substituem o módulo
|
|
# "recent" do iptables com sintaxe mais legível
|
|
#
|
|
# Aplicar:
|
|
# nft -c -f nftables.conf # valida sintaxe sem aplicar
|
|
# nft -f nftables.conf # aplica
|
|
# systemctl enable --now nftables # persistência no boot
|
|
#
|
|
# Depois de migrar, pare de carregar iptables/ip6tables no boot
|
|
# (remova essa parte do rc.local — ver rc.local.new) para não ter
|
|
# duas ferramentas de firewall competindo pelo mesmo netfilter.
|
|
# ==============================================================
|
|
|
|
flush ruleset
|
|
|
|
table inet filter {
|
|
|
|
# --- Meters (rate limit com estado por IP de origem) ---
|
|
# (declarados via "meter" inline nas regras que os usam, mais abaixo)
|
|
|
|
chain input {
|
|
type filter hook input priority filter; policy drop;
|
|
|
|
iifname "lo" accept
|
|
ct state established,related accept
|
|
|
|
# ICMP/ICMPv6: 2º maior volume medido no host, avaliado cedo
|
|
# de propósito (dados reais: ~66k pacotes/dia, ficava por
|
|
# último na versão antiga em iptables).
|
|
ip protocol icmp jump icmpv4_in
|
|
meta l4proto icmpv6 jump icmpv6_in
|
|
|
|
# Pacotes TCP malformados / scanners óbvios
|
|
tcp flags & (fin|syn|rst|psh|ack|urg) == (fin|syn|rst|psh|ack|urg) \
|
|
meter bad_packets { ip saddr timeout 5m limit rate over 5/minute } drop
|
|
tcp flags & (fin|syn|rst|psh|ack|urg) == 0x0 \
|
|
meter bad_packets6 { ip6 saddr timeout 5m limit rate over 5/minute } drop
|
|
ip frag-off & 0x1fff != 0 drop # fragmentos IPv4 (equivalente a "iptables -f")
|
|
|
|
# Anti-spoof — só faz sentido vindo da WAN
|
|
iifname "enp0s6" ip saddr 192.168.0.0/16 drop
|
|
iifname "enp0s6" ip6 saddr fe80::/10 drop
|
|
iifname "enp0s6" ip6 saddr fd00::/8 drop
|
|
ip6 daddr ff00::/8 drop
|
|
|
|
# SYN flood — limite GLOBAL (todo o servidor, todas as portas e
|
|
# clientes somados). O valor antigo (10/sec, burst 24) veio direto
|
|
# do rc.local original sem questionar, e era baixo demais pra um
|
|
# host com ~26 serviços: uma única carga de página com vários
|
|
# assets/chamadas de API em paralelo já consome a rajada inteira,
|
|
# e qualquer SYN novo depois disso (reload, outra aba, outro
|
|
# serviço) é dropado até o balde reencher — foi exatamente isso
|
|
# que quebrou o carregamento do Atlas. Esse limite é só um
|
|
# backstop contra flood real (dezenas de milhares de SYN/s); a
|
|
# proteção fina por IP já existe nos meters (ssh_meter,
|
|
# bad_packets) logo acima.
|
|
tcp flags syn limit rate 1000/second burst 200 packets accept
|
|
tcp flags syn drop
|
|
|
|
# Rate limit de conexões novas na porta SSH (8117): no máx. 3
|
|
# tentativas novas por IP a cada 60s
|
|
tcp dport 8117 ct state new \
|
|
meter ssh_meter { ip saddr timeout 60s limit rate 3/minute } accept
|
|
tcp dport 8117 ct state new meter ssh_meter6 { ip6 saddr timeout 60s limit rate 3/minute } accept
|
|
tcp dport 8117 ct state new drop
|
|
|
|
# Túneis / VPN
|
|
iifname "wg0" accept
|
|
iifname "tun0" accept
|
|
udp dport 1194 accept
|
|
|
|
# Serviços expostos na WAN
|
|
iifname "enp0s6" tcp dport { 80, 443 } accept
|
|
iifname "enp0s6" udp dport { 32100, 32101, 32110, 32400 } accept
|
|
|
|
# DHCPv6 client
|
|
udp dport 546 accept
|
|
}
|
|
|
|
chain forward {
|
|
type filter hook forward priority filter; policy accept;
|
|
|
|
ct state established,related accept
|
|
iifname "tun0" accept
|
|
iifname "wg0" accept
|
|
ip daddr 10.15.0.45 tcp dport { 80, 443 } accept
|
|
ip6 daddr 2001:db8:15::45 tcp dport { 80, 443 } accept
|
|
}
|
|
|
|
chain output {
|
|
type filter hook output priority filter; policy accept;
|
|
}
|
|
|
|
chain icmpv4_in {
|
|
icmp type echo-reply accept
|
|
icmp type echo-request limit rate 1/second burst 3 packets accept
|
|
icmp type echo-request drop
|
|
drop
|
|
}
|
|
|
|
chain icmpv6_in {
|
|
icmpv6 type destination-unreachable accept
|
|
icmpv6 type packet-too-big limit rate 5/minute accept
|
|
icmpv6 type time-exceeded limit rate 5/minute accept
|
|
icmpv6 type echo-request limit rate 1/second burst 3 packets accept
|
|
icmpv6 type echo-request drop
|
|
icmpv6 type echo-reply accept
|
|
icmpv6 type nd-router-solicit accept
|
|
icmpv6 type nd-router-advert accept
|
|
icmpv6 type nd-neighbor-solicit accept
|
|
icmpv6 type nd-neighbor-advert accept
|
|
drop
|
|
}
|
|
}
|
|
|
|
table ip nat {
|
|
chain prerouting {
|
|
type nat hook prerouting priority dstnat;
|
|
iifname "enp0s6" tcp dport 80 dnat to 10.15.0.45:80
|
|
iifname "enp0s6" tcp dport 443 dnat to 10.15.0.45:443
|
|
}
|
|
|
|
chain postrouting {
|
|
type nat hook postrouting priority srcnat;
|
|
# oifname "enp0s6" é OBRIGATÓRIO aqui — sem isso, tráfego
|
|
# container-para-container que passa pelo POSTROUTING do host
|
|
# (o Docker liga br_netfilter, então tráfego intra-bridge
|
|
# também atravessa essa chain) fica mascarado com o IP do
|
|
# gateway (10.15.0.1) em vez do IP real do container de
|
|
# origem. Foi assim que a comunicação nginx->redis/mariadb/
|
|
# anatomia-web ficou corrompida: cada lado via a conexão com
|
|
# o IP errado, o que quebrou o eBPF sockmap (que decide o
|
|
# redirect com base nesses IPs) e teria corrompido qualquer
|
|
# log/ACL por IP entre containers também.
|
|
oifname "enp0s6" ip saddr 10.15.0.0/16 masquerade
|
|
oifname "enp0s6" ip saddr 172.17.0.0/16 masquerade
|
|
oifname "wg3" masquerade
|
|
# Hairpin NAT (clientes locais acessando o IP público)
|
|
oifname "enp0s6" ip daddr 10.15.0.45 tcp dport { 80, 443 } masquerade
|
|
}
|
|
}
|
|
|
|
table ip6 nat {
|
|
chain prerouting {
|
|
type nat hook prerouting priority dstnat;
|
|
iifname "enp0s6" tcp dport 80 dnat to [2001:db8:15::45]:80
|
|
iifname "enp0s6" tcp dport 443 dnat to [2001:db8:15::45]:443
|
|
}
|
|
|
|
chain postrouting {
|
|
type nat hook postrouting priority srcnat;
|
|
ip6 saddr 2001:db8:15::/48 masquerade
|
|
oifname "enp0s6" ip6 daddr 2001:db8:15::45 tcp dport { 80, 443 } masquerade
|
|
}
|
|
}
|