Статья предназначена для тех, кто подбирает себе reverse proxy или load balancer и хочет приглядеться к Traefik v2 в этом качестве. Рассмотрена установка в Docker и взаимодействие с его контейнерами, организация как собственного HTTPS шифрования, так и проброс TCP трафика на HTTPS сервер. Без Kubernetes, без SWARM.

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

Установка Traefik.

Предлагаю пока выбрать порты 8080 и 8443, чтобы потренироваться на кошечках, не беспокоя основные сервисы. Начнём с эксперимента. Можно ли запустить Traefik вообще без настроек?

mkdir traefik && cd traefik
nano docker-compose.yml

Содержимое файла docker-compose.yml

version: '3'
services:
  traefik:
    image: traefik:v2.10.4

Предпочитаю прописывать версию, чтобы избегать неожиданного поведения и захламления образами. С уязвимостями не приходилось сталкиваться, а с неожиданным поведением новых версий разных проектов приходилось. На момент создания статьи версия была 2.10.4. Юра, поехали!

sudo docker compose -p traefik up -d

Что-то скачалось, создалось, запустилось. Вроде работает, ничто нигде не отвалилось. Эксперимент можно признать успешным. Пора прикручивать блэкджек. В смысле дашборд, порты, конфиги из меток, конфиги из статического файла. И сертификат с ключом. Ведь он уже есть на сервере, правда? Тут, кстати, у Traefik есть неприятная мозоль. Когда он сам запрашивает сертификат у ACME провайдера, то сохраняет его в JSON формате. И если нужно поделиться с каким-нибудь другим HTTPS сервисом, то придётся выдирать его из JSON и сохранять в привычном PEM формате. Благо, скрипты для этого в интернете есть. Можно и самостоятельно написать, если не лень. Там тот же PEM в виде одной длинной строки.

Файл docker-compose.yml сразу на максималках:

version: '3'
services:
  traefik-proxy:
    image: traefik:v2.10.4
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    env_file:
      - traefik.env
    ports:
      - 8080:8080
      - 8443:8443
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./logs/:/var/log/traefik/
      - ./traefik.yml:/etc/traefik/traefik.yml:ro
      - ./config/:/etc/traefik/config/:ro
      - ./cert/:/etc/traefik/cert/:ro
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.traefik-http-router.entrypoints=websecure"
      - "traefik.http.routers.traefik-http-router.rule=Host(`traefik.example.com`) &&
	     (PathPrefix(`/traefik`) ||
		  Headers(`Referer`, `https://traefik.example.com:8443/traefik/dashboard/`))"
      - "traefik.http.routers.traefik-http-router.tls=true"
      - "traefik.http.routers.traefik-http-router.service=api@internal"
      - "traefik.http.routers.traefik-http-router.middlewares=traefik-mw-auth,traefik-mw-strip"
      - "traefik.http.middlewares.traefik-mw-auth.basicauth.users=${AUTH}"
      - "traefik.http.middlewares.traefik-mw-strip.stripprefix.prefixes=${PATH}"
      # SANs
      #- "traefik.http.routers.traefik-http-router.tls.certresolver=myresolver"
      - "traefik.http.routers.traefik-http-router.tls.domains[0].main=traefik.example.com"
      - "traefik.http.routers.traefik-http-router.tls.domains[0].sans=traefik.example.com,kifeart.example.com"
    networks:
      traefik-bridge:
#        ipv4_address: 192.168.100.100
networks:
  traefik-bridge:
    name: traefik
#    ipam:
#      config:
#        - subnet: 192.168.100.0/24
#          gateway: 192.168.100.1

Прописаны привычные опции, порты, подсеть, маппинги чего-то полезного, включая файл статической конфигурации. Он должен называться traefik.toml или traefik.yml и находиться в одном из предопределённых мест. Хорошо бы, чтобы к следующему запуску этот файл уже был, пока добрый docker compose не создал папку с таким именем.

touch traefik.yml

Но самое главное! Прописаны конфиги для Traefik в виде labels.

Entrypoints, routers, middlewares, services.

Немного теории. Существуют entrypoints, в которые поступают запросы клиентов и services в контейнерах или локальной сети, которые ждут эти запросы. Между ними бесконечность, называемая Traefik. За организацию движения запросов и ответов между энтрипоинтами и сервисами отвечают routers. Если запрос нуждается в дополнительной обработке, на помощь приходят middlewares.

Labels

Давайте пробежимся лёгкой рысцой по имеющимся меткам.

traefik.enable=true

В файле статической конфигурации позже будет прописано, что Traefik должен смотреть только на те контейнеры, которые вот так себя обозначили.

traefik.http.routers.traefik-http-router.entrypoints=websecure

