CodingFuture + Puppet. Часть I: сеть и сетевой фильтр (cfnetwork + cffirehol)

    Вкратце:


    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

    Заключение


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


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

    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.