Переходим на HTTPS за 15 минут на примере TeamCity

—

от автора

Всем привет! Меня зовут Александр. Я являюсь техническим руководителем отдела интеграции в компании Visiology. Хочу поделиться с вами нашим велосипедом, как мы перевели наш CI/CD сервер на работу по HTTPS.

Исходные данные

Был сервер TeamCity, который уже был развёрнут в Docker контейнере и работал по HTTP. А хотелось заставить его работать по HTTPS без сложных настроек и без регистрации и смс бесплатно + получить универсальное решение для любого веб приложения. «Сложные» настройки TeamCity можно найти тут, а роадмап, в котором всё это обещают из коробки, тут. Времени ждать у нас нет, а задачу надо делать, так что поехали.

Кому лень всё читать, то можно заглянуть в репозиторий, все шаги я разбил по коммитам, так же там удобно смотреть изменения между шагами.

Простое веб приложение

Для начала потренируемся на кошках простом Python приложении.

main.py
import http.server import socketserver  PORT = 8000   class SimpleHandler(http.server.SimpleHTTPRequestHandler):     def do_GET(self) -> None:         self.send_response(200, 'Simple-Server-Response')         self.send_header('Content-type', 'text/plain')         self.end_headers()         self.wfile.write(b'Hello!\n')   with socketserver.TCPServer(("", PORT), SimpleHandler) as httpd:     print("serving at port", PORT)     httpd.serve_forever() 

Простое приложение, которое поднимается на 8000 порту и отвечает «Hello!». Проверим.

test@test:~/simple-server$ curl http://127.0.0.1:8000 Hello! test@test:~/simple-server$

Схематично это будет выглядеть вот так

Запакуем в Docker

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

Dockerfile
FROM python:3.9-alpine  EXPOSE 8000  COPY main.py /main.py  ENTRYPOINT ["python"]  CMD ["main.py"]

Супер! Билдим, запускаем, проверяем.

test@test:~/simple-server$ docker build -t simple-server . ... Successfully built 43d18cb628fc Successfully tagged simple-server:latest  test@test:~/simple-server$ docker run -dt --name simple-server -p 8000:8000 simple-server 7f2d3e770b3ed0f0bfabbd1849486d8c2be99c7eab05ff3c452091e9d4417aec  test@test:~/simple-server$ curl http://127.0.0.1:8000 Hello! test@test:~/simple-server$

Теперь, если у нас есть «белый» айпишник и привязанное DNS имя, то можно постучаться к этому серверу извне. Примерно так и работал наш сервер TeamCity, но было страшно за передачу паролей по HTTP, поэтому решено было перейти на HTTPS.

Мы выбрали решение на основе бесплатных сертификатов от Let’s Encrypt. Если выкинуть всю воду, то можно свести к трём шагам:

  1. Получаем сертификат

  2. Подкладываем его nginx серверу

  3. Запускаем контейнеры приложения, nginx, certbot (для обновления сертификата)

За основу я взял эту статью, но как всегда не обошлось без напильника. Потом я нашёл «всё в одном месте», там всё очень хорошо расписано.

Добавим nginx

Для начала запустим наш сервер в связке с nginx.

Создадим новую папку, например, server_data, внутри неё сделаем ещё папку data, а в ней файл nginx.conf