В Traefik роутеры делятся на http, tcp и udp. В данном случае http роутер с именем traefik-http-router должен смотреть только на энтрипоинт с именем websecure. Можно через запятую указать другие энтрипоинты. Можно вообще убрать этот конфиг. Тогда роутер будет смотреть на все имеющиеся энтрипоинты.

traefik.http.routers.traefik-http-router.rule=

Вот здесь можно проявить богатство фантазии и объединить и разъединить различные условия срабатывания роутера. В данном конкретном случае указано, что интересуют те запросы, которые пришли на хост с заданным именем traefik.example.com и заданным путём /traefik. Ну, а фишка с хедером была найдена в интернете, когда выяснилось, что дашборд плохо принимает запросы, основанные только на пути. Прошу заметить, порт потом надо будет переписать. И ещё болячка. Завершающий слэш dashboard/ обязателен, когда будете набирать в браузере. Подробнее про правила.

traefik.http.routers.traefik-http-router.tls=true

Схема запроса HTTPS, требуется найти сертификат для запрошенного имени хоста в собственном хранилище или PEM файлах и организовать TLS сессию.

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

Запрос, попавший в правила этого роутера передать себе любимому. В смысле сервису по имени api@internal.

traefik.http.routers.traefik-http-router.middlewares=traefik-mw-auth,traefik-mw-strip

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

traefik.http.middlewares.traefik-mw-auth.basicauth.users=${AUTH}

Эта миддлварь умеет делать больно авторизацию типа Basic. Переменная AUTH вынесена в файл .env, откуда будет подтянута силами docker compose. Вообще, сам docker compose (v2.20.2) смотрит только на этот файл. А всякие environment или env_file – это информация для сервисов, которые он запускает. Попытка переложить определение переменной в traefik.env засчитана не будет, вместо этого будет подставлено пустое значение. Так вот. Если сгенерировать хэш Basic пароля с помощью утилиты htpasswd, экранировать в нём знаки доллара двойным знаком доллара и присвоить переменной AUTH в файле .env, то доступ в дашборд Traefik будет ограничен тем, кто не знает пароль. Грустно это.

htpasswd -nb adm adm
adm:$apr1$HTRwmazm$qM.P8cuFBQODq1TPloov30 -> adm:$$apr1$$HTRwmazm$$qM.P8cuFBQODq1TPloov30

nano .env

AUTH=adm:$$apr1$$HTRwmazm$$qM.P8cuFBQODq1TPloov30
PATH=/traefik

Если утилита htpasswd отсутствует, придётся скачать её в составе apache2-utils. В Ubuntu это так.

sudo apt update && sudo apt install apache2-utils

Следующая миддлварь с именем traefik-mw-strip отрезает от запроса префикс /traefik.
traefik.http.middlewares.traefik-mw-strip.stripprefix.prefixes=${PATH}
Ну, или какой другой префикс придумаете. Хоть однобуквенный, хоть UUID.
Конечно же разных миддлварей намного больше.

Всё это увлекательно, но как Traefik понимает на каком IP адресе запущен контейнер с сервисом, которому нужно передать попавший в правила запрос? В Докере же сети генерируются рандомно. Вот здесь работает магия использования провайдера Docker в описанном ниже файле статической конфигурации traefik.yml. Неважно, какой адрес у контейнера. Провайдер его предоставит. Собственно, давайте переходить к этому файлу.

Статическая конфигурация

Статическая конфигурация может быть описана в формате TOML или YAML. Я предпочитаю YAML и ниже буду придерживаться его. Фломастеры у всех разные.
Файл traefik.yml:

global:
  checkNewVersion: true
  sendAnonymousUsage: false
log:
  level: DEBUG # DEBUG, INFO, WARNING, ERROR, CRITICAL
  format: common # common, json, logfmt
  filePath: /var/log/traefik/traefik.log
accesslog:
  format: common # common, json, logfmt
  filePath: /var/log/traefik/access.log
api:
  dashboard: true
entryPoints:
  web:
    address: ":8080"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
          permanent: false
  websecure:
    address: ":8443"
#  registry:
#    address: ":5000"
providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
  file:
    directory: /etc/traefik/config
    watch: true
certificatesResolvers:
  myresolver:
    acme:
      email: your-email@example.com
      storage: acme.json
      tlsChallenge: {}

Описаны такие статические элементы как логи, энтрипоинты, провайдеры, резолвер. Обратите внимание – статические, значит те, которые не хочется менять за всё время работы контейнера Traefik.

Раздел global просто как табуретка. Версию проверять, статистику не отправлять.
Раздел log указывает на путь внутри контейнера, куда писать логи. В файле docker-compose.yml он пока замаплен в папку ./logs, чтоб далеко не бегать. А когда кошечки закончатся, можно будет переписать на цивильный /var/log/traefik.

