Настройка SFTP-доступа к хранилищу в LXC-контейнере: монтирование, права и изоляция | AdminWiki

Настройка SFTP-доступа к хранилищу в LXC-контейнере: монтирование, права и изоляция

19 сентября 2026 14 мин. чтения

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.xLXD 5.x
Добавить каталогpct set 100 -mp0 /srv/data,mp=/data,ro=0,backup=0lxc config device add c1 data disk source=/srv/data path=/data shift=true
Посмотреть конфигpct config 100, файл /etc/pve/lxc/100.conflxc config show c1
Войти в контейнерpct enter 100lxc exec c1 -- bash
Список контейнеровpct listlxc list
Смещение UID/GIDдирективы lxc.idmap в конфигеshift=true у disk-устройства, диапазоны в /etc/subuid
Перезапускpct reboot 100lxc 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 100000UID 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 directorychroot-каталог или его родитель доступен на запись, владелец не rootnamei -l /data, затем chown root:root /data и chmod 755 /data
Connection refusedsshd в контейнере не запущен или порт не проброшен из хостаpct exec 100 -- service ssh status, lxc exec c1 -- service ssh status, правила проброса портов
touch: Permission denied в /data/uploadне совпадают владелец и idmapid -u sftpuser внутри контейнера и ls -ln на хосте, разница 100000
Read-only file systemro=1 в Proxmox или read-only у устройства в LXDpct 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 открывают данные и ломают модель доступа, тогда как причина обычно в одном каталоге или одном файле ключа.

Чек-лист и что делать дальше

Финальный чек-лист

  1. Bind mount активен: mount | grep /data внутри контейнера выводит строку, ls -la /data показывает данные.
  2. Владельцы совпадают: /data принадлежит root:root с правами 755, /data/upload - sftpuser:sftpgroup с правами 750.
  3. idmap или shift настроены: разница UID между хостом и контейнером равна 100000.
  4. sshd_config содержит Match User sftpuser, ChrootDirectory /data, ForceCommand internal-sftp, AllowTcpForwarding no.
  5. sshd -t проходит без ошибок, служба перезапущена.
  6. SFTP-подключение работает: put и get выполняются, cd /etc отклоняется.
  7. Ключ в порядке: .ssh 700, authorized_keys 600, владелец sftpuser.
  8. Опции noexec,nosuid,nodev добавлены, если из каталога не требуется запускать файлы.
  9. Бэкап данных настроен и проверен восстановлением.

Бэкапы и защита данных

Параметр 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 или в один уровень прав часто и объясняет ошибку, которую видит пользователь.

Источники

Приведённые материалы не описывают напрямую настройку SFTP в LXC, монтирование каталогов хост-системы, права доступа и изоляцию через sshd/chroot. Команды и конфигурации в статье следует проверять на тестовом контейнере и сверять с документацией вашей версии Proxmox VE, LXD и OpenSSH.

Поделиться:
Сохранить гайд? В закладки браузера