🏠: работа

HAProxy и компания

«Болото с ужасной быстротой засасывало нас глубже и глубже. Вот уже всё туловище моего коня скрылось в зловонной грязи, вот уже и моя голова стала погружаться в болото, и оттуда торчит лишь косичка моего парика.
Что было делать? Мы непременно погибли бы, если бы не удивительная сила моих рук. Я страшный силач. Схватив себя за эту косичку, я изо всех сил дёрнул вверх и без большого труда вытащил из болота и себя, и своего коня, которого крепко сжал обеими ногами, как щипцами.»

Рудольф Эрих Распэ «Приключения барона Мюнхгаузена»

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

Изначально был очень старый веб-сервер, на котором крутились сайты (Ubuntu 8.04 x86, Apache 2.2, PHP 5.2). Веб-сервер этот стоял за WAF (web application firewall), который сам по себе был старым и, естественно, имел множество уязвимостей. WAF был выставлен в интернет за шлюзом, который просто пробрасывал порты, т. е., WAF был начальной точкой входа, что само по себе неправильно.

Так как при проведении более-менее многолюдных мероприятий в организации и соответственном росте количества запросов к сайтам начинались тормоза и зависания — а иногда это было и на ровном месте — хотелось выяснить, почему это происходит, при какой нагрузке, собрать какую-то статистику. В WAF в качестве проксирующего сервиса используется nginx, и я обратился в их техподдержку с просьбой поставить на nginx модуль ngx_http_stub_status_module, с помощью которого можно было бы собирать данные о количестве соединений и запросов и передавать их в Zabbix.

Через некоторое время я получил ответ, что версия nginx на нашем сервере очень старая и установить этот модуль не представляется возможным. Также мне сообщили, что nginx выполняет сугубо прикладную функцию и настраивать его более изощрённо для реализации какой-то защиты и тонких настроек реверс-проксирования не планируется даже при наличии более новой версии WAF. Тогда я решил оставить WAF в покое и сделать нормальный сервис реверс-прокси и балансировки на базе HAProxy.

Целью было сделать так, чтобы на WAF и внутренние сервера поступал уже более-менее отфильтрованный трафик, чтобы они не перегружались и выполняли свою работу, не отвлекаясь на обработку мусорных запросов, заодно терминировать HTTPS до WAF (т. е., держать сертификаты SSL на HAProxy), иметь возможность оперативно пускать трафик мимо WAF и вообще, как-то улучшить контроль над системой и её отказоустойчивость, настроить мониторинг и собирать статистику. Вторым этапом нужно было обновить сам WAF, но пока выключить его без простоя было невозможно и заместить его было пока нечем. Тем не менее, разработчики WAF обещали сделать кластер из двух узлов, когда настанет подходящее для этого время.

После довольно долгой работы по сбору информации, планирования и реализации получилось следующее:

Виртуальные IP для HAProxy обеспечивает Keepalived. Суть в том, что клиенты обращаются не на реальный адрес сервера реверс-прокси/балансировщика, а на виртуальный, привязанный к сетевым адаптерам нескольких серверов. Трафик идёт на адаптер того сервера, который имеет наибольший вес. Если сервер не отвечает, то трафик начинает идти на другой сервер, привязанный к тому же виртуальному IP. Виртуальных IP-адреса я сделал два, так как балансировщик обслуживает и внутренних клиентов, обращающихся на внутренние же ресурсы.

У Keepalived обнаружилась отличная функция: трафик переключается на резервный сервер не только когда основной недоступен целиком, но и когда на нём перестаёт работать служба HAProxy. Вот кусок конфигурации /etc/keepalived/keepalived.conf:

vrrp_script chk_haproxy {
  script "/usr/bin/killall -0 haproxy"
  interval 3
  weight 50
}
vrrp_instance haproxy_DMZ {
  interface eth2
  virtual_router_id 1
  priority 100
  authentication {
    auth_type PASS
    auth_pass verySecretPasswordHere
  }
  virtual_ipaddress {
    192.168.1.100/24
  }
  track_script {
    chk_haproxy
  }
# smtp_alert
}

В данном случае к приоритету virtual_router_id добавляется 50, если служба работает. На первом сервере начальный приоритет 100, на втором — 90, соответственно, при работающей службе HAProxy на обоих серверах это 150 и 140. Когда на первом сервере служба перестаёт работать, то общий вес становится 100 и трафик начинает идти на второй сервер. При восстановлении работы службы на первом сервере всё возвращается в исходное состояние. Keepalived ещё умеет слать письма при изменении статуса, но он меня так заваливал письмами во время отладки, что я отключил это дело, тем более, что позже я настроил балансировку почтового трафика Exchange, SMTP-порт стал занятым и просто так слать письма с сервера реверс-прокси уже не получалось в любом случае, поэтому я просто удалил Postfix и занялся другими вещами, к тому же, переключение и так работает без проблем.

