Многие компании используют сертификаты, подписанные внутренними удостоверяющими центрами (Certificate Authority) для ресурсов в приватных сетях. Поскольку такие сертификаты по умолчанию не могут быть доверенными, почти на каждом этапе вокруг пайплайна могут возникать ошибки такого рода: x509 certificate signed by unknown authority
. Из-за этого до каждого компонента необходимо доставлять корневые сертификаты, используемые в компании. В статье расскажу, как это можно сделать.
Статья подготовлена на основе моего материала в курсе «CI/CD на примере Gitlab CI».
Обратите внимание: настройка инфраструктуры выходит за рамки этой статьи, поэтому в примерах, где не идёт речи о динамическом получении сертификата, я ограничился предположением, что на серверах компании внутренний корневой сертификат всегда расположен по пути /usr/local/share/ca-certificates/RootCA.crt
.
Поскольку курс в основе этого материала строится вокруг Gitlab-CI, то и нижеследующие разделы рассматривают доставку сертификатов до тех или иных этапов или компонентов, входящих в пайплайн, построенный в гитлабе с раннерами, запущенными внутри корпоративной инфраструктуры. Поехали!
gitlab-runner
Передача корневого сертификата в управляющий компонент gitlab-runner
— наиболее простая задача. gitlab-runner
открывает HTTPS-соединения только с самим сервером GitLab и, возможно, с кластером Kubernetes. Однако последний явным образом требует указания своего корневого сертификата. Для верификации подлинности сервера GitLab используется сертификат, указанный в config.toml
:
[[runners]]
executor = "kubernetes"
name = "Kubernetes Runner"
tls-ca-file = "/usr/local/share/ca-certificates/RootCA.crt" # <===
Если gitlab-runner
запущен непосредственно на хосте, этого достаточно. Если он запускается в докер-контейнере, в контейнер нужно смонтировать сертификат с хоста:
docker run ...
-v /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro ...
gitlab/gitlab-runner
Наконец, если он запускается в Kubernetes, один из вариантов — монтировать директорию с хоста
В спецификации контейнера:
volumeMounts:
- mountPath: /usr/local/share/ca-certificates
name: certs
В спецификации пода:
volumes:
- hostPath:
path: /usr/local/share/ca-certificates
name: certs
Если нет прав на монтирование hostPath
, сертификат придётся положить в Kubernetes как секрет и монтировать его оттуда, поменяв содержимое ключа volumes
. Например, если секрет создан командой
kubectl create secret generic -n runner-namespace certs --from-file=/usr/local/share/ca-certificates
ключ volumes
будет выглядеть так:
volumes:
- secret:
secretName: certs
name: certs
gitlab-runner
helper image
gitlab-runner-helper
— вспомогательная утилита, которая используется в пайплайнах для клонирования репозитория и работы с кэшем и артефактами. Обычно проблемы с сертификатами возникают тогда, когда используется собственное S3-хранилище (например, на базе ceph или minio). Как правило, происходит обращение к S3-хранилищу по HTTPS, и сервер S3 представляется корпоративным сертификатом. Единственный способ доставить туда сертификаты — пересобрать образ.
Helper image используется в тандеме с docker
или kubernetes
экзекьюторами. Чтобы настроить использование кастомного образа, укажите следующие строки в config.toml
:
[[runners]]
(...)
executor = "docker" # (или kubernetes)
[runners.docker] # (или kubernetes)
(...)
helper_image = "my.registry.local/gitlab/gitlab-runner-helper:tag"
Пайплайны
Действия выше выполняются относительно редко, однако если используется Docker или Kubernetes экзекьютор, то каждый джоб будет выполняться в свежесозданном контейнере, в котором снова нет нужных сертификатов. Поэтому необходим способ динамически доставлять сертификаты в контейнеры. Есть несколько таких способов:
- Монтировать с хостов.
- Получать из источника, их создающего.
- Создать сервер, предоставляющий сертификаты.
- Использовать образы со вшитыми сертификатами.
Монтирование с хостов
Если прописать в config.toml
[[runners]]
executor = "kubernetes"
(...)
[runners.kubernetes]
(...)
[runners.kubernetes.volumes]
[[runners.kubernetes.volumes.host_path]]
host_path = "/usr/local/share/ca-certificates"
mount_path = "/usr/local/share/ca-certificates"
name = "certs-volume"
read_only = true
то сертификаты будут монтироваться в основные и сервисные контейнеры запускаемых джобов. В случае с докер-экзекьютором аналогичный результат можно получить так:
[runners.docker]
(...)
volumes = ["/usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro"]
Минус подхода в том, что ответственность за актуальность сертификатов сдвигается на конфигурацию инфраструктуры, поэтому подход удобен, если вы этой инфраструктурой управляете. Если же вы, например, работаете в отдельной команде, которая в многопользовательском кластере Kubernetes сама подняла свой раннер, но не имеет админского доступа туда, вы не можете быть уверены в актуальности сертификатов на хосте, да и можете вовсе не иметь прав на монтирование hostPath
. Тогда можно аналогично примеру для gitlab-runner
создать в кубе секрет, содержащий ключи (имена файлов сертификатов) и значения (их содержимое):
kubectl create secret generic -n runner-namespace certs --from-file=/usr/local/share/ca-certificates
а config.toml
исправить следующим образом:
[[runners]]
executor = "kubernetes"
(...)
[runners.kubernetes]
(...)
[runners.kubernetes.volumes]
[[runners.kubernetes.volumes.secret]]
name = "certs"
mount_path = "/usr/local/share/ca-certificates"
read_only = true
Если используется докер-экзекьютор на недоступном вам build-сервере, из этого тут же следует, что у вас нет доступа к конфигурации раннера, если только не используется экзотичный вариант подключения к сокету докер-демона по tcp (подобный доступ почти всегда означает, что и доступ к хосту получить тривиально). Лучше всего договориться с администраторами build-сервера о гарантированном наличии свежих сертификатов в определённой директории, которая будет монтироваться в контейнеры пайплайна.
Один из плюсов такого подхода в том, что можно делать следующее:
build:
stage: build
image: docker:stable
services:
- name: docker:18.09.6-dind
entrypoint:
- /bin/sh
command:
- -c
- update-ca-certificates && dockerd-entrypoint.sh
Ключевой шаг, естественно, update-ca-certificates
.
Поскольку для сервисных контейнеров нельзя прописать последовательность команд аналогично script
, а писать длинные скрипты для установки сертификатов под ключом command
не очень удобно, меньшим из зол является подобное монтирование сертификатов и сравнительно короткая последовательность команд.
Отступление про сборку docker-образов
Если вы в пайплайнах собираете докер-образы, то, независимо от того, используется docker-in-docker, kaniko или другой сборщик в пайплайне, в контексте сборки сертификаты снова отсутствуют, если их не вшили в базовый образ. Предположим, что каким-либо образом в контейнер пайплайна сертификаты были доставлены (либо примонтированы с хоста, либо получены из другого источника командами в before_script
). Тогда конфигурация пайплайнов должна будет выглядеть так:
build:
stage: build
# допустим, сборка докером
image: docker:stable
services:
- name: docker:18.09.6-dind
entrypoint:
- /bin/sh
command:
- -c
- update-ca-certificates && dockerd-entrypoint.sh
# before_script:
# - script_to_get_certs.sh
script:
- mkdir -p certs
- cp /usr/local/share/ca-certificates/* certs/
- docker build (...) # или канико или что угодно ещё
- (...)
а в докер-файлах придётся всегда указывать что-нибудь вроде
FROM base-image
COPY certs/* /usr/local/share/ca-certificates/
RUN update-ca-certificates
RUN wget https://artifacts.company.ru/some/artifact # команда, которой требуется проверить сертификат
RUN go get github.com/golang/package # команда, которой может потребоваться доверять сертификату MITM-proxy
(...)
Эти добавления нужны, если во время сборки происходят обращения к внутренним сервисам, которые представляются корпоративными сертификатами. Кроме того, бывает, что выход в интернет осуществляется через MITM-proxy, который для инспекции трафика расшифровывает передаваемые данные и, соответственно, подменяет сертификат. Тогда, если во время сборки нужно обращаться в интернет, например, для доставки каких-либо пакетов, нужно доверять сертификату, который используется MITM-proxy. Поэтому для сборок в начале каждого докер-файла может потребоваться добавлять эти строки:
COPY certs/* /usr/local/share/ca-certificates/
RUN update-ca-certificates
Получать сертификаты из первоисточника
Если источник корневых сертификатов предоставляет удобный API для их получения, можно не утруждать себя хранением сертификатов на своих серверах с последующим монтированием в пайплайны. Допустим, корневой сертификат можно получить командой
curl http://pki.company.ru/api?name=RootCA -O /usr/local/share/ca-certificates/RootCA.crt
Пример со сборкой докер-образа примет такой вид:
build:
stage: build
# допустим, сборка докером
image: docker:stable
services:
- name: docker:18.09.6-dind
entrypoint:
- /bin/sh
command:
- -c
- curl http://pki.company.ru/api?name=RootCA -O /usr/local/share/ca-certificates/RootCA.crt && update-ca-certificates && dockerd-entrypoint.sh
# before_script:
# - script_to_get_certs.sh
script:
- mkdir -p certs
- curl http://pki.company.ru/api?name=RootCA -O /usr/local/share/ca-certificates/RootCA.crt
- cp /usr/local/share/ca-certificates/* certs/
- docker build (...) # или канико или что угодно ещё
- (...)
То есть добавился шаг скачивания сертификатов.
Получать сертификаты из самописного сервиса
Если API центра сертификации недостаточно функционален или требует авторизации, которая в пайплайнах слишком громоздкая, можно запустить простейший веб-сервер, откуда сертификаты можно будет скачать почти таким же образом, как в предыдущем примере. Принципиального отличия от получения их из первоисточника нет, поэтому рассмотрим последний, наиболее эффективный вариант.
Использовать образы со вшитыми сертификатами
Рано или поздно многие компании обнаруживают потребность контролировать свою инфраструктуру. Админам надоедает отвечать на одинаковые вопросы разработчиков («почему у меня не качает с гитхаба?, «что значит x509?»), безопасникам надоедает наблюдать за тем, как все докер-файлы начинаются со строчки FROM vasyapupkin/zvercd
, а разработчикам надоедает решать головоломки с изменчивыми промежуточными сертификатами и прочими вещами вне их области экспертизы. Тогда ответственные за платформу идут и пишут много-много докер-файлов наподобие такого:
FROM python:3.7
RUN curl http://pki.company.ru/api?name=RootCA -O /usr/local/share/ca-certificates/RootCA.crt && update-ca-certificates
Эти образы сохраняются в корпоративный репозиторий, а разработчики компании начинают пользоваться только этими образами в качестве базовых. Дополнительным бонусом идёт возможность преднастроить во внутренних образах всевозможные кэши и проксирующие репозитории (nexus, artifactory и прочие).
Наличие базовых образов не отменяет необходимость получать сертификаты динамически, любым из остальных перечисленных способов. Это нужно инженерам, поддерживающим актуальность корпоративных базовых образов, чтобы своевременно обновлять в них сертификаты или создавать новые базовые образы.
Совместив два-три описанных подхода вы, наконец, сможете спать спокойно и не объяснять всем в сотый раз, что означает x509: certificate signed by unknown authority
.
Singaporian
А почему ACME не использовали?
lllamnyp Автор
Могу заблуждаться, но мне казалось, ACME не столько про доверенные CA, сколько про выписку сертификатов подписанных каким-то CA?
В любом случае, статья скорее туториал, а не отражение конкретного эпизода из опыта. Поскольку, не у всех это в инфре есть, а частенько админы гитлабов и кубернетисов не админят свой удостоверяющий центр, то это может быть в принципе недоступно.
AlexGluck
А почему бы не подписывать сертификат у нескольких CA? Мы выпустили ключ и создали csr, который отправили на подпись двум CA, где первый это допустим Let's Encrypt, а второй CA организации и получили на выходе мультиподпись (перекрестную подпись). Как поступил Let's Encrypt для поддержки старых устройств
lllamnyp Автор
Не совсем понимаю вас. В статье ведь не шло речи о подписи, только о доставке существующих?
AlexGluck
Суть в следующем:
Дано:
1. У нас есть процесс, который обращается к сервису по зашифрованному каналу с использованием сертификатов.
2. У сервиса есть цепочка подписи сертификатов, если мы доверяем хотя бы одному сертификату из цепочки, то связь помечается доверенной и происходит обмен информацией.
Проблема:
У нас в перечне доверенных сертификатов нет ни одного сертификата из цепочки сервиса, из-за этого связь является не доверенной и прекращается (можем конечно игнорировать отсутствие сертииката).
Решение:
В лоб, добавить один из сертификатов в доверенные везде где нам необходимо. (Это самое идиотское, что можно сделать, но так все делают и я привык видеть такое решение)
Корректное решение: нам известно, что все сервисы имеют перечень корневых сертификатов и часто имеют процессы обновления этих перечней (например обновление пакета ca-certificates для линукс дистрибутивов). Можно контролировать сертификат сервиса, а не его клиентов и трудозатраты будут меньше. Мы возьмём и подпишем наш сертификат у ЦС чей корневой сертификат есть у всех и этот же сертификат подпишем у нашего ЦС организации. Есть исключения, но я о них умолчу с вашего позволения, чтобы не вводить низкокваллифицированных специалистов в заблуждения.
lllamnyp Автор
Малый недочёт этого подхода в том, что незачем подписывать сертификат в ЦС организации если мы уже подписали его в публичном ЦС. Более существенный недостаток в том, что публичный ЦС определённый сертификат может просто не подписать. Ну, допустим, какой нибудь test.internal.subdomain.company.com который вообще доступен только внутри сети организации по ip из rfc1918. Как проходить челленджи летсэнкрипта?
AlexGluck
Незачем конечно, но если есть какие то требования, то можно и подписать, например кубер и овирт имеют свой ЦА и чтобы работало и у клиентов и у специфичного софта можно подписывать у обоих и проблем не испытывать. Софт для овирта использующий ЦА сертификат будет корректно настраиваться по мануалам из инета, а клиенты в веб интерфейсе будут избавлены от необходимости добавлять недоверенный ЦА в свои списки.
Челенджи можно пройти с помощью acme delegate, acme proxy на бастионе?
Одно кольцо, чтобы править всеми. (с)