MAATRIX / Блог / Portainer: установка самоподписанных сертификатов

Portainer: установка самоподписанных сертификатов

Portainer: установка самоподписанных сертификатов

MAATRIX

Portainer поднимается по HTTPS сразу, но с сертификатом, который выписал сам себе. Браузер встречает красным экраном NET::ERR_CERT_AUTHORITY_INVALID, команда привыкает жать «Всё равно перейти» — и однажды так же не глядя примет чужой сертификат на подменённом хосте. Разберём, как выпустить самоподписанный сертификат под Portainer правильно и заставить браузер замолчать.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество 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 не подходитЧто делать
Панель доступна только по IPLet'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Что с сертификатами
Закрытый стенд, доступ по VPN1 / 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 — десятки моделей в одном окне. Оплата картой РФ и по СБП.