Реализация программной платформы защищённого NAS +13



В предыдущей статье было описано проектирование программной платформы NAS.
Настало время её реализовать.


Проверка


Обязательно, перед тем, как начинать проверьте работоспособность пула:


zpool status -v

Пул и все диски в нём должны быть ONLINE.


Далее я предполагаю, что на предыдущем этапе всё было сделано по инструкции, и работает, либо вы сами хорошо понимаете, что делаете.


Удобства


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


  • SSH-сервер: apt-get install openssh-server. Если вы не знаете, как настроить SSH, делать NAS на Linux пока рано можете почитать особенности его использования в данной статье, затем воспользоваться одним из мануалов.
  • tmux или screen: apt-get install tmux. Чтобы сохранять сессию при входах по SSH и использовать несколько окон.

После установки SSH надо добавить пользователя, чтобы не заходить через SSH под root (вход по умолчанию отключен и не надо его включать):


zfs create rpool/home/user
adduser user
cp -a /etc/skel/.[!.]* /home/user
chown -R user:user /home/user

Для удалённого администрирования это достаточный минимум.


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


Альтернативный вариант использовать Virtual KVM, который предоставляет IME. Там есть консоль, правда в моём случае она реализована в виде Java апплета, что не очень удобно.


Настройка


Подготовка кэша


Насколько вы помните, в описанной мной конфигурации есть отдельный SSD под L2ARC, который пока не используется, но взят "на вырост".


Необязательно, но желательно заполнить этот SSD случайными данными (в случае Samsung EVO всё-равно заполнится нулями после выполнения blkdiscard, но не на всех SSD так):


dd if=/dev/urandom of=/dev/disk/by-id/ata-Samsung_SSD_850_EVO bs=4M && blkdiscard /dev/disk/by-id/ata-Samsung_SSD_850_EVO

Отключение сжатия логов


На ZFS и так используется сжатие, потому сжатие логов через gzip будет явно лишним.
Выключаю:


for file in /etc/logrotate.d/* ; do
  if grep -Eq "(^|[^#y])compress" "$file" ; then
    sed -i -r "s/(^|[^#y])(compress)/\1#\2/" "$file"
  fi
done

Обновление системы


Тут всё просто:


apt-get dist-upgrade --yes
reboot

Создание снэпшота для нового состояния


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


zfs destroy rpool/ROOT/debian@install
zfs snapshot rpool/ROOT/debian@install

Организация файловых систем


Подготовка разделов для SLOG


Первое, что нужно сделать с целью достижения нормальной производительности ZFS — это вынести SLOG на SSD.
Напомню, что SLOG в используемой конфигурации дублируется на двух SSD: для него будут созданы устройства на LUKS-XTS поверх 4-го раздела каждой SSD:


dd if=/dev/urandom of=/etc/keys/slog.key bs=1 count=4096

cryptsetup --verbose --cipher "aes-xts-plain64:sha512" --key-size 512 --key-file /etc/keys/slog.key luksFormat /dev/disk/by-id/ata-Samsung_SSD_850_PRO-part4

cryptsetup --verbose --cipher "aes-xts-plain64:sha512" --key-size 512 --key-file /etc/keys/slog.key luksFormat /dev/disk/by-id/ata-Micron_1100-part4

echo "slog0_crypt1 /dev/disk/by-id/ata-Samsung_SSD_850_PRO-part4 /etc/keys/slog.key luks,discard" >> /etc/crypttab

echo "slog0_crypt2 /dev/disk/by-id/ata-Micron_1100-part4 /etc/keys/slog.key luks,discard" >> /etc/crypttab

Подготовка разделов для L2ARC и подкачки


Сначала надо создать разделы под swap и l2arc:


sgdisk -n1:0:48G -t1:8200 -c1:part_swap -n2::196G -t2:8200 -c2:part_l2arc /dev/disk/by-id/ata-Samsung_SSD_850_EVO

Раздел подкачки и L2ARC будут зашифрованы на случайном ключе, т.к. после перезагрузки они не требуются и их всегда возможно создать заново.
Поэтому в crypttab прописывается строка для шифрования/расшифрования разделов в plain режиме:


echo swap_crypt /dev/disk/by-id/ata-Samsung_SSD_850_EVO-part1 /dev/urandom swap,cipher=aes-xts-plain64:sha512,size=512 >> /etc/crypttab

echo l2arc_crypt /dev/disk/by-id/ata-Samsung_SSD_850_EVO-part2 /dev/urandom cipher=aes-xts-plain64:sha512,size=512 >> /etc/crypttab

Затем нужно перезапустить демоны и включить подкачку:


echo 'vm.swappiness = 10' >> /etc/sysctl.conf
sysctl vm.swappiness=10
systemctl daemon-reload
systemctl start systemd-cryptsetup@swap_crypt.service
echo /dev/mapper/swap_crypt none swap sw,discard 0 0 >> /etc/fstab
swapon -av

Т.к. активного использования подкачки на SSD не планируется, параметр swapiness, который умолчанию 60, нужно установить в 10.


L2ARC на данном этапе ещё не используется, но раздел под него уже готов:


$ ls /dev/mapper/
control  l2arc_crypt root_crypt1  root_crypt2  slog0_crypt1  slog0_crypt2  swap_crypt  tank0_crypt0  tank0_crypt1  tank0_crypt2  tank0_crypt3

Пулы tankN


Будет описано создание пула tank0, tank1 создаётся по аналогии.


Чтобы не заниматься созданием одинаковых разделов вручную и не допускать ошибок, я написал скрипт для создания шифрованных разделов под пулы:


create_crypt_pool.sh
#!/bin/bash

KEY_SIZE=512
POOL_NAME="$1"
KEY_FILE="/etc/keys/${POOL_NAME}.key"
LUKS_PARAMS="--verbose --cipher aes-xts-plain64:sha${KEY_SIZE} --key-size $KEY_SIZE"

[ -z "$1" ] && { echo "Error: pool name empty!" ; exit 1; }

shift

[ -z "$*" ] && { echo "Error: devices list empty!" ; exit 1; }

echo "Devices: $*"
read -p "Is it ok? " a

[ "$a" != "y" ] && { echo "Bye"; exit 1; }

dd if=/dev/urandom of=$KEY_FILE bs=1 count=4096

phrase="?"

read -s -p "Password: " phrase
echo
read -s -p "Repeat password: " phrase1
echo

[ "$phrase" != "$phrase1" ] && { echo "Error: passwords is not equal!" ; exit 1; }

echo "### $POOL_NAME" >> /etc/crypttab

index=0

for i in $*; do
  echo "$phrase"|cryptsetup $LUKS_PARAMS luksFormat "$i" || exit 1
  echo "$phrase"|cryptsetup luksAddKey "$i" $KEY_FILE || exit 1
  dev_name="${POOL_NAME}_crypt${index}"
  echo "${dev_name} $i $KEY_FILE luks" >> /etc/crypttab
  cryptsetup luksOpen --key-file $KEY_FILE "$i" "$dev_name" || exit 1
  index=$((index + 1))
done

echo "###" >> /etc/crypttab

phrase="====================================================="
phrase1="================================================="
unset phrase
unset phrase1

Теперь, используя данный скрипт, надо создать пул для хранения данных:


./create_crypt_pool.sh

zpool create -o ashift=12 -O atime=off -O compression=lz4 -O normalization=formD  tank0 raidz1 /dev/disk/by-id/dm-name-tank0_crypt*

Замечания о параметре ashift=12 смотрите в моих предыдущих статьях и комментариях к ним.


После создания пула, я выношу его журнал на SSD:


zpool add tank0 log mirror /dev/disk/by-id/dm-name-slog0_crypt1 /dev/disk/by-id/dm-name-slog0_crypt2

В дальнейшем, при установленном и настроенном OMV, возможно будет создавать пулы через GUI:


Создание ZFS пал в OMV WEB GUI


Включение импорта пулов и автомонтирования томов при загрузке


Для того, чтобы гарантированно включить автомонтирование пулов, выполните следующие команды:


rm /etc/zfs/zpool.cache
systemctl enable zfs-import-scan.service
systemctl enable zfs-mount.service
systemctl enable zfs-import-cache.service

На данном этапе закончена настройка дисковой подсистемы.


Операционная система


Первым делом надо установить и настроить OMV, чтобы наконец получить какую-то основу для NAS.


Установка OMV


OMV будет установлен в виде deb-пакета. Для того, чтобы это сделать, возможно воспользоваться официальной инструкцией.


Скрипт add_repo.sh добавляет репозиторий OMV Arrakis в/etc/apt/ sources.list.d, чтобы пакетная система увидела репозиторий.


add_repo.sh
cat <<EOF >> /etc/apt/sources.list.d/openmediavault.list
deb http://packages.openmediavault.org/public arrakis main
# deb http://downloads.sourceforge.net/project/openmediavault/packages arrakis main
## Uncomment the following line to add software from the proposed repository.
# deb http://packages.openmediavault.org/public arrakis-proposed main
# deb http://downloads.sourceforge.net/project/openmediavault/packages arrakis-proposed main
## This software is not part of OpenMediaVault, but is offered by third-party
## developers as a service to OpenMediaVault users.
deb http://packages.openmediavault.org/public arrakis partner
# deb http://downloads.sourceforge.net/project/openmediavault/packages arrakis partner
EOF

Обратите внимание, что по сравнению с оригиналом, репозиторий partner включен.


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


Команды для установки OMV.
./add_repo.sh
export LANG=C
export DEBIAN_FRONTEND=noninteractive
export APT_LISTCHANGES_FRONTEND=none
apt-get update
apt-get --allow-unauthenticated install openmediavault-keyring
apt-get update
apt-get --yes --auto-remove --show-upgraded     --allow-downgrades --allow-change-held-packages     --no-install-recommends     --option Dpkg::Options::="--force-confdef"     --option DPkg::Options::="--force-confold"     install postfix openmediavault
# Initialize the system and database.
omv-initsystem

OMV установлен. Он использует своё ядро, и после установки может потребоваться перезагрузка.


Перезагрузившись, интерфейс OpenMediaVault, будет доступен на порту 80 (зайдите в браузере на NAS по IP-адресу):



Логин/пароль по умолчанию: admin/openmediavault.


Настройка OMV


Далее большая часть настройки будет проходить через WEB-GUI.


Установка безопасного соединения


Сейчас надо сменить пароль WEB-администратора и сгенерировать сертификат для NAS, чтобы в дальнейшем работать по HTTPS.


Смена пароля производится на вкладке "Система->Общие настройки->Пароль Web Администратора".
Для генерация сертификата на вкладке "Система->Сертификаты->SSL" надо выбрать "Добавить->Создать".


Созданный сертификат будет виден на той же вкладке:


Сертификат


После создания сертификата, на вкладке "Система->Общие настройки" надо включить флажок "Включить SSL/TLS".


Сертификат потребуется до завершения настройки. В окончательном варианте для обращения к OMV будет использоваться подписанный сертификат.


Теперь надо перелогиниться в OMV, на порт 443 или просто приписав в браузере префикс https:// перед IP.


Если войти удалось, на вкладке "Система->Общие настройки" надо включить флажок "Принудительно SSL/TLS".


Измените порты 80 и 443 на 10080 и 10443.
И попробуйте войти по следующему адресу: https://IP_NAS:10443.
Изменение портов важно, потому что порты 80 и 443 будет использовать docker контейнер с nginx-reverse-proxy.


Первичные настройки


Минимальные настройки, которое надо сделать в первую очередь:


  • На вкладке "Система->Дата и Время" проверьте значение часового пояса и задайте сервер NTP.
  • На вкладке "Система->Мониторинг" включите сбор статистики производительности.
  • На вкладке "Система->Управление энергопотреблением" видимо стоит выключить "Мониторинг", чтобы OMV не пытался управлять вентиляторами.

Сеть


Если второй сетевой интерфейс NAS ещё не был подключен, подключите его к роутеру.


Затем:


  • На вкладке "Система->Сеть" установите имя хоста в значение "nas" (или то, которые вам нравится).
  • Настройте бондинг для интерфейсов, как показано на рисунке ниже: "Система->Сеть->Интерфейсы->Добавить->Bond".
  • Добавьте нужные правила файрволла на вкладке "Система->Сеть->Брандмауэр". Для начала достаточно доступа на порты 10443, 10080, 443, 80, 22 для SSH и разрешения получения/отправки ICMP.

Настройка бондинга


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


Интерфейсы в бондинге


При желании, возможно дополнительно настроить SSH из WEB GUI:


Настройка SSH


Репозитории и модули


На вкладке "Система->Управление обновлениями->Настройки" включите "Обновления поддерживаемые сообществом".


Сначала требуется добавить репозитории OMV extras.
Это возможно сделать просто установив плагин, либо пакет, как указано на форуме.


На странице "Система->Плагины" надо найти плагин "openmediavault-omvextrasorg" и установить его.


В результате, в меню системы появится значок "OMV-Extras" (его возможно видеть на скриншотах).


Зайдите туда и включите следующие репозитории:


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

На вкладке "Система->OMV Extras->Ядро" вы можете выбрать нужное вам ядро, в том числе ядро от Proxmox (сам я его не ставил, т.к. мне пока не нужно, потому не рекомендую):



Установите необходимые плагины (жирным выделены абсолютно необходимые, курсивом — опциональные, которые я не устанавливал):


Список плагинов.
  • openmediavault-apttool. Минимальный GUI для работы с пакетной системой. Добавляет "Сервисы->Apttool".
  • openmediavault-anacron. Добавляет возможность работы из GUI с асинхронным планировщиком. Добавляет "Система->Anacron".
  • openmediavault-backup. Обеспечивает резервное копирование системы в хранилище. Добавляет страницу "Система->Резервное копирование".
  • openmediavault-diskstats. Нужен для сбора статистики по производительности дисков.
  • openmediavault-dnsmasq. Позволяет поднять на NAS сервер DNS и DHCP. Т.к., я делаю это на роутере, мне не требуется.
  • openmediavault-docker-gui. Интерфейс управления Docker контейнерами. Добавляет "Сервисы->Docker".
  • openmediavault-ldap. Поддержка аутентификации через LDAP. Добавляет "Управление правами доступа->Служба каталогов".
  • openmediavault-letsencrypt. Поддержка Let's Encrypt из GUI. Не нужна, потому что используется встраивание в контейнер nginx-reverse-proxy.
  • openmediavault-luksencryption. Поддержка шифрования LUKS. Нужен, чтобы в интерфейсе OMV были видны шифрованные диски. Добавляет "Хранилище->Шифрование".
  • openmediavault-nut. Поддержка ИБП. Добавляет "Сервисы->ИБП".
  • openmediavault-omvextrasorg. OMV Extras уже должен быть установлен.
  • openmediavault-resetperms. Позволяет переустанавливать права и сбрасывать списки контроля доступа на общих каталогах. Добавляет "Управление правами доступа->Общие каталоги->Reset Permissions".
  • openmediavault-route. Полезный плагин для управления маршрутизацией. Добавляет "Система->Сеть->Статический маршрут".
  • openmediavault-symlinks. Предоставляет возможность создавать символические ссылки. Добавляет страницу "Сервисы->Symlinks".
  • openmediavault-unionfilesystems. Поддержка UnionFS. Может пригодиться в будущем, хотя докер и использует ZFS в качестве бэкэнда. Добавляет "Хранилище->Union Filesystems".
  • openmediavault-virtualbox. Может быть использован для встраивания в GUI возможности управления виртуальными машинами.
  • openmediavault-zfs. Плагин добавляет поддержку ZFS в OpenMediaVault. После установки появится страница "Хранилище->ZFS".

Диски


Все диски, которые есть в системе, должны быть видны OMV. Удостоверьтесь в этом, посмотрев на вкладке "Хранилище->Диски". Если не все диски видны, запустите сканирование.


Диски в системе


Там же, на всех HDD надо включить кэширование записи (кликнув на диске из списка и нажав кнопку "Редактировать").


Удостоверьтесь, что видны все шифрованные разделы на вкладке "Хранилище->Шифрование":


Шифрованные разделы


Теперь пора настроить S.M.A.R.T., указанный, как средство повышения надёжности:


  • Перейдите на вкладку "Хранилище->S.M.A.R.T->Настройки". Включите SMART.
  • Там же выберите значения температурных уровней дисков (критический, как правило 60 C, а оптимальный температурный режим диска 15-45 C).
  • Перейдите на вкладку "Хранилище->S.M.A.R.T->Устройства". Включите мониторинг для каждого диска.
  • Перейдите на вкладку "Хранилище->S.M.A.R.T->Запланированные тесты". Добавьте для каждого диска короткую самопроверку раз в сутки и длительную самопроверку раз в месяц. Причём так, чтобы периоды самопроверки не пересекались.

На этом настройку дисков возможно считать оконченной.


Файловые системы и общие каталоги


Надо создать файловые системы для предопределённых каталогов.
Сделать это возможно из консоли, либо из WEB-интерфейса OMV (Хранилище->ZFS->Выбрать пул tank0->Кнопка "Добавить"->Filesystem).


Команды для создания ФС.
zfs create -o utf8only=on -o normalization=formD -p tank0/user_data/books
zfs create -o utf8only=on -o normalization=formD -p tank0/user_data/music
zfs create -o utf8only=on -o normalization=formD -p tank0/user_data/pictures
zfs create -o utf8only=on -o normalization=formD -p tank0/user_data/downloads
zfs create -o compression=off -o utf8only=on -o normalization=formD -p tank0/user_data/videos

В итоге должна получиться следующая структура каталогов:



После этого, добавьте созданные ФС, как общие каталоги на странице "Управление правами доступа->Общие каталоги->Добавить".
Обратите внимание, что параметр "Устройство" равен пути к созданной в ZFS файловой системе, а параметр "Путь" у всех каталогов равен "/".



Резервное копирование


Резервное копирование производится двумя инструментами:


  • OMV backup plugin. Плагин OMV для резервного копирования системы.
  • zfs-auto-snapshot. Скрипт для автоматического создания снимков ZFS по расписанию и удаления старых.

Если вы воспользуетесь плагином, скорее всего получите ошибку:


lsblk: /dev/block/0:22: not a block device

Для того, чтобы её исправить, по замечанию разработчиков OMV в этой "очень нестандартной конфигурации", возможно было бы отказаться от плагина и воспользоваться средствами ZFS в виде zfs send/receive.
Либо явно указать параметр "Root device" в виде физического устройства, с которого производится загрузка.
Мне удобнее использовать плагин и делать резервное копирование ОС из интерфейса, вместо того, чтобы городить что-то своё с zfs send, потому я предпочитаю второй вариант.


Настройка резервного копирования


Чтобы резервное копирование работало, сначала создайте через ZFS файловую систему tank0/apps/backup, затем в меню "Система->Резервирование" кликните "+" в поле параметра "Общая папка" и добавьте созданное устройство, как целевое, а поле "Путь" установите в "/".


С zfs-auto-snapshot тоже есть проблемы. Если её не настроить, она будет делать снимки каждый час, каждый день, каждую неделю, каждый месяц в течение года.
В итоге получится то, что на скриншоте:


Много спама от zfs-auto-snapshot


Если вы уже на это натолкнулись, выполните следующий код для удаления автоматических снимков:


zfs list -t snapshot -o name -S creation | grep "@zfs-auto-snap" | tail -n +1500 | xargs -n 1 zfs destroy -vr

Затем настройте запуск zfs-auto-snapshot в cron.
Для начала, просто удалите /etc/cron.hourly/zfs-auto-snapshot, если вам не требуется делать снимки каждый час.


E-mail уведомления


Нотификация по e-mail была указана, как одно из средств достижения надёжности.
Потому теперь надо настроить E-mail уведомления.
Для этого, зарегистрируйте на одном из публичных серверов ящик (ну либо настройте SMTP сервер самостоятельно, если у вас действительно есть причины это сделать).


После чего надо зайти на страницу "Система->Уведомление" и вписать:


  • Адрес SMTP сервера.
  • Порт SMTP сервера.
  • Имя пользователя.
  • Адрес отправителя (обычно первая компонента адреса совпадает с именем).
  • Пароль пользователя.
  • В поле "Получатель" ваш обычный адрес, на который NAS будет отправлять уведомления.

Крайне желательно включить SSL/TLS.


Пример настройки для Yandex показан на скриншоте:


E-mail уведомления


Настройка сети вне NAS


IP-адрес


Я использую белый статический IP-адрес, который стоит плюсом 100 рублей в месяц. Если нет желания платить и ваш адрес динамический, но не за NAT, возможно корректировать внешние DNS записи через API выбранного сервиса.
Тем не менее, стоит иметь ввиду, что адрес не за NAT может внезапно стать адресом за NAT: как правило, провайдеры не дают никаких гарантий.


Роутер


В качестве роутера у меня Mikrotik RouterBoard, похожий на тот, что на картинке ниже.


Mikrotik Routerboard


На роутере требуется сделать три вещи:


  • Настроить статические адреса для NAS. В моём случае, адреса выдаются по DHCP, и надо сделать так, чтобы адаптерам с определённым MAC адресом всегда выдавался один и тот же IP адрес. В RouterOS это делается на вкладке "IP->DHCP Server" кнопкой "Make static".
  • Настроить DNS сервер так, чтобы он для имени "nas", а также имён, оканчивающихся на ".nas" и ".NAS.cloudns.cc" (где "NAS" — зона на ClouDNS или подобном сервисе) отдавал IP системы. Где это сделать в RouterOS, показано на скриншоте ниже. В моём случае, это реализовано путём сопоставления имени с регулярным выражением: "^.*\.nas$|^nas$|^.*\.NAS.cloudns.cc$"
  • Настроить проброс портов. В RouterOS это делается на вкладке "IP->Firewall", далее останавливаться я на этом не буду.

Настройка DNS в RouterOS


ClouDNS


С CLouDNS всё просто. Заводите аккаунт, подтверждаете. NS записи уже у вас будут прописаны. Далее требуется минимальная настройка.


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


Создание зоны в ClouDNS


Во-вторых, в этой зоне вы должны прописать следующие A-записи:


  • nas, www, omv, control и пустое имя. Для обращения к интерфейсу OMV.
  • ldap. Интерфейс PhpLdapAdmin.
  • ssp. Интерфейс для смены паролей пользователей.
  • test. Тестовый сервер.

Остальные доменные имена будут добавляться по мере добавления служб.
Кликайте на зону, далее "Add new record", выбираете A-тип, вводите имя зоны и IP адрес роутера, за которым стоит NAS.


Добавленные A-записи


Во-вторых, требуется получить доступ к API. В ClouDNS он платный, так что предварительно надо его оплатить. В других сервисах он бесплатный. Если знаете, что лучше, и это поддерживается Lexicon, напишите пожалуйста в комментариях.


Получив доступ к API, туда надо добавить нового пользователя API.


Добавление пользователя API ClouDNS


В поле "IP address" надо вписать IP роутера: это адрес, с которого будет доступен API. После того, как пользователь будет добавлен, вы сможете использовать API, авторизовавшись по auth-id и auth-password. Их надо будет передавать в Lexicon, как параметры.



На этом настройка ClouDNS закончена.


Настройка контейнеризации


Настройка Docker


Если вы установили плагин openmediavault-docker-gui, пакет docker-ce уже должен был подтянуться по зависимостям.


Дополнительно, установите пакет docker-compose, поскольку в дальнейшем он будет использован для управления контейнерами:


apt-get install docker-compose

Также создайте файловую систему под конфигурацию сервисов:


zfs create -p /tank0/docker/services

Все настройки, образы и контейнеры докера хранятся в /var/lib/docker. Он туда интенсивно пишет (надо помнить, что это SSD), но главное, создаёт снэпшоты, клоны и файловые системы с именами в виде хэшей.


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



Чтобы этого избежать, надо локализовать каталог с данными на отдельной файловой системе.
Изменить расположение базового пути докера не сложно, это возможно сделать даже через GUI плагина, но тогда возникнет проблема: пулы перестанут монтироваться при загрузке, т.к. докер создаст свои каталоги в точке монтирования, и она будет не пуста.


Решается эта проблема заменой каталога докера в /var/lib на символическую ссылку:


service docker stop
zfs create -p /tank0/docker/lib
rm -rf /var/lib/docker
ln -s  /tank0/docker/lib /var/lib/docker
service docker start

В результате:


$ ls -l /var/lib/docker
lrwxrwxrwx 1 root root 17 Apr  7 12:35 /var/lib/docker -> /tank0/docker/lib

Теперь надо создать межконтейнерную сеть:


docker network create docker0

На этом первичная настройка Docker закончена и возможно приступать к созданию контейнеров.


Настройка контейнера с nginx-reverse-proxy


После того как Docker настроен, возможно приступить к реализации диспетчера.


Найти все конфигурационные файлы вы можете здесь, либо под спойлерами.


Для него используются два образа: nginx-proxy и letsencrypt-dns.


Напомню, что порты интерфейса OMV требуется изменить на 10080 и 10443, потому что диспетчер будет работать на портах 80 и 443.


/tank0/docker/services/nginx-proxy/docker-compose.yml
version: '2'

networks:
  docker0:
    external:
      name: docker0

services:
  nginx-proxy:
    networks:
      - docker0
    restart: always
    image: jwilder/nginx-proxy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./certs:/etc/nginx/certs:ro
      - ./vhost.d:/etc/nginx/vhost.d
      - ./html:/usr/share/nginx/html
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - ./local-config:/etc/nginx/conf.d
      - ./nginx.tmpl:/app/nginx.tmpl
    labels:
      - "com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy=true"

  letsencrypt-dns:
    image: adferrand/letsencrypt-dns
    volumes:
      - ./certs/letsencrypt:/etc/letsencrypt
    environment:
      - "LETSENCRYPT_USER_MAIL=MAIL@MAIL.COM"
      - "LEXICON_PROVIDER=cloudns"
      - "LEXICON_PROVIDER_OPTIONS=--auth-id=CLOUDNS_ID --auth-password=CLOUDNS_PASSWORD --delegated=NAS.cloudns.cc"

В данном конфиге настраиваются два контейнера:


  • nginx-reverse-proxy — cам обратный прокси.
  • letsencrypt-dns — ACME клиент Let's Encrypt.

Для создания и запуска контейнера с nginx-reverse-proxy используется образ jwilder/nginx-proxy.


docker0 — межконтейнерная сеть, которая была создана ранее, ей не управляет docker-compose.
nginx-proxy — сервис обратного прокси, собственной персоной. Он смотрит в сеть docker0. При этом, порты 80 и 443 в секции ports пробрасываются на аналогичные порты хоста (значит, на хосте будут открыты такие же порты, а данные с них будут перенаправляться на порты в сети docker0, которые слушает прокси).
Параметр restart: always означает, что нужно запускать этот сервис при перезагрузке.


Тома:


  • Внешний каталог certs отображается в /etc/nginx/certs — там лежат сертификаты, включая сертификаты, полученные от Let's Encrypt. Это общий каталог между контейнером с прокси и контейнером с ACME клиентом.
  • ./vhost.d:/etc/nginx/vhost.d — конфигурация отдельных виртуальных хостов. Сейчас не использую.
  • ./html:/usr/share/nginx/html — статичный контент. Там не нужно ничего настраивать.
  • /var/run/docker.sock, отображаемый в /tmp/docker.sock — сокет для связи с демоном Docker на хосте. Нужен для работы docker-gen внутри оригинального образа.
  • ./local-config, отображаемый в /etc/nginx/conf.d — дополнительные конфигурационные файлы nginx. Требуется для тюнинга параметров, о которых ниже.
  • ./nginx.tmpl, отображаемый в /app/nginx.tmpl — шаблон для конфигурационного файла nginx, из которого docker-gen создаст конфиг.

Контейнер letsencrypt-dns создаётся из образа adferrand/letsencrypt-dns. Он включает упомянутый выше ACME клиент и Lexicon, для общения с провайдером DNS зоны.


Общий каталог certs/letsencrypt отображается в /etc/letsencrypt внутри контейнера.


Чтобы это заработало, требуется настроить ещё несколько переменных окружения внутри контейнера:


  • LETSENCRYPT_USER_MAIL=MAIL@MAIL.COM — почта пользователя Let's Encrypt. Лучше тут указать реальную почту, на которую будут приходить всякие сообщения.
  • LEXICON_PROVIDER=cloudns — провайдер для Lexicon. В моём случае — cloudns.
  • LEXICON_PROVIDER_OPTIONS=--auth-id=CLOUDNS_ID --auth-password=CLOUDNS_PASSWORD --delegated=NAS.cloudns.cc — CLOUDNS_ID на последнем скриншоте в секции по настройке ClouDNS подчёркнут красным. CLOUDNS_PASSWORD — это пароль, который вы задали для пользования API. NAS.cloudns.cc, где NAS — имя вашей DNS зоны. Для cloudns нужен потому, что по умолчанию будут передаваться первые два компонента домена (cloudns.cc), а ClouDNS API требует указывать зону в запросе.

После этой настройки будут два независимо работающих контейнера: прокси и агент для получения сертификата.
При этом, прокси будет искать сертификат в каталогах, указанных в конфиге, но не в структуре каталогов, которую создаст агент Let's encrypt:


$ ls ./certs/letsencrypt/
accounts  archive  csr  domains.conf  keys  live  renewal  renewal-hooks

Для того, чтобы прокси начал видеть полученные сертификаты, требуется немного исправить шаблон.


/tank0/docker/services/nginx-proxy/nginx.tmpl
{{ $CurrentContainer := where $ "ID" .Docker.CurrentContainerID | first }}

{{ define "upstream" }}
    {{ if .Address }}
        {{/* If we got the containers from swarm and this container's port is published to host, use host IP:PORT */}}
        {{ if and .Container.Node.ID .Address.HostPort }}
            # {{ .Container.Node.Name }}/{{ .Container.Name }}
            server {{ .Container.Node.Address.IP }}:{{ .Address.HostPort }};
        {{/* If there is no swarm node or the port is not published on host, use container's IP:PORT */}}
        {{ else if .Network }}
            # {{ .Container.Name }}
            server {{ .Network.IP }}:{{ .Address.Port }};
        {{ end }}
    {{ else if .Network }}
        # {{ .Container.Name }}
        {{ if .Network.IP }}
            server {{ .Network.IP }} down;
        {{ else }}
            server 127.0.0.1 down;
        {{ end }}
    {{ end }}

