Jump to content
Калькуляторы

Файрвол iptables в режиме юнита systemd

В свое время пришлось потратить несколько дней, чтобы разобраться с systemd и настроить файрвол в режиме автозапуска.

Делюсь максимально простым и переносимым конечным решением.

Состоит из двух файлов: собственно unit-файл для systemd, и управляющий скрипт для iptables.

Скрытый текст

firewall.service


# systemd firewall unit
#
# Install:
# 1. Place files to /etc/firewall/
# 2. Symlink firewall.service to /etc/systemd/system/firewall.service
# 3. systemctl enable firewall
# 4. systemctl start firewall
# 5. systemctl status firewall
#

[Unit]
Description=Add Firewall Rules to iptables
After=network.target
StopWhenUnneeded=true

[Service]
Type=oneshot
ExecStart=/bin/bash /etc/firewall/firewall.sh start
ExecStop=/bin/bash /etc/firewall/firewall.sh stop
StandardOutput=null
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
Alias=firewall.service

 

firewall.sh


#!/bin/bash

terminate() {
    if [ -n "$1" ]; then echo "$1"; fi
    exit
}

IPT=`which iptables` \
  && "$IPT" --version \
  || terminate "iptables not found!"
IPS=`which ipset` \
  && "$IPS" --version \
  || terminate "ipset not found!"
FW="$IPT -t filter"
NAT="$IPT -t nat"
MARK="$IPT -t mangle"

IF_LOOP=lo
IF_MNT=eno1.10
IF_INT=eno1.20
IF_EXT=eno1.100

IP_INT=(`ip -o -4 addr show dev $IF_INT | sed -ne 's/.*inet \(.*\) brd.*/\1/p'`)
IP_EXT=(`ip -o -4 addr show dev $IF_EXT | sed -ne 's/.*inet \(.*\) brd.*/\1/p'`)

ips_init() {

# Reset IP sets
$IPS flush
$IPS destroy

# Define blacklists
$IPS create BLOCKS hash:ip timeout 3600

# Define bogons networks
$IPS create BOGONS hash:net
$IPS add    BOGONS 0.0.0.0/8        # source this
$IPS add    BOGONS 10.0.0.0/8       # private network
$IPS add    BOGONS 100.64.0.0/10    # CG-NAT
$IPS add    BOGONS 127.0.0.0/8      # loopback addresses
$IPS add    BOGONS 169.254.0.0/16   # link-local subnet
$IPS add    BOGONS 172.16.0.0/12    # private network
$IPS add    BOGONS 192.0.0.0/24     # reserved
$IPS add    BOGONS 192.0.2.0/24     # test network
$IPS add    BOGONS 192.42.172.0/24
$IPS add    BOGONS 192.88.99.0/24   # anycast relay
$IPS add    BOGONS 192.168.0.0/16   # private network
$IPS add    BOGONS 198.18.0.0/15    # test inter-network
$IPS add    BOGONS 198.51.100.0/24  # test network
$IPS add    BOGONS 203.0.113.0/24   # test network
$IPS add    BOGONS 224.0.0.0/4      # multicast
$IPS add    BOGONS 240.0.0.0/4      # reserved

# Define own private networks
$IPS create INT hash:net
$IPS add    INT 127.0.0.0/8
$IPS add    INT 10.0.0.0/8

# Define own public networks
$IPS create EXT hash:net
$IPS add    EXT AA.AA.AA.0/22
$IPS add    EXT BB.BB.BB.0/21

# Define admin IPs
$IPS create ACL-SSH hash:net
$IPS add    ACL-SSH aa.aa.aa.0/24
$IPS add    ACL-SSH bb.bb.bb.3

# Define servers IPs
$IPS create ACL-SRV hash:net
$IPS add    ACL-SRV CC.CC.CC.0/24

# Define staff IPs
$IPS create ACL-STAFF hash:ip
$IPS add    ACL-STAFF dd.dd.dd.0/24

}

do_start() {
echo "Start firewall (protected access)"
ips_init
$FW -F
$FW -X
$FW -Z
$FW -P INPUT DROP
$FW -P OUTPUT ACCEPT
$FW -P FORWARD DROP

# Define public services (EXT)
TCP_PUBLIC=53,80,443
UDP_PUBLIC=53,123

# Define private services (INT)
TCP_PRIVATE=53,80,443,21,20
UDP_PRIVATE=53,123,69

## Filter input invalid and bad packets
$FW -N VALIDATE
$FW -A VALIDATE -p tcp ! --syn -m state --state NEW -j DROP
$FW -A VALIDATE -p tcp --tcp-flags ALL ALL -j DROP
$FW -A VALIDATE -p tcp --tcp-flags ALL NONE -j DROP
$FW -A VALIDATE -p tcp --tcp-flags ALL SYN -m state --state ESTABLISHED -j DROP
$FW -A VALIDATE -p icmp --fragment -j DROP
$FW -A VALIDATE -m state --state INVALID -j DROP
$FW -A VALIDATE -d 255.255.255.255 -j DROP
$FW -A VALIDATE -j RETURN


## Public (internet) services
$FW -N EXT

  # Allow ICMP ping
  $FW -A EXT -p icmp --icmp-type 0 -j ACCEPT
  $FW -A EXT -p icmp --icmp-type 8 -j ACCEPT

  # Allow SSH
  $FW -A EXT -p tcp --dport 22 -m set --match-set ACL-SSH src -j ACCEPT

  # Allow services
  $FW -A EXT -p tcp -m multiport --dports $TCP_PUBLIC -j ACCEPT
  $FW -A EXT -p udp -m multiport --dports $UDP_PUBLIC -j ACCEPT

# Return
$FW -A EXT -j RETURN


## Private (intranet) services
$FW -N INT

  # Allow ICMP ping
  $FW -A INT -p icmp --icmp-type 0 -j ACCEPT
  $FW -A INT -p icmp --icmp-type 8 -j ACCEPT

  # Allow SSH
  $FW -A INT -p tcp --dport 22 -m set --match-set ACL-SSH src -j ACCEPT

  # Allow services
  $FW -A INT -p tcp -m multiport --dports $TCP_PRIVATE -j ACCEPT
  $FW -A INT -p udp -m multiport --dports $UDP_PRIVATE -j ACCEPT

  # Allow internal services
  $FW -A INT -p tcp -m multiport --dports $TCP_SERVER -m set --match-set ACL-SRV src -j ACCEPT
  $FW -A INT -p udp -m multiport --dports $UDP_SERVER -m set --match-set ACL-SRV src -j ACCEPT

  # Allow restricted services
  $FW -A INT -p tcp -m multiport --dports $TCP_RESTRICT -m set --match-set ACL-STAFF src -j ACCEPT
  $FW -A INT -p udp -m multiport --dports $UDP_RESTRICT -m set --match-set ACL-STAFF src -j ACCEPT

# Return
$FW -A INT -j RETURN


## Main processing
$FW -A INPUT -i $IF_LOOP -j ACCEPT
$FW -A INPUT  -m set --match-set BLOCKS src -j DROP
$FW -A OUTPUT -m set --match-set BLOCKS dst -j DROP
$FW -A INPUT -j VALIDATE
$FW -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
$FW -A INPUT -i $IF_EXT -m set --match-set BOGONS src -j DROP
$FW -A INPUT -i $IF_INT -m set --match-set INT src -j INT
$FW -A INPUT -i $IF_EXT -j EXT

$IPT -L -vnx
}

do_stop() {
echo "Stop firewall (allow any)"
ips_init
$FW -F
$FW -X
$FW -Z
$FW -P INPUT ACCEPT
$FW -P OUTPUT ACCEPT
$FW -P FORWARD DROP
$IPT -L -vnx
}

