Реализация программной платформы защищённого NAS
В предыдущей статье было описано проектирование программной платформы 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 <> /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 -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_PROVIDER_OPTIONS=--auth-id=CLOUDNS_ID --auth-password=CLOUDNS_PASSWORD --delegated=NAS.cloudns.cc"
В данном конфиге настраиваются два контейнера:
- nginx-reverse-proxy — cам обратный прокси.
- letsencrypt-dns — ACME клиент Let’s Encrypt.
Для создания и запуска контейнера с nginx-reverse-proxy используется образ jwilder/nginx-proxy.
docker0
— межконтейнерная сеть, которая была создана ранее, ей не управляет docker-compose.nginx-proxy
— сервис обратного прокси, собственной персоной. Он смотрит в сеть docker0. При этом, порты 80 и 443 в секции ports пробрасываются на аналогичные порты хоста (значит, на хосте будут открыты такие же порты, а данные с них будут перенаправляться на порты в сети docker0, которые слушает прокси).
Параметр restart: always
означает, что нужно запускать этот сервис при перезагрузке.
Тома:
- Внешний каталог
certs
отображается в/etc/nginx/certs
— там лежат сертификаты, включая сертификаты, полученные от Let’s Encrypt. Это общий каталог между контейнером с прокси и контейнером с ACME клиентом. ./vhost.d:/etc/nginx/vhost.d
— конфигурация отдельных виртуальных хостов. Сейчас не использую../html:/usr/share/nginx/html
— статичный контент. Там не нужно ничего настраивать./var/run/docker.sock
, отображаемый в/tmp/docker.sock
— сокет для связи с демоном Docker на хосте. Нужен для работы docker-gen внутри оригинального образа../local-config
, отображаемый в/etc/nginx/conf.d
— дополнительные конфигурационные файлы nginx. Требуется для тюнинга параметров, о которых ниже../nginx.tmpl
, отображаемый в/app/nginx.tmpl
— шаблон для конфигурационного файла nginx, из которого docker-gen создаст конфиг.
Контейнер letsencrypt-dns создаётся из образа adferrand/letsencrypt-dns. Он включает упомянутый выше ACME клиент и Lexicon, для общения с провайдером DNS зоны.
Общий каталог certs/letsencrypt
отображается в /etc/letsencrypt
внутри контейнера.
Чтобы это заработало, требуется настроить ещё несколько переменных окружения внутри контейнера:
LETSENCRYPT_USER_MAIL=MAIL@MAIL.COM
— почта пользователя Let’s Encrypt. Лучше тут указать реальную почту, на которую будут приходить всякие сообщения.LEXICON_PROVIDER=cloudns
— провайдер для Lexicon. В моём случае —cloudns
.LEXICON_PROVIDER_OPTIONS=--auth-id=CLOUDNS_ID --auth-password=CLOUDNS_PASSWORD --delegated=NAS.cloudns.cc
— CLOUDNS_ID на последнем скриншоте в секции по настройке ClouDNS подчёркнут красным. CLOUDNS_PASSWORD — это пароль, который вы задали для пользования API. NAS.cloudns.cc, где NAS — имя вашей DNS зоны. Для cloudns нужен потому, что по умолчанию будут передаваться первые два компонента домена (cloudns.cc), а ClouDNS API требует указывать зону в запросе.
После этой настройки будут два независимо работающих контейнера: прокси и агент для получения сертификата.
При этом, прокси будет искать сертификат в каталогах, указанных в конфиге, но не в структуре каталогов, которую создаст агент Let’s encrypt:
$ ls ./certs/letsencrypt/
accounts archive csr domains.conf keys live renewal renewal-hooks
Для того, чтобы прокси начал видеть полученные сертификаты, требуется немного исправить шаблон.
{{ $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-