Кнопка включения дашборда находится в разделе api.

Раздел entryPoints. Имейте в виду, что энтрипоинты подразумеваются в сети контейнера, а снаружи к ним надо ещё открыть доступ через ports: файла docker-compose.yml. Описанная здесь схема перехода с HTTP на HTTPS на самом деле создаёт виртуальный роутер. Вы можете найти его в дашборде под именем web-to-websecure@internal. Адреса записаны в краткой форме, а полная форма – 0.0.0.0:8443.

Далее providers. Провайдер docker передаёт в Traefik информацию о контейнерах через сокет unix:. А поскольку указано, что exposedByDefault=false, то Traefik будет игнорировать те контейнеры, в которых не помечено traefik.enable=true. Вечеринка для избранных.
Провайдер file. Указана папка внутри контейнера, где неожиданно для Traefik могут появляться динамические конфиги. В docker-compose.yml эта папка замаплена в ./config на хосте. Поскольку опция watch включена, то Traefik будет периодически следить за изменениями в файлах. Можно указать провайдера в виде только одного файла. Тогда directory должно быть заменено на file и файл соответствующе назван и замаплен с хоста.
Конечно же, всего провайдеров не два, а намного больше. Возможно, какие-то из них уже применяются в вашем окружении.
И наконец certificatesResolvers. По умолчанию Traefik использует Letsencrypt. Можно переопределить. Указан tlsChallenge как TLS-ALPN-01 challenge на 443 порте. Но это потом.

Динамическая конфигурация

Раз выяснилось, что Traefik есть дело до содержимого /etc/traefik/config, давайте положим туда конфиг, например, с информацией о сертификатах. Если docker compose ещё не запускался с новым docker-compose.yml, то папки ./config на хосте пока не существует.

mkdir config
nano config/certificates.yml

Содержимое файла certificates.yml

tls:
  certificates:
    - certFile: /etc/traefik/cert/fullchain.pem
      keyFile: /etc/traefik/cert/privkey.pem
      stores:
        - default
#    - certFile: /etc/traefik/cert/selfsigned.pem
#      keyFile: /etc/traefik/cert/selfkey.pem
#      stores:
#        - default
  stores:
    default:
      defaultCertificate:
        certFile: /etc/traefik/cert/fullchain.pem
        keyFile: /etc/traefik/cert/privkey.pem
  options:
    default:
      sniStrict: true

Вы уже догадались по именам файлов, что они скопированы из Certbot. Кстати, действительно, скопируйте или прилинкуйте ключ и сертификат в папку ./cert. Возможно, её придётся сначала создать. При желании даже с помощью docker compose, там все папки прописаны. Пока давайте сделаем вид, что Traefik не обучен сам запрашивать ACME challendge и оставим в таком виде.

В разделе tls.certificates указаны пути к PEM файлам. Traefik проходит по этим путям и внимательно читает и извлекает все Certificate Subject Alternative Name (SAN) из файлов, чтобы потом сопоставлять нужные сертификаты клиентам в соединениях. Проверьте в лог-файле, чего он извлекает. Сертификатов может быть несколько разных. Их перечисление определяется правилами YAML или TOML.

В разделе tls.stores.default.defaultCertificate указан сертификат, который Traefik подставляет когда ни один не подошёл под запрос. Если информации о дефолтном сертификате нет или подходящего файла нет, то сгенерирует и выдаст самоподписанный. Можете понаблюдать за этим в браузере, зайдя на https://traefik.example.com:8443/traefik/dashboard/ и перемещая certificates.yml из папки и обратно. Конфигурация же динамическая. Надеюсь, не надо объяснять, что имя сервера здесь вымышленное для примера.

В разделе tls.options.default указана опция sniStrict. Если ни один сертификат не подошёл, то сразу 404. Зайдите теперь браузером по IP адресу, совсем другие ощущения. Вообще TLS опции есть разные. Если интересно, https://doc.traefik.io/traefik/https/tls/

Сами вы можете создать здесь конфигурацию с роутером и сервисом для какого-нибудь Cockpit на примере файла для Registry ниже.

Конфигурация переменными

Здесь затронем максимально кратко. В docker-compose.yml для сервиса traefik-proxy через секцию env_file: указан файл traefik.env, куда можно прописать статические переменные и их значения для Traefik. Сохраните там, например, такое:

echo TRAEFIK_API_DISABLEDASHBOARDAD=true > traefik.env