Теперь, собственно, о HAProxy. Основные источники информации — блог на haproxy.com с меткой Basics, статья там же о защите от DDoS-атак и, конечно, документация. Что я там вкратце реализовал:

  • HTTPS-соединение идёт только по протоколам TLS 1.2 и TLS 1.3, на этот счёт есть очень удобный Mozilla SSL Configuration Generator.
  • Разрешены только HTTP/1.1 и HTTP/2.0.
  • Запрещены подключения без указания имён хостов (кроме почтовых URL).
  • Если IP создаёт больше 480 соединений за минуту или запрашивает один и тот же URL (часть до знака вопроса) больше 30 раз за 30 секунд, то ему выдаётся код 429 Too many requests (см. статью Introduction to HAProxy Stick Tables). Исключение — внутренние сети компании.
  • Запрещено обращение на URL, где после наклонной черты идёт точка, например, https://example.com/site/.default, за некоторыми исключениями.
  • Всё, кроме нескольких исключений, перенаправляется на HTTPS.
  • Перенаправления, выбор бэкендов, исключения из HTTPS, чёрные списки реализованы с помощью карт и списков доступа.

С переводом трафика Exchange через реверс-прокси возникли некоторые сложности — оказалось, что Outlook на Windows 7 не может подключиться к почтовому серверу, так как на старых ОС используются устаревшие протоколы шифрования, которые на моём HAProxy запрещены. Нужно ставить патч KB3140245 и править реестр (или ставить MicrosoftEasyFix51044), после этого всё работает нормально.

Также, для Outlook на HAProxy пришлось добавить список заголовков, так как без него Outlook не желал устанавливать соединение с сервером. Я вынес заголовки с отдельный файл и добавил их в /etc/haproxy/haproxy.cfg, после этого всё завелось:

global
  h1-case-adjust-file /etc/haproxy/headers.list
frontend fe_web
  bind :80
  bind :443 ssl crt /etc/ssl/certs/example alpn h2,http/1.1
  option h1-case-adjust-bogus-client # for Outlook clients

Другая проблема не решена до сих пор — при отправке писем скриптами из Powershell они не доходят до получателя, при этом в логах HAProxy сказано, что соединение неожиданно прервал клиент на этапе передачи данных. При этом, 1С отправляет письма без проблем. Пока приходится указывать в скриптах прямой адрес почтового сервера или править файл hosts.

В целом всё было настроено, кроме мониторинга, и я в рабочее время посматривал на таблицу соединений, чтобы примерно определить разумные пороги блокировок, когда в среду 13 апреля началась DDoS-атака:

Вывод таблицы с количеством запросов в минуту во время DDoS-атаки

В минуту было свыше миллиона запросов, заметная их часть использовала протокол HTTP неведомой версии 1.2 и такой же непонятный HTTP-метод ST. Все эти запросы всё равно не достигали цели, так как сильно превышали лимит подключений в минуту, HTTP/1.2 и так был запрещён, а метод ST я тут же поспешил заблокировать, так что до нижестоящих серверов этот вал не доходил и, в принципе, можно было оставить так и ждать, когда атака выдохнется, но всплыл неожиданный нюанс — так как всё это записывалось в логи, место на диске заканчивалось примерно за полдня. Стало понятно, что нужно ставить fail2ban для динамической блокировки IP-адресов с помощью встроенного линуксового файрволла netfilter, потому что чистить логи и переключаться с сервера на сервер вручную — занятие довольно нелепое.

Fail2ban — очень остроумная штука. Он смотрит в указанный лог и считает количество определённых строк, заданных регулярным выражением — например, сообщение об ошибке после неправильно введённого пароля — и если количество этих ошибок превышает заданное количество за отведённое время (положим, 5 неправильно набранных паролей за 10 минут), то fail2ban создаёт запрещающее правило на файрволле для IP-адреса этого клиента на заданное время (допустим, на 3 часа). По прошествии этого времени запрет снимается, и клиент опять может загрузить страницу и пытаться войти.

Настроив fail2ban, я с удивлением обнаружил, что блокировать адреса-то он начинает, но через короткое время по непонятной причине перестаёт это делать. Оказалось, что не я один столкнулся с такой проблемой, когда fail2ban просто не успевает обрабатывать эту Ниагару из логов, и я последовал советам — во-первых, установить самую свежую версию fail2ban вручную, так как версия в стандартных репозиториях отставала, а во-вторых, использовать nftables (интерфейс управления netfilter) вместо стандартного iptables, так как nftables быстрее. Получились такие настройки:

### /etc/fail2ban/filter.d/haproxy-ddos.conf ###

[INCLUDES]
before = common.conf

[Definition]
failregex = ^%(__prefix_line)s<ADDR>:\\d+.\*?\\sPR--\\s.\*$
ignoreregex = ^%(__prefix_line)smessage repeated

### /etc/fail2ban/jail.local ###

[DEFAULT]
ignoreip = 127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 ::1
banaction = nftables-multiport
banaction_allports = nftables-allports

[haproxy-ddos]
enabled  = true
filter   = haproxy-ddos
logpath  = /var/log/haproxy.log
bantime  = 6h
findtime = 1m
maxretry = 100

Вдобавок, я решил помочь fail2ban простеньким скриптом, срабатывающим каждую минуту — вывод списка адресов из таблицы HAProxy, которые генерируют пятизначные числа запросов и более, и сразу добавлять их в «тюрьму» fail2ban. Менее наглых он уже пристрелит сам.

for ip in $(echo "show table st_per_ip_rate" |socat stdio /var/run/haproxy/admin.sock |egrep "[0-9]{5}$" |cut -d '=' -f 2 |cut -d ' ' -f 1)
do
  fail2ban-client set haproxy-ddos banip $ip
done

После этого всё пошло как по маслу. Едва появившись, участники ботнета сразу же блокировались, логи перестали забивать диск и атака уже никак не влияла на работу системы. На пике было заблокировано немногим менее 5000 адресов.

