Мой блог
Docker для параноика: SSH-доступ для rootless-контейнеров без передачи приватного ключа
Организация безопасного SSH-доступа внутри изолированных сред часто упирается в архитектурные компромиссы. Когда приложению или системе автоматизации требуются операции с репозиториями и удаленными серверами, разработчики нередко прибегают к прямому монтированию ключей, создавая серьезные уязвимости. Я подготовил подробный материал о том, как настроить изолированную среду для работы с репозиториями и серверами без риска утечки секретов.
В этой публикации я детально разберу архитектуру, которая позволяет запустить полноценный SSH-клиент внутри rootless-контейнера без передачи самого файла приватного ключа. Мы рассмотрим особенности сопоставления UID, настройку специального агента аутентификации и сопутствующие меры защиты на уровне операционной системы.















Введение и требования к безопасности
Полноценный SSH-клиент может потребоваться контейнеру для самых разных задач: клонирования приватных репозиториев через git, выполнения команд на удаленных машинах, передачи файлов по scp или обслуживания серверной инфраструктуры. Классический подход с монтированием директории ~/.ssh напрямую внутрь контейнера нарушает базовые принципы изоляции учетных данных. Даже монтирование в режиме только для чтения защищает файл лишь от изменения, но не от прямого чтения со стороны запущенного ПО.
Чтобы исключить подобные риски на уровне архитектуры, я выставил жесткие требования к целевой конфигурации:
- Демон Docker работает в rootless-режиме без привилегий суперпользователя на хосте;
- процесс внутри контейнера функционирует от имени непривилегированного пользователя;
- у контейнера полностью удалены лишние Linux capabilities и запрещено получение новых привилегий;
- корневая файловая система монтируется только для чтения;
- материал приватного ключа физически отсутствует в пространстве имён контейнера;
- категорически запрещено использование небезопасного параметра
StrictHostKeyChecking=no; - ключ не передается через переменные окружения и не монтируется с хоста в читаемом виде.
Вместо передачи самого секретного файла мы будем использовать Unix-сокет агента аутентификации. Однако при работе в защищенном rootless-режиме этот метод сталкивается с техническими нюансами, связанными с преобразованием идентификаторов.
Зачем нужен SSH в контейнере и альтернативные подходы
Типичные сценарии использования SSH в изолированных средах включают в себя автоматизированное развертывание, работу с подмодулями репозиториев и применение стандартных утилит OpenSSH. Альтернативным вариантом может стать полный отказ от SSH внутри контейнера с вынесением всех операций в отдельный API-сервис или промежуточный слой наподобие MCP-серверов, популярных в системах с ИИ-агентами.
Тем не менее, внешний сервис не всегда заменяет локальный git. Если приложение оперирует собственной рабочей копией, перенос операций во внешний сервис порождает две независимые файловые системы, требующие постоянной синхронизации. При использовании общей директории через volume возникает риск компрометации через Git hooks, если запуск хуков не изолирован должным образом. Именно поэтому для ряда задач оптимальным выбором остается локальный OpenSSH, работающий через защищенный сокет.


Особенности сопоставления UID в rootless Docker
Прежде чем переходить к настройке агента, необходимо разобраться в механизме пользовательских пространств имён. В стандартной конфигурации идентификатор пользователя внутри контейнера совпадает с хостовым. В rootless-режиме демон запускается от непривилегированного пользователя, а ядро Linux выполняет маппинг UID через пространства имён на основе файлов /etc/subuid и /etc/subgid.
Например, внутренний пользователь app с UID 1000 может отображаться на хосте как UID 100999. Проверка клиентов Unix-сокета выполняется на стороне хоста, поэтому стандартный агент аутентификации отклонит подключение из-за несовпадения идентификаторов, даже если права доступа к самому файлу сокета установлены максимально широко. Обычное применение параметра chmod 0666 не решит проблему, так как агент проверяет реальный UID подключившегося процесса.
Настройка изолированного SSH-агента на хосте
Для согласования идентификаторов я запускаю отдельный экземпляр агента с помощью служебной службы systemd. Сам юнит работает от привилегированного пользователя для безопасного чтения файла ключа, а утилита setpriv понижает права непосредственно для процесса агента до вычисленного mapped UID.

Для реализации этой схемы на хосте создается специальная структура каталогов:
/opt/ssh-access/.ssh/git/— защищенная директория с парой ключей Ed25519;/opt/ssh-access/sockets/git/— каталог для размещения Unix-сокета;/opt/ssh-access/ssh_known_hosts— файл с предварительно проверенными ключами удаленных серверов.
Корневой каталог настраивается с правами 0711, что позволяет процессам проходить по пути, но исключает листинг содержимого. Директория сокета создается с правами 0710, а владельцем назначается mapped UID. Такой подход позволяет монтировать в контейнер не сам сокет, а родительский каталог, благодаря чему агент может пересоздавать сокет без перезапуска контейнера.
Запуск и проверка защищенного контейнера
При сборке собственного Dockerfile я создаю пользователя с фиксированным идентификатором 1000:1000. Запуск контейнера выполняется с жесткими ограничениями привилегий, подключением временных файловых систем и двумя точками монтирования: каталога с сокетом и файла доверенных хостов.
В процессе запуска контейнера я применяю следующие флаги:
--user 1000:1000задает непривилегированного пользователя;--read-onlyпереводит корневую ФС в режим чтения;--cap-drop ALLудаляет все системные возможности;--security-opt no-new-privilegesблокирует эскалацию прав;--vподключает сокет и файл известных хостов только для чтения (:ro).
После старта контейнера проверка через команду ssh-add -l подтверждает успешную загрузку ключа. Утилиты git и ssh начинают работать штатно, используя переданный переменной окружения SSH_AUTH_SOCK путь к сокету.
Эксплуатация, ограничения и границы защиты
При обслуживании инфраструктуры важно учитывать особенности поведения bind-монтирования. Полное удаление и повторное создание директории сокета на хосте приводит к повреждению существующих привязок внутри запущенных контейнеров — в таком случае источник начинает отображаться как удаленный объект. Чтобы избежать простоев, следует удалять и пересоздавать файлы внутри каталога, не затрагивая саму примонтированную директорию.



Описанная архитектура гарантирует, что файл приватного ключа никогда не попадает в пределы контейнера. Процесс может выполнять операции подписи и аутентификации исключительно через вызовы агента, однако доступ к сокету равносилен делегированному доступу к заложенным ключам. Такой подход в сочетании с изоляцией rootless Docker позволяет выстроить надежную и безопасную среду для выполнения автоматизированных задач.
