SFTP-доступ к хранилищу внутри LXC-контейнера собирается из трёх частей: монтирование каталога хост-системы в контейнер, настройка прав с учётом UID/GID mapping и изоляция пользователя в sshd через ChrootDirectory. Команды ниже ориентированы на Proxmox VE 8.x (управление через pct) и чистый LXD 5.x (lxc config), конфигурация sshd соответствует синтаксису OpenSSH 9.x.
Схема, которая закрывает задачу с минимальным риском: непривилегированный контейнер, bind mount одного каталога данных, SFTP-пользователь без shell, вход по SSH-ключу. В такой конфигурации пользователь не получает интерактивную оболочку, не выходит за пределы chroot и не может использовать смонтированный каталог как мост на хост-систему.
Два условия проверяют до первой команды. Bind mount не заменяет резервное копирование: файлы лежат на хосте, и их сохранность обеспечивают отдельные задачи бэкапа. Версии меняют синтаксис: параметры pct и lxc отличаются между релизами, поэтому шаги стоит прогнать на тестовом контейнере и сверить с документацией своей версии.
Что нужно решить до настройки SFTP в LXC
Работа делится на три слоя: хранилище (какой каталог хоста попадёт в контейнер и с какими флагами), права (кто владелец файлов и как UID хоста соотносятся с UID контейнера), изоляция (что разрешено SFTP-пользователю после входа). Каждый слой проверяют отдельно, потому что ошибка в одном не компенсируется аккуратностью в другом: корректный chroot не спасёт данные, если каталог открыт на запись всем пользователям контейнера.
Порядок проверок: сначала монтирование и видимость данных, потом владельцы и права, затем sshd и chroot. Так вы не станете искать причину ошибки Permission denied в конфиге sshd, если дело в idmap.
Когда bind mount лучше отдельного хранилища
Bind mount отдаёт в контейнер существующий каталог хоста: /srv/data виден внутри как /data без копирования. Для массивов на ZFS или BTRFS это нулевые затраты по месту и никакого дублирования. Вариант подходит, когда данные уже лежат на хосте и нужны нескольким контейнерам или хостовым сервисам: например, каталог /srv/data на 500 ГБ использует бэкап-скрипт на хосте и одновременно принимает файлы по SFTP.
Отдельный dataset или диск внутри контейнера изолировать проще: границы хранилища совпадают с границами контейнера, снапшоты и квоты задаются штатными средствами ZFS или BTRFS, а случайное удаление не задевает хостовые данные. Плата за это - перенос данных внутрь и бэкап, привязанный к файлам контейнера.
Критерий выбора: каталог нужен только одному контейнеру и никому больше, выносите его в отдельный dataset и подключайте как единственное хранилище. Каталог общий или уже содержит терабайты, применяйте bind mount, но добавьте опции noexec,nosuid,nodev и продумайте idmap. Bind mount расширяет поверхность атаки: контейнер получает прямой доступ к каталогу хост-системы, и chroot внутри контейнера эту границу не двигает.
Чем отличаются Proxmox VE и чистый LXD
Логика работы одинаковая: монтирование, права, chroot. Различаются инструмент управления и место хранения конфигурации.
| Задача | Proxmox VE 8.x | LXD 5.x |
|---|---|---|
| Добавить каталог | pct set 100 -mp0 /srv/data,mp=/data,ro=0,backup=0 | lxc config device add c1 data disk source=/srv/data path=/data shift=true |
| Посмотреть конфиг | pct config 100, файл /etc/pve/lxc/100.conf | lxc config show c1 |
| Войти в контейнер | pct enter 100 | lxc exec c1 -- bash |
| Список контейнеров | pct list | lxc list |
| Смещение UID/GID | директивы lxc.idmap в конфиге | shift=true у disk-устройства, диапазоны в /etc/subuid |
| Перезапуск | pct reboot 100 | lxc restart c1 |
В Proxmox конфиг контейнера лежит в /etc/pve/lxc/100.conf на кластерной файловой системе и синхронизируется между узлами. Правка файла вручную при работающем кластере без понимания последствий приводит к рассинхрону и отказу запуска контейнера, поэтому используйте pct.
Тестовый контейнер удобно держать вне продакшена. Timeweb Cloud предоставляет серверы и VDS с root-доступом, на которых можно развернуть LXC и проверить idmap, chroot и права до переноса настроек на боевую площадку.
Монтирование каталога хоста в LXC-контейнер
Дальше примеры для каталога /srv/data на хосте, точки /data внутри контейнера, ID 100 в Proxmox и имени c1 в LXD.
Bind mount в Proxmox VE через pct
pct set 100 -mp0 /srv/data,mp=/data,ro=0,backup=0
Разбор параметров: mp0 это индекс точки монтирования (следующая будет mp1), mp=/data задаёт путь внутри контейнера, ro=0 оставляет запись разрешённой, backup=0 исключает каталог из задачи vzdump. Если параметр mp не указать, путь внутри контейнера совпадёт с хостовым.
После команды в /etc/pve/lxc/100.conf появится строка: mp0: /srv/data,mp=/data,ro=0,backup=0
Проверка: pct reboot 100, затем pct enter 100, далее mount | grep /data и ls -la /data. В непривилегированном контейнере без idmap файлы отобразятся как nobody (UID 65534), и запись от root контейнера в такой каталог не пройдёт.
Bind mount в LXD через lxc config device
lxc config device add c1 data disk source=/srv/data path=/data shift=true
Параметр shift=true согласует владельцев с idmap контейнера: LXD сдвигает UID/GID на диапазон subuid, поэтому внутри контейнера файлы принадлежат ожидаемым пользователям. Без shift владельцем станет nobody:nogroup, и запись под обычной учётной записью не сработает.
Устройство подключается на лету, но при включении shift на каталоге с данными безопаснее перезапустить контейнер: lxc restart c1. Проверка: lxc exec c1 -- df -h /data и lxc exec c1 -- ls -la /data.
Важно про shift на боевом каталоге: владельцы файлов на хосте изменятся, и хостовые сервисы, писавшие в /srv/data под своими UID, потеряют доступ. Снимите статистику до изменения: find /srv/data -printf '%u:%g\n' | sort | uniq -c.
Проверка монтирования и типичные ошибки
Внутри контейнера достаточно трёх команд: mount | grep /data показывает строку монтирования, ls -la /data показывает содержимое и владельцев, touch /data/test проверяет запись. Если файл создаётся, монтирование и права на уровне каталога в порядке.
- Каталог не появился: в Proxmox точка монтирования применяется не всегда на лету, помогает pct reboot 100; в LXD проверьте устройство в выводе lxc config show c1.
- Permission denied при touch: не совпадают UID. Сравните ls -ln внутри контейнера и ls -ln на хосте, разница должна равняться 100000.
- Read-only file system: в конфиге остался ro=1 либо каталог хоста смонтирован только для чтения.
- Данные видны не полностью: поверх точки монтирования внутри контейнера лежат свои файлы, bind mount их скрывает, а не объединяет.
Не монтируйте в контейнер /, /etc, /var/lib, /root и /dev. Такие точки ломают работу контейнера и дают ему запись в критичные каталоги хост-системы.
Права доступа к смонтированному хранилищу
Права настраивают на двух сторонах: на хосте задают владельца файлов, внутри контейнера задают права каталогов и владельца SFTP-пользователя. Связывает их отображение UID.
UID/GID mapping в непривилегированном контейнере
В непривилегированном контейнере root контейнера соответствует UID 100000 на хосте, весь диапазон UID внутри сдвинут на эту величину. Для LXD диапазон задан в /etc/subuid и /etc/subgid на хосте: cat /etc/subuid показывает строку вида root:100000:65536. В Proxmox отображение описано директивами lxc.idmap в конфиге контейнера, по умолчанию используется сдвиг на 100000.
Пример: пользователь sftpuser с UID 1001 внутри контейнера владеет файлами на хосте как UID 101001. Порядок действий: создайте пользователя в контейнере, посмотрите id -u sftpuser, прибавьте 100000 и выставьте владельца на хосте: chown -R 101001:101001 /srv/data. В LXD ту же задачу решает shift=true.
Менять idmap или включать shift на работающем контейнере с данными рискованно: владельцы пересчитываются, сервисы теряют доступ. Сначала снапшот (zfs snapshot pool/data@before-idmap), затем проверка на тестовом контейнере.
Права на каталог для SFTP-пользователя
Схема, которая работает с chroot: корень chroot принадлежит root и открыт только на чтение и выполнение, каталог для загрузки принадлежит SFTP-пользователю. Внутри контейнера:
- useradd -m -s /usr/sbin/nologin sftpuser
- chown root:root /data; chmod 755 /data
- chown sftpuser:sftpgroup /data/upload; chmod 750 /data/upload
Каталог /data остаётся недоступным на запись для sftpuser: именно этого требует sshd для chroot. Запись идёт в подкаталог /data/upload, где владелец сам пользователь.
Для общих каталогов выручает ACL: setfacl -m u:sftpuser:rwx /data/upload выдаёт доступ конкретному пользователю без смены владельца, проверка - getfacl /data/upload. Чтобы новые файлы наследовали группу, поставьте setgid на каталог: chmod 2750 /data/upload. Права новых файлов задаёт umask: при umask 002 файлы получают 664, при 022 - 644.
Права 777 в этом сценарии не нужны: они открывают данные любому пользователю контейнера, а chroot от этого не защищает. Смежные ошибки доступа к данным контейнеров, включая UID/GID и bind mount, разобраны в руководстве по правам доступа контейнеров и защите хост-системы.
Изоляция SFTP-пользователя через sshd и chroot
Пользователь получает только SFTP, без shell и без выхода за пределы каталога. Всё это делается директивами Match внутри контейнера.
Конфигурация sshd для SFTP-only доступа
Блок добавляется в конец /etc/ssh/sshd_config внутри контейнера. Директивы Match действуют до следующей Match или до конца файла, поэтому общие настройки должны идти выше.
Match User sftpuser
ChrootDirectory /data
ForceCommand internal-sftp
AllowTcpForwarding no
PermitTunnel no
X11Forwarding no
PasswordAuthentication no
- Match User sftpuser ограничивает блок одной учётной записью.
- ChrootDirectory /data делает корнем сессии каталог /data: за его пределы пользователь не выйдет.
- ForceCommand internal-sftp запускает встроенный SFTP-сервер и не даёт shell, даже если он прописан в /etc/passwd.
- AllowTcpForwarding no и PermitTunnel no запрещают туннели и проброс портов через соединение.
- X11Forwarding no отключает проброс X11.
- PasswordAuthentication no оставляет вход только по ключу.
Создание пользователя: useradd -m -s /usr/sbin/nologin sftpuser. Оболочка nologin полезна как вторая линия защиты, основной ограничитель - ForceCommand internal-sftp.
После правки проверьте синтаксис и перезапустите службу: sshd -t, затем systemctl restart ssh. В контейнере без systemd используйте service ssh restart. Готовые конфигурации SFTP-серверов и варианты изоляции в chroot собраны в материале о настройке FTP и SFTP серверов.
Требования к chroot-каталогу
sshd отказывает в подключении, если chroot-каталог доступен пользователю на запись. Требования: владелец root:root, права не шире 755, все родительские каталоги в пути тоже принадлежат root. Проверка одной командой: namei -l /data, она показывает владельца и права для каждого уровня пути.
Ошибка Bad ownership or modes for chroot directory появляется именно здесь: кто-то сменил владельца /data на sftpuser или выдал группе запись. Лечится возвратом root:root и прав 755.
Опция noexec на каталоге данных не мешает SFTP: internal-sftp не запускает файлы. Она помешает сценариям, где из этого каталога запускаются скрипты хуков или обработчиков.
Проверка SFTP-подключения
Подключение с клиента: sftp -v sftpuser@10.0.0.10. Ключ -v показывает этап согласования, включая выбор внутреннего SFTP-сервера. В сессии проверьте pwd, ls, put test.txt, get test.txt. Команда cd /etc должна вернуть ошибку вида Can't change directory to '/etc', а ls / показать содержимое /data, не файловую систему контейнера.
Аутентификация проходит до применения chroot, поэтому sshd читает ключ по пути в реальной файловой системе: обычно /home/sftpuser/.ssh/authorized_keys, а при явной директиве AuthorizedKeysFile в Match-блоке это может быть /data/.ssh/authorized_keys. Права: каталог .ssh 700, файл authorized_keys 600, владелец sftpuser.
Логи на стороне контейнера: journalctl -u ssh -f. Попытка подключиться обычным ssh не должна давать интерактивную сессию: ForceCommand internal-sftp перехватывает её.
Как не навредить хост-системе
chroot ограничивает пользователя внутри контейнера, но не защищает хост от самого контейнера. Границу между контейнером и хостом формируют опции монтирования, тип контейнера и профили безопасности.
Опции монтирования для снижения риска
noexec запрещает запуск исполняемых файлов из каталога, nosuid игнорирует биты setuid и setgid, nodev запрещает обращения к файлам устройств. Для каталога данных набор noexec,nosuid,nodev разумен: SFTP-хранилище не должно быть местом запуска кода.
В Proxmox опции задаются raw-строкой в /etc/pve/lxc/100.conf: lxc.mount.entry: /srv/data data none bind,create=dir,noexec,nosuid,nodev 0 0. Целевой путь указывают без ведущего слэша, он считается от корня контейнера. При использовании lxc.mount.entry точка mp0 для того же каталога не нужна: два монтирования одного источника конфликтуют.
В LXD у disk-устройства отдельных флагов noexec и nosuid нет, их добавляют через raw.lxc: lxc config set c1 raw.lxc 'lxc.mount.entry = /srv/data data none bind,create=dir,noexec,nosuid,nodev 0 0'. Строка применяется при старте контейнера, синтаксическая ошибка помешает запуску, поэтому обкатайте её на тестовом контейнере.
Обратная сторона: noexec сломает сценарии, где из каталога запускаются скрипты, а nodev помешает работать с loop-устройствами и образами. Если такие задачи есть, выносите их в отдельное хранилище без ограничений.
Привилегированный или непривилегированный контейнер
| Параметр | Непривилегированный | Привилегированный |
|---|---|---|
| root контейнера на хосте | UID 100000 | UID 0 |
| Требования | idmap в Proxmox, shift=true в LXD | дополнительная настройка не нужна |
| Риск для хоста | ниже: ядро ограничивает capabilities | выше: ошибка в конфиге или уязвимость ядра даёт доступ к хосту |
| Перевод позже | пересоздание и перенос данных | пересоздание и перенос данных |
Для SFTP-доступа к общему каталогу выбирайте непривилегированный контейнер: он снимает часть рисков автоматически, а неудобство ровно одно, нужно настроить idmap или shift. Перевод существующего контейнера между режимами делают пересозданием и переносом данных через rsync, поэтому тип выбирают до запуска в работу.
Профили безопасности контейнера оставляйте включёнными: в Proxmox профиль AppArmor задаётся строкой lxc.apparmor.profile, в LXD применяется собственный профиль, seccomp ограничивает набор системных вызовов. Отключение профиля ради быстрого решения убирает последний барьер между контейнером и хостом. Как устроены такие ограничения и как разбирать отказы SELinux, описано в материале про SELinux и rootless Podman.
Диагностика типичных ошибок
Большинство сбоев сводится к шести сценариям. Таблица ниже даёт причину и первую проверку для каждого.
| Ошибка | Причина | Что проверить |
|---|---|---|
| Permission denied (publickey) | sshd не находит ключ или у файла неверные права | путь из AuthorizedKeysFile, владелец sftpuser, права 700 на .ssh и 600 на authorized_keys |
| Bad ownership or modes for chroot directory | chroot-каталог или его родитель доступен на запись, владелец не root | namei -l /data, затем chown root:root /data и chmod 755 /data |
| Connection refused | sshd в контейнере не запущен или порт не проброшен из хоста | pct exec 100 -- service ssh status, lxc exec c1 -- service ssh status, правила проброса портов |
| touch: Permission denied в /data/upload | не совпадают владелец и idmap | id -u sftpuser внутри контейнера и ls -ln на хосте, разница 100000 |
| Read-only file system | ro=1 в Proxmox или read-only у устройства в LXD | pct config 100, lxc config show c1 |
| Пользователь видит корень файловой системы контейнера | ChrootDirectory не применился: опечатка или перекрытие Match-блока | sshd -T -C user=sftpuser,host=localhost,addr=10.0.0.1 |
Проверка конфигурации sshd
sshd -t проверяет синтаксис без применения, sshd -T печатает итоговый конфиг с учётом include-файлов и блоков Match. Настройки конкретного пользователя видны так: sshd -T -C user=sftpuser,host=localhost,addr=10.0.0.1. В выводе найдите строки chrootdirectory и forcecommand, они показывают, что реально получит пользователь.
Перезапуск: systemctl restart ssh, в контейнере без systemd - service ssh restart. Если юнит называется sshd, используйте имя из вашей системы, его подскажет systemctl status sshd.
Логи и трассировка SFTP
Логи внутри контейнера: journalctl -u ssh -f, либо pct exec 100 -- journalctl -u ssh -n 50. На клиенте sftp -v показывает этап, на котором прерывается соединение. Права и контексты: ls -lZ /data, getfacl /data/upload.
При включённом SELinux отказы смотрят через ausearch -m avc -ts recent, а исправляют контекстами, не переключением в permissive. Решения вида выключить SELinux или поставить 777 открывают данные и ломают модель доступа, тогда как причина обычно в одном каталоге или одном файле ключа.
Чек-лист и что делать дальше
Финальный чек-лист
- Bind mount активен: mount | grep /data внутри контейнера выводит строку, ls -la /data показывает данные.
- Владельцы совпадают: /data принадлежит root:root с правами 755, /data/upload - sftpuser:sftpgroup с правами 750.
- idmap или shift настроены: разница UID между хостом и контейнером равна 100000.
- sshd_config содержит Match User sftpuser, ChrootDirectory /data, ForceCommand internal-sftp, AllowTcpForwarding no.
- sshd -t проходит без ошибок, служба перезапущена.
- SFTP-подключение работает: put и get выполняются, cd /etc отклоняется.
- Ключ в порядке: .ssh 700, authorized_keys 600, владелец sftpuser.
- Опции noexec,nosuid,nodev добавлены, если из каталога не требуется запускать файлы.
- Бэкап данных настроен и проверен восстановлением.
Бэкапы и защита данных
Параметр backup=0 исключает точку монтирования из vzdump: данные на хосте остаются вне бэкапа контейнера. Бэкап самого контейнера: vzdump 100 --mode snapshot --storage backup. Для данных используйте снапшоты файловой системы (zfs snapshot pool/data@2026-09-19, btrfs subvolume snapshot) и копирование (rsync -aHAX --delete /srv/data/ /backup/data/). Расписание удобно держать в systemd-таймерах или cron.
Снапшот не заменяет копию на другом носителе: сбой пула уносит и данные, и снапшоты. Проверяйте восстановление на тестовом контейнере, иначе бэкап остаётся набором файлов с неизвестной пригодностью. Схема с квотами ZFS и приёмом файлов от внешних систем разобрана в статье про SFTP и SCP на ZFS для CI/CD.
Дальше имеет смысл ограничить доступ по адресам и времени, выдать квоты на каталог (ZFS quota или project quota на XFS) и вынести крупные данные в отдельный dataset. Перед изменениями в продакшене соберите ту же конфигурацию в тестовом контейнере: расхождение в одну строку idmap или в один уровень прав часто и объясняет ошибку, которую видит пользователь.
Источники
- Дедупликация списка поставщиков на Python: почему детерминированные этапы важнее similarity score — материал о рисках автоматического объединения записей, различающихся только внешним контекстом; полезен как иллюстрация того, почему проверки и детерминированные шаги важнее «похожести».
- Яндекс Маршрутизация против альтернатив: сравнение решений для построения маршрутов в 2026 году — сравнение решений и критериев выбора для DevOps-инженера, включая точность геокодирования и дорожного графа.
- TerraMaster F4-212 — описание сетевого накопителя на 4 диска на базе процессора Realtek RTD1619B Arm Cortex-A55 с поддержкой BTRFS, Snapshot и инструмента аварийного восстановления TFSS; пример хранилища, для которого актуальны вопросы снапшотов и бэкапов.
Приведённые материалы не описывают напрямую настройку SFTP в LXC, монтирование каталогов хост-системы, права доступа и изоляцию через sshd/chroot. Команды и конфигурации в статье следует проверять на тестовом контейнере и сверять с документацией вашей версии Proxmox VE, LXD и OpenSSH.