Атака продлилась до вечера воскресенья 17 апреля, и к утру понедельника все адреса были автоматически разблокированы.

Через некоторое время я настроил мониторинг:

Панель HAProxy и fail2ban в Zabbix

Напоследок расскажу про сертификаты SSL. Одна из очень удобных вещей в HAProxy — автоматический выбор сертификатов. Просто указываешь в секции frontend каталог, где они лежат, и при заходе на сайт подключается подходящий действующий сертификат, просроченные игнорируются.

frontend fe_https
bind :443 ssl crt /etc/ssl/certs/haproxy alpn h2,http/1.1

Так как, в числе прочего, сейчас имеются проблемы с выпуском коммерческих SSL-сертификатов и время ожидания их выпуска увеличилось до месяца, то на днях возникла ситуация, когда срок действия сертификата на одном из сайтов истёк, а новый ещё не был выпущен. Решение проблемы — Let’s Encrypt, который в России пока ещё работает, но так как порты 80 и 443 уже заняты, Certbot (агент Let’s Encrypt по выпуску и обновлению сертификатов) должен работать на другом порту, для чего нужно настроить HAProxy:

# На фронтенде :80
  # Let's Encrypt URL
  acl letsencrypt_url path_beg /.well-known/acme-challenge/
  # Не пробрасывать на HTTPS
  http-request redirect scheme https if !{ ssl_fc } !no-https-domains !letsencrypt_url
  # Бэкенд Let's Encrypt
  use_backend be_letsencrypt if letsencrypt_url

# Бэкенд
  backend be_letsencrypt
    server letsencrypt 127.0.0.1:54321

После этого, установив Certbot, как рекомендуют, из пакета snap, выпускаем сертификат:

