Концепция контейнеризации на базе Docker, и ему подобных технологий, для многих разработчиков стала незаменимым инструментом доставки своих продуктов конечным пользователям в виде полностью подготовленной среды использования. В большинстве случаев, особенно это касается open source продуктов, для распространения используются бесплатные реестры такие как Docker Hub или Github Container Registry. Но когда количество продуктов и размеры образов начинают увеличиваться, а также требуется более расширенное управление репозиториями и доступом к ним, то разработчики и компании начинают задумываться о своих собственных хранилищах для контейнерных образов (images). К счастью вариантов для развертывания своего собственного экземпляра Container Registry предостаточно, начиная от самого простого варианта Docker Registry и до более масштабных систем таких как HARBOR, Dragonfly и прочие.
Для своих задач я выбрал самый простой вариант на базе Docker Registry, который разворачивается за минуту имеет достаточно много различных настроек и интеграций с различными облачными сервисами. Работает он достаточно предсказуемо, надежно, а также написан на языке который я знаю. Изначально доступ к registry нужен был только для одного пользователя - меня самого, поэтому я выбрал обычный механизм доступа на основе htpasswd
Этот механизм очень простой, все пользователи указанные в файле htpasswd
имеют полный доступ ко всему реестру, что является сложностью при появлении задачи раздельного доступа к реестру, которая у меня и возникла в скором времени. Среди возможных способов доступа к Docker Registry имеется механизм авторизации на основе token, который позволяет организовать управляемый доступ в зависимости от репозитория и действий пользователя (pull, push, delete). Однако реализация данного механизма является не очень простой задачи. Требуется отдельный auth сервер, сертификаты с ключами, которыми будет подписываться и проверяться token. Также хотелось получить единый интерфейс управления пользователями, доступами и запиясми репозиториев. К сожалению, из простых вышеперечисленных решений я не нашел такого, которое бы решало следующие задачи:
разделение доступа к репозиториям на основе действий пользователей (pull/push);
доступ на основе ролей (RBAC);
просмотр списка репозиториев и информацию об образах;
удаление образов;
механизм поиска по записям;
анонимный доступ и доступ для зарегистрированных пользователей;
прочие функции (логирование, генератор сертификатов и т.п.).
Скорее всего я плохо искал и что-то похожее уже реализовано, но в итоге я решил реализовать свой сервис RegistryAdmin, который будет решать выше поставленные задачи.
По больше части энтузиазм написать свой, более-менее серьезный, open source проект возник благодаря популярному ИТ-подкасту Radio-T и особенно ведущему Umputun. В одно время я часто изучал код его проектов и заимствовал различные идеи в своих разработках (в том числе и в этом проекте), а также контрибьютил в некоторые из них, чтобы внести свой вклад в развитие проектов, которыми я пользуюсь. Некоторые технологии, которые я изучил и использую в повседневной работе, стали мне известны именно благодаря данному подкасту, за что хочу высказать ОГРОМНУЮ БЛАГОДАРНОСТЬ всем ведущим за их труд. Прошу не расценивать данный абзац как рекламу или что-то подобное т.к. таковым он не является, а имеет место искренний душевный порыв высказать благодарность.
Некоторые особенности реализации проекта
Изначально я хотел быстро написать простенький сервис, который будет интегрирован в существующий экземпляр, уже развернутого private registry, и просто выдавать токены авторизации существующим пользователям. Но после детального изучения документации на сайте Docker стало ясно, что быстро не получится.
Во-первых, для подписи и проверки токенов необходимы только RSA ключи (подпись текстовым секретом не подходит). Сертификат должен содержать адрес или имя хоста registry, по которому осуществляется запрос данных из реестра, т.е. в нём обязательно должны быть определенны поля subjectAltName. Поэтому механизм генерации RSA ключей для работы с токеном я решил встроить непосредственно в RegistryAdmin, чтобы можно было обойтись без сторонник утилит таких как openssl (хотя их также можно использовать). Сгенерированные ключи могут быть использованы и для TLS подключения.
Во-вторых, в пользовательском web-интерфейсе мне было необходимо реализовать полнотекстовый поиск по репозиториям, однако API registry позволяет получать список репозиториев только используя вариант курсора. По этой причине я решил реализовать синхронизацию списка репозиториев и данных по этим репозиториям во внутренней базе RegistryAdmin (на базе SQLite).
Для синхронизации в режиме реального времени на стороне registry настроен механизм уведомлений (notifications) о событиях, которые инициируют механизм актуализации данных в базе RegistryAdmin. Также доступен ручной механизм запуска синхронизации данных, который также запускается автоматически через заданный интервал времени.
Сам RegistryAdmin является обычным REST API приложением, который взаимодействует непосредственно с экземпляром registry. В качестве фреймворка для UI был выбран React-Admin, который достаточно легко и просто встраивается в различные REST API системы. Файлы UI интегрированны в бинарник при помощи библиотеки embed.
Помимо авторизации на основе токена, также поддерживается обычная авторизация через htpasswd файл, который синхронизируется со списком пользователей RegistryAdmin. Однако стоит иметь ввиду, что доступ к реестру на основе htpasswd не поддерживает разделение прав доступа на основе действий пользователя (pull/push).
Для управления и просмотра данных реестра через UI предусмотрены встроенные роли:
Admin - полные права для работы с реестром и репозиориями;
Manager - просмотр только списка всех репозиториев и списков доступа;
User - может только просматривать разрешенный список репозиториев.
Также в сервисе реализованы специальные права для анонимных и зарегистрированных пользователей, которые имеют специальные ID и не хранятся в базе RegistryAdmin.
В итоге получился простой по функционалу и немного усложненный, в части первичной настройки, сервис RegistryAdmin.
Учитывая, что я занимаюсь в основном backend разработкой, то тесты написаны только к самому сервису. В планах есть задача реализовать тесты к frontend части, но пока не нашел на это свободного времени. Было бы очень хорошо если бы, кто-нибудь оказал помощь в этом вопросе.
Установка и настройка RegistryAdmin
Дистрибутив RegistryAdmin доступен в двух вариантах:
В данном обзоре я буду рассматривать вариант установки в среде docker, как наиболее предпочтительный, с использованием docker-compose.
Изначально нужно определится с архитектурой системы, чтобы чётко понимать какие настройки для чего нужны. В данном примере я буду выполнять развертывание для следующей схемы узлов:
В качестве сервера registry.local выступает сервер с развернутой средой docker. В качестве внешних клиентов может выступать любой узел внутри сети, которому доступен хост registry.local
-
Создаем папку для стека контейнеров Docker Private Registry и RegistryAdmin
mkdir registry-admin cd registry-admin
-
В папке создаем файлы конфигурации (или копируем из примера), рекомендуется придерживаться следующей структуры:
registry-admin\ config\ registry-config.yml token-ra-config.yml docker-compose.yml
где:
registry-config.yml - файл конфигурации для registry
token-ra-config.yml - конфигурационный файл RegistryAdmin -
Задать владельца для директории и файлов. По-умолчанию в контейнере определен владелец с
UID=1000
. Данный UID может быть переопределен через переменную окруженияAPP_UID
в файле docker-compose.yml.chown -R 1000:1000 ./
-
Указываем основные параметры для RegistryAdmin и указываем пути и параметры для подключения к реестру и генерации ключей для токенов. Также мы будем использовать данные ключи для настройки TLS подключения.
hostname: tnas.local ssl: type: static port: 443 cert: /app/certs/cert.crt key: /app/certs/cert.key registry: host: https://registry port: 5000 auth_type: token issuer: registry_token_issuer service: container_registry certs: path: /app/certs key: /app/certs/cert.key public_key: /app/certs/cert.pub ca_root: /app/certs/cert.crt ip: 192.168.12.69# <- paste a real IP of docker host which publish the container fqdns: [registry,registry-admin,tnas.local,registry.local] store: type: embed admin_password: "super-secret" embed: path: /app/data/store.db
По указанному в параметре
--cets.path
пути будут автоматически созданы все необходимые ключи и сертификат. При запуске RegistryAdmin проверяет наличие файлов в данном каталоге и создает новые если директория пуста. Если данный параметр не задан, то по-умолчанию ключи создаются в домашней директории пользователя под которым запускается сервис. -
Определяем параметры для самого Private Docker Registry.
URL указанный в параметре realm должен быть доступен клиентам, которые осуществляют авторизацию напрямую в сервисе RegistryAdmin.
version: 0.1 log: accesslog: disabled: false level: debug formatter: text fields: service: registry storage: filesystem: rootdirectory: /var/lib/registry maxthreads: 100 delete: enabled: true http: addr: ":5000" net: tcp tls: certificate: /certs/cert.crt key: /certs/cert.key # - lientcas: # - /certs/cert.crt auth: token: # external ip or host accessible for clients from outside of container realm: https://registry.local:8443/api/v1/registry/auth service: container_registry issuer: registry_token_issuer rootcertbundle: /certs/cert.crt notifications: events: includereferences: true endpoints: - name: ra-listener disabled: false url: https://registry-admin/api/v1/registry/events headers: # 'admin:super-secret' base64 encode string Authorization: [Basic YWRtaW46c3VwZXItc2VjcmV0] timeout: 1s threshold: 5 backoff: 3s ignoredmediatypes: - application/octet-stream ignore: mediatypes: - application/octet-stream
-
Определить параметры для docker-compose
version: '2.1' services: registry-admin: restart: unless-stopped image: zebox/registry-admin:latest ports: - 8080:80 - 8443:443 environment: - APP_UID=1000 - RA_CONFIG_FILE=/app/config/token-ra-config.yml volumes: - ./certs:/app/certs - ./config:/app/config - ./data:/app/data registry: restart: unless-stopped image: registry:2 ports: - 50554:5000 volumes: - ./data:/var/lib/registry - ./certs:/certs - ./config/registry-config.yml:/etc/docker/registry/config.yml depends_on: - registry-admin # override container running command for add self-signed certificate to trusted CA command: ["/bin/sh", "-c", "cp /certs/cert.crt /usr/local/share/ca-certificates && /usr/sbin/update-ca-certificates; registry serve /etc/docker/registry/config.yml"]
ВАЖНО! Переопределение команды (
command
) запуска контейнера с registry требуется для того, чтобы обойти ошибку доверия самоподписанного сертификата для корректной работыnofitfications
внутри контейнера registry. Данная строка выполняет добавление сертификата в список доверенных при запуске контейнера registry. В случае если Вы не используете TLS подключение к RegistryAdmin переопределениеcommand
не требуется. Также не требуется переопределение если используются доверенные сертификаты такие как Let's Encrypt, но в этом случае для HTTP TLS необхоимо указывать сертификат содержащий полную цепочку CA fullchain.pem. -
Запускаем контейнеры
docker-compose up -d
Если все параметры верны, то сервис должен запуститься и быть доступен через браузер по адресу https://registry.local:8443
Если у Вас стоит задача развернуть систему с возможностью доступа из сети Интернет, то необходимо учитывать особенности использования доверенных TLS сертификатов. RegistryAdmin поддерживает получение сертификатов Let's Encrypt через протокол ACME (режим SSL AUTO). Если используется такой механизм получения публичного сертификата, то для доступа к registry по HTTPS следует определить параметр letsencrypt и указать путь к сертификату который был указан в параметре --ssl.acme-location
для RegistryAdmin.
letsencrypt:
cachefile: /path/to/cache-file
email: emailused@letsencrypt.com
hosts: [you-registry.domain.org]
В случае получения сертификата LE через HTTP-01 challenge, то как для registry так и для RegistryAdmin в качестве сертификата следует указывать fullchain.pem файл, иначе при взаимодействии между сервисами будет возникать ошибка доверия к сертификату - x509 certificate signed by unknown authority.
Использование RegistryAdmin
После того как успешно произведена настройка и запуск системы можно приступать к её использованию. Для входа в админку необходимо ввести данные доступа по-умолчанию:
username: admin
password: super-secret
После успешной авторизации отобразится страница со списком репозиториев, но т.к. у нас новый экземпляр registry, то данных в нем еще нет. В случае интеграции с существующим registry можно было произвести принудительную синхронизацию существующих данных.
Добавим новый репозиторий в реестр. Для этого произведем авторизацию с внешнего docker клиента под данными администратора и сделаем push нового образа в реестр. Но т.к. в данном примере мы используем самоподписанные сертификаты, то docker клиент будет ругаться на доверие к сертификату. Чтобы решить эту проблему есть два способа:
-
На клиенте добавить наш реестр в список небезопасных реестров в файле конфигурации докер демона - daemon.json. После внесения измененний потребуется перезагрузка службы докера.
# https://docs.docker.com/config/daemon/ # /etc/docker/daemon.json (Linux) # C:\ProgramData\docker\config\daemon.json (Windows) { "insecure-registries": ["registry.local:50554"] }
Добавить сгенерированные сертифкаты в список доверенных сертификатов для клиентского хоста.
После решения вопроса с доверием сертификатов можно приступать к выполнению аутентификацию в реестре на клиенте.
После успешной аутентификации выполним загрузку первого образа в реестр.
Добавим пользователя с ролью user для последующего назначения ему соответствующего доступа.
Определим права доступа к репозиторию alpine для пользователя user1. Разрешим пользователю делать только pull.
Выполним аутентификацию на docker клиенте под пользователем user1
После успешной аутентификации выполним pull из репозитория alpine нашего реестра.
Как видим pull работает, попробуем выполнить push.
Как видно на скриншоте попытка push завершилась ошибкой. Добавим разрешение на возможность делать push для пользователя user1.
Попробуем снова выполнить push под текущим пользователем.
Как мы видим в репозитории alpine появился новый тэг, после добавления соответсвующего доступа действие pull было успешно выполнено.
Стоит обратить внимание, что в данном случае, при попытке удаления тега test будет также удален и тэг 3.14, потому-что у них одинаковая сигнатура digest, а registry хранит и удаляет элементы именно по этому хэшу. Также стоит имееть ввиду, что при удалении тэга из реестра удаляется только manifest файл, а данные удаляются путем запуска встроенного сборщика мусора (garbage collector).
Заключение
Итоговым результатом получилась, на мой взгляд, простая админка для управления записями реестра и доступом к Docker Private Registry, хотя есть некоторые особенности при настройке доверенных сертификатов, а также требуется понимание сетевого взаимодействия между сервисами. Надеюсь, что данный сервис может быть кому-то полезен.
В процессе реалзации были решины достаточно интересные задачи такие как работа с сертификатами, встраивание приложений SPA в бинарный файл, а также настройка CI/CD для автоматического тестирования и сборки проекта.
Всем желающим поучаствовать в тестировании и улучшении проекта добро поожаловать!
Комментарии (5)
dimkus
18.01.2023 06:23Если я правильно понял, то представленная в статье Админка по сути является проксёй для docker container registry, которая обеспечивает безопасность репозитория (user management) и Web GUI
zebox Автор
18.01.2023 06:55Не совсем прокси т.к. запросы к реестру выполняются напрямую. Это сбоку стоящий сервис auth + UI на который сам registry перенаправляет клиентские запросы. После получения токена, клиент напрямую общается с registry.
Только через такой механизм аутентификации (
silly
илиtoken
) можно организовать раздельный доступ к container regitry.dimkus
18.01.2023 11:04Принято. Отличное решение. С токеном можно прикрутить любой способ авторизации. OAuth, LDAP, Username/Password и т.д.
Поставил бы плюсик, но к сожалению пока статус не позволяет этого делать.
kozlyuk
Непонятно, каким из перечисленных требований не удовлетворил Harbor. Там нет генерации сертификатов для выдачи токенов, потому что Harbor не использует этот штатный механизм (в этом они не молодцы, а вы молодец), но сертификаты часто генерировать и не нужно. Самый гибкий контроллер доступа, который позволяет фильтровать по тэгам (чего не может ни Harbor, ни ваш проект) — https://github.com/cesanta/docker_auth. Если бы ваш GUI стыковался с ним, генерируя конфиг, ваш проект был бы проще, а результат функциональнее.
P. S. Склейка SQL из строк в коде — караул.
zebox Автор
Генерация сертификатов на стороне сервиса, на момент реализации, мне казалась логичным решением. Хотя и соглашусь, что по факту они генерируются один раз при развертывании. Что касается фильтра по тэгам, то он у меня реализован на странице самого репозитория.
Про SQL строки, мне честно говоря это тоже видится, мягко говоря, не очень. Но я не хотел использовать всякие варианты с hibernate. Такая склейка, по большей части, обусловлена адаптацией под концепцию со стороны фреймворка React-Admin. Готов выслушать предложения по оптимизации этой части.