Portainer: установка самоподписанных сертификатов
Portainer поднимается по HTTPS сразу, но с сертификатом, который выписал сам себе. Браузер встречает красным экраном NET::ERR_CERT_AUTHORITY_INVALID, команда привыкает жать «Всё равно перейти» — и однажды так же не глядя примет чужой сертификат на подменённом хосте. Разберём, как выпустить самоподписанный сертификат под Portainer правильно и заставить браузер замолчать.
Содержание
- Откуда берётся самоподписанный сертификат Portainer
- Когда самоподписанный сертификат — правильный выбор, а когда костыль
- Выпускаем сертификат руками: SAN, срок и алгоритм
- Подключаем сертификат к Portainer: флаги, монтирование, проверка
- Свой мини-CA: как убрать предупреждение насовсем
- Ошибки, на которых застревают чаще всего
- Какой сервер под Portainer с собственным TLS взять в MAATRIX
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Откуда берётся самоподписанный сертификат Portainer
При первом запуске Portainer генерирует пару ключ-сертификат: docker exec portainer ls -l /data/certs покажет cert.pem и key.pem с правами 600. Openssl в образе нет, смотрим снаружи:
openssl s_client -connect 127.0.0.1:9443 -servername portainer.example.internal </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName
В выводе subject и issuer совпадают — это и есть определение самоподписанного сертификата, а сам s_client завершает сессию строкой Verify return code: 18 (self-signed certificate).
Браузер ругается по двум причинам. Первая: цепочка не ведёт к доверенному корню, подпись проверить нечем. Вторая: имя в сертификате не совпадает с адресом — ходите по IP, а внутри записано имя portainer, и Chrome добавит вторую ошибку, NET::ERR_CERT_COMMON_NAME_INVALID. Firefox покажет SEC_ERROR_UNKNOWN_ISSUER, curl 8.x — curl: (60) SSL certificate problem: self-signed certificate.
Полезная мелочь: удалите оба файла из /data/certs и перезапустите контейнер — пара сгенерируется заново, это штатный способ откатить эксперименты.
Когда самоподписанный сертификат — правильный выбор, а когда костыль
Честно: если у панели есть публичное доменное имя и открыт порт 80, самоподписанный сертификат не нужен — реверс-прокси с Let's Encrypt выдаст валидный бесплатно и продлит сам, см. установку и настройку Portainer на VPS. Свой сертификат оправдан в другом.
| Ситуация | Почему ACME не подходит | Что делать |
|---|---|---|
| Панель доступна только по IP | Let's Encrypt не выдаёт сертификаты на IP-адреса | Самоподписанный с IP SAN |
Внутренняя зона .internal, .lan | Имя не резолвится извне, HTTP-01 и DNS-01 не пройдут | Свой мини-CA |
| Portainer ↔ агент, Docker API | Внутренний трафик, публичное имя не нужно | Свой CA на оба конца |
Минусы знать лучше заранее. Доверие раздаётся руками: каждый ноутбук, телефон и CI-раннер придётся отдельно научить вашему корню. Отозвать скомпрометированный сертификат нечем: CRL и OCSP вы поднимать не будете. Автопродления нет — забыли про срок, и в понедельник утром вас ждёт NET::ERR_CERT_DATE_INVALID.
Развернуть за пару минут
Готовый образ на VPS MAATRIX: NVMe, AMD EPYC, root-доступ. Локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Развернуть PortainerВыпускаем сертификат руками: SAN, срок и алгоритм
SAN обязателен. Chrome перестал смотреть на поле Common Name ещё в 58-й версии, а Go (на нём написаны и Portainer, и Docker) с версии 1.15 отдаёт на такой сертификат x509: certificate relies on legacy Common Name field, use SANs instead. Ошибка вылезает не в браузере, а в логах — когда Portainer стучится к агенту или Docker API.
Срок — 397 дней, не десять лет. Apple и Google ограничивают жизнь листовых сертификатов 398 днями, Chrome на превышении отдаёт ERR_CERT_VALIDITY_TOO_LONG. Формально это требование к публичным центрам, и для добавленного вручную корня браузеры обычно делают исключение — но оно менялось не раз, а Safari на iOS строже.
Алгоритм. ECDSA P-256 дешевле для сервера: openssl speed rsa2048 ecdsap256 на тестовой машине с 2 vCPU дал около 1900 подписей в секунду против 38 000. Но на панели разницы не видно, а RSA-2048 совместим со старыми Java-клиентами и Android до 8.
mkdir -p /opt/portainer/certs && cd /opt/portainer/certs
umask 077
openssl req -x509 -nodes -newkey rsa:2048 -sha256 -days 397 \
-keyout key.pem -out cert.pem \
-subj "/C=RU/O=Example Lab/CN=portainer.example.internal" \
-addext "subjectAltName=DNS:portainer.example.internal,DNS:portainer,IP:203.0.113.10" \
-addext "extendedKeyUsage=serverAuth"
Ключ -addext есть с OpenSSL 1.1.1 (Ubuntu 24.04 — 3.0.13, Debian 13 — 3.5.x). Флаг -nodes обязателен: ключ с паролем Portainer не прочитает, спросить его негде.
В SAN перечисляйте все адреса, включая IP: без записи IP: вход по адресу даст x509: cannot validate certificate for 203.0.113.10 because it doesn't contain any IP SANs. Проверка: openssl x509 -in cert.pem -noout -ext subjectAltName -dates, затем chmod 600 key.pem.
Подключаем сертификат к Portainer: флаги, монтирование, проверка
Способов три: флаги командной строки, загрузка через Settings → SSL certificate и подмена файлов в томе /data/certs. Берите первый, он воспроизводим. Файл /opt/portainer/compose.yaml:
services:
portainer:
image: portainer/portainer-ce:2.45.0
container_name: portainer
restart: unless-stopped
command: >
--sslcert /certs/cert.pem
--sslkey /certs/key.pem
--http-disabled
ports:
- "127.0.0.1:9443:9443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
- /opt/portainer/certs:/certs:ro
volumes:
portainer_data:
Монтирование :ro не даёт панели переписать ваши файлы. На AlmaLinux и Rocky с SELinux добавьте флаг Z — /opt/portainer/certs:/certs:ro,Z, иначе получите permission denied при верных правах.
Поднимаем docker compose up -d --force-recreate, смотрим docker logs --tail 20 portainer и проверяем curl -k -s https://127.0.0.1:9443/api/status. Живая панель отвечает JSON вида {"Version":"2.45.0","InstanceID":"..."}. Если порт не слушает, дело почти всегда в ключе: open /certs/key.pem: permission denied означает userns-remap, при котором root внутри контейнера — не тот root, что владеет файлом.
Флаг --http-disabled закрывает порт 9000: если в конфиге прокси остался proxy_pass http://127.0.0.1:9000, доступ пропадёт целиком — меняйте на https://127.0.0.1:9443. Публикация на loopback рассчитана на схему с прокси; когда панель смотрит наружу сама, ставьте 9443:9443 и закрывайте её ufw allow from 203.0.113.0/24 to any port 9443 proto tcp.
Свой мини-CA: как убрать предупреждение насовсем
Одиночный сертификат придётся импортировать на каждое устройство и заново при каждом перевыпуске. Мини-CA решает это один раз: в доверенные добавляется корень, под ним выпускаются сертификаты для панели и агентов. Ограничение в 398 дней на CA не распространяется, корень живёт долго.
openssl req -x509 -nodes -newkey rsa:4096 -sha256 -days 3650 \
-keyout ca.key -out ca.crt \
-subj "/C=RU/O=Example Lab/CN=Example Lab Root CA" \
-addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
-addext "keyUsage=critical,keyCertSign,cRLSign"
openssl req -nodes -newkey rsa:2048 -keyout portainer.key -out portainer.csr \
-subj "/C=RU/O=Example Lab/CN=portainer.example.internal"
cat > portainer.ext <<'EXT'
basicConstraints=CA:FALSE
keyUsage=critical,digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
subjectAltName=DNS:portainer.example.internal,IP:203.0.113.10
EXT
openssl x509 -req -in portainer.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-out portainer.crt -days 397 -sha256 -extfile portainer.ext
openssl verify -CAfile ca.crt portainer.crt
Последняя команда должна ответить portainer.crt: OK. Кладём пару в /opt/portainer/certs, перезапускаем контейнер и раздаём ca.crt:
- Linux: Debian и Ubuntu —
cp ca.crt /usr/local/share/ca-certificates/example-lab.crt && update-ca-certificates; RHEL и AlmaLinux — каталог/etc/pki/ca-trust/source/anchors/иupdate-ca-trust. - Windows (от администратора):
certutil -addstore -f "ROOT" ca.crt; macOS:sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ca.crt - Firefox держит своё хранилище: Настройки → Приватность и защита → Сертификаты → Импортировать. На Windows и macOS помогает
security.enterprise_roots.enabledвabout:config. - iOS: мало установить профиль — корень включают в Настройки → Основные → Об этом устройстве → Доверие сертификатам.
Не хочется возиться с openssl — то же делает mkcert 1.4.4: mkcert -install заводит локальный CA и прописывает его в хранилища системы и Firefox, mkcert portainer.example.internal 203.0.113.10 выдаёт пару.
Честный риск: ca.key — универсальный ключ подделки для всех устройств, где стоит ваш корень: с ним выписывается валидный сертификат на любой домен, включая банковский. Держите его с chmod 400, не на сервере с панелью и не в git.
Ошибки, на которых застревают чаще всего
NET::ERR_CERT_COMMON_NAME_INVALID при валидном корне. Подпись верна, но SAN не содержит адрес, по которому вы пришли: выпустили на имя, а ходите по IP. Смотрите openssl x509 -in cert.pem -noout -ext subjectAltName и перевыпускайте.
curl: (60) SSL certificate problem: certificate has expired. Срок вышел. Чтобы не узнавать об этом от пользователей, повесьте на еженедельный cron строку openssl x509 -in /opt/portainer/certs/cert.pem -noout -checkend 2592000 — она возвращает ненулевой код за 30 дней до истечения, и по нему удобно слать уведомление через logger или почту.
Корень импортировали, а браузер ругается. Причин три: подсунули листовой сертификат вместо ca.crt; сидите в Firefox, который системное хранилище на Linux не читает; в корне забыли basicConstraints=critical,CA:TRUE. Проверка: openssl x509 -in ca.crt -noout -text | grep -A1 "Basic Constraints".
Панель за nginx: битая консоль контейнера. proxy_ssl_verify по умолчанию выключен, так что на самоподписанный бэкенд nginx проксирует сразу. Ломается веб-консоль, которой нужен WebSocket: в location / обязательны proxy_http_version 1.1;, proxy_set_header Upgrade $http_upgrade; и proxy_set_header Connection "upgrade";. Включили proxy_ssl_verify on — понадобятся ещё proxy_ssl_trusted_certificate, proxy_ssl_server_name on; и proxy_ssl_name, иначе в логе появится upstream SSL certificate does not match.
Кнопки «продолжить» нет вовсе. Домен попал в HSTS: либо вы раньше отдавали его с валидным сертификатом и заголовком Strict-Transport-Security, либо взяли имя в зоне .dev или .app — они целиком в HSTS-preload. Для частных имён с 2024 года зарезервирована зона .internal, её и берите. Лечится в chrome://net-internals/#hsts: поле Delete domain security policies, имя хоста.
Агент на 9001 и Docker API на 2376. Агент генерирует свой самоподписанный сертификат, и сервер его не проверяет — защита держится на общем секрете, так что AGENT_SECRET задавайте обязательно. Окружению через Docker API нужен комплект от того же мини-CA: CA, клиентский сертификат и ключ, а в сертификате демона снова обязателен IP SAN.
Какой сервер под Portainer с собственным TLS взять в MAATRIX
Сам TLS ресурсов не ест: рукопожатия на панели с парой пользователей на графике CPU не видны. Сервер выбирают под то, что крутится под панелью.
| Сценарий | vCPU / RAM / NVMe | Что с сертификатами |
|---|---|---|
| Закрытый стенд, доступ по VPN | 1 / 2 ГБ / 25 ГБ | Самоподписанный с IP SAN, импорт на 2–3 машины |
| Прод без домена у панели | 2 / 4 ГБ / 50 ГБ | Мини-CA, корень роздан всей команде |
| Несколько нод с агентами | 4 / 8 ГБ / 100 ГБ | Мини-CA на все узлы, сертификаты для 9001 и 2376 |
Про нижнюю границу честно: на 1 ГБ панель запустится и HTTPS отдаст, но первая же сборка через docker build упрётся в лимит и OOM-killer погасит не тот процесс. Гигабайт — это стенд, а не продакшен.
Локация. Для панели над европейской инфраструктурой логичен UK — Лондон: до европейских точек обмена 8–20 мс, до Москвы 45–60 мс, для веб-интерфейса неощутимо, плюс понятная юрисдикция и GDPR. Сервисы для российской аудитории и 152-ФЗ — повод взять RU-площадку; контейнеры, которые ходят к зарубежным ИИ-API, лучше живут на US, Нью-Йорк, с чистым IP.
Ещё аргумент за постоянный белый IP: сертификат с IP: в SAN привязан к адресу намертво, и смена IP означает перевыпуск и обход всех устройств заново. У MAATRIX адрес закреплён за сервером, а при переходе на домен и Let's Encrypt поменяется только конфиг прокси. Оплата картой российского банка, через СБП, криптовалютой или токеном MAAT — иностранная карта не нужна даже для лондонской площадки. Разбор остальных поломок — в статье про частые ошибки Portainer на сервере.
Развернуть за пару минут
Готовый образ на VPS MAATRIX: NVMe, AMD EPYC, root-доступ. Локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Развернуть PortainerОбсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Частые вопросы
Можно ли просто нажать «Всё равно перейти»?
На домашнем стенде допустимо. Но так теряется единственная защита от подмены: браузер больше не отличит вашу панель от чужой на том же адресе.
Portainer перезапишет мой сертификат при обновлении?
Нет, если он подан флагами --sslcert и --sslkey, а каталог смонтирован как :ro. Своё Portainer генерирует, только когда в /data/certs пусто и флаги не заданы.
Мини-CA или Let's Encrypt?
Есть публичный домен и открытый порт 80 — Let's Encrypt, он бесплатный и продлевается сам. Панель на IP или во внутренней зоне — только мини-CA: ACME на такие адреса сертификаты не выдаёт в принципе.
Нужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.