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

b93ed1af987a1fda471d1080d6b804e5.jpg

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


Проверка

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

zpool status -v

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

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


Удобства

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


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

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

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

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

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

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


Настройка


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

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

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

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


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

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

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


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

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

apt-get dist-upgrade --yes
reboot


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

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

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


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


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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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


Пулы tankN

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

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


create_crypt_pool.sh
#!/bin/bash

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

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

shift

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

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

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

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

phrase="?"

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

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

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

index=0

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

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

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

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

./create_crypt_pool.sh

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

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

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

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

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

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


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

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

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

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


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

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


Установка OMV

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

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


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

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

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


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

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

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

eosfraeilpg2dn770ef5bvr-ple.png

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


Настройка OMV

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


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

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

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

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

Сертификат

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

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

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

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

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


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

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


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


Сеть

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

Затем:


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

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

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

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

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

Настройка SSH


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

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

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

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

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

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


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

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

dqsejqlsyn0x6wdbyfeb2kvlwmc.png

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


Список плагинов.
  • 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→Устройства». Включите мониторинг для каждого диска.
    83r-dcwtrhth5icaketuw1zlis8.png
  • Перейдите на вкладку «Хранилище→S.M. A.R.T→Запланированные тесты». Добавьте для каждого диска короткую самопроверку раз в сутки и длительную самопроверку раз в месяц. Причём так, чтобы периоды самопроверки не пересекались.
    lhu8qffcenb8utgcekfoy_j0oee.png

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


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

Надо создать файловые системы для предопределённых каталогов.
Сделать это возможно из консоли, либо из 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

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

uauyykwki2nmmmpj6pe3-lqxrie.png

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

xc9vdaubqewzyhqfm80k_751fo8.png


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

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


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

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

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

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

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

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

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

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

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

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

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


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

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

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


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

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

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

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


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


IP-адрес

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


Роутер

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

Mikrotik Routerboard

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


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

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


ClouDNS

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

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

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

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


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

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

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

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

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

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

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

z_6kyvkyj9gqlbi_s-dudfoirzs.png

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


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


Настройка Docker

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

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

apt-get install docker-compose

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

zfs create -p /tank0/docker/services

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

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

wnbyecyxrlrsf0cssx78zqmoxdu.png

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

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

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

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

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

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

docker network create docker0

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


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

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

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

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

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


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

networks:
  docker0:
    external:
      name: docker0

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

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

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


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

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

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

Тома:


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

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

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

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


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

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

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

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


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

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

{{ end }}

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

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

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

# Apply fix for very long server names
server_names_hash_bucket_size 128;

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

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

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

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

access_log off;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

{{ if $is_https }}

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

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

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

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

© Habrahabr.ru