
В предыдущей статье было описано проектирование программной платформы 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-initsystemOMV установлен. Он использует своё ядро, и после установки может потребоваться перезагрузка.
Перезагрузившись, интерфейс 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 = 10driver = 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 для решения проблемы ненамеренных перезагрузок. Об этом будет другая статья.
На этом всё.
С Богом!





