Про остальные можно узнать на https://doc.traefik.io/traefik/reference/static-configuration/env/
Вы же не забыли про файл .env? Вот без него точно никуда. По крайней мере в вышеописанной конфигурации с внешними переменными для docker compose. Надеюсь, доступ к вашему дашборду защищён не паролем adm.

AUTH=adm:$$apr1$$HTRwmazm$$qM.P8cuFBQODq1TPloov30
PATH=/traefik

Напомню путь к дашборду Traefik:
https://traefik.example.com:8443/traefik/dashboard/.

Хотя нет. Посмотрите сначала, как он редиректит с HTTP:
http://traefik.example.com:8080/traefik/dashboard/

С пациентом всё ясно. А может он не только себя показывать, но и на другие контейнеры смотреть?

Проброс на HTTP сервер через свой HTTPS

Все любят котиков контейнеры. Давайте развернём систему управления контейнерами Portainer CE и пропустим её через :8443/portainer.

Создайте рядом с директорией traefik другую директорию и романтично назовите её portainer.

cd ..
mkdir portainer
nano portainer/docker-compose.yml

Содержимое файла docker-compose.yml

version: "3"
services:
  portainer:
    image: portainer/portainer-ce:2.18.4-alpine
    container_name: portainer
    command: -H unix:///var/run/docker.sock
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    expose:
      - 9000
      - 9443
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./data/:/data/
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=poportainer"
      - "traefik.http.routers.portainer-http-router.entrypoints=websecure"
      - "traefik.http.routers.portainer-http-router.rule=Host(`traefik.example.com`) && (PathPrefix(`/portainer`)"
      - "traefik.http.routers.portainer-http-router.service=portainer-http-service"
      - "traefik.http.routers.portainer-http-router.tls=true"
      - "traefik.http.routers.portainer-http-router.middlewares=portainer-mw-strip"
      - "traefik.http.middlewares.portainer-mw-strip.stripprefix.prefixes=/portainer"
      - "traefik.http.services.portainer-http-service.loadbalancer.server.port=9000"
      # SANs
      #- "traefik.http.routers.portainer-http-router.tls.certresolver=myresolver"
      - "traefik.http.routers.portainer-http-router.tls.domains[0].main=traefik.example.com"
      - "traefik.http.routers.portainer-http-router.tls.domains[0].sans=traefik.example.com,kifeart.example.com"
    networks:
      portainer-used:
      portainer-unused:
networks:
  portainer-used:
    name: poportainer
  portainer-unused:

Если пока не касаться меток, предназначенных для Traefik, обратите внимание на остальные детали. Информацию о контейнерах Portainer получает от Docker так же через unix: сокет. Порты объявлены не через ports:, а через expose:, то есть на хосте открывать не надо. Для примера объявлено несколько сетей, что иногда бывает в разных compose сборках. И указано конкретное значение метки traefik.docker.network, чтобы наш прокси ничего не перепутал и не соединил случайным образом.

С самими метками, надеюсь, уже понятно по предыдущему примеру, чего хотелось бы передать в Traefik. Давайте повторим на новом примере. Обработка контейнера включена, сеть контейнера явно задана, роутер с именем portainer-http-router наблюдает только за энтрипоинтом websecure, где собирает запросы на хост traefik.example.com с путём, начинающимся на /portainer, после чего передаёт на миддлварь portainer-mw-strip для подстрижки этого '/portainer'. Далее запрос покидает роутер через сервис, указанный в роутере. Совершенно случайно этим сервисом оказался portainer-http-service на 9000 порте, который на самом деле контейнер Portainer в вышеуказанной сети в доме, который построил Джек.

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

sudo docker compose -f ~/portainer/docker-compose.yml -p portainer up -d
sudo docker compose -f ~/traefik/docker-compose.yml -p traefik up -d

Если в каком-то другом месте, поправьте пути как надо. Если душа просит переходить в нужную папку и запускать там docker compose up -d, то так и сделайте. По идее что-то открылось на https://traefik.example.com:8443/portainer.

Ну и? На что похож Portainer? На 404? Да. Это потому, что у контейнера traefik нет доступа в сеть poportainer. Поскольку вы явно указывали имя и контейнера и сети, то можно легко их использовать и дать руками доступ в консоли:

sudo docker network connect poportainer traefik

А так пришлось бы догадываться об автоматически назначенных именах portainer_portainer-used и traefik-traefik-proxy-1. Если связка Traefik-Portainer нужна не на пять минут, а как долговременный сервис, наверное, удобней прописать это дело в traefik/docker-compose.yml.

services:
  traefik-proxy:
  # ...
    networks:
      poportainer:
networks:
  poportainer:
    external: true