{{ end }}

# If we receive X-Forwarded-Proto, pass it through; otherwise, pass along the
# scheme used to connect to this server
map $http_x_forwarded_proto $proxy_x_forwarded_proto {
  default $http_x_forwarded_proto;
  ''      $scheme;
}

# If we receive X-Forwarded-Port, pass it through; otherwise, pass along the
# server port the client connected to
map $http_x_forwarded_port $proxy_x_forwarded_port {
  default $http_x_forwarded_port;
  ''      $server_port;
}

# If we receive Upgrade, set Connection to "upgrade"; otherwise, delete any
# Connection header that may have been passed to this server
map $http_upgrade $proxy_connection {
  default upgrade;
  '' close;
}

# Apply fix for very long server names
server_names_hash_bucket_size 128;

# Default dhparam
{{ if (exists "/etc/nginx/dhparam/dhparam.pem") }}
ssl_dhparam /etc/nginx/dhparam/dhparam.pem;
{{ end }}

# Set appropriate X-Forwarded-Ssl header
map $scheme $proxy_x_forwarded_ssl {
  default off;
  https on;
}

gzip_types text/plain text/css application/javascript application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;

log_format vhost '$host $remote_addr - $remote_user [$time_local] '
                 '"$request" $status $body_bytes_sent '
                 '"$http_referer" "$http_user_agent"';

