Доменные имена с валидным SSL для локальных Docker-контейнеров

image

Ранее (11 февраля 2019) пост уже публиковался мной с таким заголовком, но был отправлен в небытие по причине смерти домена, а как следствие и сервиса с ним связанного. Причин тогда было две — статистика скачивания докер-образа оставляла желать лучшего, и цена за продление домена (что был зарегистрирован в nic) стала для меня неожиданно выше той, которую я был морально готов заплатить за него.

Но есть время не продлять домены разбрасывать камни, а есть время извиниться за сделанную ошибку. Все, кому сервис был полезен, кто им пользовался и однажды заметил что он (localhost.tools) не резольвится — приношу свои извинения, и исправляю ситуацию (лучше поздно чем никогда, верно?).

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

Итак, докер. В личном списке технологий, что изменили индустрию в лучшую сторону — именно его бы поставил, пожалуй, на первое место. Сборка и запуск сложных сервисов (с чертовой дюжиной зависимостей в виде баз данных, коллекторов и прочего) используя его сводится к одной/двум командам, и это ли не прекрасно? Про деплой я вообще помалкиваю — красота, да и только (естественно что есть нюансы, но сейчас не о них).

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

А потом ещё тебе хочется проверить работу используя https — и приходится либо использовать свой корневой сертификат (mkcert), либо всегда использовать curl --insecure ..., а когда над приложениями работают различные команды — количество запар начинает возрастать в прогрессии, и порою геометрической.

Столкнувшись с такой проблемой в очередной раз — в голове промелькнула мысль «хватит это терпеть!», и результатом работы на паре выходных стал сервис, который решает это неудобство, о чем будет рассказано ниже.


Мир Нас спасёт реверс-прокси

По-хорошему, нам нужна какая-то доменная зона, все под-домены из которой всегда будут резольвить локалхост (127.0.0.1). Беглые поиски навели на домены вида *.localho.st, *.lvh.me и другие, но как к ним прикрутить валидный SSL сертификат? Повозившись со своим корневым сертификатом удалось завести curl без ошибок, но не все браузеры корректно принимали его, и продолжали вываливать ошибку. Кроме того — крайне не хотелось всей этой «возни» с SSL.

«Что ж, зайдём с другой стороны!» — мной был приобретен домен с именем indocker.app, делегирован на CloudFlare, настроен требуемый резольвинг (все под-домены резольвят 127.0.0.1):

$ dig +noall +answer -t A foo.indocker.app # IPv4
foo.indocker.app.   7131    IN  A   127.0.0.1

$ dig +noall +answer -t AAAA foo.indocker.app # IPv6
foo.indocker.app.   86400   IN  AAAA    ::1

$ dig +noall +answer foo.bar.baz.indocker.app # any depth
foo.bar.baz.indocker.app. 86400 IN  A   127.0.0.1

После этого был бережно взял certbot, который используя DNS верификацию и API ключ от доменной зоны выдал мне TLS сертификат на зону *.indocker.app (если что, в конце поста есть ссылка на репозиторий с исходниками, и там подробно описано как именно это делается).

Теперь у нас есть и валидный TLS сертификат (пускай и на 3 месяца, и только для под-доменов одного уровня). Остается как-то научиться проксировать все запросы, что приходят на локалхост в нужный контейнер.

И тут на сцену врывается Traefik (спойлер — он прекрасен). Запустив его локально и пробросив в его контейнер докер-сокет — он умеет проксировать запросы в тот контейнер, у которого есть необходимый docker label. Таким образом, нам нет необходимости в какой-либо дополнительной конфигурации, кроме как при запуске указать нужный label у контейнера, к которому мы хотим получить доступ по доменному имени!

Таким образом у нас появляется артефакт в виде докер-образа с пред-настроенным Traefik-ом и wildcard SSL сертификатом (да, он публичный).


Приватный ключ от SSL в публичном образе?

Да, но я считаю что это не страшно, так как он на доменную зону, которая всегда резольвит локалхост. MitM в этом случае не имеет особого смысла в принципе.


Что делать, когда сертификат протухнет?

Просто стянуть свежий образ, перезапустив контейнер. У проекта настроен CI, который в автоматическом режиме, раз в месяц (на данный момент) обновляет сертификат и публикует свежий образ.


Хочу попробовать!

Нет ничего проще. Первым делом, убедись что локальные порты 80 и 443 у тебя свободны, и выполни:

# Создаём docker-сеть для нашего реверс-прокси
$ docker network create indocker-app-network
$ docker run -d --rm \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -p 80:80 -p 443:443 \
  --network indocker-app-network \
  --name indocker.app \
  quay.io/indocker/app:1 # 1 тут - это мажорное значение версии

И теперь проверки ради создадим простой docker-compose файл, и запустим сервисы в нём описанные:

version: '3.8'

services:
  my-nginx:
    image: nginx:latest
    labels:
      - traefik.enable=true
      # Router docs: https://doc.traefik.io/traefik/routing/providers/docker/#routers
      - traefik.http.routers.my-nginx-router.rule=Host(`my-nginx.indocker.app`)
      - traefik.http.routers.my-nginx-router.service=my-nginx-service
      # Service docs: https://doc.traefik.io/traefik/routing/providers/docker/#services
      - traefik.http.services.my-nginx-service.loadbalancer.server.port=80
      - traefik.http.services.my-nginx-service.loadbalancer.healthcheck.path=/
      - traefik.http.services.my-nginx-service.loadbalancer.healthcheck.interval=5s
    networks: [indocker-app-network]
    security_opt: \[no-new-privileges:true]

  whoami:
    image: containous/whoami:latest
    labels:
      - traefik.enable=true
      - traefik.http.routers.whoami-router.rule=Host(`whoami.indocker.app`)
      - traefik.http.routers.whoami-router.entrypoints=http # force HTTP instead of HTTPS
      - traefik.http.routers.whoami-router.service=whoami-service
      - traefik.http.services.whoami-service.loadbalancer.server.port=8080
    command: --port 8080
    networks: [indocker-app-network]
    security_opt: [no-new-privileges:true]

networks:
  indocker-app-network:
    external: true
$ docker-compose up -d

И теперь можем потестировать:

$ curl -sS https://my-nginx.indocker.app | grep Welcome
Welcome to nginx!

Welcome to nginx!

$ curl -sS http://whoami.indocker.app Hostname: 849834ab3299 IP: 127.0.0.1 IP: 172.20.0.3 RemoteAddr: 172.20.0.2:33734 GET / HTTP/1.1 Host: whoami.indocker.app User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0) Accept: */* Accept-Encoding: gzip Referer: X-Forwarded-For: 172.20.0.1 X-Forwarded-Host: whoami.indocker.app X-Forwarded-Port: 80 X-Forwarded-Proto: http X-Forwarded-Server: edc8405eccc6 X-Real-Ip: 172.20.0.1

Как видим — работает :)


Так как домены в зоне .app находятся в HSTS preload list — при попытке открытия браузером любого поддомена используя http — запрос будет автоматически перенаправлен на схему https. Касается это только браузеров.

Кроме того, можем стукнуться браузером на адрес monitor.indocker.app, и увидеть дашборд локально запущенного Traefik:

hcw86xl3ovumy4w_bajqj6vuy98.png


Где живёт документация, описание?

Всё, как не сложно догадаться, живёт по адресу indocker.app. Более того, морда — отзывчивая, и умеет смотреть запущен ли у тебя локально демон. Все исходники живут в этом репозитории — если у кого-то появится желание сделать нечто похожее, но со своими особенностями — дерзайте!


Сколько стоит?

Нисколько. Совсем. Сделав данную штуку для себя и своей команды пришло понимание того, что она может пригодиться и другим разработчикам. Более того — денег стоит только доменное имя, всё остальное — юзается без необходимости в оплате.


А в чем отличие от localhost.tools, который был сдохнут?

Во-первых — это домен. Он более привлекателен (как мне кажется), и приобретен не у российского регистратора (что важно). На момент написания этих строк он оплачен до 2027 года, и в ближайшее время докину ещё на пяток лет вперёд.

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

И в третьих — это автоматизация. На его обновление и поддержку врядли потребуется много ресурсов, так как всякие там renovate да dependabot (в связке CI) почти всё делают за меня.

Кстати, о планах — есть желание сделать морду более информативной, и выводить прямо там свою борду (если демон запущен) с описанием запущенных контейнеров, статистикой, конфигами и прочим. Что-то вроде dockstation, только на минималках.

Монетизировать его или ограничивать функционал для не-pro пользователей смысла не вижу и не планирую от слова совсем (docker hub — привет).

© Habrahabr.ru