Только теперь запускать придётся после Portainer, поскольку сеть с таким именем создаётся там. Или можно насоздавать сетей руками и выдавать контейнерами через такой вот external.
Доступ получен, traefik перезапущен, на https://traefik.example.com:8443/portainer появилось симпатичное приглашение залогиниться. Логин admin, пароль admin. Поменяйте в разделе Settings / Users. Кто первый встал, того и тапки. После формальности загляните в Home / primary / Networks / poportainer. Вот он результат трудов - контейнер traefik в этой сети имеет свой адрес. И хоть в сети portainer_portainer-unused и есть одинокий контейнер, но Traefik он не интересует. В дашборде самого Traefik есть HTTP сервис portainer-http-service@docker с IP адресом, совпадающим с адресом контейнера portainer в сети poportainer. Провайдер Docker!

Кстати. Помните одинокую переменную TRAEFIK_API_DISABLEDASHBOARDAD для Traefik? Её тут видно в соответствующем контейнере.

Проброс TCP на HTTPS

Соединять нешифрованный трафик любой Squid умеет. А можно соединить с сервером, который сам организует свои TLS сессии? Можно через TCP, но поскольку снаружи сессии доступно очень мало информации, выбор топоров для этой каши небольшой. Можно применить только четыре правила: HostSNI(``), HostSNIRegexp(``), ClientIP(``), ALPN(``). Зато рассматривает Traefik TCP правила в приоритете перед HTTP, что позволяет смешивать роутеры на одном энтрипоинте с заранее известным результатом.

Надеюсь, вы обратили внимание, что в секции expose у Portainer указан также и 9443? Portainer запускает на этом порте HTTPS версию, даже если не открывать к нему доступ. Но мы воспользуемся. Если в (Portainer) Settings / SSL certificate предоставить файлы ключа и сертификата, то они будут использованы против вас в TLS соединении. Если не давать, то будет самоподписанный сертификат на localhost. Добавьте в portainer/docker-compose.yml в секцию labels собственно вот эти TCP labels, выровняйте их с остальными, как того требует YAML:

    labels:
    #...
      - "traefik.tcp.routers.portainer-tcp-router.entrypoints=websecure"
      - "traefik.tcp.routers.portainer-tcp-router.rule=!ClientIP(`8.8.8.8`)
 #     - "traefik.tcp.routers.portainer-tcp-router.rule=HostSNI(`megatest.example.com`)"
      - "traefik.tcp.routers.portainer-tcp-router.tls.passthrough=true"
      - "traefik.tcp.routers.portainer-tcp-router.service=portainer-tcp-service"
      - "traefik.tcp.services.portainer-tcp-service.loadbalancer.server.port=9443"

И перезапустите.

sudo docker compose -f ~/portainer/docker-compose.yml -p portainer up -d

Если хотите протестировать правило на SNI, то сначала придётся подшаманить в hosts на клиенте или на DNS сервере, отвечающем за ваш домен. Или оставить в правиле уже настроенный traefik.example.com. Да сейчас от HTTPS сервера Portainer ничего и не нужно. Просто убедиться, что он жив, Traefik с ним соединяет (TCP to HTTPS) и можно посмотреть на знаменитый сертификат localhost. Добавленные метки TCP конфига по смыслу такие же как HTTP. Роутер portainer-tcp-router следит за энтрипоинтом websecure, если там срабатывает правило "любой клиент кроме Гугла", то передаёт уже не запросы, а пакеты на сервис portainer-tcp-service, которым так удачно оказался контейнер portainer. В доме, который построил Джек.

Когда закончите, закомментируйте это rule=!ClientIP(8.8.8.8). Оно же перекрыло HTTP доступ к дашборду и Portainer.

HTTPS over IP

Факультатив, можно не читать. Про Traefik тут ничего нового. Новое разве что про Docker, Portainer и Registry. Просто хочется показать путь между граблями, на котором Traefik может действительно помочь.

Вы уже наверняка облазили Portainer и нашли там секцию App Templates. А в ней шаблон контейнера Registry. Да, можно развернуть собственное хранилище Docker образов двумя кликами. Разверните, загляните в него, скопируйте IP адрес и переходите в раздел Settings / Registries. Там добавьте custom registry и укажите скопированный IP адрес и порт. Как удобно, что Portainer догадывается про 5000 порт в подсказке! Наверняка ребята разбираются в вопросе. Сохраните и возвращайтесь в раздел Containers. А что если сохранить Registry в Registry? Вот умора то! Откройте его контейнер (или любой другой), в секции Create image переключите Registry на свой новенький локальный, задайте имя образа и примените Create. Скорость исполнения желаний поражает. Образ готов, найдите его в разделе Images, откройте и попытайте отправить в реестр (кнопка Push в секции Image tags). Ага... "Failure, http: server gave HTTP response to HTTPS client". Да где я тебе HTTPS response возьму, собака? Может, в консерватории что-то не в порядке? Давайте вернёмся в Settings / Registries и чётко недвусмысленно обозначим URL как http. А там в редактировании при наведении на знак вопроса всплывает чёткое и недвусмысленное "Any protocol or trailing slash will be stripped if present." То есть нетъ. Победа была так близка. Ладно, придётся курить мануал.

