Traefik, docker и docker registry

Под катом вы увидите:

  1. Использования Traefik в качестве обратного прокси для маршрутизации трафика внутрь docker контейнеров.

  2. Использование Traefik для автоматического получения Let«s Encrypt сертификатов

  3. Использование Traefik для разграничения доступа к docker registry при помощи basic auth

  4. Все перечисленное выше будет настраиваться исключительно внутри 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://:5000/v2/_catalog, где  — это 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:

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(``)"
Разберем каждую новую строку (нажать)
- "traefik.enable=true"

Эта метка оповещает traefik, что данный сервис нужно опубликовать

- "traefik.http.routers.registry.rule=Host(``)"

Эта метка указывает traefik, что доменное имя необходимо связать с данным сервисом. Запросы, в заголовках которых будет указано данное имя, будут перенаправлены в текущий контейнер.

- "traefik.http.services.registry.loadbalancer.server.port=5000"

Мы также можем указать порт, на который будет перенаправлен трафик, но в данном случае это делать не обязательно. Если docker файл нашего сервиса открывает наружу всего лишь 1 порт, этот порт будет выбран автоматически. 

Перезапустим оба наших сервиса:

docker-compose up -d

Откроем в браузере страницу http://:5000/v2/_catalog, где  — это полное доменное имя нашего сервиса, описанное в метке 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="
      - "--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="

Настраиваем адрес для регистрации в центре сертификации

- "--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(``)"
      - "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://:5000/v2/_catalog, где  — это полное доменное имя нашего сервиса, описанное в метке 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="
      - "--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.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.http.routers.traefik.entrypoints=websecure"
  - "traefik.http.routers.traefik.tls.certresolver=myresolver"

Регистрируем роутер для направления трафика домена во внутренний dashboard.

- "traefik.http.routers.traefik.service=api@internal"

Явно указываем имя сервиса в который мы будем перенаправлять трафик.

api@internal — это зарезервированное имя сервиса. Перенаправление в dashboard не будет работать без этой строки.

Мы не указывали подобную строку в контейнере registry, так как регистрация имени сервиса и привязка роутера к сервису выполнялась автоматически.

Перезапустим traefik:

docker-compose up -d

Откроем в браузере страницу http://, где  — это полное доменное имя нашего для доступа к 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="
      - "--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.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(``)"
      - "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 метки, заменив  в примере ниже.

Новый 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="
      - "--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="
      - "traefik.enable=true"
      - "traefik.http.routers.traefik.rule=Host(``)"
      - "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="

Регистрируем новый middleware с именем auth и функцией авторизации. Этот middleware мы затем сможем использовать в любом стороннем докер контейнере.

- "traefik.http.routers.traefik.middlewares=traefik-compress,auth"

Добавляем middleware с именем auth в цепочку обработки трафика для сервиса traefik

Внимание: Если вы используете переменные среды (например .env файл) в вашем docker-compose.yml вместо прямого указания , то вы не должны экранировать $.  Генерация пароля в этом случае будет выглядеть так:  

echo $(htpasswd -nbB  "")

После перезапуска 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="
      - "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="

Зарегистрируем на этом контейнере 2 набора роутеров и сервисов:

  • registry — роутер и сервис с таким именем будут предоставлять полный доступ (чтение\запись)

  • guest-registry — роутер и сервис с таким именем будут предоставлять гостевой доступ (чтение)

Так же мы создаем одноименные middleware для basic авторизации и добавляем их в роутеры.

Вы можете заметить, что мы добавили основного пользователя в оба набора правил. Это сделано потому, что наборы методов не пересекаются. Если сделать их пересекающимися — роутинг не будет работать как задумано.

Перезапустим registry:

docker-compose up -d
Проведем простую проверку с помощью Postman

Авторизовываемся пользователем с ограниченными правами.

Делаем Get запрос — работает.

751deb1f619c791bfc854558331af5d8.png

Делаем Post запрос — 401.

6bf0858e9c6eb9f7c02a4c56006613f2.png

Авторизовываемся пользователем с полными правами.

Делаем Get запрос — работает.

c20843e814856c55b1014993821d1875.png

Делаем Post запрос — работает. Авторизация пройдена, но сам запрос отклоняется registry, так как не является допустимым. Мы не стали подбирать правильный запрос для экономии времени.

4d42cb76030f4558e830e4c72090da7f.png

Заключение

На мой взгляд traefik гораздо удобнее классического nginx, если мы живем внутри docker контейнеров.

Единственная проблема, с которой мы столкнулись при миграции на traefik — это невозможность использовать отрицания в правилах маршрутизации. Подробнее о проблеме можно почитать по ссылке.

© Habrahabr.ru