Вкратце:


  1. cfnetworkPuppet API для полной настройки сети и фильтра через ресурсы Puppet. Идеально дружит с Hiera и потенциально другими "data providers" в концепции Puppet.
  2. cffirehol — "meta-provider" конкретной реализации настройки фильтра для cfnetwork на базе замечательного генератора FireHOL
  3. Пока поддерживаются только Debian 8+ (Jessie и выше) и Ubuntu 14.04+ (Trusty и выше)


Тематический цикл:



Лирическое вступление: так уж сложилось, что автор крайне параноидален на тему контроля развёрнутых систем и автоматизации. Долгие годы копился опыт встречаемых проблем и относительно местечковых решений. После ухода с предыдущего места работы стало понятно, что осязаемого багажа в сфере администрирования в общем-то и нет. Впрочем, то, что было, тащить с собой дальше особо и не хотелось. Так родился новый велосипед. А теперь к делу.

Примечание: По тексту целенаправленно используется фильтр или сетевой фильтр вместо другого "русского" термина брандмауэр, которое насилует неокрепший детский слух, глаза и мозг автора.

Чем не устраивают существующие решения?


  • Слабая интеграция конфигурации сети и сетевого фильтра — дополнительная возня с (пере-)конфигурацией и высокий риск ошибок; сложности в создания plug&play модулей отдельных сервисов, которые дружат с системой безопасности.
  • Отсутствие наглядности для аудита — конфигурация либо не лаконична и размазана по многим файлам, либо вообще существует в нечитаемом для человека виде.
  • Излишне низкоуровневая конфигурация фильтра без абстракций — те же проблемы, что и пунктом выше. Можно сравнить с написанием веб-странички на ассемблере.
  • Отсутствие "интеллектуальности" установок по умолчанию — для некоторых топорность является преимуществом, но не для автора.
  • Настройку сетевого стека вообще обходят стороной — ванильные настройки ядра далеко не всегда то, что вы хотите видеть на боевой системе даже до начала тестирования и оптимизации, не говоря уже о безопасности.

Общая концепция настройки сети и фильтра


Никаких новых теорий — обыденность из разных мест.

  1. Каждый логических сетевой интерфейс имеет уникальное значимое имя вроде world, dmz, office и т.д. local зарезервировано для loopback интерфейса.
  2. Настройки стандартных логических интерфейсов задаются и доступны генератору правил фильтра.
  3. Поддержка особого типа интерфейса any — генератор фильтра должен быть достаточно интеллектуальным чтобы не плодить лишние правила там, где они никогда не сработают. К примеру, если для исходящих или входящих задан список разрешённых адресов, то правила не должны добавиться на интерфейсы, где такие соединения не предполагаются конфигурацией сети в принципе.
  4. Вместо прямого указания портов, используется ассоциативное имя, за которым может скрываться целый набор портов и протоколов.
  5. Временная донастройка сети и фильтра должна легко производиться на целевой машине без централизованного управления при восстановлении после сбоев.
  6. Достаточная сетевая безопасность должна быть достигнута ещё до включения AppArmor или SeLinux.
  7. Динамическая защита должна быть реализована отдельно, но интерфейс черных списков задаётся на этом уровне.

Выбор технологий


  • 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/IPv6
  • comment — любой однострочный комментарий (принудительно вырезаются переводы строки)
  • 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


Заключение


Как видно, конфигурация сети и фильтра элементарна, чиста и лаконична, а самое главное удобна для изменений без горы магический чисел.

Пока нет длительной истории в боевом режиме. Обкатка проходит на паре реальных серверов и примерно десятке виртуалок без серьёзной нагрузки. Поэтому интересуют добровольцы, у которых разросся парк систем, а подход к администрированию ещё не успел подстроиться или же не до конца устраивает.

Комментарии (0)