Чтобы ускорить процесс, сразу скажу, что с /etc/docker/daemon.json способ не рабочий для Portainer. Может быть Docker что-то себе помечает, но Portainer своё поведение не меняет. А вот с самоподписанным сертификатом очень даже рабочий, и Traefik здесь может помочь. Мне поначалу казалось, что достаточно взять portainer:/etc/ssl/certs/ca-certificates.crt, дописать в него свой самоподписанный и положить на место. Но оказалось, что показалось. Способ тоже не рабочий. Давайте уже делать по официальному руководству. План работ такой:

  • пересоздать Registry без маппинга порта

  • в Traefik создать энтрипоинт :5000

  • в Traefik создать HTTP сервис registry

  • в Traefik создать между ними HTTP роутер :5000 <-> registry

  • создать самоподписанный сертификат

  • положить его на хосте в папки в traefic/cert и /etc/docker/certs.d/192.168.100.100:5000/ под именем ca.crt, причём именно с номером порта, всё серьёзно

  • направить Portainer по новому адресу

Пересоздавать Registry приходится потому, что Portainer мило мапит в него случайный порт хоста. А Docker услужливо открывает этот порт на файрволе для всего интернета. Можете проверить http://traefik.example.com:<random port>/v2/_catalog - где вместо <random port> то, что присвоено хосту в 0.0.0.0:<random port> у вашего Registry.

Поскольку отредактировать контейнер возможности нет, придётся начать с удаления. В Portainer в списке контейнеров найдите свой Registry, отметьте чекбокс для него и нажмите Remove. Затем снова App Templates -> Registry, впишите имя registry, потому что оно позже будет использоваться, нажмите Show advanced options, в открывшемся удалите порт маппинг и нажмите Deploy the container. Половина дела сделана.

Раз порты не нужны, Portainer обиделся и не стал создавать сеть для Registry, а присоединил его к общей сети bridge. У неё рандомное адресное пространство и здесь можно встретить любой контейнер. Это некомфортно, поэтому предлагаю добавить registry в сеть traefik. Можно сделать это в Portainer, открыв контейнер Registry и пролистав вниз до сетей. А можно с помощью Docker:

sudo docker network connect traefik registry

Вы заметили, что в файле traefik/docker-compose.yml есть возможность прибить гвоздями ipv4_address и subnet? Это не случайно. В сертификате магия провайдера Docker не работает, нужно записать IP адрес раз и навсегда. Заодно это поможет соседу Registry всегда получать 192.168.100.2 после перезагрузки. Раскомментируйте ipv4_address и всё в секции networks от ipam до gateway.

Энтрипоинт не может быть добавлен динамически. Поэтому отредактируйте также файл статической конфигурации traefik/traefik.yml, раскомментировав энтрипоинт registry и соответствующий адрес (0.0.0.0:5000).

Остался роутер, сервис и сертификат. Как всё сложно.

nano traefic/config/registry.yml

Содержимое файла registry.yml

http:
  routers:
    registry-http-router:
      rule: Host(`192.168.100.100`)
      entrypoints:
        - registry
      service: registry-http-service
      tls:
        domains:
          - main: "IP:192.168.100.100"
            sans: "IP:192.168.100.100"
#        certResolver: do-not-use-it
  services:
    registry-http-service:
      loadBalancer:
        servers:
          - url: http://192.168.100.2:5000
tls:
  certificates:
    - certFile: /etc/traefik/cert/192.168.100.100-cer.pem
      keyFile: /etc/traefik/cert/192.168.100.100-key.pem

Прошу обратить внимание на url сервиса. И http, и конкретные адрес и порт.

Дальше генерация сертификата. Скопируйте пример из документации Docker, подправьте имена файлов, срок валидности и вуаля. Можно даже создать сразу в traefic/cert, всё равно нигде кроме Portainer он не нужен.

cd traefic/cert
openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 \
  -nodes -keyout 192.168.100.100-key.pem -out 192.168.100.100-cer.pem -subj "/CN=192.168.100.100" \
  -addext "subjectAltName=IP:192.168.100.100"

Проверьте, есть ли в получившемся сертификате CA:TRUE?

openssl x509 -in 192.168.100.100-cer.pem -noout -text