access_log off;

{{ if $.Env.RESOLVERS }}
resolver {{ $.Env.RESOLVERS }};
{{ end }}

{{ if (exists "/etc/nginx/proxy.conf") }}
include /etc/nginx/proxy.conf;
{{ else }}
# HTTP 1.1 support
proxy_http_version 1.1;
proxy_buffering off;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $proxy_connection;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
proxy_set_header X-Forwarded-Ssl $proxy_x_forwarded_ssl;
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;

# Mitigate httpoxy attack (see README for details)
proxy_set_header Proxy "";
{{ end }}

{{ $enable_ipv6 := eq (or ($.Env.ENABLE_IPV6) "") "true" }}
server {
    server_name _; # This is just an invalid value which will never trigger on a real hostname.
    listen 80;
    {{ if $enable_ipv6 }}
    listen [::]:80;
    {{ end }}
    access_log /var/log/nginx/access.log vhost;
    return 503;
}

{{ if (and (exists "/etc/nginx/certs/default.crt") (exists "/etc/nginx/certs/default.key")) }}
server {
    server_name _; # This is just an invalid value which will never trigger on a real hostname.
    listen 443 ssl http2;
    {{ if $enable_ipv6 }}
    listen [::]:443 ssl http2;
    {{ end }}
    access_log /var/log/nginx/access.log vhost;
    return 503;

    ssl_session_tickets off;
    ssl_certificate /etc/nginx/certs/default.crt;
    ssl_certificate_key /etc/nginx/certs/default.key;
}
{{ end }}