nginx.conf
server {   resolver     127.0.0.11;   listen       80; # public server port    set $simple_server_url http://simple-server:8000;    location / {     proxy_pass $simple_server_url;   } }

127.0.0.11 — DNS сервер в докер сети по умолчанию

слушаем на 80 порту

устанавливем переменную simple_server_url, а не сразу пишем в секции location, потому что перед стартом nginx все proxy_pass должны быть доступны, а если контейнер с приложением к этому моенту не стартанёт, то nginx не запустится

simple-server — здесь DNS имя контейнера с нашим python приложением

В папке server_data создадим файл docker-compose.yml

docker-compose.yml
version: '3.8'  services:   simple-server:     container_name: simple-server     image: simple-server     restart: unless-stopped     networks:       nginx_net:    nginx:     container_name: nginx     image: nginx:1.21.1     restart: unless-stopped     volumes:       - ./data/nginx.conf:/etc/nginx/conf.d/default.conf     ports:       - 80:80     networks:       nginx_net:  networks:   nginx_net:

Запускаем docker-compose up -d из папки server_data. Проверяем уже на 80 порту, потому что наше приложение теперь находится за nginx.

test@test:~/server_data$ docker-compose up -d Creating nginx         ... done Creating simple-server ... done test@test:~/server_data$ curl http://127.0.0.1 Hello! test@test:~/server_data$

Отлично! Теперь у нас есть приложение, которое работает за nginx по HTTP. Самое время перевести нашу конфигурацию на HTTPS. Для дальнейшего продолжения убедитесь, что в файле nginx.conf значение переменной $server_url — верное DNS имя именно этой машины, без этого получить SSL сертификат не получится.

Получаем сертификат

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

Для данного скрипта важны названия сервисов из docker-compose.yml.

init-letsencrypt.sh
#!/bin/bash  if ! [ -x "$(command -v docker-compose)" ]; then   echo 'Error: docker-compose is not installed.' >&2   exit 1 fi  domains=(example.org www.example.org) rsa_key_size=4096 data_path="./data/certbot" email="" # Adding a valid address is strongly recommended staging=1 # Set to 1 if you're testing your setup to avoid hitting request limits  if [ -d "$data_path" ]; then   read -p "Existing data found for $domains. Continue and replace existing certificate? (y/N) " decision   if [ "$decision" != "Y" ] && [ "$decision" != "y" ]; then     exit   fi fi   if [ ! -e "$data_path/conf/options-ssl-nginx.conf" ] || [ ! -e "$data_path/conf/ssl-dhparams.pem" ]; then   echo "### Downloading recommended TLS parameters ..."   mkdir -p "$data_path/conf"   curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > "$data_path/conf/options-ssl-nginx.conf"   curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem > "$data_path/conf/ssl-dhparams.pem"   echo fi  echo "### Creating dummy certificate for $domains ..." path="/etc/letsencrypt/live/$domains" mkdir -p "$data_path/conf/live/$domains" docker-compose run --rm --entrypoint "\   openssl req -x509 -nodes -newkey rsa:$rsa_key_size -days 1\     -keyout '$path/privkey.pem' \     -out '$path/fullchain.pem' \     -subj '/CN=localhost'" certbot echo   echo "### Starting nginx ..." docker-compose up --force-recreate -d nginx echo  echo "### Deleting dummy certificate for $domains ..." docker-compose run --rm --entrypoint "\   rm -Rf /etc/letsencrypt/live/$domains && \   rm -Rf /etc/letsencrypt/archive/$domains && \   rm -Rf /etc/letsencrypt/renewal/$domains.conf" certbot echo   echo "### Requesting Let's Encrypt certificate for $domains ..." #Join $domains to -d args domain_args="" for domain in "${domains[@]}"; do   domain_args="$domain_args -d $domain" done  # Select appropriate email arg case "$email" in   "") email_arg="--register-unsafely-without-email" ;;   *) email_arg="--email $email" ;; esac  # Enable staging mode if needed if [ $staging != "0" ]; then staging_arg="--staging"; fi  docker-compose run --rm --entrypoint "\   certbot certonly --webroot -w /var/www/certbot \     $staging_arg \     $email_arg \     $domain_args \     --rsa-key-size $rsa_key_size \     --agree-tos \     --force-renewal" certbot echo  echo "### Reloading nginx ..." docker-compose exec nginx nginx -s reload

В скрипте init-letsencrypt.shменяем значения переменных в 8, 11 и 12 строчке. Значение параметра staging лучше оставить 1, пока скрипт не отработает без ошибок. Если вдруг ваше DNS имя содержит не только латинские буквы, дефис и цифры, то адрес надо писать в формате Punycode.

Ещё нужно будет поменять файлы docker-compose.yml и nginx.conf. В файле docker-compose.yml добавляется сервис certbot, а в сервисе nginx добавляются общие с certbot volumes, открываем 443 порт и добавляем секцию command, которая перезагружает nginx каждые 6 часов, чтобы подгрузить новые сертификаты, если они изменились. Сервис certbot отвечает за взаимодействие с сервером Let’s Encrypt и обновлением SSL сертификата. Приведу файл целиком.

docker-compose.yml
version: '3.8'  services:   simple-server:     container_name: simple-server     image: simple-server     restart: unless-stopped     networks:       nginx_net:    nginx:     container_name: nginx     image: nginx:1.21.1     restart: unless-stopped     volumes:       - ./data/nginx.conf:/etc/nginx/conf.d/default.conf       - ./data/certbot/conf:/etc/letsencrypt       - ./data/certbot/www:/var/www/certbot     ports:       - 80:80       - 443:443     command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"     networks:       nginx_net:    certbot:     container_name: certbot     image: certbot/certbot:v1.17.0     restart: unless-stopped     entrypoint:  "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"     networks:       nginx_net:     volumes:       - ./data/certbot/conf:/etc/letsencrypt       - ./data/certbot/www:/var/www/certbot  networks:   nginx_net: 

Но ещё требуется изменить файл nginx.conf, в него добавится специальная секция location, которая будет перенаправлять запросы к certbot.

nginx.conf
server {   resolver     127.0.0.11;   listen       80; # public server port      location /.well-known/acme-challenge/ { root /var/www/certbot; }    set $simple_server_url http://simple-server:8000;    location / {     proxy_pass $simple_server_url;   } } 

Запускаем скрипт init-letsencrypt.sh с параметром staging=1. Если всё настроено правильно, то будет сообщение с поздравлением.

Тогда можно запускать скрипт с параметром staging=0. Если что-то пошло не так, а у меня такое было пару раз, то советую проверить следующее:

  1. Проверить правильность конфига nginx.

  2. Проверить docker-compose.yml на наличие общих volume у nginx и certbot.

  3. Добавить set -x в начало скрипта для дебага (будут выводиться исполняемые команды).

  4. Для дебага в скрипте init-letsencrypt.sh можно убрать аргумент --rm в 69 строчке, тогда контейнер не удалится и можно будет вытащить из него лог. И добавить аргумент -v перед \ в 73 строчке для более полного логирования.

  5. Если совсем беда, то можно включить debug логирование на nginx.

Не забудьте запустить скрипт с параметром staging=0, иначе сертификат не появится и HTTPS работать не будет.

Окончательная настройка

Итак, осталось совсем чуть-чуть. Нужно настроить nginx для работы по HTTPS и всё. Откроем 443 порт, добавим путь к SSL сертификатам, пути к настройкам letsencrypt и редирект с HTTP на HTTPS. Во всех примерах ниже будет фигурировать <URL>, его нужно менять на ваш, в файле ниже это 2, 10 и 11 строчка. Не забываем про Punycode, если требуется. Пару раз при тесте certbot создавал папки с префиксом www., это ещё одна возможная причина неуспешной конфигурации.

nginx.conf
server {   resolver     127.0.0.11;   listen       80; # public server port   listen       443 ssl;    ssl_certificate /etc/letsencrypt/live/<URL>/fullchain.pem;   ssl_certificate_key /etc/letsencrypt/live/<URL>/privkey.pem;      include /etc/letsencrypt/options-ssl-nginx.conf;   ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;    location /.well-known/acme-challenge/ { root /var/www/certbot; }    if ($server_port = 80) { set $https_redirect 1; }   if ($host ~ '^www\.') { set $https_redirect 1; }   if ($https_redirect = 1) { return 301 https://$server_url$request_uri; }    set $simple_server_url http://simple-server:8000;    location / {     proxy_pass $simple_server_url;   } }

Запускаем наши сервисы

docker-compose up --force-recreate -d

И заходим в браузере на <URL> по HTTP, должен сработать редирект на HTTPS.

Всё получилось, поздравляю! Таким способом можно перевести любое веб приложение на работу по HTTPS, даже для такого, которое не поддерживает HTTPS из коробки.

Перейдём к TeamCity

TeamCity можно долго оптимизировать, в этой статье я возьму «минимальную» конфигурацию, чтобы «просто запустилось». Для TeamCity единственное отличие — это настройка nginx.conf для работы с сокетами и некоторые дополнительные оптимизации. Берём рекомендованный конфиг отсюда и объединяем с нашим БЕЗ строчкии http. Ещё важное отличие, что для работы TeamCity нужна правильно настроенная секция server_name, в примере с Python приложением её можно было не заполнять. Получаем такой:

nginx.conf
# ... default settings here proxy_read_timeout     1200; proxy_connect_timeout  240; client_max_body_size   0;    # maximum size of an HTTP request. 0 allows uploading large artifacts to TeamCity  map $http_upgrade $connection_upgrade { # WebSocket support     default upgrade;     '' ''; }  server {   server_name <URL> www.<URL>; # public server host name    resolver 127.0.0.11;   listen       80; # public server port   listen       443 ssl; # public server port    ssl_certificate /etc/letsencrypt/live/<URL>/fullchain.pem;   ssl_certificate_key /etc/letsencrypt/live/<URL>/privkey.pem;    include /etc/letsencrypt/options-ssl-nginx.conf;   ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;    location /.well-known/acme-challenge/ { root /var/www/certbot; }    if ($server_port = 80) { set $https_redirect 1; }   if ($host ~ '^www\.') { set $https_redirect 1; }   if ($https_redirect = 1) { return 301 https://$server_url$request_uri; }    set $teamcity_server_url http://teamcity-server-instance:8111;    location / { # public context (should be the same as internal context)     proxy_pass $teamcity_server_url; # full internal address     proxy_http_version  1.1;     proxy_set_header    Host $server_name:$server_port;     proxy_set_header    X-Forwarded-Host $http_host;    # necessary for proper absolute redirects and TeamCity CSRF check     proxy_set_header    X-Forwarded-Proto $scheme;     proxy_set_header    X-Forwarded-For $remote_addr;     proxy_set_header    Upgrade $http_upgrade; # WebSocket support     proxy_set_header    Connection $connection_upgrade; # WebSocket support   } }

И файл docker-compose.yml

docker-compose.yml
version: '3.8'  services:   teamcity_server:     container_name: teamcity-server-instance     image: jetbrains/teamcity-server:2021.1     restart: unless-stopped     tty: true     networks:       nginx_net:    nginx:     container_name: nginx     image: nginx:1.21.1     restart: unless-stopped     volumes:       - ./data/nginx.conf:/etc/nginx/conf.d/default.conf       - ./data/certbot/conf:/etc/letsencrypt       - ./data/certbot/www:/var/www/certbot     ports:       - 80:80       - 443:443     command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"     networks:       nginx_net:    certbot:     container_name: certbot     image: certbot/certbot:v1.17.0     restart: unless-stopped     entrypoint:  "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"     networks:       nginx_net:     volumes:       - ./data/certbot/conf:/etc/letsencrypt       - ./data/certbot/www:/var/www/certbot    networks:   nginx_net: 

Запускаем с помощью команды docker-compose up -d, переходим в браузер по URL и настраиваем TeamCity.

Добавляем агента

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

docker run -dt -e SERVER_URL="https://<URL>" \   --name teamcity-agent-instance jetbrains/teamcity-agent:2021.1

Агент должен появиться в веб интерфейсе TeamCity на вкладке с агентами.

Агент в локальной сети

Предположим, что у нас есть 2 сервера в локальной сети. Первый будет отвечать за сервер TeamCity, а второй будет билд агентом, но у него не будет доступа в интернет. Будем считать, что сеть защённая и там можно общаться по HTTP. Тогда запуск агента с таким параметром SERVER_URL="https://<URL>" не подойдёт, а подойдёт SERVER_URL="http://<SERVER_LOCAL_IP>". Придётся ещё поменять конфиг nginx — добавить туда секцию для локальной сети

nginx.conf
# ... default settings here proxy_read_timeout     1200; proxy_connect_timeout  240; client_max_body_size   0;    # maximum size of an HTTP request. 0 allows uploading large artifacts to TeamCity  map $http_upgrade $connection_upgrade { # WebSocket support     default upgrade;     '' ''; }  server {   resolver      127.0.0.11;   listen        80; #  server port   server_name   <SERVER_LOCAL_IP>; # private server host name    set $teamcity_url http://teamcity-server-instance:8111;    location / { # public context (should be the same as internal context)     proxy_pass $teamcity_url; # full internal address     proxy_http_version  1.1;     proxy_set_header    Host $server_name:$server_port;     proxy_set_header    X-Forwarded-Host $http_host;    # necessary for proper absolute redirects and TeamCity CSRF check     proxy_set_header    X-Forwarded-Proto $scheme;     proxy_set_header    X-Forwarded-For $remote_addr;     proxy_set_header    Upgrade $http_upgrade; # WebSocket support     proxy_set_header    Connection $connection_upgrade; # WebSocket support   } }  server {   resolver      127.0.0.11;   listen        80; # public server port   listen        443 ssl;   server_name   <URL>; # public server host name    ssl_certificate /etc/letsencrypt/live/<URL>/fullchain.pem;   ssl_certificate_key /etc/letsencrypt/live/<URL>/privkey.pem;    include /etc/letsencrypt/options-ssl-nginx.conf;   ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;    location /.well-known/acme-challenge/ { root /var/www/certbot; }    if ($server_port = 80) { set $https_redirect 1; }   if ($host ~ '^www\.') { set $https_redirect 1; }   if ($https_redirect = 1) { return 301 https://<URL>$request_uri; }    set $teamcity_server_url http://teamcity-server-instance:8111;    location / {     proxy_pass $teamcity_server_url;     proxy_http_version  1.1;     proxy_set_header    Host $server_name:$server_port;     proxy_set_header    X-Forwarded-Host $http_host;    # necessary for proper absolute redirects and TeamCity CSRF check     proxy_set_header    X-Forwarded-Proto $scheme;     proxy_set_header    X-Forwarded-For $remote_addr;     proxy_set_header    Upgrade $http_upgrade; # WebSocket support     proxy_set_header    Connection $connection_upgrade; # WebSocket support   } }

На вкладке в с агентами не должно быть изменений, но теперь общение между сервером и агентом будет идти в локальной сети.

Ещё улучшений?

А ещё можно развернуть сервер и билд агент в Docker Swarm или Kubernetes, но это уже совсем другая история «лучшее — враг хорошего», и мы решили остановиться. Полученная конфигурация позволила решить все наши задачи в полном объёме и не требует трудоёмкой поддержки. Добавление агентов происходит одной командой, что очень удобно.

Ещё раз приведу ссылку на репозиторий с шагами. Спасибо за внимание!

UPD

Добавил статью о Проблемы при сборке Docker образов внутри Docker контейнеров на TeamCity

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

Про что ещё рассказать?

41.67% Использование шаблонов в TeamCity для сборок Docker образов5
0% Интеграция TeamCity с Yandex Container Registry0
25% Настройка Build chain в TeamCity для сборок с разными VCS3
33.33% Проблемы при сборке Docker образов внутри Docker контейнеров4

Проголосовали 12 пользователей. Воздержались 7 пользователей.

ссылка на оригинал статьи https://habr.com/ru/articles/571324/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *