Apps: "Netfilter", "Iptables", "Docker".
Задача: Наладить посредством защитного экрана "Netfilter" ограничения доступа к сетевым сервисам как несущей операционной системы под управлением "Linux", так и к приложениям, запущенным в среде контейнеризации "Docker".
Ранее я уже рассказывал о простой настройке защитного экрана с помощью утилиты "Iptables". Применительно к случаю ограничения сетевого доступа к docker-контейнерам это не сработает. Дело в том, что для реализации функционала своей сложной сетевой подсистемы "Docker" использует "Netfilter", внедряя в таблице "nat" свои правила преобразований, которые перехватывают в цепи "PREROUTING" адресованный docker-контейнерам трафик, делая тем самым бессмысленными попытки фильтрации в типовой цепи "INPUT" таблицы "filter". Специально для того, чтобы пользователи "Docker" могли корректно фильтровать трафик, адресованный docker-контейнерам, в цепь "FORWARD" таблицы "filter" была добавлена подцепь "DOCKER-USER" - только туда можно добавлять свои правила, не опасаясь повредить работоспособности сервисов "Docker" (документация).
При том, что разработчики "Docker" предусмотрели специальную цепь для размещения в ней пользовательских правил - и это работает для тривиальных вариантов вроде ограничения по исходящим IP-адресам, - не разъяснённым в официальной документации оказалась ситуация с фильтрацией трафика к TCP/UDP-портам, которые автоматически транслируются посредством DNAT для docker-контейнеров, запущенных с опцией вроде "-p 8080:80". Попытка отфильтровать обращения к порту "8080" окажется безрезультатной, так как направленные к нему пакеты ещё до цепи "FORWARD" оказались перемаркированы в таблице "nat", а фильтровать обращения к порту "80" нерационально, так как при множестве запущенных docker-контейнеров это повлияет на них всех, что не является нашей целью, скорее всего. В описанной ситуации приходится для каждого фильтрующего правила извлекать из таблицы состояний "conntrack" оригинальный номер порта каждого пакета опциями "ctorigdstport" и "ctdir". Далее демонстрируется пример такого подхода.
Оптимизация журналирования событий "Netfilter".
Есть смысл заранее настроить вывод сообщений сетевого фильтра в отдельный журнальный файл, чтобы проще было впоследствии разбираться, что было зря заблокировано:
# vi /etc/rsyslog.d/00-iptables.conf
:msg,contains,"[netfilter] " -/var/log/iptables.log
& stop
& stop
Перезапускаем сервис журналирования для применения изменений в конфигурации:
# /etc/init.d/rsyslog restart
Предварительная проверка состояния "Netfilter".
Прежде всего нужно в обязательном порядке ознакомиться с текущим набором правил фильтрации сетевого трафика! Лично я это правило возвожу в ранг аксиомы.
Просмотр правил основной таблицы "filter", с цепями обработки входящего, транзитного и исходящего трафика:
# iptables -L -n -v --line-numbers
Просмотр правил таблиц трансляции и модификации:
# iptables -t nat -L -n -v --line-numbers
# iptables -t mangle -L -n -v --line-numbers
# iptables -t mangle -L -n -v --line-numbers
Скрипт задания пользовательской конфигурации защитного экрана.
# cd /usr/local/etc
# vi ./set-custom-iptables-rules.sh && chmod ug+x ./set-custom-iptables-rules.sh
# vi ./set-custom-iptables-rules.sh && chmod ug+x ./set-custom-iptables-rules.sh
#!/bin/bash
# Перечень подсетей предприятия
NET_LAN="10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
# Перечень IP-адресов группы серверов проекта "Custom"
NET_CUSTOM="10.20.30.40/32,10.30.40.50/32,192.168.10.20/32,172.16.20.30/32"
# Подсеть локального пула docker-контейнеров
NET_DOCKER="100.127.255.0/24"
# Сетевой интерфейс входящего трафика
# (он же обычно используется для исходящего трафика)
# IFIN="eth0"
IFIN="$(ip -4 route ls | grep default | grep -Po '(?<=dev )(\S+)')"
# Предохранение от сбоя выявления интерфейса
[ -z "${IFIN}" ] && exit 1
# Предварительно удаляем цепочки правил только нашего проекта "Custom"
iptables -F CUSTOM_HOST 2>/dev/null
iptables -F CUSTOM_DOCKER 2>/dev/null
iptables -D INPUT -j CUSTOM_HOST 2>/dev/null
iptables -D DOCKER-USER -j CUSTOM_DOCKER 2>/dev/null
iptables -X CUSTOM_HOST 2>/dev/null
iptables -X CUSTOM_DOCKER 2>/dev/null
# Создаём цепочки правил только для нашего проекта "Custom"
iptables -N CUSTOM_HOST
iptables -N CUSTOM_DOCKER
# Внедряем свою цепь правил в начало глобальной для входящего трафика
iptables -I INPUT 1 -j CUSTOM_HOST
# Внедряем свою цепь правил в начало специфичной для docker-трафика
# (https://docs.docker.com/network/iptables/)
iptables -I DOCKER-USER 1 -j CUSTOM_DOCKER
# --- # Operating system access rules:
# Явно обрубаем все соединения, не укладывающиеся в логику защитного экрана
iptables -A CUSTOM_HOST -m conntrack --ctstate INVALID -j DROP
# Разрешаем прохождение пакетов в уже установленных соединениях и создание ответных
iptables -A CUSTOM_HOST -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Разрешаем входящие соединения от "локальной петли"
iptables -A CUSTOM_HOST -i lo -j ACCEPT
# Разрешаем входящие SSH-подключения
iptables -A CUSTOM_HOST -p TCP --dport 22 -j ACCEPT
# Разрешаем входящие ICMP ping-запросы
iptables -A CUSTOM_HOST -p ICMP --icmp-type echo-request -j ACCEPT
# Разрешаем входящие запросы от сервера мониторинга к zabbix-агенту
iptables -A CUSTOM_HOST -s 10.20.30.41/32 -p TCP --dport 10050 -j ACCEPT -m comment --comment "Zabbix"
# Разрешаем входящие запросы от сервера резервного копирования к Bacula-агенту
iptables -A CUSTOM_HOST -s 10.20.30.42/32 -p TCP --dport 9102 -j ACCEPT -m comment --comment "Bacula"
# Явно отвергаем все неразрешённые ранее входящие в операционную систему соединения
iptables -A CUSTOM_HOST -i ${IFIN} -j LOG --log-prefix "[netfilter] "
iptables -A CUSTOM_HOST -i ${IFIN} -j REJECT -m comment --comment "Reject all unapproved connections"
# --- # Docker containers access rules:
# Разрешаем прохождение пакетов в уже установленных соединениях и создание ответных
iptables -A CUSTOM_DOCKER -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Разрешаем входящие запросы от сервера управления к агенту Portainer
iptables -A CUSTOM_DOCKER -s 10.20.30.43/32,${NET_DOCKER} -p TCP -m conntrack --ctorigdstport 9991 --ctdir ORIGINAL -j ACCEPT -m comment --comment "Portainer Agent"
# Разрешаем входящие запросы от пользователей к Nginx
iptables -A CUSTOM_DOCKER -s ${NET_LAN},${NET_DOCKER} -p TCP -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL -j ACCEPT -m comment --comment "Nginx"
# Разрешаем входящие запросы от приложений к MongoDB
iptables -A CUSTOM_DOCKER -s ${NET_CUSTOM},${NET_DOCKER} -p TCP -m conntrack --ctorigdstport 27017 --ctdir ORIGINAL -j ACCEPT -m comment --comment "MongoDB"
# Явно отвергаем все неразрешённые ранее входящие в docker-подсистему соединения извне
iptables -A CUSTOM_DOCKER -i ${IFIN} -j LOG --log-prefix "[netfilter] "
iptables -A CUSTOM_DOCKER -i ${IFIN} -j REJECT -m comment --comment "Reject all unapproved connections"
exit $?
# Перечень подсетей предприятия
NET_LAN="10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
# Перечень IP-адресов группы серверов проекта "Custom"
NET_CUSTOM="10.20.30.40/32,10.30.40.50/32,192.168.10.20/32,172.16.20.30/32"
# Подсеть локального пула docker-контейнеров
NET_DOCKER="100.127.255.0/24"
# Сетевой интерфейс входящего трафика
# (он же обычно используется для исходящего трафика)
# IFIN="eth0"
IFIN="$(ip -4 route ls | grep default | grep -Po '(?<=dev )(\S+)')"
# Предохранение от сбоя выявления интерфейса
[ -z "${IFIN}" ] && exit 1
# Предварительно удаляем цепочки правил только нашего проекта "Custom"
iptables -F CUSTOM_HOST 2>/dev/null
iptables -F CUSTOM_DOCKER 2>/dev/null
iptables -D INPUT -j CUSTOM_HOST 2>/dev/null
iptables -D DOCKER-USER -j CUSTOM_DOCKER 2>/dev/null
iptables -X CUSTOM_HOST 2>/dev/null
iptables -X CUSTOM_DOCKER 2>/dev/null
# Создаём цепочки правил только для нашего проекта "Custom"
iptables -N CUSTOM_HOST
iptables -N CUSTOM_DOCKER
# Внедряем свою цепь правил в начало глобальной для входящего трафика
iptables -I INPUT 1 -j CUSTOM_HOST
# Внедряем свою цепь правил в начало специфичной для docker-трафика
# (https://docs.docker.com/network/iptables/)
iptables -I DOCKER-USER 1 -j CUSTOM_DOCKER
# --- # Operating system access rules:
# Явно обрубаем все соединения, не укладывающиеся в логику защитного экрана
iptables -A CUSTOM_HOST -m conntrack --ctstate INVALID -j DROP
# Разрешаем прохождение пакетов в уже установленных соединениях и создание ответных
iptables -A CUSTOM_HOST -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Разрешаем входящие соединения от "локальной петли"
iptables -A CUSTOM_HOST -i lo -j ACCEPT
# Разрешаем входящие SSH-подключения
iptables -A CUSTOM_HOST -p TCP --dport 22 -j ACCEPT
# Разрешаем входящие ICMP ping-запросы
iptables -A CUSTOM_HOST -p ICMP --icmp-type echo-request -j ACCEPT
# Разрешаем входящие запросы от сервера мониторинга к zabbix-агенту
iptables -A CUSTOM_HOST -s 10.20.30.41/32 -p TCP --dport 10050 -j ACCEPT -m comment --comment "Zabbix"
# Разрешаем входящие запросы от сервера резервного копирования к Bacula-агенту
iptables -A CUSTOM_HOST -s 10.20.30.42/32 -p TCP --dport 9102 -j ACCEPT -m comment --comment "Bacula"
# Явно отвергаем все неразрешённые ранее входящие в операционную систему соединения
iptables -A CUSTOM_HOST -i ${IFIN} -j LOG --log-prefix "[netfilter] "
iptables -A CUSTOM_HOST -i ${IFIN} -j REJECT -m comment --comment "Reject all unapproved connections"
# --- # Docker containers access rules:
# Разрешаем прохождение пакетов в уже установленных соединениях и создание ответных
iptables -A CUSTOM_DOCKER -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Разрешаем входящие запросы от сервера управления к агенту Portainer
iptables -A CUSTOM_DOCKER -s 10.20.30.43/32,${NET_DOCKER} -p TCP -m conntrack --ctorigdstport 9991 --ctdir ORIGINAL -j ACCEPT -m comment --comment "Portainer Agent"
# Разрешаем входящие запросы от пользователей к Nginx
iptables -A CUSTOM_DOCKER -s ${NET_LAN},${NET_DOCKER} -p TCP -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL -j ACCEPT -m comment --comment "Nginx"
# Разрешаем входящие запросы от приложений к MongoDB
iptables -A CUSTOM_DOCKER -s ${NET_CUSTOM},${NET_DOCKER} -p TCP -m conntrack --ctorigdstport 27017 --ctdir ORIGINAL -j ACCEPT -m comment --comment "MongoDB"
# Явно отвергаем все неразрешённые ранее входящие в docker-подсистему соединения извне
iptables -A CUSTOM_DOCKER -i ${IFIN} -j LOG --log-prefix "[netfilter] "
iptables -A CUSTOM_DOCKER -i ${IFIN} -j REJECT -m comment --comment "Reject all unapproved connections"
exit $?
При корректном описании правил фильтрации многократный запуск скрипта безвреден:
# /usr/local/etc/set-custom-iptables-rules.sh
Сохранение и применение правил.
Учитывая то, что "Docker" распределяет трафик между своими контейнерами посредством "Iptables", для сохранения вышеописанных правил фильтрации мы не можем применить инструмент вроде "iptables-persistent", так как при запуске несущей операционной системы он будет восстанавливать все предыдущие состояния, что повредит подсистеме "Docker", вероятно уже до этого сформировавшей свои таблицы управления трафика.
Нужно сделать так, чтобы скрипт добавления наших правил запускался автоматически после старта операционной системы и подсистемы контейнеризации "Docker". Ранее это делалось через скрипты "/etc/rc.local" подсистемы "System-V", но в нынешней эпохе принято идти путём регистрации короткоживущей службы "Systemd":
# vi /etc/systemd/system/set-custom-iptables-rules.service
[Unit]
Description=Service script of adding custom rules for filtering of network traffic
Requires=network.target docker.service containerd.service
After=docker.service
[Service]
ExecStart=/usr/local/etc/set-custom-iptables-rules.sh
ExecReload=/usr/local/etc/set-custom-iptables-rules.sh
Restart=/usr/local/etc/set-custom-iptables-rules.sh
Type=oneshot
RemainAfterExit=yes
[Install]
WantedBy=default.target
Description=Service script of adding custom rules for filtering of network traffic
Requires=network.target docker.service containerd.service
After=docker.service
[Service]
ExecStart=/usr/local/etc/set-custom-iptables-rules.sh
ExecReload=/usr/local/etc/set-custom-iptables-rules.sh
Restart=/usr/local/etc/set-custom-iptables-rules.sh
Type=oneshot
RemainAfterExit=yes
[Install]
WantedBy=default.target
# systemctl daemon-reload
# systemctl enable set-custom-iptables-rules.service
# systemctl start set-custom-iptables-rules
# systemctl status set-custom-iptables-rules
# systemctl enable set-custom-iptables-rules.service
# systemctl start set-custom-iptables-rules
# systemctl status set-custom-iptables-rules
Наладка ротации файлов журнала событий.
В системах, несущих десятки docker-контейнеров с разномастными сервисами, при попытке ограничения к таковым сетевого доступа наверняка окажется заблокировано что-то нужное и полезное, уведомления о чём будут попадать в журнал событий, которые мы преднастроили в самом начале этой публикации. Так вот, файл журнала будет расти быстро, и его желательно вовремя усекать и ротировать:
# vi /etc/logrotate.d/iptables
/var/log/iptables.log {
size 30M
missingok
notifempty
rotate 5
compress
delaycompress
copytruncate
su root root
}
size 30M
missingok
notifempty
rotate 5
compress
delaycompress
copytruncate
su root root
}
Проверяем корректность конфигурации, не воздействуя при этом на файлы журналов:
# logrotate -d /etc/logrotate.d/iptables