Короткий ответ
Если имя 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 тоже.
| Проверка | TLS | Auth |
|---|---|---|
| curl -v без -k | цепочка | WWW-Authenticate |
| openssl s_client | SAN/expiry | нет |
| docker login | оба | оба |
Возможные причины
- Корневой/промежуточный CA не установлен на Ubuntu-клиенте.
- SAN не содержит
registry.example. - Истёк cert.
- htpasswd пользователя нет / bcrypt не тот, registry v2.
- Смешали HTTP (
insecure) и клиенты ходят HTTPS. - Клиент тянет по IP, сертификат на имя.
- Редко: клиент требует 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 || true3. 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 — ожидаемо.