{{ range $host, $containers := groupByMulti $ "Env.VIRTUAL_HOST" "," }}

{{ $host := trim $host }}
{{ $is_regexp := hasPrefix "~" $host }}
{{ $upstream_name := when $is_regexp (sha1 $host) $host }}

# {{ $host }}
upstream {{ $upstream_name }} {

{{ range $container := $containers }}
    {{ $addrLen := len $container.Addresses }}

    {{ range $knownNetwork := $CurrentContainer.Networks }}
        {{ range $containerNetwork := $container.Networks }}
            {{ if (and (ne $containerNetwork.Name "ingress") (or (eq $knownNetwork.Name $containerNetwork.Name) (eq $knownNetwork.Name "host"))) }}
                ## Can be connected with "{{ $containerNetwork.Name }}" network

                {{/* If only 1 port exposed, use that */}}
                {{ if eq $addrLen 1 }}
                    {{ $address := index $container.Addresses 0 }}
                    {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork) }}
                {{/* If more than one port exposed, use the one matching VIRTUAL_PORT env var, falling back to standard web port 80 */}}
                {{ else }}
                    {{ $port := coalesce $container.Env.VIRTUAL_PORT "80" }}
                    {{ $address := where $container.Addresses "Port" $port | first }}
                    {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork) }}
                {{ end }}
            {{ else }}
                # Cannot connect to network of this container
                server 127.0.0.1 down;
            {{ end }}
        {{ end }}
    {{ end }}
{{ end }}
}

{{ $default_host := or ($.Env.DEFAULT_HOST) "" }}
{{ $default_server := index (dict $host "" $default_host "default_server") $host }}

{{/* Get the VIRTUAL_PROTO defined by containers w/ the same vhost, falling back to "http" */}}
{{ $proto := trim (or (first (groupByKeys $containers "Env.VIRTUAL_PROTO")) "http") }}

{{/* Get the NETWORK_ACCESS defined by containers w/ the same vhost, falling back to "external" */}}
{{ $network_tag := or (first (groupByKeys $containers "Env.NETWORK_ACCESS")) "external" }}

{{/* Get the HTTPS_METHOD defined by containers w/ the same vhost, falling back to "redirect" */}}
{{ $https_method := or (first (groupByKeys $containers "Env.HTTPS_METHOD")) "redirect" }}

{{/* Get the SSL_POLICY defined by containers w/ the same vhost, falling back to "Mozilla-Intermediate" */}}
{{ $ssl_policy := or (first (groupByKeys $containers "Env.SSL_POLICY")) "Mozilla-Intermediate" }}

{{/* Get the HSTS defined by containers w/ the same vhost, falling back to "max-age=31536000" */}}
{{ $hsts := or (first (groupByKeys $containers "Env.HSTS")) "max-age=31536000" }}

{{/* Get the VIRTUAL_ROOT By containers w/ use fastcgi root */}}
{{ $vhost_root := or (first (groupByKeys $containers "Env.VIRTUAL_ROOT")) "/var/www/public" }}

{{/* Get the first cert name defined by containers w/ the same vhost */}}
{{ $certName := (first (groupByKeys $containers "Env.CERT_NAME")) }}

{{/* Get the best matching cert  by name for the vhost. */}}
{{ $vhostCert := (closest (dir "/etc/nginx/certs") (printf "%s.crt" $host))}}

{{/* vhostCert is actually a filename so remove any suffixes since they are added later */}}
{{ $vhostCert := trimSuffix ".crt" $vhostCert }}
{{ $vhostCert := trimSuffix ".key" $vhostCert }}

{{/* Use the cert specified on the container or fallback to the best vhost match */}}
{{ $cert := (coalesce $certName $vhostCert) }}

{{ $is_https := (and (ne $https_method "nohttps") (ne $cert "") (or (and (exists (printf "/etc/nginx/certs/letsencrypt/live/%s/fullchain.pem" $cert)) (exists (printf "/etc/nginx/certs/letsencrypt/live/%s/privkey.pem" $cert))) (and (exists (printf "/etc/nginx/certs/%s.crt" $cert)) (exists (printf "/etc/nginx/certs/%s.key" $cert)))) ) }}

{{ if $is_https }}

{{ if eq $https_method "redirect" }}
server {
    server_name {{ $host }};
    listen 80 {{ $default_server }};
    {{ if $enable_ipv6 }}
    listen [::]:80 {{ $default_server }};
    {{ end }}
    access_log /var/log/nginx/access.log vhost;
    return 301 https://$host$request_uri;
}
{{ end }}

server {
    server_name {{ $host }};
    listen 443 ssl http2 {{ $default_server }};
    {{ if $enable_ipv6 }}
    listen [::]:443 ssl http2 {{ $default_server }};
    {{ end }}
    access_log /var/log/nginx/access.log vhost;

    {{ if eq $network_tag "internal" }}
    # Only allow traffic from internal clients
    include /etc/nginx/network_internal.conf;
    {{ end }}

    {{ if eq $ssl_policy "Mozilla-Modern" }}
    ssl_protocols TLSv1.2;
    ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
    {{ else if eq $ssl_policy "Mozilla-Intermediate" }}
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:!DSS';
    {{ else if eq $ssl_policy "Mozilla-Old" }}
    ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:DES-CBC3-SHA:HIGH:SEED:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!RSAPSK:!aDH:!aECDH:!EDH-DSS-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA:!SRP';
    {{ else if eq $ssl_policy "AWS-TLS-1-2-2017-01" }}
    ssl_protocols TLSv1.2;
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES128-SHA256:AES256-GCM-SHA384:AES256-SHA256';
    {{ else if eq $ssl_policy "AWS-TLS-1-1-2017-01" }}
    ssl_protocols TLSv1.1 TLSv1.2;
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA:AES256-GCM-SHA384:AES256-SHA256:AES256-SHA';
    {{ else if eq $ssl_policy "AWS-2016-08" }}
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA:AES256-GCM-SHA384:AES256-SHA256:AES256-SHA';
    {{ else if eq $ssl_policy "AWS-2015-05" }}
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA:AES256-GCM-SHA384:AES256-SHA256:AES256-SHA:DES-CBC3-SHA';
    {{ else if eq $ssl_policy "AWS-2015-03" }}
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA:AES256-GCM-SHA384:AES256-SHA256:AES256-SHA:DHE-DSS-AES128-SHA:DES-CBC3-SHA';
    {{ else if eq $ssl_policy "AWS-2015-02" }}
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA:AES256-GCM-SHA384:AES256-SHA256:AES256-SHA:DHE-DSS-AES128-SHA';
    {{ end }}

    ssl_prefer_server_ciphers on;
    ssl_session_timeout 5m;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;

        {{ if (and (exists (printf "/etc/nginx/certs/letsencrypt/live/%s/fullchain.pem" $cert)) (exists (printf "/etc/nginx/certs/letsencrypt/live/%s/privkey.pem" $cert))) }}
    ssl_certificate /etc/nginx/certs/letsencrypt/live/{{ (printf "%s/fullchain.pem" $cert) }};
    ssl_certificate_key /etc/nginx/certs/letsencrypt/live/{{ (printf "%s/privkey.pem" $cert) }};
        {{ else if (and (exists (printf "/etc/nginx/certs/%s.crt" $cert)) (exists (printf "/etc/nginx/certs/%s.key" $cert))) }}
    ssl_certificate /etc/nginx/certs/{{ (printf "%s.crt" $cert) }};
    ssl_certificate_key /etc/nginx/certs/{{ (printf "%s.key" $cert) }};
    {{ end }}

    {{ if (exists (printf "/etc/nginx/certs/%s.dhparam.pem" $cert)) }}
    ssl_dhparam {{ printf "/etc/nginx/certs/%s.dhparam.pem" $cert }};
    {{ end }}

    {{ if (exists (printf "/etc/nginx/certs/%s.chain.pem" $cert)) }}
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate {{ printf "/etc/nginx/certs/%s.chain.pem" $cert }};
    {{ end }}

    {{ if (and (ne $https_method "noredirect") (ne $hsts "off")) }}
    add_header Strict-Transport-Security "{{ trim $hsts }}" always;
    {{ end }}

    {{ if (exists (printf "/etc/nginx/vhost.d/%s" $host)) }}
    include {{ printf "/etc/nginx/vhost.d/%s" $host }};
    {{ else if (exists "/etc/nginx/vhost.d/default") }}
    include /etc/nginx/vhost.d/default;
    {{ end }}

    location / {
        {{ if eq $proto "uwsgi" }}
        include uwsgi_params;
        uwsgi_pass {{ trim $proto }}://{{ trim $upstream_name }};
        {{ else if eq $proto "fastcgi" }}
        root   {{ trim $vhost_root }};
        include fastcgi.conf;
        fastcgi_pass {{ trim $upstream_name }};
        {{ else }}
        proxy_pass {{ trim $proto }}://{{ trim $upstream_name }};
        {{ end }}

        {{ if (exists (printf "/etc/nginx/htpasswd/%s" $host)) }}
        auth_basic  "Restricted {{ $host }}";
        auth_basic_user_file    {{ (printf "/etc/nginx/htpasswd/%s" $host) }};
        {{ end }}
        {{ if (exists (printf "/etc/nginx/vhost.d/%s_location" $host)) }}
        include {{ printf "/etc/nginx/vhost.d/%s_location" $host}};
        {{ else if (exists "/etc/nginx/vhost.d/default_location") }}
        include /etc/nginx/vhost.d/default_location;
        {{ end }}
    }
}