Да куда денется, конечно есть. Осталось перезапустить Traefik, чтобы он открыл новый энтрипоинт и переехал на 192.168.100.100. Так-то роутер, сервис и сертификат уже подтянулись в настройки. И следом перезапустите Registry. Скорее всего у него пока ещё 192.168.100.3 после первого запуска. Можно сделать это через Portainer. На всякий случай если что-то пойдёт не так:

sudo docker compose -f ~/traefik/docker-compose.yml -p traefik up -d

В Portainer перепишите URL своего локального custom registry на 192.168.100.100:5000. Напомню, это в Settings / Registries. Снова найдите и откройте ранее созданный образ в Images, переключите Registry на локальный, добавьте тег и попытайтесь запушить под этим новым тегом. Что-то вроде 192.168.100.100:5000/reg:latest. Теперь пишет "certificate signed by unknown authority". То есть кто-то нашёл сертификат для IP 192.168.100.100 и подставил его в TLS сессию. Кто бы это мог быть?

Осталось заставить Portainer уважать наш знаменитый в узких кругах CA. Давайте сделаем это через Docker.

sudo mkdir /etc/docker/certs.d/192.168.100.100:5000
sudo ln -s $PWD/192.168.100.100-cer.pem /etc/docker/certs.d/192.168.100.100:5000/ca.crt

Уже работает. Можете проверить, запушив наконец образ. Вот так Traefik оказался HTTPS представителем HTTP сервера.

На этом всё. Когда закончите и отполируете настройку своего Traefik, не забудьте log level поменять на WARNING или INFO, энтрипоинты на :80 и :443 или может какие другие, маппинг логов на хосте подальше в /var/log/traefik, подпишитесь на мой телеграм канал с курсами успешного успеха. Его пока нет, но это, надеюсь, не должно помешать...

– Постой, а как же сертификаты Letsencrypt?
– А уже всё настроено. Почти. Актуальный email впишите резолверу myresolver в файле traefik.yml и будет полностью настроено. Те роутеры с tls=true, которые будут использовать этот резолвер, должны указать в tls.domains.main, какое доменное имя их интересует. При желании и все альтернативные имена в tls.domains.sans. И тогда для всех страждущих, обратившихся к myresolver, будут запрошены сертификаты. В пределах лимита запросов к acme-v02.api.letsencrypt.org. Пример label конфигурации в файле docker-compose.yml ниже коммента # SANs, пример динамической конфигурации в файле registry.yml как единственный коммент. Официальная документация по сертификатам: https://doc.traefik.io/traefik/https/acme/