do_zero() {
echo "Zero firewall (allow outgoing only)"
ips_init
$FW -F
$FW -X
$FW -Z
$FW -P INPUT DROP
$FW -P OUTPUT ACCEPT
$FW -P FORWARD DROP
$FW -A INPUT -i $IF_LOOP -j ACCEPT
$FW -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
$IPT -L -vnx
}

do_status() {
echo "Show firewall status"
$IPT -L -vnx
echo ""
echo "Loopback [$IF_LOOP]  Management [$IF_MNT]"
echo "Internal: $IF_INT"
for IP in ${IP_INT[@]}; do
echo "          - $IP"
done
echo "External: $IF_EXT"
for IP in ${IP_EXT[@]}; do
IP=${IP%/*}
echo "          - $IP"
done
}

do_usage() {
echo "Usage: $0 {start|stop|status|zero}"
}

case "$1" in
  start|enable)
    do_start
    ;;
  stop|disable)
    do_stop
    ;;
  zero|base)
    do_zero
    ;;
  status|"")
    do_status
    ;;
  usage|help)
    do_usage
    ;;
  *)
    echo "Invalid mode"
    do_usage
    ;;
esac

В do_start каждый задает свой набор правил.

 

В /etc создается подкаталог firewall, в него копируются оба файла.

Затем на firewall.service создается симлинк в /etc/systemd/system/.

Затем готовый юнит подключается и запускается.

При необходимости скрипт /etc/firewall/firewall.sh можно запускать и автономно, он поддерживает некоторые дополнительные режимы.

Может кому пригодится.

 

Но собственно я бы хотел попросить покритиковать правила и посоветовать более оптимальные.

Эти правила я составлял наверное лет 5 назад и с тех пор особо о них не думал; работает и работает.

Скрытый текст

В принципе логика в этих правилах достаточно простая.

1. Разрешается все на loopback.

2. Блокируется черный список (набор BLOCKS, который будет пополнятся динамически при попытках подбора пароля или при различном флуде).

3. Проверяется корректность пакета (цепочка VALIDATE).

4. Разрешаются уже установленные соединения.

5. На внешнем интерфейсе фильтруются приватные адреса. Далее пакет передается в цепочку EXT, где явно задаются разрешенные сервисы.

6. На внутреннем интерфейсе разрешаются только адреса из списка. Далее пакет передается в цепочку INT, где явно задаются разрешенные сервисы

Но недавно посмотрел на дефолтный набор правил, который создает ufw, и там все как-то гораздо сложнее.

 

Может быть поделитесь своими наборами правил?

Share this post


Link to post
Share on other sites

Чисто IMHO: городить портянки в баше, описывая правила через iptables -A это уже не по фэншую. На мой взгляд лучше вынести описание правил в отдельный файл и загружать правила через ip[6]tables-restore /path/to/rules. То же самое и с ipset. Читабельность улучшается на порядки.

Share this post


Link to post
Share on other sites

Имеется ввиду iptables-save и iptables-restore? Раньше так и делал, но скрипт оказался удобнее, в нем можно использовать переменные и циклы.

Но в принципе ничего не мешает в do_* заменить прямые указания на загрузку/выгрузку через файлы.

Мне больше интересно содержимое правил.

Например практически дефолтный ufw делает так:

Скрытый текст

*filter
:INPUT DROP [277:8560]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [15:600]
:ufw-after-forward - [0:0]
:ufw-after-input - [0:0]
:ufw-after-logging-forward - [0:0]
:ufw-after-logging-input - [0:0]
:ufw-after-logging-output - [0:0]
:ufw-after-output - [0:0]
:ufw-before-forward - [0:0]
:ufw-before-input - [0:0]
:ufw-before-logging-forward - [0:0]
:ufw-before-logging-input - [0:0]
:ufw-before-logging-output - [0:0]
:ufw-before-output - [0:0]
:ufw-logging-allow - [0:0]
:ufw-logging-deny - [0:0]
:ufw-not-local - [0:0]
:ufw-reject-forward - [0:0]
:ufw-reject-input - [0:0]
:ufw-reject-output - [0:0]
:ufw-skip-to-policy-forward - [0:0]
:ufw-skip-to-policy-input - [0:0]
:ufw-skip-to-policy-output - [0:0]
:ufw-track-forward - [0:0]
:ufw-track-input - [0:0]
:ufw-track-output - [0:0]
:ufw-user-forward - [0:0]
:ufw-user-input - [0:0]
:ufw-user-limit - [0:0]
:ufw-user-limit-accept - [0:0]
:ufw-user-logging-forward - [0:0]
:ufw-user-logging-input - [0:0]
:ufw-user-logging-output - [0:0]
:ufw-user-output - [0:0]
-A INPUT -j ufw-before-logging-input
-A INPUT -j ufw-before-input
-A INPUT -j ufw-after-input
-A INPUT -j ufw-after-logging-input
-A INPUT -j ufw-reject-input
-A INPUT -j ufw-track-input
-A FORWARD -j ufw-before-logging-forward
-A FORWARD -j ufw-before-forward
-A FORWARD -j ufw-after-forward
-A FORWARD -j ufw-after-logging-forward
-A FORWARD -j ufw-reject-forward
-A FORWARD -j ufw-track-forward
-A OUTPUT -j ufw-before-logging-output
-A OUTPUT -j ufw-before-output
-A OUTPUT -j ufw-after-output
-A OUTPUT -j ufw-after-logging-output
-A OUTPUT -j ufw-reject-output
-A OUTPUT -j ufw-track-output
-A ufw-after-input -p udp -m udp --dport 137 -j ufw-skip-to-policy-input
-A ufw-after-input -p udp -m udp --dport 138 -j ufw-skip-to-policy-input
-A ufw-after-input -p tcp -m tcp --dport 139 -j ufw-skip-to-policy-input
-A ufw-after-input -p tcp -m tcp --dport 445 -j ufw-skip-to-policy-input
-A ufw-after-input -p udp -m udp --dport 67 -j ufw-skip-to-policy-input
-A ufw-after-input -p udp -m udp --dport 68 -j ufw-skip-to-policy-input
-A ufw-after-input -m addrtype --dst-type BROADCAST -j ufw-skip-to-policy-input
-A ufw-after-logging-forward -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW BLOCK] "
-A ufw-after-logging-input -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW BLOCK] "
-A ufw-before-forward -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A ufw-before-forward -p icmp -m icmp --icmp-type 3 -j ACCEPT
-A ufw-before-forward -p icmp -m icmp --icmp-type 11 -j ACCEPT
-A ufw-before-forward -p icmp -m icmp --icmp-type 12 -j ACCEPT
-A ufw-before-forward -p icmp -m icmp --icmp-type 8 -j ACCEPT
-A ufw-before-forward -j ufw-user-forward
-A ufw-before-input -i lo -j ACCEPT
-A ufw-before-input -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A ufw-before-input -m conntrack --ctstate INVALID -j ufw-logging-deny
-A ufw-before-input -m conntrack --ctstate INVALID -j DROP
-A ufw-before-input -p icmp -m icmp --icmp-type 3 -j ACCEPT
-A ufw-before-input -p icmp -m icmp --icmp-type 11 -j ACCEPT
-A ufw-before-input -p icmp -m icmp --icmp-type 12 -j ACCEPT
-A ufw-before-input -p icmp -m icmp --icmp-type 8 -j ACCEPT
-A ufw-before-input -p udp -m udp --sport 67 --dport 68 -j ACCEPT
-A ufw-before-input -j ufw-not-local
-A ufw-before-input -d 224.0.0.251/32 -p udp -m udp --dport 5353 -j ACCEPT
-A ufw-before-input -d 239.255.255.250/32 -p udp -m udp --dport 1900 -j ACCEPT
-A ufw-before-input -j ufw-user-input
-A ufw-before-output -o lo -j ACCEPT
-A ufw-before-output -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A ufw-before-output -j ufw-user-output
-A ufw-logging-allow -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW ALLOW] "
-A ufw-logging-deny -m conntrack --ctstate INVALID -m limit --limit 3/min --limit-burst 10 -j RETURN
-A ufw-logging-deny -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW BLOCK] "
-A ufw-not-local -m addrtype --dst-type LOCAL -j RETURN
-A ufw-not-local -m addrtype --dst-type MULTICAST -j RETURN
-A ufw-not-local -m addrtype --dst-type BROADCAST -j RETURN
-A ufw-not-local -m limit --limit 3/min --limit-burst 10 -j ufw-logging-deny
-A ufw-not-local -j DROP
-A ufw-skip-to-policy-forward -j DROP
-A ufw-skip-to-policy-input -j DROP
-A ufw-skip-to-policy-output -j ACCEPT
-A ufw-track-output -p tcp -m conntrack --ctstate NEW -j ACCEPT
-A ufw-track-output -p udp -m conntrack --ctstate NEW -j ACCEPT
-A ufw-user-limit -m limit --limit 3/min -j LOG --log-prefix "[UFW LIMIT BLOCK] "
-A ufw-user-limit -j REJECT --reject-with icmp-port-unreachable
-A ufw-user-limit-accept -j ACCEPT

 

В чем-то это мне нравится больше моего варианта, но все же избыточный, нужно многое сокращать.

Ну и богоны почему-то не фильтруются.

Share this post


Link to post
Share on other sites

Надо добавить разрешающее правило для icmp type 3, иначе получится собственная Path MTU Discovering Black Hole.

 

Правило INPUT -i $IF_EXT -m set --match-set BOGONS src -j DROP лучше поставить сразу после INPUT -i $IF_LOOP -j ACCEPT. Правило -m state --state ESTABLISHED,RELATED -j ACCEPT должно идти самым последним в цепочке.

Share this post


Link to post
Share on other sites

Спасибо, поправлю.

 

1 час назад, taf_321 сказал:

Правило -m state --state ESTABLISHED,RELATED -j ACCEPT должно идти самым последним в цепочке.

Обычно это правило ставят как можно раньше — чтобы не прогонять по цепочке пакет, который уже был разрешен ранее.

Share this post


Link to post
Share on other sites

По большому счету, тут systemd так, с боку припеку :) Не намного отличается от того же rc-local.service, запускающего /etc/rc.local start.

 

У systemd есть своя подсистема systemd-firewalld,запускающая динамический firewalld. Но это уже на любителя. Лично я им не пользуюсь, а firewall у меня настраивает биллинг и сопутствующий софт, максимально используя "процедурную" структуру, держа в корне только правила выбора дочерних цепочек (а тут нет дискриминации в использовании слова "дочерний"?).

 

С одной стороны такая система удобна тем, что каждая функция независима от остальных, легко модифицируется, и зменяется. С другой стороны, разбросанность фрагментов немного неудобно. Это компенсируется комментариями в самом фаерволе - каждый агент вставляя свои 5 копеек в корень добавляет в комментарий информацию о том, кто это сделал. :)

 

PS. У bash есть удобная встроенная команда вместо внешней which:
IPT=$(command -v iptables)
Мне нравится. Чем меньше дергается внешних команд, тем устойчивей и безопасней скрипт.

Share this post


Link to post
Share on other sites
14 минут назад, vop сказал:

У bash есть удобная встроенная команда вместо внешней which

Не знал. Да, заменю.

Share this post


Link to post
Share on other sites
27 minutes ago, alibek said:

Не знал. Да, заменю.

Кстати, для системных скриптов рекомендуют указывать полные пути к внешним утилитам. Считается (кем-то?...), что безопасность таких скриптов увеличивается.

 

command -V /sbin/iptables тоже отрабатывает правильно.

Share this post


Link to post
Share on other sites

Ну смысл как бы в том, что некий зловред может переименоваться под легальную утилиту и скопироваться куда-нибудь в path повыше, чтобы запускаться вместо утилиты.

Но по-моему тут почки уже отвалились и Боржоми пить поздно.

Share this post


Link to post
Share on other sites
1 hour ago, alibek said:

Ну смысл как бы в том, что некий зловред может переименоваться под легальную утилиту и скопироваться куда-нибудь в path повыше, чтобы запускаться вместо утилиты.

Но по-моему тут почки уже отвалились и Боржоми пить поздно.

Тут подразумевается лучшая устойчивость системных скриптов, пока будешь спасать и чистить систему от врагов. :)

 

Share this post


Link to post
Share on other sites
В 30.04.2021 в 19:31, vop сказал:

У systemd есть своя подсистема systemd-firewalld,запускающая динамический firewalld.

Емнип, это нашлёпка над стандартным линуховым iptables

Share this post


Link to post
Share on other sites
2 hours ago, pppoetest said:

Емнип, это нашлёпка над стандартным линуховым iptables

 

Забавно то, что абсолютно все фаервольство в линуксах является нашлепкой над стандартным iptables. А в некоторых случаях стандартный iptables является нашлепкой над nftables :) :)

Share this post


Link to post
Share on other sites
11 часов назад, vop сказал:

 

Забавно то, что абсолютно все фаервольство в линуксах является нашлепкой над стандартным iptables.

Скажем так, iptables это юзерспейс-тул для netfilter. И я решительно не понимаю, нахрена плодить сущности которые являются никому не нужной прослойкой.

 

11 часов назад, vop сказал:

А в некоторых случаях стандартный iptables является нашлепкой над nftables :) :)

Нуда, сабж депрекейтед, емнип с 2018го.

Share this post


Link to post
Share on other sites
2 часа назад, pppoetest сказал:

нахрена плодить сущности которые являются никому не нужной прослойкой.

Удобство.

Мне на десктопе понравился ufw, пользоваться им намного удобнее, чем напрямую iptables.

Если бы итоговые правила не были столь избыточными, то и на сервере можно было бы использовать.

Share this post


Link to post
Share on other sites
5 часов назад, alibek сказал:

Удобство.

В чём? Обычный баш-скрипт делает всё тоже самое. Тут бритва оккама во все поля.

Share this post


Link to post
Share on other sites
On 4/30/2021 at 9:02 AM, alibek said:

многое сокращать

Там (как и у меня :)) default policy ACCEPT, a будет DROP.

-A ufw-skip-to-policy-forward -j DROP
-A ufw-skip-to-policy-input -j DROP

-P DROP

j xxx ACCEPT

Edited by h3ll1

Share this post


Link to post
Share on other sites
On 4/30/2021 at 8:13 AM, taf_321 said:

Чисто IMHO

То что я понял, надо разбиратся с нфт. Есть iptables-nft правила и трансляции. Но и nft похож намного. Человек если хочет понять что там есть, он будет, если нет.

 

On 4/30/2021 at 8:13 AM, taf_321 said:

То же самое и с ipset

A как сделать с nft+tc (тут ipset+tc).

tc filter add ... src basic match "'nftSET src'"

 

Edited by h3ll1

Share this post


Link to post
Share on other sites
В 30.04.2021 в 19:20, alibek сказал:

Обычно это правило ставят как можно раньше — чтобы не прогонять по цепочке пакет, который уже был разрешен ранее.

И как результат ниже перечисленные запретительные правила не отрабатывают. Опять же мое IMHO, но лучше рисовать правила как для stateless FW. Оно как бы надежнее, и в случае чего можно достаточно просто отказаться от conntrack, ибо нужен он как неизбежное зло только для NAT.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this