certbot certonly --standalone --preferred-challenges http-01 --http-01-port 54321 --keep --agree-tos --expand -m ssl@example.com -d example.com -d www.example.com

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Requesting a certificate for example.com and www.example.com

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/example.com/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/example.com/privkey.pem
This certificate expires on 2022-08-18.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If you like Certbot, please consider supporting our work by:
 * Donating to ISRG / Let`s Encrypt:   https://letsencrypt.org/donate
 * Donating to EFF:                    https://eff.org/donate-le
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

В принципе, всё, дальше Certbot будет сам обновлять сертификат, запуская периодические проверки, запланированные не в стандартном cron, а в systemd timers (отдельная интересная тема). Теперь осталось слепить сертификаты и закрытые ключи для использования в HAProxy:

#!/bin/bash

LE_CERT_DIR=/etc/letsencrypt/live
HAPROXY_CERT_DIR=/etc/ssl/certs/haproxy

# Cat the certificate chain and the private key together for haproxy
for path in $(find $LE_CERT_DIR/* -type d -exec basename {} \;); do
  cat $LE_CERT_DIR/$path/{fullchain,privkey}.pem > $HAPROXY_CERT_DIR/${path}.pem
done

До автоматизации создания готовых сертификатов для HAProxy и перечитывания его конфигурации для применения изменений руки пока не дошли, но я займусь этим в ближайшее время. В Certbot есть опция --deploy-hook, возможно, лучше всего работать с помощью неё.

На этом пока всё. Конечно, моя конфигурация неидеальна как с точки зрения архитектуры и безопасности, так и с практической стороны — нужно синхронизировать конфигурацию между узлами Haproxy, желательно передавать статус fail2ban на сервер-партнёр, а теперь ещё и синхронизировать сертификаты SSL. Тем не менее, сегодняшнее положение вещей гораздо лучше, чем то, с чего этот рассказ начинался.

WAF разработчики переустановили и настроили, теперь он тоже кластеризован, и трафик на него балансируется с HAProxy. А веб-сервер с сайтами тоже подновлён, насколько это возможно — переехали с помощью коллег на Ubuntu 14.04, более свежую ОС не позволяет ставить древняя система управления контентом, не работающая с PHP новее 5-й версии, но это хотя бы даёт совместимый с Hyper-V гигабитный сетевой интерфейс, который уже не виснет на ровном месте и не тормозит, так что всё к лучшему.

Всем добра и мирного неба!

Кластеры Hyper-V, порядок в домашних каталогах, SSO для DokuWiki

Долго не мог себя заставить что-то написать, но надо, а то забывается всё.

Построил два кластера Hyper-V

Базой послужили сервера, которые освободились в результате виртуализации сервисов, которые на них крутились, занимая в среднем 10% от их мощности. Теперь появилась возможность использовать ресурсы более рационально и добавить отказоустойчивости.

Первый кластер — продуктивный на немолодых, но вполне боеспособных серверах HP Proliant 380DL Gen8. Сейчас три узла, на каждом по 192 ГБ памяти, планируется ещё два добавить в следующем году, когда для них докупят память и серверные лицензии. В качестве ОС выступает Windows Server 2019 Datacenter. Подключены полки HP P2000 и NetApp AFF-220 с флеш-массивом.

Второй — тестовый на более новых ProLiant DL380 Gen9 — их всего два, поэтому продуктив на них строить нет смысла. ОС — бесплатная Windows Hyper-V Server 2019. Подключена вторая полка HP P2000. Там крутится всё тестовое барахло, которое не нужно резервно копировать.

PS C:\> get-vm -ComputerName (Get-ClusterNode -Cluster hvc)

Name           State   CPUUsage(%) MemoryAssigned(M) Uptime             Status             Version
----           -----   ----------- ----------------- ------             ------             -------
t-docker1      Running 0           4096              5.22:48:09.8380000 Работает нормально 9.0
t-sql2017-1    Running 0           8192              5.22:48:27.3800000 Работает нормально 9.0
t-w10-1        Off     0           0                 00:00:00           Работает нормально 9.0
t-w7-2         Off     0           0                 00:00:00           Работает нормально 9.0
t-w7-3         Off     0           0                 00:00:00           Работает нормально 9.0
vmls-bpm-exch  Running 0           4096              5.22:47:09.9290000 Работает нормально 9.0
vmls-haproxy1  Off     0           0                 00:00:00           Работает нормально 9.0
vmls-jibri1    Running 0           4096              5.22:48:16.5620000 Работает нормально 9.0
vmls-jitsi1    Running 0           16384             5.22:47:28.6320000 Работает нормально 9.0
vmls-lk-test   Running 0           4096              5.22:49:02.0820000 Работает нормально 9.0
vmws-trueconf1 Running 0           8192              5.22:47:28.2950000 Работает нормально 9.0
t-w11-1        Running 0           4096              5.22:13:14.2280000 Работает нормально 9.0
vmls-jibri2    Off     0           0                 00:00:00           Работает нормально 9.0
vmls-jibri3    Off     0           0                 00:00:00           Работает нормально 9.0
vmls-jibri4    Off     0           0                 00:00:00           Работает нормально 9.0
vmls-jibri5    Off     0           0                 00:00:00           Работает нормально 9.0
vmus-wp        Running 0           2048              5.22:13:49.3510000 Работает нормально 9.0
web2-dev2      Running 0           2048              5.22:13:49.2880000 Работает нормально 9.0

Что интересно, на некоторых серверах Gen8 кэш RAID-контроллера показывался в мониторинге как сбойный, вылечилось обновлением прошивки.

Освободилась куча жёстких дисков: так как все виртуалки теперь хранятся на полках и место на нодах нужно только для операционной системы и установочных образов, я пересоздал на всех серверах локальное хранилище, сделав там RAID1 + spare, а лишние диски выдернул. Теперь запас на замену солидный.

С течением времени Hyper-V мне нравится всё больше — мало того, что можно виртуальные машины переносить между нодами даже без наличия кластера, кластеры создавать на базе бесплатного Hyper-V Server и для его функционирования не нужен никакой платный управляющий сервер, как в случае с VMware, так с 2016 версии там появилась автобалансировка нагрузки, что для компаний малого и среднего размера делает фактически ненужными инструменты типа Virtual Machine Manager. В принципе, даже без этого механизма примитивный балансировщик можно написать и самому, благо, Hyper-V прекрасно управляется через Powershell.

В следующем году, если всё будет нормально, купят ещё и устройство проброса USB-токенов по сети, тогда можно будет перенести последний бастион VMware 5.5, где живёт 1С и ему подобные вещи. Логично было бы, конечно, заняться конвертацией лицензий в электронные, но не всё возможно конвертировать, а в случае 1С имеется некое антропогенное сопротивление.

Теперь обновлять ноды кластера можно прямо посреди рабочего дня:

Suspend-ClusterNode -Name "node1" -Drain # Выгнать всех с ноды
# Далее обновлять и перезагружать ноду
Resume-ClusterNode -Name "node1" -Failback Immediate # Вернуть ноду в строй и тащить ВМ обратно

С кластеростроением связано и то, что я

Прошил SAN-свитчи

Началось с того, что после подключения к кластеру новой модной флеш-полки NetApp и перенесения туда некоторого количества виртуалок, в один прекрасный день решили обновить ноды, и после перезагрузки последней из них все виртуальные машины, лежащие на этой полке, потеряли свои жёсткие диски. Вторая полка — Хьюлет Паккард — продолжала работать как ни в чём не бывало. Началась паника, в результате после перезагрузки SAN-свитчей всё восстановилось.

Обнаружилось, что после перезагрузки кластерной ноды полка перестаёт эту ноду видеть. Дело в том, что NetApp использует виртуальный WWN, и, скорее всего, SAN-свитчи HP 8/8 с древней прошивкой v7.0.0c не вполне корректно с этим делом работают. Я предложил прошить их до максимально возможной версии, что после одобрения руководства и проделал. На своей рабочей машине я развернул FTP-сервер и прошил оба свитча в такой последовательности: v7.0.0c → v7.0.2e1 → v7.1.2b1 → v7.2.1g → v7.3.2b → v7.4.2d. Шить надо последовательно, прибавляя единицу ко второй цифре. Первую итерацию перестраховался, прошив до последней доступной мне версии в рамках одной и той же второй цифры.

Процесс обновления до Fabric OS v7.2.1

Моё предположение оказалось верным — после прошивки всё заработало как нужно, полка перестала терять хосты после их перезагрузки.

Написал скрипт контроля домашних каталогов пользователей

Два года назад я уже делал отчёт по домашним каталогам, теперь пришло время навести порядок с этим делом. Внезапно выяснилось, что групповой политики создания каталогов не было, поэтому при создании учётки и прописывании пути к каталогу он не создавался. Раньше это делали, как и всё остальное, руками, поэтому были ошибки в именах каталогов, права на папки были розданы невесть как, при выключении учётки каталог никуда не девался и продолжал валяться в общей куче, при переименовании логина пользователя каталог оставался со старым именем — в общем, бардак.

Конечно, можно было бы создать групповую политику, но она бы не учитывала всего того, что уже навертели, как и, например, смену логина, поэтому хотелось сделать что-то погибче.

После некоторых умственных борений, проб и ошибок я пришёл к такой логике: пляшем от того, что каталог может быть либо прописан, либо не прописан в учётной записи пользователя.

Если он прописан, он может:

  • Не совпадать с логином, и корректной папки не существует, но при этом есть та, которая прописана — тогда нужно переименовать текущую папку, а путь скорректировать.
  • Не совпадать с логином, и папки по прописанному пути не существует, но корректная папка существует — скорректировать путь.
  • Не совпадать с логином, но существуют и корректная папка, и неправильная — написать человеку, чтобы разобрался.
  • Не совпадать с логином, но не существует ни той папки, что прописана, ни корректной - создать и прописать правильный каталог.
  • Совпадать с логином, но каталога не существует — создать каталог.

Если домашний каталог не прописан:

  • Если есть папка, совпадающая с логином — прописать её.
  • Если папки нет — создать её и прописать в учётку.

Также, скрипт смотрит в список прав на каталоги (с помощью прекрасного модуля NTFSSecurity), и если там явно заданных (неунаследованных) прав больше единицы или в списке отсутствует логин пользователя — сбрасывает разрешения на каталог и задаёт полные права для одного пользователя. Какие разрешения были - на всякий случай сообщается человеку.

В итоге, я получаю подобные письма:

Алексеев Олег Владимирович: не был настроен диск Z, создана и прописана папка (Alekseevov).  
Анциферова Ольга Николаевна: переименована папка диска Z для соответствия с логином: Rumyanceva → antsiferovaon  
В папке Rumyanceva были выставлены неверные явные разрешения - их 4, выданы BUILTIN\Администраторы, DOMAIN\antsiferovaon  
Внукова Валентина Геннадьевна: прописанная папка диска Z (vnukovavg) не существовала и была создана.  
Горбунова Ирина Васильевна: прописанной папки диска Z не существует (Berdisheva), но существует совпадающая с логином (Gorbunova). Путь скорректирован.  
Петрова Юлия Сергеевна: прописанная папка диска Z (petrovayus) не совпадала с логином (petrovays), но так как обоих каталогов не существовало, был создан и прописан правильный.  
Фанова Ирина Анатольевна: прописанная папка диска Z (Fanovaia) не совпадает с логином (Fanova), но существуют оба каталога. Необходимо разобраться.

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

Лишний каталог Abramian пуст и был удалён.
Лишний каталог Afanasieva перенесён в архив (3 файла, 0.35 МБ).
Лишний каталог Ageenko перенесён в архив (5 файлов, 0.92 МБ).
Лишний каталог Akimova пуст и был удалён.
Лишний каталог Aksenov пуст и был удалён.
Лишний каталог Alekseeva перенесён в архив (3 файла, 1.45 МБ).
Лишний каталог Alekseevmv пуст и был удалён.
Лишний каталог Alexandrov перенесён в архив (2795 файлов, 755.9 МБ).

Теперь система сама следит за порядком в домашних каталогах, красота.

Настроил единый вход (SSO) для DokuWiki

Памятуя об успехе настройки Squid и прочитав, что DokuWiki также имеет механизм работы с Active Directory и Single sign-on, решил реализовать это на практике. Ведь очень удобно, когда открываешь страницу wiki и сразу авторизуешься под своей учёткой с соответствующими правами.

Делал как и раньше в случае со Сквидом — через keytab-файл, только в этом случае пароль делал случайный, потому что нет никакого смысла задавать его извне - вводить этот пароль никуда не придётся. Перенёс keytab на сервер wiki, настроил /etc/krb5.conf. Далее нужно настраивать саму DokuWiki.

/var/www/html/conf/local.php

<?php
$conf['title'] = 'Wiki';
$conf['lang'] = 'ru';
$conf['license'] = '0';
// группа админов wiki
$conf['superuser'] = '@wiki-admins';
$conf['target']['interwiki'] = '_blank';
$conf['target']['extern'] = '_blank';
$conf['userewrite'] = '1';
$conf['useslash'] = 1;

/var/www/html/conf/local.protected.php

$conf['useacl']         = 1;
$conf['authtype'] = 'authad';
$conf['disableactions'] = 'register';

$conf['plugin']['authad']['account_suffix'] = '@domain.ru';
$conf['plugin']['authad']['base_dn'] = 'DC=domain,DC=ru';
$conf['plugin']['authad']['domain_controllers'] = 'DC1.domain.ru,DC2.domain.ru,DC3.domain.ru';
$conf['plugin']['authad']['domain'] = 'domain.ru';
$conf['plugin']['authad']['recursive_groups']   = 1;
$conf['plugin']['authad']['sso'] = 1;
// Пользователь AD wiki-admin должен входить в группу wiki-admins
$conf['plugin']['authad']['ad_username'] = 'wiki-admin';
$conf['plugin']['authad']['ad_password'] = 'P@ssw0rd';
// $conf['plugin']['authad']['debug'] = 1;

Потом Apache.

# Для начала надо установить модуль для Kerberos:
apt install libapache2-mod-auth-gssapi
# И настроить его в /etc/apache2/apache2.conf
<Directory "/var/www/html">
        AuthType GSSAPI
        AuthName "GSSAPI Wiki SSO"
        GssapiBasicAuth On
        Require valid-user
</Directory>
# Чтобы не парить людям мозги насчёт необходимости набирать полное имя
# wiki.domain.ru вместо просто wiki (это необходимо для работы SSO), делаем редирект.
# /etc/apache2/sites-available/000-default.conf
<If "%{HTTP_HOST} != 'wiki.domain.ru'">
    Redirect "/" "http://wiki.domain.ru/"
</If>

Ну, а дальше можно создавать в AD группы, включать туда пользователей и раздавать для этих групп права на соответствующие разделы и страницы.

Что сделал - 10

Продолжаем бахвалиться, юбилейный выпуск. Сегодня в программе сплошное построение велосипедов, что логично — скоро открытие велосезона.

Авторесет виртуальной машины при недоступности HTTP или если она не отвечает на пинг

Есть виртуальная машина на Ubuntu 8.04, перевезённая мной на Hyper-V с железки несколько месяцев назад, где крутится веб-сервис, управляемый CMS отечественного производства RBC Contents 4.0. В каталоге веб-сервера находится более миллиона файлов. Операционная система старая, несовместимая с виртуальной средой, поэтому сетевые адаптеры там пришлось ставить не современные синтетические, а старые эмулируемые (100 Мбит). Мало того, что версия системы управления контентом античных времён — она уже больше 10 лет вообще не существует в природе, как и компания, её выпускавшая. Инструкций тоже нет, в интернете нашлись два документа по три странички, писанных на коленке и совершенно бесполезных. В общем, классический «чёрный ящик», который невозможно тронуть, чтобы он, чего доброго, не развалился, но, вместе с тем, необходимо поддерживать его работоспособность.

Ящик время от времени по неизвестным причинам либо перестаёт отвечать по сети, либо вешается намертво; особенно радует, когда он это делает в три часа ночи. В журналах пусто. Так как всегда решением проблемы является нажатие кнопки сброса, я написал скрипт, который в нерабочее время каждые 10 минут проверяет доступность ящика по сети и порта HTTP. Если что-то не отвечает, скрипт ищет машину в кластере и жмёт ей на ресет, после чего пишет мне письмо, почему он это сделал.

$name = "webserver"
$ip = "192.168.1.10"

if (!(Test-Connection $ip -Count 4 -Quiet -OutVariable ping) -or `
!(Test-NetConnection $ip -Port 80 -OutVariable http).TcpTestSucceeded) {
$vm = Get-Clustergroup |? {$_.grouptype -eq 'VirtualMachine' -and $_.name -eq "$name"} |get-vm

  if ($vm -and $vm.State -eq "Running") {

  $vm |Restart-VM -force

  $body = "<p>Причины:</p>"
  $body += "<ul>"
    if (!$ping) {$body += "<li>Нет сетевого соединения</li>"}
    if (!$http.TcpTestSucceeded) {$body += "<li>Нет ответа по HTTP</li>"}
  $body += "</ul>"

  Send-MailMessage -SmtpServer mail.domain.ru -From support@domain.ru -Subject "Hyper-V - сервер $name был принудительно перезагружен" `
  -To admin@domain.ru -Body $body -BodyAsHtml -Encoding utf8
  }
}

Всё же, виртуализация в любом случае имеет преимущества, потому что ящик примерно так же вис и в свою бытность на железке, а до кнопки ресета там просто так дотянуться было нельзя.

Нужно, конечно, сделать поизящнее — привязать срабатывание скрипта к Zabbix, это дело ближайшего будущего.

Группы рассылки на базе групп подразделений компании

Обратились в просьбой сделать группу рассылки на один из отделов компании. Чтобы не возвращаться больше к этой теме в будущем, решили сделать такие группы сразу для всех отделов (кроме руководства, конечно).

Сначала я сделал скриптом динамические группы рассылки, т. е., в рассылку входят члены группы того или иного отдела. Но выяснилось, что так не работает, потому что структура отделов иерархическая, а динамическая группа рассылки не умеет смотреть в группу домена рекурсивно. Ведь тот или иной сотрудник входит в несколько разных вложенных друг в друга отделов, и должен входить во все группы рассылки этих отделов.

Пришлось создать обычные группы рассылки, а членство в группах спрашивать у домена.

function Get-ADGroupMemberEnabledAndMail {
param(
[parameter(mandatory=$true)]
$group
)
Get-ADGroupMember "$group" -Recursive |? objectclass -eq 'user' |% {
    Get-ADUser -filter "name -eq '$($_.name)' -and enabled -eq 'True'" -Properties mail
    }
}

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

По результатам работы скрипт шлёт отчёт, причём, я распробовал всякие html-символы, чтобы сделать его более читаемым и лаконичным. Примерный вид:

Отдел по развитию (группа рассылки)
Управление по повышению (группа рассылки)
Бумажкин Иван Ильич Дирекция по улучшению (группа рассылки)
Бюрократов Тихон Тарасович Группа по превозмоганию (группа рассылки)

Всё отлично работает, но есть одна мысль — ведь можно вместо довольно большого количества тяжёлых запросов к контроллерам домена брать подразделения, к которым принадлежит пользователь, просто из выгрузки штатного расписания, которую уже давно приводит в порядок мой парсер, написанный полтора года назад. Да, всё равно нужно будет получать дополнительную информацию о пользователе из домена, но хотя бы не придётся рекурсивно лопатить десятки групп. С другой стороны, мы имеем дело с реальным положением вещей, которые были приведены в соответствие с выгрузкой ранее. В общем, есть над чем подумать.

Обнаружение лишних объектов в корне DFS

Удивительным образом — видимо, не до конца разобрались в своё время в хитросплетениях раздачи прав в DFS — в корень списка папок на распределённых файловых ресурсах каким-то образом попадают пользовательские ярлыки, файлы и даже иногда каталоги. Это бывает нечасто, но тем не менее, приятнее от этого не становится, а пытаться кардинально решить эту проблему, экспериментируя с перенастройкой прав на работающей системе — это рискованная авантюра, так что опять будет паллиатив.

Удалить этот скапливающийся хлам вручную бывает непросто — нужно искать, на каком конкретно сервере он лежит и удалять уже оттуда, потому что если админ подключен к DFS на одном сервере, а хлам лежит на другом, то через DFS админ удалить хлам не сможет, даже имея все полномочия — система пишет, что файлы не найдены. Ещё нюанс — из команд Powershell, относящихся к DFS, я не смог получить путей к его корням на локальных серверах, т. е., элегантно извлечь данные из одного источника не вышло. Пришлось просто перечислить все локальные пути в явном виде в скрипте, благо, их немного.

А как вообще отделить хлам от легитимных папок? Оказывается, папки в DFS имеют атрибут d----l (reparse point), то есть, это даже и не папки, а ссылки. Поэтому, вычислить хлам можно так:

dir `
"\\server1\c$\DFS_roots\*\*",
"\\server2\c$\корни_DFS\*\*",
"\\server3\c$\DFS_roots\*\*" |
? mode -ne 'd----l'

Скрипт, конечно, данные сам не удаляет (мало ли что может туда попасть?), а шлёт письмо, где перечислены потенциально лишние объекты, обёрнутые в команды для их удаления.

Автодобавление разрешения на почтовые ящики для группы

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

Как обычно, хотелось дать разрешения на уровне организации Exchange, чтобы сделать это один раз и больше к теме не возвращаться. Вместе с тем, не хотелось и давать слишком много привилегий этой группе — разрешений по управлению почтовой системой ей не требовалось. Кроме того, были мысли дать права только на чтение.

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

Не стал писать нового скрипта, дополнил старые — в один, который контролирует видимость в глобальной адресной книге, добавил сам функционал раздачи прав на ящики, где они ещё не были розданы, а сводный отчёт по Exchange дополнил данными по явно заданным разрешениям на ящики, раз уж всё равно ковырял эту тему.

$accessNotGranted = $allUserMbxes |? {!((Get-MailboxPermission $_).user -match "$fullControlGroup")}
if ($accessNotGranted) {
    $accessNotGranted |
    Add-MailboxPermission -User "$fullControlGroup" -AccessRights FullAccess -InheritanceType All
}

Что сделал - 9

Отчёт по VMware ESXi

Разобрался с PowerCLI, наконец.

Изображение без описания

Также, установил веб-морду на ESXi 5.5, это гораздо удобнее отдельного клиента, и она поддерживает управление машинами версии 10.

Перевод внешних прямых DNS-зон на хостинг, а обратных c Windows DNS на BIND9

До этого в конторе было два локальных внешних DNS-сервера, где крутились все зоны. Я предложил перевести DNS на внешний хостинг, чтобы не держать эти сервера у себя, что и было после долгих согласований проделано начальником. Правда, пришлось писать на Powershell конвертер DNS-записей из формата выгрузки Windows DNS в формат, принимаемый Руцентром (там, похоже, тоже крутится BIND), потому что просто передать зоны не получалось. Я не лез в процесс переноса, просто сформировал файлики, чтобы их можно было импортировать на хостинге.

Всё прошло хорошо, но оказалось, что избавиться от локальных внешних серверов DNS полностью всё равно нельзя — у компании в собственности внешний IP-диапазон и нужно обеспечивать обратные зоны (PTR), иначе почта ходить не будет.

Тогда я поднял две виртуалки с Ubuntu Server, поставил туда BIND9, оба сервера сделал подчинёнными (кэширующими) для всех прямых зон — они синхронизировались с Руцентром, а для обратных зон один сервер первичный, а второй забирает эти обратные зоны с первичного. Ресурсов обе эти машинки едят меньше, чем один старый сервер на Windows. Всё чудесно работает, и минус 2 лицензии серверной винды. Внешний хостинг DNS в любом случае полезен — он повышает отказоустойчивость, а стоит очень недорого.

Система распознавания текста (OCR) для файлов PDF

В компании закуплены лицензии FineReader, но на всех желающих не хватает, закупать их — тема трудная и долгая, а работать как-то надо, причём, прямо сейчас. Темы закупки ПО и её методов, когда вместо корпоративной лицензии всё покупается поштучно и от случая к случаю и последствиях такого подхода затрагивать не буду, это не моя область ответственности, хотя смотреть на это иногда бывает больно. Короче говоря, нужен какой-то простой способ распознавания текста в файлах PDF по типу механизма сжатия, сделанного мною ранее. Я уже довольно давно знаю о существовании очень неплохой программы gImageReader, которая является оболочкой к OCR-движку Tesseract, который я и задействовал для решения этой задачи. Сборкой этого движка для Windows занимается Маннгеймская университетская библиотека, за что ей огромное спасибо.

Сам Тессеракт не воспринимает файлов PDF, ему картинки подавай, так что пришлось сначала прогонять файл через GhostScript, который преобразует PDF в набор картинок PNG.

& "$ghostScript" -dBATCH -dNOPAUSE -sDEVICE=pnggray -r300 "-sOutputFile=$($pdf.basename)-%04d.png" "$($pdf.fullname)"

Ну, а дальше картинки скармливаются Тессеракту, он делает из них соответствующее количество текстовых файлов, которые потом лепятся в один и выдаются пользователю.

(dir *.png) -match "$($pdf.basename)" |% {
    & "$tesseract" ".\$($_.name)" "$($_.basename)" -l rus+eng
}
gc ((dir *.txt) -match "$($pdf.basename)") -Encoding UTF8 |Out-File "$path\$($pdf.basename).txt" -Encoding default

Возьмём для теста какой-то договор из интернета:

Исходник - какой-то договор из интернета (фрагмент)

Результат распознавания:

- при производстве земляных и строительных работ соглассвывать
предполагаемые работы с Главным управлением по государственной охране
объектов культурного наследия Тверской области.

2. Срок Договора

2.1. Срок аренды Участка устанавливается с 2 ^\*\_\_ по 10.04.2019 года.
2.2. Договор вступает в силу со дня его государственной регистрации. |

3. Размер и условия внесения арендной платы

3.1. Арендатор ежегодно уплачивает Арендодателю арендную плату.
3.2. Размер арендной платы за Участок определяется в соответствии с
Расчетом арендной платы, являющимся неотъемлемой частью настоящего
Договора (приложение № 2).
3.3. Порядок определения размера арендной платы за пользование земельными
участками, устанавливается органом государственной власти Тверской области.
3.4. Арендная плата вносится следующими частями:
3.4.1. юридическими лицами:
- не позднее 15.04. - 1/4 годовой суммы;
- не позднее 15.07. - 1/4 годовой суммы;
- не позднее 15.10. - 1/2 годовой суммы.
путем перечисления на реквизиты, указываемые Арендодателем в асчете
арендной платы на текущий год. Арендатор обязан ежегодно до внесения
первого арендного платежа в текущем году уточнять у Арендодателя реквизиты,
на которые перечисляется арендная плата. :
В случае заключения Договора аренды после 15 сентября (в первый год
аренды) арендная плата за период до конца года, в том числе сумма,

По-моему, для бесплатного движка это очень круто.

Для ускорения обработки можно применить Powershell 7, где есть параллельный цикл, чтобы выжать из железа всё, на что оно способно.

Движемся дальше.

P. S. Чуть позже доделал скрипт, теперь он, помимо PDF, работает с TIF-TIFF (в т. ч., многостраничными), JPG-JPEG и PNG. Тут проще, потому что Тессеракт сам умеет работать с этими форматами без предварительных преобразований. Также, сделал вариант под Powershell 7 с параллельными циклами, что работает гораздо быстрее на одном и том же компьютере.

Влияние одного параметра на общую производительность системы

Несколько месяцев назад я настроил на работе прокси-сервер squid, он успешно прошёл стадию тестирования и с декабря прошлого года переведён в боевой режим.

Статистика за февраль

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

Нагрузка на 6 виртуальных процессоров вчера

Собственно, когда 6 процессоров не справляются, это вызывает вопрос — а всё ли нормально настроено, потому что тупо забрасывать ресурсами кривую систему глубоко неправильно, тем более, просто так добавлять процессоры к виртуальной машине неразумно — этим ситуацию можно скорее ухудшить, ведь реальные процессоры делятся на количество виртуальных, и чем больше их плодить, тем меньше будет выхлоп от отдельно взятого виртуального процессора. Именно поэтому количество виртуальных процессоров лучше максимально сокращать.

Программа top показала, что сам squid весьма скромен в потреблении ресурсов, а процессор загружен процессами аутентификации kerberos negotiate_kerberos_auth. Оказалось, что в kerberos есть replay cache, который пытается бороться с потенциальной подменой ключей шифрования, но, во-первых, он несовершенен, о чём прямо упомянуто в описании, а во-вторых, сильно тормозит более-менее нагруженную систему.

# man negotiate_kerberos_auth

Kerberos can keep a replay cache to detect the reuse of Kerberos
tickets (usually only possible in a 5 minute window). If squid is under
high load with Negotiate (Kerberos) proxy authentication requests the
replay cache checks can create high CPU load. If the environment does
not require high security the replay cache check can be disabled for
MIT based Kerberos implementations by adding the below to the startup
script or use the -t none option.

Добавляем параметр -t none к команде аутентификации:

auth_param negotiate program /usr/lib/squid/negotiate_kerberos_auth -s HTTP/proxy.domain.ru@DOMAIN.RU -t none

Результат очень радует:

Нагрузка на 4 виртуальных процессора сегодня

Хотел сразу оставить два vCPU, но перестраховался, оставил пока четыре, вечерком сделаю.

В общем, вопрос роли личности в истории, возможно, дискуссионный, но роль какого-то мелкого параметра в работе целой системы совершенно точно может быть определяющей.