Под катом вы увидите:
Использования Traefik в качестве обратного прокси для маршрутизации трафика внутрь docker контейнеров.
Использование Traefik для автоматического получения Let’s Encrypt сертификатов
Использование Traefik для разграничения доступа к docker registry при помощи basic auth
Все перечисленное выше будет настраиваться исключительно внутри docker-compose.yml и не потребует передачи отдельных конфигурационных файлов внутрь контейнеров.
Актуальность вопроса
Практически все инструкции, которые есть в интернете, используют несколько дополнительных файлов с конфигурациями, которые нужно будет скопировать в контейнер при запуске. Мы нашли способ сделать все необходимые настройки исключительно внутри compose файла.
Помимо этого в интернете мало информации на тему использования traefik для контроля доступа к docker registry. Описанную ниже технику можно использовать для контроля доступа к любому приложению, реализующему Rest API.
Поиск решения
Вот ссылка на официальную статью по развертыванию docker registry. Крутим страницу вниз и видим пример развертывания через docker-compose. Я перепечатаю пример ниже:
registry:
restart: always
image: registry:2
ports:
- 5000:5000
environment:
REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt
REGISTRY_HTTP_TLS_KEY: /certs/domain.key
REGISTRY_AUTH: htpasswd
REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm
volumes:
- /path/data:/var/lib/registry
- /path/certs:/certs
- /path/auth:/auth
Нам предлагают терминировать https трафик прямо внутри сервиса registry, чего мы делать не будем. Мы не станем усложнять себе жизнь и копировать сертификаты внутрь сервиса. Кроме того у нас есть другие https сервисы, которым также нужны сертификаты, так что у нас уже есть единая точка входа, где происходит автоматическая генерация сертификатов для новых сервисов с помощью let's encrypt - это контейнер с traefik.
Также нам предлагают использовать базовую авторизацию для доступа к registry, однако не предлагают способа разделения прав доступа. То есть авторизованный пользователь будет иметь полный доступ к сервису, сможет читать и писать в любом репозитории. На сайте есть пример использования nginx, но и там мы видим единственного пользователя с полными правами.
В интернете мы находим идеальный пример использования nginx для авторизации и разделения прав доступа. Мы сделаем то же самое, но через traefik.
Итак, приступаем.
Базовая конфигурация Registry
Сперва запустим сервис registry через отдельный compose файл. Создадим новую папку “registry", в которой создадим compose файл:
mkdir registry
cd registry
nano docker-compose.yml
Вставим в файл следующее содержимое и сохраним:
version: '2.4'
services:
registry:
restart: always
image: registry:2
ports:
- 5000:5000
Запустим сервис
docker-compose up -d
Откроем в браузере страницу http://<IP>:5000/v2/_catalog , где <IP> - это ip адрес докер машины.
В ответ увидим страницу с текстом:
{"repositories":[]}
Значит, все работает. Если это не так - проверьте firewall.
Базовая конфигурация Traefik
Знакомство с traefik начнем с базовой конфигурации.
Позже мы добавим SSL, сжатие трафика и авторизацию с аутентификацией.
Сперва запустим сервис registry через отдельный compose файл. Создадим новую папку “registry", в которой создадим compose файл:
mkdir traefik
cd traefik
nano docker-compose.yml
Вставим в файл следующее содержимое и сохраним:
version: "2.4"
services:
traefik:
image: "traefik:v2.4"
container_name: "traefik"
command:
- "--api.insecure=true"
ports:
- "8080:8080"
Разберем каждую новую строку (нажать)
command:
- "--api.insecure=true"
Через command передаются параметры запуска нашего приложения.
Включаем доступ к dashboard в insecure режиме. Это означает, что dashboard будет доступен напрямую в точке входа с названием traefik. Если указанная точка входа traefik не настроена, она будет автоматически создана на порту 8080.
- "8080:8080"
Перенаправление с порта 8080 на docker машине в аналогичный порт контейнера traefik. Эта настройка также как и предыдущая необходима, чтобы попасть в dashboard traefik
Запускаем наш сервис:
docker-compose up -d
Открываем страницу с IP адресом докер машины и портом 8080:
![](https://habrastorage.org/getpro/habr/upload_files/13b/f29/201/13bf2920195d4ffe23da99efe030e536.png)
Подключение Registry к Traefik (настройка домена)
Мы могли бы упростить себе задачу, объединив оба сервиса в одном compose файле, но мы не будем этого делать, чтобы показать более сложный сценарий использования.
В случае объединения нам не пришлось бы прописывать общие сети.
Новый compose для Traefik:
Version: "2.4"
services:
traefik:
image: "traefik:v2.4"
container_name: "traefik"
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
ports:
- "80:80"
- "8080:8080"
networks:
- registry_default
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
networks:
registry_default:
external: true
Разберем каждую новую строку (нажать)
- "--providers.docker=true"
Включаем докер провайдер. После этого traefik будет следить за появлением специальных меток на других контейнерах и перенастраивать все согласно этим меткам.
- "--providers.docker.exposedbydefault=false"
Запрещаем автоматическое добавление HTTP сервисов и HTTP маршрутов в traefik. Если этого не сделать, то traefik опубликует все docker контейнеры, в которых есть expose порта наружу автоматически. В качестве доменного имени он будет использовать имя контейнера.
Таким образом, без этой строки мы неявно открываем публичный доступ ко всем своим контейнерам! Все, что нужно для атаки - угадать имя контейнера! Я проверил эту теорию на практике: после добавления в файл hosts строки “IP_докер_машины имя_контейнера”, страница “http://имя_контейнера" открылась в браузере.
- "80:80"
Перенаправление стандартного веб порта 80 (http) на docker машине в аналогичный порт контейнера traefik. Это нужно для обработки и маршрутизации полезного трафика.
networks:
- registry_default
networks:
registry_default:
external: true
Подключение к коммутатору, обслуживающему контейнеры из другого compose файла. Дело в том, что для каждого compose файла создается виртуальный коммутатор с именем “имя_родительской_папки_default, таким образом сервисы внутри одного compose файла могут легко друг с другом взаимодействовать.
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
Подключаем docker.sock докер машины внутрь контейнера traefik. Это необходимо, чтобы traefik мог отслеживать изменения в чужих контейнерах. Он будет перенастраивать свою конфигурацию на лету, исходя из меток, принадлежащих другим контейнерам.
Новый compose для Registry:
version: '2.4'
services:
registry:
restart: always
image: registry:2
ports:
- 5000:5000
labels:
- "traefik.enable=true"
- "traefik.http.routers.registry.rule=Host(`<REGISTRY.FQDN>`)"
Разберем каждую новую строку (нажать)
- "traefik.enable=true"
Эта метка оповещает traefik, что данный сервис нужно опубликовать
- "traefik.http.routers.registry.rule=Host(`<REGISTRY.FQDN>`)"
Эта метка указывает traefik, что доменное имя <REGISTRY.FQDN> необходимо связать с данным сервисом. Запросы, в заголовках которых будет указано данное имя, будут перенаправлены в текущий контейнер.
- "traefik.http.services.registry.loadbalancer.server.port=5000"
Мы также можем указать порт, на который будет перенаправлен трафик, но в данном случае это делать не обязательно. Если docker файл нашего сервиса открывает наружу всего лишь 1 порт, этот порт будет выбран автоматически.
Перезапустим оба наших сервиса:
docker-compose up -d
Откроем в браузере страницу http://<REGISTRY.FQDN>:5000/v2/_catalog , где <REGISTRY.FQDN> - это полное доменное имя нашего сервиса, описанное в метке compose файла.
В ответ увидим страницу в текстом:
{"repositories":[]}
Значит все работает.
Добавление SSL (настройка https)
Мы будем автоматически получать и продлять SSL сертификаты через Let's Encrypt.
Новый compose для Traefik:
version: "2.4"
services:
traefik:
image: "traefik:v2.4"
container_name: "traefik"
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.myresolver.acme.httpchallenge=true"
- "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.myresolver.acme.email=<EMAIL>"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
- "8080:8080"
networks:
- registry_default
volumes:
- "letsencrypt:/letsencrypt"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
volumes:
letsencrypt:
networks:
registry_default:
external: true
Разберем каждую новую строку (нажать)
- "--entrypoints.web.address=:80"
Меняем имя стандартного entrypoint с http на web для удобства.
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
Добавляем автоматическое перенаправление трафика с entrypoint web на websecure. Другими словами перенаправление с HTTP на HTTPS
- "--entrypoints.websecure.address=:443"
Создаем новый entrypoint на 443 порту с именем websecure
- "--certificatesresolvers.myresolver.acme.httpchallenge=true"
Настраиваем режим выдачи сертификатов Let’s Encrypt через http challenge
- "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"
Настраиваем entrypoint для http challenge
- "--certificatesresolvers.myresolver.acme.email=<EMAIL>"
Настраиваем <email> адрес для регистрации в центре сертификации
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
Меняем стандартное расположение файла acme.json. В этот файл будут записываться выданные сертификаты. Дело в том, что стандартное расположение файла “/acme.json" в корне не позволяет хранить этот файл на подключенном томе.
- "443:443"
Перенаправление стандартного веб порта 443 (https) на docker машине в аналогичный порт контейнера traefik. Это нужно для обработки и маршрутизации полезного трафика.
volumes:
- "letsencrypt:/letsencrypt"
volumes:
letsencrypt:
Подключаем именованный том для постоянного хранения SSL сертификатов. Теперь даже после пересоздания контейнера нам не придется заново получать все сертификаты.
Именованный том будет храниться здесь: /var/lib/docker/volumes/<имя тома>
Новый compose для Registry:
version: '2.4'
services:
registry:
restart: always
image: registry:2
ports:
- 5000:5000
labels:
- "traefik.enable=true"
- "traefik.http.routers.registry.rule=Host(`<REGISTRY.FQDN>`)"
- "traefik.http.routers.registry.entrypoints=websecure"
- "traefik.http.routers.registry.tls.certresolver=myresolver"
Разберем каждую новую строку (нажать)
- "traefik.http.routers.registry.entrypoints=websecure"
Меняем entrypoint со стандартного http (web) на websecure
- "traefik.http.routers.registry.tls.certresolver=myresolver"
Задаем имя резолвера для работы SSL сертификатов
Перезапустим оба наших сервиса:
docker-compose up -d
Откроем в браузере страницу http://<REGISTRY.FQDN>:5000/v2/_catalog , где <REGISTRY.FQDN> - это полное доменное имя нашего сервиса, описанное в метке compose файла.
Мы увидим, что:
схема сменилась с http на https автоматически
соединение защищено сертификатом выданным Let's Encrypt
В случае проблем с получением сертификата, traefik будет использовать само-подписанный сертификат. Если это произошло, следует использовать команду docker logs traefik
для просмотра логов.
Настройка домена и SSL для dashboard
Как ни странно, маршрутизация трафика во встроенный dashboard отличается от маршрутизации трафика во внешние сервисы, запущенные в докер контейнерах. Я потратил не мало времени, пытаясь использовать те же правила, что и раньше.
Новый compose для Traefik:
version: "2.4"
services:
traefik:
image: "traefik:v2.4"
container_name: "traefik"
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.myresolver.acme.httpchallenge=true"
- "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.myresolver.acme.email=<EMAIL>"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
- "8080:8080"
networks:
- registry_default
volumes:
- "letsencrypt:/letsencrypt"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`<TRAEFIK.FQDN>`)"
- "traefik.http.routers.traefik.entrypoints=websecure"
- "traefik.http.routers.traefik.tls.certresolver=myresolver"
- "traefik.http.routers.traefik.service=api@internal"
volumes:
letsencrypt:
networks:
registry_default:
external: true
Разберем каждую новую строку (нажать)
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`<TRAEFIK.FQDN>`)"
- "traefik.http.routers.traefik.entrypoints=websecure"
- "traefik.http.routers.traefik.tls.certresolver=myresolver"
Регистрируем роутер для направления трафика домена <traefik.fqdn> во внутренний dashboard.
- "traefik.http.routers.traefik.service=api@internal"
Явно указываем имя сервиса в который мы будем перенаправлять трафик.
api@internal - это зарезервированное имя сервиса. Перенаправление в dashboard не будет работать без этой строки.
Мы не указывали подобную строку в контейнере registry, так как регистрация имени сервиса и привязка роутера к сервису выполнялась автоматически.
Перезапустим traefik:
docker-compose up -d
Откроем в браузере страницу http://<TRAEFIK.FQDN>, где <TRAEFIK.FQDN> - это полное доменное имя нашего для доступа к traefik dashboard, описанное в метке compose файла.
Добавление сжатия трафика
Сжатие сильно ускоряет загрузку сайтов на клиенте. Обязательно нужно включать.
Новый compose для Traefik:
version: "2.4"
services:
traefik:
image: "traefik:v2.4"
container_name: "traefik"
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.myresolver.acme.httpchallenge=true"
- "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.myresolver.acme.email=<email>"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
- "8080:8080"
networks:
- registry_default
volumes:
- "letsencrypt:/letsencrypt"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
labels:
- "traefik.http.middlewares.traefik-compress.compress=true"
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`<TRAEFIK.FQDN>`)"
- "traefik.http.routers.traefik.entrypoints=websecure"
- "traefik.http.routers.traefik.tls.certresolver=myresolver"
- "traefik.http.routers.traefik.service=api@internal"
- "traefik.http.routers.traefik.middlewares=traefik-compress"
volumes:
letsencrypt:
networks:
registry_default:
external: true
Разберем каждую новую строку (нажать)
- "traefik.http.middlewares.traefik-compress.compress=true"
Регистрируем новый middleware с именем traefik-compress и функцией сжатия трафика. Этот middleware мы затем сможем использовать в любом стороннем докер контейнере.
- "traefik.http.routers.traefik.middlewares=traefik-compress"
Добавляем middleware с именем traefik-compress в цепочку обработки трафика для сервиса traefik
Новый compose для Registry:
version: '2.4'
services:
registry:
restart: always
image: registry:2
ports:
- 5000:5000
labels:
- "traefik.enable=true"
- "traefik.http.routers.registry.rule=Host(`<REGISTRY.FQDN>`)"
- "traefik.http.routers.registry.entrypoints=websecure"
- "traefik.http.routers.registry.tls.certresolver=myresolver"
- "traefik.http.routers.registry.middlewares=traefik-compress"
Разберем каждую новую строку (нажать)
- "traefik.http.routers.registry.middlewares=traefik-compress"
Добавляем middleware с именем traefik-compress в цепочку обработки трафика для сервиса registry
Добавление basic авторизации для доступа к Dashboard
Сначала мы собираемся сгенерировать комбинацию пользователя и пароля для базовой аутентификации с использованием htpasswd. Если он у вас не установлен, вам нужно сначала сделать это (например, для сервера Ubuntu):
apt-get install apache2-utils
Мы должны экранировать каждый символ “$” в нашем зашифрованном пароле (заменить $ на $$), если мы используем пароль напрямую в docker-compose.yml
echo $(htpasswd -nbB USER "PASS") | sed -e s/\\$/\\$\\$/g
Пример вывода команды (результат будет разный при каждом запуске команды):
USER:$$2y$$05$$iPGcI0PwxkDoOZUlGPkIFe31e47F5vewcjlhzhgf0EHo45H.dFyKW
Вывод команды нужно поместить в наш docker-compose.yml внутрь traefik метки, заменив <USER-PASSWORD-OUTPUT>
в примере ниже.
Новый compose для Registry:
version: "2.4"
services:
traefik:
image: "traefik:v2.4"
container_name: "traefik"
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.myresolver.acme.httpchallenge=true"
- "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.myresolver.acme.email=<EMAIL>"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
- "8080:8080"
networks:
- registry_default
volumes:
- "letsencrypt:/letsencrypt"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
labels:
- "traefik.http.middlewares.traefik-compress.compress=true"
- "traefik.http.middlewares.auth.basicauth.users=<USER-PASSWORD-OUTPUT>"
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`<TRAEFIK.FQDN>`)"
- "traefik.http.routers.traefik.entrypoints=websecure"
- "traefik.http.routers.traefik.tls.certresolver=myresolver"
- "traefik.http.routers.traefik.service=api@internal"
- "traefik.http.routers.traefik.middlewares=traefik-compress,auth"
volumes:
letsencrypt:
networks:
registry_default:
external: true
Разберем каждую новую строку (нажать)
- "traefik.http.middlewares.auth.basicauth.users=<USER-PASSWORD-OUTPUT>"
Регистрируем новый middleware с именем auth и функцией авторизации. Этот middleware мы затем сможем использовать в любом стороннем докер контейнере.
- "traefik.http.routers.traefik.middlewares=traefik-compress,auth"
Добавляем middleware с именем auth в цепочку обработки трафика для сервиса traefik
Внимание: Если вы используете переменные среды (например .env файл) в вашем docker-compose.yml вместо прямого указания <USER-PASSWORD-OUTPUT>, то вы не должны экранировать $. Генерация пароля в этом случае будет выглядеть так:
echo $(htpasswd -nbB <USER> "<PASS>")
После перезапуска docker контейнера (docker-compose up -d) мы увидим окно базовой авторизации, когда откроем dashboard traefik в браузере.
Разделение прав доступа пользователей Registry
Новый compose для Registry:
version: '2.4'
services:
registry:
restart: always
image: registry:2
ports:
- 5000:5000
labels:
- "traefik.enable=true"
- "traefik.http.routers.registry.rule=Host(`REGISTRY.FQDN`) && Method(`POST`, `PUT`, `DELETE`, `PATCH`)"
- "traefik.http.routers.registry.entrypoints=websecure"
- "traefik.http.routers.registry.tls.certresolver=myresolver"
- "traefik.http.routers.registry.service=registry"
- "traefik.http.services.registry.loadbalancer.server.port=5000"
- "traefik.http.routers.registry.middlewares=auth-registry,traefik-compress"
- "traefik.http.middlewares.auth-registry.basicauth.users=<ADMIN-PASSWORD-OUTPUT>"
- "traefik.http.routers.guest-registry.rule=Host(`REGISTRY.FQDN`) && Method(`GET`, `HEAD`)"
- "traefik.http.routers.guest-registry.entrypoints=websecure"
- "traefik.http.routers.guest-registry.tls.certresolver=myresolver"
- "traefik.http.routers.guest-registry.service=guest-registry"
- "traefik.http.services.guest-registry.loadbalancer.server.port=5000"
- "traefik.http.routers.guest-registry.middlewares=aguest-registry,traefik-compress"
- "traefik.http.middlewares.aguest-registry.basicauth.users=<USER-PASSWORD-OUTPUT>"
Зарегистрируем на этом контейнере 2 набора роутеров и сервисов:
registry
- роутер и сервис с таким именем будут предоставлять полный доступ (чтение\запись)guest-registry
- роутер и сервис с таким именем будут предоставлять гостевой доступ (чтение)
Так же мы создаем одноименные middleware для basic авторизации и добавляем их в роутеры.
Вы можете заметить, что мы добавили основного пользователя в оба набора правил. Это сделано потому, что наборы методов не пересекаются. Если сделать их пересекающимися - роутинг не будет работать как задумано.
Перезапустим registry:
docker-compose up -d
Проведем простую проверку с помощью Postman
Авторизовываемся пользователем с ограниченными правами.
Делаем Get запрос - работает.
![](https://habrastorage.org/getpro/habr/upload_files/751/deb/1f6/751deb1f619c791bfc854558331af5d8.png)
Делаем Post запрос - 401.
![](https://habrastorage.org/getpro/habr/upload_files/6bf/085/8e9/6bf0858e9c6eb9f7c02a4c56006613f2.png)
Авторизовываемся пользователем с полными правами.
Делаем Get запрос - работает.
![](https://habrastorage.org/getpro/habr/upload_files/c20/843/e81/c20843e814856c55b1014993821d1875.png)
Делаем Post запрос - работает. Авторизация пройдена, но сам запрос отклоняется registry, так как не является допустимым. Мы не стали подбирать правильный запрос для экономии времени.
![](https://habrastorage.org/getpro/habr/upload_files/4d4/2cb/760/4d42cb76030f4558e830e4c72090da7f.png)
Заключение
На мой взгляд traefik гораздо удобнее классического nginx, если мы живем внутри docker контейнеров.
Единственная проблема, с которой мы столкнулись при миграции на traefik - это невозможность использовать отрицания в правилах маршрутизации. Подробнее о проблеме можно почитать по ссылке.