{{ end }}

{{ if or (not $is_https) (eq $https_method "noredirect") }}

server {
    server_name {{ $host }};
    listen 80 {{ $default_server }};
    {{ if $enable_ipv6 }}
    listen [::]:80 {{ $default_server }};
    {{ end }}
    access_log /var/log/nginx/access.log vhost;

    {{ if eq $network_tag "internal" }}
    # Only allow traffic from internal clients
    include /etc/nginx/network_internal.conf;
    {{ end }}

    {{ if (exists (printf "/etc/nginx/vhost.d/%s" $host)) }}
    include {{ printf "/etc/nginx/vhost.d/%s" $host }};
    {{ else if (exists "/etc/nginx/vhost.d/default") }}
    include /etc/nginx/vhost.d/default;
    {{ end }}

    location / {
        {{ if eq $proto "uwsgi" }}
        include uwsgi_params;
        uwsgi_pass {{ trim $proto }}://{{ trim $upstream_name }};
        {{ else if eq $proto "fastcgi" }}
        root   {{ trim $vhost_root }};
        include fastcgi.conf;
        fastcgi_pass {{ trim $upstream_name }};
        {{ else }}
        proxy_pass {{ trim $proto }}://{{ trim $upstream_name }};
        {{ end }}
        {{ if (exists (printf "/etc/nginx/htpasswd/%s" $host)) }}
        auth_basic  "Restricted {{ $host }}";
        auth_basic_user_file    {{ (printf "/etc/nginx/htpasswd/%s" $host) }};
        {{ end }}
        {{ if (exists (printf "/etc/nginx/vhost.d/%s_location" $host)) }}
        include {{ printf "/etc/nginx/vhost.d/%s_location" $host}};
        {{ else if (exists "/etc/nginx/vhost.d/default_location") }}
        include /etc/nginx/vhost.d/default_location;
        {{ end }}
    }
}

{{ if (and (not $is_https) (exists "/etc/nginx/certs/default.crt") (exists "/etc/nginx/certs/default.key")) }}
server {
    server_name {{ $host }};
    listen 443 ssl http2 {{ $default_server }};
    {{ if $enable_ipv6 }}
    listen [::]:443 ssl http2 {{ $default_server }};
    {{ end }}
    access_log /var/log/nginx/access.log vhost;
    return 500;

    ssl_certificate /etc/nginx/certs/default.crt;
    ssl_certificate_key /etc/nginx/certs/default.key;
}
{{ end }}

{{ end }}
{{ end }}

Видно, что по умолчанию nginx будет искать сертификаты типа /etc/nginx/certs/%s.crt и /etc/nginx/certs/%s.pem, где %s — имя сертификата (по умолчанию — имя хоста, но его возможно изменять через переменные).


Агент же хранит сертификаты в структуре каталогов /etc/nginx/certs/letsencrypt/live/%s/{fullchain.pem, privkey.pem}, и потому в нескольких местах шаблона дополнены условия для таких имён сертификатов:


{{
$is_https :=
(and
  (ne $https_method "nohttps")
  (ne $cert "")
  (or
    (and
      (exists (printf "/etc/nginx/certs/letsencrypt/live/%s/fullchain.pem" $cert))
      (exists (printf "/etc/nginx/certs/letsencrypt/live/%s/privkey.pem" $cert))
    )
    (and
      (exists (printf "/etc/nginx/certs/%s.crt" $cert))
      (exists (printf "/etc/nginx/certs/%s.key" $cert))
    )
  )
)
}}

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


/tank0/docker/services/nginx-proxy/certs/letsencrypt/domains.conf
*.NAS.cloudns.cc NAS.cloudns.cc

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


/tank0/docker/services/nginx-proxy/local-config/max_upload_size.conf
client_max_body_size 20G;

Настройка закончена, пора запустить контейнер:


docker-compose up

Проверьте работоспособность (всё скачалось и запустилось), нажмите Ctrl+C и запустите контейнер в отвязанном от консоли режиме:


docker-compose up -d

Настройка контейнера с тестовым сервером


Тестовый сервер — это минимальный nginx, который должен выводить страницу приветствия. Нужно, чтобы он мог легко запускаться и останавливаться, а его контейнер быстро пересоздавался.
Он будет первым и пока единственным сервисом, который будет работать в составе NAS.


Файлы конфигурации находятся здесь.


Вот его docker-compose файл:


/tank0/docker/services/test_nginx/docker-compose.yml
version: '2'

networks:
  docker0:
    external:
      name: docker0

services:
  nginx-local:
    restart: always
    image: nginx:alpine
    expose:
      - 80
      - 443
    environment:
      - "VIRTUAL_HOST=test.NAS.cloudns.cc"
      - "VIRTUAL_PROTO=http"
      - "VIRTUAL_PORT=80"
      - CERT_NAME=NAS.cloudns.cc
    networks:
      - docker0

Каждому контейнеру со службой нужно указать следующие параметры:


  • docker0 — внешняя сеть. Это указано в заголовке.
  • expose — выставить порты в сеть, где работает контейнер. Как правило, порт 80 для протокола HTTP и 443 для протокола HTTPS.
  • VIRTUAL_HOST=test.NAS.cloudns.cc — в данной переменной указан виртуальный хост, по которому nginx-reverse-proxy будет перенаправлять запрос на этот контейнер.
  • VIRTUAL_PROTO=http — протокол по которому nginx-reverse-proxy будет взаимодействовать с данным сервисом. Если сертификата нет, это HTTP.
  • VIRTUAL_PORT=80 — порт на который будет обращаться nginx-reverse-proxy.
  • CERT_NAME=NAS.cloudns.cc — имя внешнего сертификата. В данном случае, у всех сервисов сертификат один, потому имя везде одинаковое. NAS — имя DNS зоны.
  • networks — в данной секции для всех фронтэндов, которые общаются с nginx-reverse-proxy должна быть указана сеть docker0.

