Вкратце:
- cfnetwork — Puppet API для полной настройки сети и фильтра через ресурсы Puppet. Идеально дружит с Hiera и потенциально другими "data providers" в концепции Puppet.
- cffirehol — "meta-provider" конкретной реализации настройки фильтра для
cfnetwork
на базе замечательного генератора FireHOL- Пока поддерживаются только Debian 8+ (Jessie и выше) и Ubuntu 14.04+ (Trusty и выше)
Тематический цикл:
- Часть I: сеть и сетевой фильтр (cfnetwork + cffirehol)
- Часть II: доступ и стандартное окружение (cfauth + cfsystem)
Лирическое вступление: так уж сложилось, что автор крайне параноидален на тему контроля развёрнутых систем и автоматизации. Долгие годы копился опыт встречаемых проблем и относительно местечковых решений. После ухода с предыдущего места работы стало понятно, что осязаемого багажа в сфере администрирования в общем-то и нет. Впрочем, то, что было, тащить с собой дальше особо и не хотелось. Так родился новый велосипед. А теперь к делу.
Примечание: По тексту целенаправленно используется
фильтр
или сетевой фильтр
вместо другого "русского" термина брандмауэр
, которое насилует неокрепший детский слух, глаза и мозг автора.Чем не устраивают существующие решения?
- Слабая интеграция конфигурации сети и сетевого фильтра — дополнительная возня с (пере-)конфигурацией и высокий риск ошибок; сложности в создания plug&play модулей отдельных сервисов, которые дружат с системой безопасности.
- Отсутствие наглядности для аудита — конфигурация либо не лаконична и размазана по многим файлам, либо вообще существует в нечитаемом для человека виде.
- Излишне низкоуровневая конфигурация фильтра без абстракций — те же проблемы, что и пунктом выше. Можно сравнить с написанием веб-странички на ассемблере.
- Отсутствие "интеллектуальности" установок по умолчанию — для некоторых топорность является преимуществом, но не для автора.
- Настройку сетевого стека вообще обходят стороной — ванильные настройки ядра далеко не всегда то, что вы хотите видеть на боевой системе даже до начала тестирования и оптимизации, не говоря уже о безопасности.
Общая концепция настройки сети и фильтра
Никаких новых теорий — обыденность из разных мест.
- Каждый логических сетевой интерфейс имеет уникальное значимое имя вроде
world
,dmz
,office
и т.д.local
зарезервировано дляloopback
интерфейса. - Настройки стандартных логических интерфейсов задаются и доступны генератору правил фильтра.
- Поддержка особого типа интерфейса
any
— генератор фильтра должен быть достаточно интеллектуальным чтобы не плодить лишние правила там, где они никогда не сработают. К примеру, если для исходящих или входящих задан список разрешённых адресов, то правила не должны добавиться на интерфейсы, где такие соединения не предполагаются конфигурацией сети в принципе. - Вместо прямого указания портов, используется ассоциативное имя, за которым может скрываться целый набор портов и протоколов.
- Временная донастройка сети и фильтра должна легко производиться на целевой машине без централизованного управления при восстановлении после сбоев.
- Достаточная сетевая безопасность должна быть достигнута ещё до включения AppArmor или SeLinux.
- Динамическая защита должна быть реализована отдельно, но интерфейс черных списков задаётся на этом уровне.
Выбор технологий
- Puppet 4 + Puppet DB + Hiera — автор честно пытался привить себе любовь хотя бы к одному из Ansible и Chef, но четвёртая версия Puppet взяла своё. Хотя, Ansible выглядит интересным для периодических задач по содержанию систем и изначальному развёртывания Puppet.
- Ruby — по сути предопределённый выбор для расширений Puppet. К слову, автору пришлось изучить этот ЯП в ходе проекта, о чём совершенно не жалеет.
- FireHOL — это первый сторонний генератор
iptables
, которому автор смог доверить свой сетевой фильтр за более чем 10 лет активного администрирования серверов. Все остальные генераторы субъективно меркнут.
Что получилось
Сам интерфейс состоит из основного класса
cfnetwork
и набора типов cfnetwork::*
для задания настроек сети и сетевого фильтра. Есть возможность задавать все настройки программно через Puppet DSL или же через поставщика данных вроде Hiera.Краткое описание API с неполным списком параметров. С полным можно ознакомиться на английском языке.
класс cfnetwork
main
— настройки типаcfnetwork::iface
для основного интерфейса.dns
— список DNS серверов или специальные значения:
'$recurse'
— поставить локальный сервер.'$serve'
— то же самое, но и ещё и обслуживать клиентов на$service_face
.
is_router
— выполняет ли эта машина функцию сетевого маршрутизатора.optimize_10gbe
— подогнать настройки TCP по умолчанию для максимальной производительности соединений через 10+Gbit интерфейсы вместо публичных "интернетных" с ориентировочной задержкой в 50-100ms.- Удобства для использования Hiera.
Все значения, кромеifaces
имеютlookup_options: { merge: hash }
(документация).
ifaces
— набор конфигураций второстепенных интерфейсов типаcfnetwork::iface
.describe_services
— набор описаний ресурсов типаcfnetwork::describe_service
(описание сервисов).service_ports
— набор *cfnetwork::service_port
(входящие соединения).client_ports
— набор *cfnetwork::client_ports
(исходящие соединения).dnat_ports
— набор *cfnetwork::dnat_ports
.router_ports
— набор *cfnetwork::router_ports
.
тип cfnetwork::iface
— конфигурация интерфейса.
title
— ассоциативный идентификатор, который будет использоваться в других ресурсах.device
— системный сетевой интерфейс.address
— основной адрес IPv4/IPv6 вместе с маской сети в формате"address/cidr"
.extra_addresses
— дополнительные адреса в таком же формате.extra_routes
— дополнительные настройки маршрутизации (тоже важно для генератора фильтра).gateway
— подразумевает маршут по умолчанию, что используется в генераторе фильтра.force_public = auto
— крайне важная настройка для фильтра:
- По умолчанию, если
$address
принадлежит 10/8, 172.16/12 или 192.168/16, тоfalse
, иначеtrue
. - Если
true
:
- Автоматически добавляет SNAT или MASQUERADE для исходящих соединений.
- Автоматически включает TCP SYNPROXY для входящих соединений, включая DNAT.
- Ставит политику
DROP
вместоREJECT
по умолчанию. - Ограничивает входящие ping до 1/сек. через hashlimit для одного IP.
- Устанавливает глобальный чёрный список входящих IP, за исключение особого белого списка.
- По умолчанию, если
тип cfnetwork::describe_service
— описание сервиса (протоколов и портов).
title
— название ресурса используется по всех названиях портов.server
— список серверных портов в форматеproto/portnum
. Пример:[ 'tcp/80', 'tcp/443' ]
.
тип cfnetwork::client_port
— описание исходящего соединений.
Терминология взята из FireHOL..
title = '<iface>:<service>[:<tag>]'
src
,dst
,user
,group
,comment
тип cfnetwork::service_port
— описание входящего соединений.
title = '<iface>:<service>[:<tag>]'
src
,dst
,comment
тип cfnetwork::router_port
— описание разрешённого маршрутизируемого соединения.
title = '<iface>/<outface>:<service>[:<tag>]'
src
,dst
,comment
тип cfnetwork::dnat_port
— описание одновременно машрутизируемого соединения и трансляции адреса назначения
title = '<iface>/<outface>:<service>[:<tag>]'
src
,dst
,comment
to_dst
— адрес перенаправления (IPv4 и IPv6)to_port
— порт перенаправления (не обязательно)
Описание унифицированных параметров:
<iface>
— название ассоциированного ресурсаcfnetwork::iface
или же:
'local'
— как уже сказано выше — только локальный трафик, но учитывайте, что трафик на СВОЙ же внешний IP тоже идёт черезlocal
!'any'
— специальная замысловатая логина на базеsrc
,dst
иto_dst
чтобы не создавать заведомо неиспользуемые правила. При отсутствии этих параметров, добавляется на все возможные интерфейсы, где имеет смысл. (Например,local
не имеет смысла вrouter_port
)
<outface>
— то же самое, но для второго интерфейса в случае сdnat_port
иrouter_port
<service>
— название описания сервиса вcfnetwork::describe_service
<tag>
— необязательная часть, которая идёт вcomment
.
Добавлена для избежания конфликта имён ресурсов без необходимости явно использовать "virtual resources"src
,dst
— списки исходящих и целевых адресов IPv4/IPv6comment
— любой однострочный комментарий (принудительно вырезаются переводы строки)user
,group
— проверка пользователя и группы для исходящих соединений (настоящий параноик обязан их использовать даже дляlocal
)
класс cfnetwork::sysctl
— возможность тонкой настройки сетевого стека, стандартные ключи выведены в виде параметров класса.
класс cffirehol
— генератор фильтра
enable=false
— нужно принудительно включить после того, как убедитесь, что конфиг фильтра соответствует ожиданиямsynproxy_public=true
— флаг включения SYNPROXY на публичных интерфейсахip_whitelist
/ip_blacklist
— статические списки.ip_blacklist
не следует вообще задавать тут, а нужно запихивать динамически вipset
из постоянно обновляемых баз и систем динамической защиты, но это отдельная история.
Предопределённый наборы:
whitelist4
иwhitelist6
— IPv4 и IPv6 сети белого спискаblacklist4
— индивидуальные IPv4 адреса чёрного спискаblacklist4net
иblacklist6net
— IPv4 и IPv6 сети чёрного списка
Поскольку в Debian и Ubuntu не было достаточно свежего пакета FireHOL, и поскольку стандартные запускаются лишь ПОСЛЕ поднятия сетевых интерфейсов, пришлось сделать свои сборки .deb пакетов.
Примечание: в описании каждого Puppet модуля из серии
cfxxx
есть раздел "Implicitly created resources", где описываются все определяемые ресурсы сетевого фильтра.Живой пример
Полноценное развёртывание инфраструктуры в Vagrant, используя не освещённые в этой статье модули, можно посмотреть здесь.
Для наглядности приводится конфигурация сети и фильтра маршрутизатора:
настройки Hiera
classes:
- cfnetwork
# После того, как конфиг проверен через `/sbin/firehol try`
#cffirehol::enable: true
cfnetwork::is_router: true
cfnetwork::main:
device: eth1
address: '192.168.1.30/24'
extra_addresses: '192.168.1.40/24'
gateway: '192.168.1.1'
# принудительная имитация публичного интерфейса
force_public: true
cfnetwork::ifaces:
vagrant:
device: eth0
method: dhcp
# просто доводим до сведения
extra_routes: ['10.0.1.1/25']
infradmz:
device: eth2
address: '10.10.1.254/24'
dbdmz:
device: eth3
address: '10.10.2.254/24'
webdmz:
device: eth4
address: '10.10.2.254/24'
cfnetwork::describe_services:
testdb:
server: 'tcp/1234'
cfhttp:
server:
- 'tcp/80'
- 'tcp/443'
# DNAT для входящих HTTP соединений (не лучшее решение в боевом режиме)
cfnetwork::dnat_ports:
'main/webdmz:cfhttp':
dst: '192.168.1.40'
to_dst: '10.10.2.10'
cfnetwork::router_ports:
# Разрешить локальному NTP, DNS, APT стучаться во внешний мир
'infradmz/main:cfhttp:apt':
src: 'maint.example.com'
'infradmz/main:ntp':
src: 'maint.example.com'
# Разрешить Puppet Server (r10k) скачивать модули
'infradmz/main:cfhttp:puppet': {}
# Разрешить серверам из DMZ обращаться к инфраструктурным сервисам
'any/infradmz:ntp':
src: '10.10.0.0/16'
dst: 'maint.example.com'
'any/infradmz:dns':
src: '10.10.0.0/16'
dst: 'maint.example.com'
'any/infradmz:aptproxy':
src: '10.10.0.0/16'
dst: 'maint.example.com'
'any/infradmz:puppet':
src: '10.10.0.0/16'
dst: 'puppet.example.com'
# Разрешить веб серверам обращаться к базам данных
'webdmz/dbdmz:testdb': {}
сгенерированный конфиг генератора фильтра (именно так выходит)
Да, есть небольшой процент избыточности, но оптимизацию возможно провести в последующих версиях. Например, нет смысл иметь два правила, одно из которых частных случай другого. В принципе, код генерации уже оброс множеством "интеллектуальных" условий по мере внедрения в жизнь.
Это конфигурация вступает в силу автоматически при
cffirehol::enable=true
!/etc/firehol/firehol.conf
# This file is autogenerated by cffirehol Puppet Module
# Any changes made here may be overwritten at any time
version 6
# Defaults
#----------------
DEFAULT_INTERFACE_POLICY="DROP"
DEFAULT_ROUTER_POLICY="DROP"
FIREHOL_LOG_MODE="NFLOG"
FIREHOL_TRUST_LOOPBACK="0"
FIREHOL_DROP_ORPHAN_TCP_ACK_FIN="1"
FIREHOL_INPUT_ACTIVATION_POLICY="DROP"
FIREHOL_OUTPUT_ACTIVATION_POLICY="DROP"
FIREHOL_FORWARD_ACTIVATION_POLICY="DROP"
# Custom Services
#----------------
server_dns_ports="tcp/53 udp/53"
client_dns_ports="any"
# Use to open all TCP ports (e.g. for local)
server_alltcp_ports="tcp/1:65535"
client_alltcp_ports="any"
# Use to open all UDP ports (e.g. for local)
server_alludp_ports="udp/1:65535"
client_alludp_ports="any"
# Use to open all TCP and UDP ports (e.g. for local)
server_allports_ports="udp/1:65535 tcp/1:65535"
client_allports_ports="any"
server_cfhttp_ports="tcp/80 tcp/443"
client_cfhttp_ports="default"
server_testdb_ports="tcp/1234"
client_testdb_ports="default"
server_cfssh_ports="tcp/22"
client_cfssh_ports="default"
server_smtp_ports="tcp/25"
client_smtp_ports="default"
server_cfsmtp_ports="tcp/25 tcp/465 tcp/587"
client_cfsmtp_ports="default"
server_puppet_ports="tcp/8140"
client_puppet_ports="default"
# Setup of ipsets
#----------------
ipset4 create whitelist4 hash:net
ipset6 create whitelist6 hash:net
ipset4 create blacklist4 hash:ip
ipset4 create blacklist4net hash:net
ipset6 create blacklist6net hash:net
# note: hardcoded list is not expected to be large
ipset4 add whitelist4 "10.0.0.0/8"
# Protection on public-facing interfaces
#----------------
# main
blacklist4 input inface "eth1" ipset:blacklist4net ipset:blacklist4 except src ipset:whitelist4
blacklist6 input inface "eth1" ipset:blacklist6net ipset:blacklist6 except src ipset:whitelist6
iptables -t raw -N cfunroute_main
iptables -t raw -A cfunroute_main -s "10.0.0.0/8,172.16.0.0/12,224.0.0.0/4,127.0.0.1/8" -j DROP
iptables -t raw -A cfunroute_main -d "10.0.0.0/8,172.16.0.0/12,224.0.0.0/4,127.0.0.1/8" -j DROP
iptables -t raw -A PREROUTING -i "eth1" -j cfunroute_main
# cfauth:
synproxy4 input inface main dst "192.168.1.30/24" dport "22" src "192.168.0.0/16" accept
synproxy4 forward inface main dst "192.168.1.40" dport "80" dnat to "10.10.2.10"
synproxy4 forward inface main dst "192.168.1.40" dport "443" dnat to "10.10.2.10"
iptables -t nat -N cfpost_snat_main
iptables -t nat -A cfpost_snat_main -s 192.168.1.30,192.168.1.40 -j RETURN
iptables -t nat -A cfpost_snat_main -j SNAT --to-source=192.168.1.30
iptables -t nat -A POSTROUTING -o "eth1" -j cfpost_snat_main
# vagrant
blacklist4 input inface "eth0" ipset:blacklist4net ipset:blacklist4 except src ipset:whitelist4
blacklist6 input inface "eth0" ipset:blacklist6net ipset:blacklist6 except src ipset:whitelist6
# cfauth:
iptables -t nat -A POSTROUTING -o "eth0" -j MASQUERADE
# Custom Headers
#----------------
# NAT
#----------------
dnat4 to "10.10.2.10" inface "eth1" proto "tcp" dport "80" dst "192.168.1.40"
dnat4 to "10.10.2.10" inface "eth1" proto "tcp" dport "443" dst "192.168.1.40"
# Interfaces
#----------------
interface "eth1" "main"
policy deny
protection bad-packets
client icmp accept
server4 ping accept with hashlimit ping upto 1/s burst 2
# cfauth:
server4 "cfssh" accept src "192.168.0.0/16"
# cfsystem:
client "http" accept uid "root"
# cfsystem:
client "https" accept uid "root"
# cfsystem:
client "ntp" accept uid "root ntpd"
# cfsystem:
client "cfsmtp" accept uid "root Debian-exim"
# cfsystem:
client "puppet" accept uid "root"
# cfnetwork:
client "dns" accept
interface "eth0" "vagrant"
policy deny
protection bad-packets
client icmp accept
server4 ping accept with hashlimit ping upto 1/s burst 2
# cfauth:
server4 "cfssh" accept src "10.0.0.0/8 192.168.0.0/16 172.16.0.0/12"
# cfsystem:
client "http" accept uid "root"
# cfsystem:
client "https" accept uid "root"
# cfsystem:
client "ntp" accept uid "root ntpd"
# cfsystem:
client "cfsmtp" accept uid "root Debian-exim"
# cfsystem:
client "puppet" accept uid "root"
# cfnetwork:
client4 "dns" accept dst "10.10.1.10"
interface "eth2" "infradmz"
policy reject
client icmp accept
server icmp accept
# cfauth:
server4 "cfssh" accept src "10.0.0.0/8"
# cfsystem:
client "http" accept uid "root"
# cfsystem:
client "https" accept uid "root"
# cfsystem:
client "ntp" accept uid "root ntpd"
# cfsystem:
client "cfsmtp" accept uid "root Debian-exim"
# cfsystem:
client "puppet" accept uid "root"
# cfnetwork:
client4 "dns" accept dst "10.10.1.10"
interface "eth3" "dbdmz"
policy reject
client icmp accept
server icmp accept
# cfauth:
server4 "cfssh" accept src "10.0.0.0/8"
# cfsystem:
client "http" accept uid "root"
# cfsystem:
client "https" accept uid "root"
# cfsystem:
client "ntp" accept uid "root ntpd"
# cfsystem:
client "cfsmtp" accept uid "root Debian-exim"
# cfsystem:
client "puppet" accept uid "root"
interface "eth4" "webdmz"
policy reject
client icmp accept
server icmp accept
# cfauth:
server4 "cfssh" accept src "10.0.0.0/8"
# cfsystem:
client "http" accept uid "root"
# cfsystem:
client "https" accept uid "root"
# cfsystem:
client "ntp" accept uid "root ntpd"
# cfsystem:
client "cfsmtp" accept uid "root Debian-exim"
# cfsystem:
client "puppet" accept uid "root"
interface "lo" "local"
policy reject
client icmp accept
server icmp accept
# cfauth:
server4 "cfssh" accept src "10.0.0.0/8 192.168.0.0/16"
# cfsystem:
client "http" accept uid "root"
# cfsystem:
client "https" accept uid "root"
# cfsystem:
client "ntp" accept uid "root ntpd"
# cfsystem:
server "smtp" accept
# cfsystem:
client "smtp" accept
# cfsystem:
client "cfsmtp" accept uid "root Debian-exim"
# cfsystem:
client "puppet" accept uid "root"
# Routers
#----------------
router "main_infradmz" inface "eth1" outface "eth2"
policy drop
client icmp accept
# apt:
client4 "cfhttp" accept src "10.10.1.10"
client4 "ntp" accept src "10.10.1.10"
# puppet:
client "cfhttp" accept
router "main_webdmz" inface "eth1" outface "eth4"
policy drop
client icmp accept
server4 "cfhttp" accept dst "10.10.2.10"
router "vagrant_infradmz" inface "eth0" outface "eth2"
policy drop
client icmp accept
server4 "ntp" accept dst "10.10.1.10" src "10.10.0.0/8"
server4 "dns" accept dst "10.10.1.10" src "10.10.0.0/8"
server4 "aptproxy" accept dst "10.10.1.10" src "10.10.0.0/8"
server4 "puppet" accept dst "10.10.1.11" src "10.10.0.0/8"
router "infradmz_infradmz" inface "eth2" outface "eth2"
policy reject
server icmp accept
client icmp accept
server4 "ntp" accept dst "10.10.1.10" src "10.10.0.0/8"
server4 "dns" accept dst "10.10.1.10" src "10.10.0.0/8"
server4 "aptproxy" accept dst "10.10.1.10" src "10.10.0.0/8"
server4 "puppet" accept dst "10.10.1.11" src "10.10.0.0/8"
router "dbdmz_infradmz" inface "eth3" outface "eth2"
policy reject
server icmp accept
client icmp accept
server4 "ntp" accept dst "10.10.1.10" src "10.10.0.0/8"
server4 "dns" accept dst "10.10.1.10" src "10.10.0.0/8"
server4 "aptproxy" accept dst "10.10.1.10" src "10.10.0.0/8"
server4 "puppet" accept dst "10.10.1.11" src "10.10.0.0/8"
router "webdmz_infradmz" inface "eth4" outface "eth2"
policy reject
server icmp accept
client icmp accept
server4 "ntp" accept dst "10.10.1.10" src "10.10.0.0/8"
server4 "dns" accept dst "10.10.1.10" src "10.10.0.0/8"
server4 "aptproxy" accept dst "10.10.1.10" src "10.10.0.0/8"
server4 "puppet" accept dst "10.10.1.11" src "10.10.0.0/8"
router "webdmz_dbdmz" inface "eth4" outface "eth3"
policy reject
server icmp accept
client icmp accept
server "testdb" accept
конфигурация сети
Модуль сам не будет пытаться менять настройки сети на лету — это нужно будет делать ручками или рестартом.
/etc/network/interfaces.d/*
#
# Generated by cfnetwork::iface puppet module
#
auto lo
iface lo inet loopback
source /etc/network/interfaces.d/*
#
# Generated by cfnetwork::iface puppet module
#
auto eth3
iface eth3 inet static
address 10.10.2.254
netmask 24
up sysctl --ignore net.ipv6.conf.eth3.disable_ipv6=1
#
# Generated by cfnetwork::iface puppet module
#
auto eth2
iface eth2 inet static
address 10.10.1.254
netmask 24
up sysctl --ignore net.ipv6.conf.eth2.disable_ipv6=1
#
# Generated by cfnetwork::iface puppet module
#
auto eth1
iface eth1 inet static
address 192.168.1.30
netmask 24
gateway 192.168.1.1
dns-nameservers 10.10.1.10
dns-search example.com
up ip addr add 192.168.1.40/24 dev eth1
up sysctl --ignore net.ipv6.conf.eth1.disable_ipv6=1
#
# Generated by cfnetwork::iface puppet module
#
auto eth0
iface eth0 inet dhcp
netmask 255.255.255.0
up ip route add 10.0.1.1/25 dev eth0
up sysctl --ignore net.ipv6.conf.eth0.disable_ipv6=1
#
# Generated by cfnetwork::iface puppet module
#
auto eth4
iface eth4 inet static
address 10.10.2.254
netmask 24
up sysctl --ignore net.ipv6.conf.eth4.disable_ipv6=1
Заключение
Как видно, конфигурация сети и фильтра элементарна, чиста и лаконична, а самое главное удобна для изменений без горы магический чисел.
Пока нет длительной истории в боевом режиме. Обкатка проходит на паре реальных серверов и примерно десятке виртуалок без серьёзной нагрузки. Поэтому интересуют добровольцы, у которых разросся парк систем, а подход к администрированию ещё не успел подстроиться или же не до конца устраивает.