
В предыдущей статье было описано проектирование программной платформы 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 создаётся по аналогии.
Чтобы не заниматься созданием одинаковых разделов вручную и не допускать ошибок, я написал скрипт для создания шифрованных разделов под пулы:
#!/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:
Включение импорта пулов и автомонтирования томов при загрузке
Для того, чтобы гарантированно включить автомонтирование пулов, выполните следующие команды:
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, чтобы пакетная система увидела репозиторий.
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 включен.
Для установки и первичной инициализации надо выполнить команды, приведённые ниже.
./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:
Репозитории и модули
На вкладке "Система->Управление обновлениями->Настройки" включите "Обновления поддерживаемые сообществом".
Сначала требуется добавить репозитории 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 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 показан на скриншоте:
Настройка сети вне NAS
IP-адрес
Я использую белый статический IP-адрес, который стоит плюсом 100 рублей в месяц. Если нет желания платить и ваш адрес динамический, но не за NAT, возможно корректировать внешние DNS записи через API выбранного сервиса.
Тем не менее, стоит иметь ввиду, что а��рес не за NAT может внезапно стать адресом за NAT: как правило, провайдеры не дают никаких гарантий.
Роутер
В качестве роутера у меня 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", далее останавливаться я на этом не буду.
ClouDNS
С CLouDNS всё просто. Заводите аккаунт, подтверждаете. NS записи уже у вас будут прописаны. Далее требуется минимальная настройка.
Во-первых, нужно создать необходимые зоны (зона с именем NAS, подчёркнутая на скриншоте красным — это то, что вы должны создать, с другим названием, конечно).
Во-вторых, в этой зоне вы должны прописать следующие A-записи:
- nas, www, omv, control и пустое имя. Для обращения к интерфейсу OMV.
- ldap. Интерфейс PhpLdapAdmin.
- ssp. Интерфейс для смены паролей пользователей.
- test. Тестовый сервер.
Остальные доменные имена будут добавляться по мере добавления служб.
Кликайте на зону, далее "Add new record", выбираете A-тип, вводите имя зоны и IP адрес роутера, за которым стоит NAS.
Во-вторых, требуется получить доступ к API. В ClouDNS он платный, так что предварительно надо его оплатить. В других сервисах он бесплатный. Если знаете, что лучше, и это поддерживается Lexicon, напишите пожалуйста в комментариях.
Получив доступ к API, туда надо добавить нового пользователя API.
В поле "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 -o com.sun:auto-snapshot=false -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.
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_OPTIONS=--delegated NAS.cloudns.cc" - "LEXICON_PROVIDER_OPTIONS=--auth-id=CLOUDNS_ID --auth-password=CLOUDNS_PASSWORD"
В данном конфиге настраиваются два контейнера:
- 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
Для того, чтобы прокси начал видеть полученные сертификаты, требуется немного исправить шаблон.
{{ $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.
*.NAS.cloudns.cc NAS.cloudns.cc
И ещё один маленький нюанс. Для того, чтобы в будущем вы могли загружать файлы приемлемого размера в облако, и их не резал прокси, установите для него параметр client_max_body_size хотя бы гигабайт в 20, как это показано ниже.
client_max_body_size 20G;
Настройка закончена, пора запустить контейнер:
docker-compose up
Проверьте работоспособность (всё скачалось и запустилось), нажмите Ctrl+C и запустите контейнер в отвязанном от консоли режиме:
docker-compose up -d
Настройка контейнера с тестовым сервером
Тестовый сервер — это минимальный nginx, который должен выводить страницу приветствия. Нужно, чтобы он мог легко запускаться и останавливаться, а его контейнер быстро пересоздавался.
Он будет первым и пока единственным сервисом, который будет работать в составе NAS.
Файлы конфигурации находятся здесь.
Вот его docker-compose файл:
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"
А браузер покажет следующую страницу:
Если, в итоге, у вас появилась страница, как на скриншоте выше, могу вас поздравить: всё настроено и работает правильно.
Теперь этот контейнер больше не нужен, остановите по Ctrl+C, выполнив затем docker-compose down.
Настройка контейнера с local-rpoxy
После настройки прокси, неплохо бы поднять контейнер с nginx-default с сервером, проксирующим запросы для хоста nas, omv и подобных через внешнюю сеть на порты 10080 и 10443 ОС хостовой машины.
Файлы конфигурации находятся здесь.
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 зоны, запрос переводился на хост.
# 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-файлы находятся здесь.
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 (о нём потом) будет находить пользователей.
После этого вы можете выполнить следующую команду для проверки.
$ 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 плагин вы уже должны были установить.
Всё показано на скриншоте:
Взаимодействие с источником питания
Сначала настройте ИБП по инструкции, которая идёт вместе с ним, и подключите его к 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:
Нереализованное
- Проблемы с настройкой — отдельная большая тема. Возможно, что с первого раза что-то не заработает. В таком случае, вам в помощь
docker-compose exec, а также внимательное изучение докуменатции и исходников. - LDAP сервер не мешало бы настроить лучше, особенно в плане безопасности (использовать SSL везде, добавить пользователя для чтения и т.п.).
- Пока совершенно не затронуты вопросы доверенной загрузки и повышения безопасности, я об этом знаю, но в другой раз.
- Пользователь ValdikSS дал очень полезный совет использовать DropbearSSH, внедрённый в initramfs для решения проблемы ненамеренных перезагрузок. Об этом будет другая статья.
На этом всё.
С Богом!





