Контейнер настроен, теперь нужно его поднять. Выполнив docker-compose up, зайдите по адресу test.NAS.cloudns.cc.


На консоль должно быть выведено примерно следующее:


$ docker-compose up

Creating testnginx_nginx-local_1
Attaching to testnginx_nginx-local_1
nginx-local_1  | 172.22.0.5 - - [29/Jul/2018:15:32:02 +0000] "GET / HTTP/1.1" 200 612 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537 (KHTML, like Gecko) Chrome/67.0 Safari/537" "192.168.2.3"
nginx-local_1  | 2018/07/29 15:32:02 [error] 8#8: *2 open() "/usr/share/nginx/html/favicon.ico" failed (2: No such file or directory), client: 172.22.0.5, server: localhost, request: "GET /favicon.ico HTTP/1.1", host: "test.NAS.cloudns.cc", referrer: "https://test.NAS.cloudns.cc/"
nginx-local_1  | 172.22.0.5 - - [29/Jul/2018:15:32:02 +0000] "GET /favicon.ico HTTP/1.1" 404 572 "https://test.NAS.cloudns.cc/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537 (KHTML, like Gecko) Chrome/67.0 Safari/537" "192.168.2.3"

А браузер покажет следующую страницу:


Запущенный Nginx


Если, в итоге, у вас появилась страница, как на скриншоте выше, могу вас поздравить: всё настроено и работает правильно.


Теперь этот контейнер больше не нужен, остановите по Ctrl+C, выполнив затем docker-compose down.


Настройка контейнера с local-rpoxy


После настройки прокси, неплохо бы поднять контейнер с nginx-default с сервером, проксирующим запросы для хоста nas, omv и подобных через внешнюю сеть на порты 10080 и 10443 ОС хостовой машины.


Файлы конфигурации находятся здесь.


/tank0/docker/services/nginx-local/docker-compose.yml
version: '2'

networks:
  docker0:
    external:
      name: docker0

services:
  nginx-local:
    restart: always
    image: nginx:alpine
    expose:
      - 80
      - 443
    environment:
      - "VIRTUAL_HOST=NAS.cloudns.cc,nas,nas.*,www.*,omv.*,nas-controller.nas"
      - "VIRTUAL_PROTO=http"
      - "VIRTUAL_PORT=80"
      - CERT_NAME=NAS.cloudns.cc
    volumes:
      - ./local-config:/etc/nginx/conf.d
    networks:
      - docker0

С конфигурацией docker-compose всё должно быть понятно, и останавливаться на её описании я не буду.
Единственное, что хочу заметить, это то, что один из доменов NAS.cloudns.cc. Это сделано для того, чтобы при обращении к NAS только по имени DNS зоны, запрос переводился на хост.


/tank0/docker/services/nginx-local/local-config/default.conf
# If we receive X-Forwarded-Proto, pass it through; otherwise, pass along the
# scheme used to connect to this server
map $http_x_forwarded_proto $proxy_x_forwarded_proto {
  default $http_x_forwarded_proto;
  ''      $scheme;
}

# If we receive X-Forwarded-Port, pass it through; otherwise, pass along the
# server port the client connected to
map $http_x_forwarded_port $proxy_x_forwarded_port {
  default $http_x_forwarded_port;
  ''      $server_port;
}

# Set appropriate X-Forwarded-Ssl header
map $scheme $proxy_x_forwarded_ssl {
  default off;
  https on;
}

access_log on;
error_log on;
# HTTP 1.1 support
proxy_http_version 1.1;
proxy_buffering off;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
proxy_set_header X-Forwarded-Ssl $proxy_x_forwarded_ssl;
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;

# Mitigate httpoxy attack (see README for details)
proxy_set_header Proxy "";

server {
        server_name _; # This is just an invalid value which will never trigger on a real hostname.
        listen 80;
        return 503;
}

server {
        server_name www.* nas.* omv.* "";

        listen 80;
        location / {
                proxy_pass https://172.21.0.1:10443/;
        }
}

# nas-controller
server {
        server_name nas-controller.nas;

        listen 80 ;
        location / {
                proxy_pass https://nas-controller/;
        }
}

  • 172.21.0.1 — сеть хоста. Перенаправление запроса всегда производится на порт 443, потому что раньше был сгенерирован сертификат и OMV работает по HTTPS. Пусть также и останется даже для внутреннего общения.
  • https://nas-controller/ — по-идее, это интерфейс на котором работает IPMI, и если обратиться к nas, как к nas-controller.nas, запрос будет перенаправлен на внешний адрес nas-controller. Не особенно полезно.

Установка и настройка LDAP


Настройка LDAP-сервера


LDAP-сервер — это центральный компонент системы управления пользователями.
Он также работает внутри Docker контейнера. В котором, помимо него, запущены интерфейсы для администрирования и смены паролей.


Файлы конфигурации и LDIF-файлы находятся здесь.


/tank0/docker/services/ldap/docker-compose.yml
version: "2"

networks:
  ldap:
  docker0:
    external:
      name: docker0

services:
  open-ldap:
    image: "osixia/openldap:1.2.0"
    hostname: "open-ldap"
    restart: always
    environment:
      - "LDAP_ORGANISATION=NAS"
      - "LDAP_DOMAIN=nas.nas"
      - "LDAP_ADMIN_PASSWORD=ADMIN_PASSWORD"
      - "LDAP_CONFIG_PASSWORD=CONFIG_PASSWORD"
      - "LDAP_TLS=true"
      - "LDAP_TLS_ENFORCE=false"
      - "LDAP_TLS_CRT_FILENAME=ldap_server.crt"
      - "LDAP_TLS_KEY_FILENAME=ldap_server.key"
      - "LDAP_TLS_CA_CRT_FILENAME=ldap_server.crt"
    volumes:
      - ./certs:/container/service/slapd/assets/certs
      - ./ldap_data/var/lib:/var/lib/ldap
      - ./ldap_data/etc/ldap/slapd.d:/etc/ldap/slapd.d
    networks:
      - ldap
    ports:
      - 172.21.0.1:389:389
      - 172.21.0.1::636:636

  phpldapadmin:
    image: "osixia/phpldapadmin:0.7.1"
    hostname: "nas.nas"
    restart: always
    networks:
      - ldap
      - docker0
    expose:
      - 443
    links:
      - open-ldap:open-ldap-server
    volumes:
      - ./certs:/container/service/phpldapadmin/assets/apache2/certs
    environment:
      - VIRTUAL_HOST=ldap.*
      - VIRTUAL_PORT=443
      - VIRTUAL_PROTO=https
      - CERT_NAME=NAS.cloudns.cc

      - "PHPLDAPADMIN_LDAP_HOSTS=open-ldap-server"
      #- "PHPLDAPADMIN_HTTPS=false"
      - "PHPLDAPADMIN_HTTPS_CRT_FILENAME=certs/ldap_server.crt"
      - "PHPLDAPADMIN_HTTPS_KEY_FILENAME=private/ldap_server.key"
      - "PHPLDAPADMIN_HTTPS_CA_CRT_FILENAME=certs/ldap_server.crt"
      - "PHPLDAPADMIN_LDAP_CLIENT_TLS_REQCERT=allow"

  ldap-ssp:
    image: openfrontier/ldap-ssp:https
    volumes:
      #- ./ssp/mods-enabled/ssl.conf:/etc/apache2/mods-enabled/ssl.conf
      - /etc/ssl/certs/ssl-cert-snakeoil.pem:/etc/ssl/certs/ssl-cert-snakeoil.pem
      - /etc/ssl/private/ssl-cert-snakeoil.key:/etc/ssl/private/ssl-cert-snakeoil.key
    restart: always
    networks:
      - ldap
      - docker0
    expose:
      - 80
    links:
      - open-ldap:open-ldap-server
    environment:
      - VIRTUAL_HOST=ssp.*
      - VIRTUAL_PORT=80
      - VIRTUAL_PROTO=http
      - CERT_NAME=NAS.cloudns.cc

      - "LDAP_URL=ldap://open-ldap-server:389"
      - "LDAP_BINDDN=cn=admin,dc=nas,dc=nas"
      - "LDAP_BINDPW=ADMIN_PASSWORD"
      - "LDAP_BASE=ou=users,dc=nas,dc=nas"
      - "MAIL_FROM=admin@nas.nas"
      - "PWD_MIN_LENGTH=8"
      - "PWD_MIN_LOWER=3"
      - "PWD_MIN_DIGIT=2"
      - "SMTP_HOST="
      - "SMTP_USER="
      - "SMTP_PASS="

В конфиге описано три сервиса:


  • open-ldap — LDAP-сервер.
  • phpldapadmin — WEB-интерфейс для его администрирования. Через него возможно добавлять и удалять пользователей, группы и т.п..
  • ldap-ssp — WEB-интерфейс для смены паролей пользователями.

LDAP-сервер требует настройки некоторых параметров, которые задаются через переменные окружения:


  • LDAP_ORGANISATION=NAS — имя организации. Может быть произвольным.
  • LDAP_DOMAIN=nas.nas — домен. Тоже произвольный. Указать лучше тот же, что и доменное имя.
  • LDAP_ADMIN_PASSWORD=ADMIN_PASSWORD — пароль администратора.
  • LDAP_CONFIG_PASSWORD=CONFIG_PASSWORD — пароль для конфигурации.

По-идее, не мешает добавить ещё и пользователя "только для чтения", но потом.


Тома:


  • /container/service/slapd/assets/certs отображён в локальный каталог certs — сертификаты. Сейчас не используется.
  • ./ldap_data/ — локальный каталог, подкаталоги которого проброшены в два каталога внутри контейнеров. Тут LDAP хранит свою базу.

Сервер работает во внутренней сети ldap, но его порты 389 (незащищённый LDAP) и 636 (LDAP по SSL, пока не используемый) проброшены в сеть хоста.


PhpLdapAdmin работает в двух сетях: он обращается к серверу LDAP в сети ldap и открывает порт 443 в сети docker0, для того, чтобы к нему мог обратиться nginx-reverse-proxy.


Настройки:


  • VIRTUAL_HOST=ldap.* — хост, которому nginx-reverse-proxy будет сопоставлять контейнер.
  • VIRTUAL_PORT=443 — порт для nginx-reverse-proxy.
  • VIRTUAL_PROTO=https — протокол для nginx-reverse-proxy.
  • CERT_NAME=NAS.cloudns.cc — имя сертификата, одинаковое для всех.

Блок переменных после этого предназначен для настройки SSL и сейчас не обязателен.


SSP доступен по HTTP и тоже работает в двух сетях.
Тома, в этом контейнере не используются, и проброшенный сертификат остался от старых экспериментов.


Переменные для настройки — это ограничения на длину пароля и учётные данные для доступа к серверу LDAP.


  • LDAP_URL=ldap://open-ldap-server:389 — адрес и порт LDAP сервера (см. секцию links).
  • LDAP_BINDDN=cn=admin,dc=nas,dc=nas — логин администратора и домен для аутентификации.
  • LDAP_BINDPW=ADMIN_PASSWORD — пароль администратора, который должен совпадать с паролем, указанным для контейнера open-ldap.
  • LDAP_BASE=ou=users,dc=nas,dc=nas — это базовый путь, по которому содержатся учётные данные пользователей.

Установите на хостовой машине утилиты для работы с LDAP и инициализируйте LDAP каталог:


apt-get install ldap-utils
ldapadd -x -H ldap://172.21.0.1  -D "cn=admin,dc=nas,dc=nas" -W -f ldifs/inititialize_ldap.ldif
ldapadd -x -H ldap://172.21.0.1  -D "cn=admin,dc=nas,dc=nas" -W -f ldifs/base.ldif
ldapadd -x -H ldap://172.21.0.1  -D "cn=admin,cn=config" -W -f ldifs/gitlab_attr.ldif

В gitlab_attr.ldif добавляется атрибут, по которому Gitlab (о нём потом) будет находить пользователей.
После этого вы можете выполнить следующую команду для проверки.


Проверка работоспособности LDAP сервера
$ ldapsearch -x -H ldap://172.21.0.1 -b dc=nas,dc=nas -D "cn=admin,dc=nas,dc=nas" -W

Enter LDAP Password: 
# extended LDIF
#
# LDAPv3
# base <dc=nas,dc=nas> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#

# nas.nas
dn: dc=nas,dc=nas
objectClass: top
objectClass: dcObject
objectClass: organization
o: NAS
dc: nas

# admin, nas.nas
dn: cn=admin,dc=nas,dc=nas
objectClass: simpleSecurityObject
objectClass: organizationalRole
cn: admin
description: LDAP administrator
...

# ldap_users, groups, nas.nas
dn: cn=ldap_users,ou=groups,dc=nas,dc=nas
cn: ldap_users
gidNumber: 500
objectClass: posixGroup
objectClass: top

# search result
search: 2
result: 0 Success

# numResponses: 12
# numEntries: 11

На этом настройка LDAP сервера закончена. Управлять сервером вы можете через WEB-интерфейс.


Настройка OMV для входа по LDAP


Если LDAP сервер настроен и работает, OMV настраивается на работу с ним очень просто: указываете хост, порт, данные для авторизации, корневой каталог для поиска пользователей и атрибут для определения того, что найденная запись — аккаунт пользователя.


LDAP плагин вы уже должны были установить.


Всё показано на скриншоте:


Настройка OMV для работы с LDAP


Взаимодействие с источником питания


Сначала настройте ИБП по инструкции, которая идёт вместе с ним, и подключите его к NAS по USB.
Плагин для работы с ИБП вы должны были установить ранее.
Теперь остаётся только настроить NUT через GUI OMV.
Зайдите на страницу "Сервисы->ИБП", включите ИБП, в поле идентификатор введите любую строку, описывающую ИБП, например "eaton".


В поле "Директивы конфигурации драйверов" введите следующее:


driver = usbhid-ups
port = auto
desc = "Eaton 9130 700 VA"
vendorid = 0463
pollinterval = 10

  • driver = usbhid-ups — ИБП подключен по USB, потому используется драйвер USB HID.
  • vendorid — это идентификатор производителя ИБП, который может быть получен командой lsusb.
  • pollinterval — интервал опроса ИБП в cекундах.

Остальные параметры возможно посмотреть в документации.


Вывод lsusb, строка с ИБП указана стрелкой:


# lsusb 
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
--> Bus 001 Device 003: ID 0463:ffff MGE UPS Systems UPS
Bus 001 Device 004: ID 046b:ff10 American Megatrends, Inc. Virtual Keyboard and Mouse
Bus 001 Device 002: ID 046b:ff01 American Megatrends, Inc. 
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

"Режим отключения" надо установить в "низкий заряд батареи".
Должно получиться примерно так, как показано на скриншоте:


Настройка ИБП


Выключите ИБП и снова включите. Если были настроены уведомления, вам на почту придёт письмо о потере питания.
На этом настройка ИБП окончена.


Заключение


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


Если хотите действовать по-другому, OMV позволит вам это.


Доступно управление сетями из WEB-интерфейса, причём в некотором плане это более удобно, чем через консоль:



Для Docker тоже есть весьма понятный WEB-интерфейс:



Кроме того, OMV может рисовать красивые графики.


График использования сети:


График использования сети


График использования памяти:


График использования памяти


График использования CPU:


График использования CPU


Нереализованное


  • Проблемы с настройкой — отдельная большая тема. Возможно, что с первого раза что-то не заработает. В таком случае, вам в помощь docker-compose exec, а также внимательное изучение докуменатции и исходников.
  • LDAP сервер не мешало бы настроить лучше, особенно в плане безопасности (использовать SSL везде, добавить пользователя для чтения и т.п.).
  • Пока совершенно не затронуты вопросы доверенной загрузки и повышения безопасности, я об этом знаю, но в другой раз.
  • Пользователь ValdikSS дал очень полезный совет использовать DropbearSSH, внедрённый в initramfs для решения проблемы ненамеренных перезагрузок. Об этом будет другая статья.

На этом всё.
С Богом!





К сожалению, не доступен сервер mySQL