Комментарии (7)


  1. mayorovp
    31.08.2023 09:25
    +1

    Вы будете смеяться, но то, что было описано в labels, можно повторить в формате TOML или YAML. Я предпочитаю YAML и ниже буду придерживаться его. Фломастеры у всех разные.
    Файл traefik.yml […]

    А ведь так хорошо начиналось-то...


    Нет, то что было описано в labels, повторить в traefik.yml невозможно. Потому что traefik.yml — это статическая конфигурация, а labels — динамическая, и с точки зрения traefik это принципиально разные вещи.


    Описаны такие статические элементы как логи, энтрипоинты, провайдеры, резолвер. Обратите внимание – статические, значит те, которые не хочется менять за всё время работы контейнера Traefik. Так-то можно и пару роутеров сюда засунуть.

    Нет, нельзя туда засунуть роутеры.


    Раз выяснилось, что Traefik есть дело до содержимого /etc/traefik/config, давайте положим туда конфиг, например, с информацией о сертификатах.

    А это вообще работает?


    Для примера объявлено несколько сетей, что иногда бывает в разных compose сборках. И указано конкретное значение метки traefik.docker.network, чтобы наш прокси ничего не перепутал и не соединил случайным образом.

    А в динамической конфигурации это вообще работает? Это ж настройка провайдера...


    1. anzay911 Автор
      31.08.2023 09:25

      Нельзя? Жаль. Просто видел там роутеры у других людей. Спасибо, сейчас поправлю.


      1. mayorovp
        31.08.2023 09:25

        Роутеры там вы могли видеть либо в traefik v1 (тут точно не знаю), либо в файловой динамической конфигурации, за которую отвечает настроенный вами file provider. Однако файловая динамическая конфигурация и статическая конфигурация — совершенно не одно и то же, и в traefik v2 они не могут заменять друг друга.


  1. mayorovp
    31.08.2023 09:25
    +5

    Так, с критикой автора закончил, теперь расскажу как делать правильно.


    Самое первое — направление подключения. Добавлять все сети в docker-compose.yaml траефика — дело нудное и сложное в автоматизации, поступать следует строго наоборот.


    Нужно создать сеть traefik и заводить в неё все контейнеры которым нужен доступ наружу:


    services:
      traefik:
        image: traefik
        restart: unless-stopped
        ports:
          - "80:80"
          - "443:443"
        volumes:
          - /etc/traefik:/etc/traefik
    
    networks:
      default:
        name: traefik

    Почему внутреннее имя сети default? Потому что от неё вы не избавитесь, она всё равно будет создана, а пространство адресов следует экономить.


    Ну и в статическом конфиге неплохо бы указать эту самую сеть, чтобы traefik знал какой адрес у контейнера использовать:


    [providers.docker]
    watch = true
    exposedbydefault = false
    network = "traefik"

    Кстати, рекомендую по возможности использовать конфигурацию формата toml, а не yaml. Она гораздо компактнее.


    Теперь в эту настроенную сеть traefik можно добавлять контейнеры. Вот пример такого добавления:


    services:
      api:
        image: ...
        restart: unless-stopped
        expose:
          - 80
        networks:
          default:
            aliases:
              - api_internal
          traefik:
        labels:
          - traefik.enable=true
          - traefik.http.routers.api-$COMPOSE_PROJECT_NAME.rule=Host(`api.example.org`)
          - traefik.http.services.api-$COMPOSE_PROJECT_NAME.loadbalancer.server.port=80
    
    networks:
      traefik:
        name: traefik
        external: true

    На что тут следует обязательно обратить внимание — это на то, что хотя динамическая конфигурация пишется индивидуально для контейнеров, пространства имён всех роутеров, сервисов и мидлварей глобальны. И если забыть использовать для них уникальный суффикс (например $COMPOSE_PROJECT_NAME) — можно "напороться" на удивительные плавающие баги из-за конфликтов настроек.


    Также обратите внимание, что два сервиса находящиеся в сети traefik могут "решить" связаться по этой сети если попытаются использовать для этого имена сервисов (а эти имена могут оказаться неуникальны для разных проектов docker compose). Именно потому я назначил сервису псевдоним api_internal в сети default — чтобы другие сервисы могли ссылаться на него как api_internal. Тут требуется некоторое соглашение в команде, чтобы в соседнем проекте не оказалось сервиса api_internal. В идеале бы вообще выключить разрешение имён докера в сети traefik, но кажется докер так не умеет.


    1. Bone
      31.08.2023 09:25

      А можете поделиться рецептом, как настроить доступ к MySQL через traefik (если такое вообще возможно)? Допустим у меня есть два сайта, у них отдельные docker-compose.yml в которых поднято всё нужное, https работает через traefik, но вот к MySQL приходится обращаться напрямую, по ip и открывая порт наружу.


      1. mayorovp
        31.08.2023 09:25

        Делаете контейнер с ssh сервером, даёте ему порт наружу, и точно так же выделяете ему сетку. Тот, кому нужен доступ к MySQL, может установить туннель до этого контейнера, а точку назначения указать через доменное имя. Поскольку доменное имя точки назначения резолвится сервером, а сервер в докере — автоматически появляется возможность указывать имена контейнеров.


        Иными словами, работает это как-то так. Служебный контейнер:


        services:
          ssh-gateway:
            image: …
            restart: unless-stopped
            ports:
              - "2222:22"
            volumes:
              - ssh-gateway:/etc/ssh
        
        networks:
          default:
            name: ssh-gateway
        
        volumes:
          ssh-gateway:

        И сервис которому нужен доступ через туннель:


        services:
          db:
            image: postgres
            restart: unless-stopped
            expose:
              - 5432
            networks:
              default:
                aliases:
                  - db-internal
              ssh-gateway:
                aliases:
                  - db.dev.my-project.example.org
        
        networks:
          ssh-gateway:
            name: ssh-gateway
            external: true

        Теперь, когда нужен доступ к контейнеру, достаточно поднять туннель и можно подключаться к локальному концу туннеля:


        ssh -N -L 5432:db.dev.my-project.example.org:5432 -p 2222 example.org


      1. mayorovp
        31.08.2023 09:25

        Так, вариант с ssh — универсален, однако я сейчас понял что конкретно к MySQL можно попробовать и через traefik подключиться.


        План действий тут будет такой. Во-первых, для контейнера нужно настроить TCP роутер, а не HTTP, указав правило на HostSNI:


            labels:
              - traefik.enable=true
              - traefik.tcp.routers.mysql-$COMPOSE_PROJECT_NAME.rule=HostSNI(`mysql.example.org`)
              - traefik.tcp.services.mysql-$COMPOSE_PROJECT_NAME.loadbalancer.server.port=3306

        Во-вторых, надо уговорить клиента использовать TLS соединение с сервером.


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