Вкратце:
- 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)
- Часть III: установка Puppet Server (cfpuppetserver)
- Часть IV: централизованное управление (cftotalcontrol)
- Часть V: базы данных (cfdb)
- Часть VI: актуальные чёрные списки и защищённый стук
Лирическое вступление: так уж сложилось, что автор крайне параноидален на тему контроля развёрнутых систем и автоматизации. Долгие годы копился опыт встречаемых проблем и относительно местечковых решений. После ухода с предыдущего места работы стало понятно, что осязаемого багажа в сфере администрирования в общем-то и нет. Впрочем, то, что было, тащить с собой дальше особо и не хотелось. Так родился новый велосипед. А теперь к делу.
Примечание: По тексту целенаправленно используется фильтр или сетевой фильтр вместо другого "русского" термина брандмауэр, которое насилует неокрепший детский слух, глаза и мозг автора.
Чем не устраивают существующие решения?
- Слабая интеграция конфигурации сети и сетевого фильтра — дополнительная возня с (пере-)конфигурацией и высокий риск ошибок; сложности в создания 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,commentto_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!
# 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
конфигурация сети
Модуль сам не будет пытаться менять настройки сети на лету — это нужно будет делать ручками или рестартом.
# # 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
Заключение
Как видно, конфигурация сети и фильтра элементарна, чиста и лаконична, а самое главное удобна для изменений без горы магический чисел.
Пока нет длительной истории в боевом режиме. Обкатка проходит на паре реальных серверов и примерно десятке виртуалок без серьёзной нагрузки. Поэтому интересуют добровольцы, у которых разросся парк систем, а подход к администрированию ещё не успел подстроиться или же не до конца устраивает.
