Короткий ответ

Если имя registry.example резолвится и :5000 открыт, типичный отказ — TLS (свой CA не в trust store dockerd/Ubuntu) или htpasswd/token 401. На клиенте host.example положите CA в /usr/local/share/ca-certificates/, update-ca-certificates, restart docker. На сервере registry проверьте cert SAN=registry.example, не IP из gist, и файл htpasswd. Не insecure-registries. Не отключайте проверку сертификата в каждом скрипте.

Сеть/DNS/прокси без TLS — сначала образ не скачивается.

Симптомы и как отличить

  • x509: certificate signed by unknown authority;
  • certificate is valid for localhost, not registry.example;
  • curl --cacert работает, docker pull нет (CA не в системном store);
  • 401 Unauthorized на /v2/ после login опечатка htpasswd;
  • браузер ругается, Engine тоже.
ПроверкаTLSAuth
curl -v без -kцепочкаWWW-Authenticate
openssl s_clientSAN/expiryнет
docker loginобаоба

Возможные причины

  1. Корневой/промежуточный CA не установлен на Ubuntu-клиенте.
  2. SAN не содержит registry.example.
  3. Истёк cert.
  4. htpasswd пользователя нет / bcrypt не тот, registry v2.
  5. Смешали HTTP (insecure) и клиенты ходят HTTPS.
  6. Клиент тянет по IP, сертификат на имя.
  7. Редко: клиент требует TLS1.2, кто-то выключил на прокси.

Диагностика

1. Сертификат с клиента

echo | openssl s_client -connect registry.example:5000 -servername registry.example 2>/dev/null | openssl x509 -noout -subject -dates -ext subjectAltName
curl -svS --max-time 10 https://registry.example:5000/v2/ -o /dev/null

Смотрите verify error, HTTP/1.1 401 (для registry 401 на /v2/ без токена часто норма, если TLS уже ок).

2. Trust store Ubuntu

ls /usr/local/share/ca-certificates/
awk -v cmd='openssl x509 -noout -subject' '/BEGIN/{close(cmd)}{print | cmd}' < /etc/ssl/certs/ca-certificates.crt | grep -i example || true

3. Auth

curl -svS -u 'puller:REDACTED' https://registry.example:5000/v2/_catalog

Пароль не логируйте. 200 vs 401.

На стороне registry (если доступ есть), типичный compose-сервис registry:2 с volume certs и REGISTRY_AUTH_HTPASSWD_PATH.

# на хосте registry, пример путей — подставьте свои
sudo ls -l /opt/registry/auth/htpasswd /opt/registry/certs/

Решение

Сценарий A. Свой CA, клиент Ubuntu

sudo cp /path/to/corp-root.crt /usr/local/share/ca-certificates/corp-root.crt
sudo update-ca-certificates
sudo systemctl restart docker
docker pull registry.example:5000/app/web:1.2.3

Только PEM. Не кладите ключ сервера на клиент.

Альтернатива Docker-specific (хуже сопровождать): /etc/docker/certs.d/registry.example:5000/ca.crt. Формат каталога с портом обязателен.

sudo mkdir -p /etc/docker/certs.d/registry.example:5000
sudo cp /path/to/ca.crt /etc/docker/certs.d/registry.example:5000/ca.crt
sudo systemctl restart docker

Сценарий B. Неверный SAN / expired

Выпустите сертификат с SAN DNS registry.example. Обновите на сервере registry и reverse-proxy. Клиентский trust не поможет на чужое имя.

Сценарий C. htpasswd

Создавайте bcrypt (документация distribution):

# на безопасной машине
htpasswd -B -c htpasswd puller

Файл монтируется в registry без попадания в git. Пользователь puller только read, pusher отдельно. После смены пароля docker logout / login на host.example.

Проверка:

docker login registry.example:5000
docker pull registry.example:5000/app/web:1.2.3

Сценарий D. HTTP-only lab

Не прод. Если всё же HTTP: это insecure-registries в daemon.json — отдельный риск, документируйте. Предпочтительнее даже в лабе внутренний CA.

Сценарий E. Reverse-proxy

proxy_set_header Host, таймауты на большие слои, client_max_body_size. Обрыв на push большого слоя — не htpasswd. TLS терминация на proxy: cert там.

Клиент Docker проверяет имя из URL, не из CN в стиле «как браузер 2010». SAN должен содержать registry.example. IP в SAN нужен только если pull идёт по IP (не надо). Цепочка: leaf + intermediate в том файле, который отдаёт nginx/registry; root — в trust store клиента. Если отдали только leaf, openssl s_client покажет verify error:num=21. update-ca-certificates без restart docker оставляет старый пул в уже запущенном dockerd.

Порт 5000 на WAN без ACL — не «TLS спасёт». Слои образов содержат исходники и иногда секреты. Слушайте на внутренней VIP, клиенты host.example ходят напрямую, люди — через jump. htpasswd bcrypt (htpasswd -B): algorithm apr1 registry может отвергнуть. После смены htpasswd процесс registry нужно перечитать/рестартнуть по его документации; клиенты делают docker logout/login.

Garbage collect registry не удаляет слой, пока на него ссылается хоть один тег/референс. Удаление тега с секретом в истории не мгновенно чистит blob. Считайте секрет скомпрометированным независимо от GC.

Время: если NTP на host.example в будущем, cert «not yet valid»; в прошлом — expired. Сверьте timedatectl до перевыпуска сертификатов.

Как проверить, что проблема устранена

curl -sS -o /dev/null -w '%{http_code}\n' https://registry.example:5000/v2/
docker login registry.example:5000
docker pull registry.example:5000/app/web:1.2.3
echo | openssl s_client -connect registry.example:5000 -servername registry.example 2>/dev/null | openssl x509 -noout -dates

Код 401 или 200 на /v2/ без ошибки x509 в curl (без -k). Pull успешен. Даты сертификата в будущем.

Если не помогло

  • Работает curl, docker x509: забыли restart docker после update-ca-certificates.
  • certs.d путь без :5000 при registry на 5000.
  • Промежуточный CA не в цепочке, только root — openssl verify fail.
  • Время хоста убежало — «not yet valid».
  • 403 от WAF на POST слоёв.

Профилактика

  • Мониторинг expiry cert registry (30 дней).
  • CA корпоративный в golden image Ubuntu.
  • Разделение pull/push учёток.
  • Запрет insecure-registries в CIS-чеклисте.
  • Бэкап htpasswd и ключей как секретов, не в образе.

Каталог certs.d чувствителен к имени: registry.example:5000 и registry.example — разные папки. Порт в URL pull обязан совпасть. После копирования ca.crt проверьте права чтения у пользователя dockerd (root). SELinux на Ubuntu нет, но AppArmor не должен мешать чтению /etc/docker/certs.d.

FAQ

401 на GET /v2/ — это поломка?

Часто нет: анониму отказано. Смотрите тело и заголовок WWW-Authenticate. После login pull должен пройти.

Нужен ли client cert (mTLS)?

Только если так спроектировали. Тогда certs.d client.cert/key. Не путать с CA.

Let's Encrypt на внутреннем имени?

Не выпустит. Внутренний CA или split DNS + публичное имя.

privileged registry контейнеру?

Нет. Registry — обычный процесс + volume + порты.

Почему login ок, push 401, pull ок?

Разные scope. htpasswd один пользователь без push — ожидаемо.