Симптомы: ErrImagePull, ImagePullBackOff и 401 Unauthorized
Типичная картина: деплой применился, Pod создан, но контейнер не стартует. В статусе появляется ErrImagePull, позже — ImagePullBackOff, а в событиях (Events) видно 401 Unauthorized. Это означает, что kubelet на ноде не смог аутентифицироваться в реестре образов при попытке скачать image из private registry.
Важно различать: ErrImagePull — первая неудачная попытка скачать образ, ImagePullBackOff — повторные попытки с увеличивающейся задержкой. В обоих случаях первопричина одна: образ недоступен, имя/тег неверные, либо реестр требует авторизацию или валидный TLS.
Дальше фокус только на ситуации с 401, когда реестр доступен, но Kubernetes не применяет нужные учётные данные: imagePullSecrets, секрет типа kubernetes.io/dockerconfigjson, привязка к ServiceAccount и типовые нюансы Harbor/GitLab/registry v2.
Шаг 1. Быстро подтвердить, что это именно auth (kubectl describe pod)
Начните с диагностики «в лоб». Нас интересуют две вещи: текст ошибки в Events и то, видит ли Pod какие-то imagePullSecrets.
kubectl describe pod -n NAMESPACE POD_NAME
Внизу часто будет сообщение вида:
Failed to pull image "registry.example.com/team/app:1.2.3": rpc error: code = Unknown desc = failed to pull and unpack image ...: failed to resolve reference ...: unexpected status from HEAD request ...: 401 Unauthorized
Ориентиры для быстрой развилки:
401 Unauthorized— kubelet не применил корректные credentials (или применил не к тому host).403 Forbidden— аутентификация прошла, но нет прав на репозиторий/проект.404 Not Found— чаще неверный repo/tag или вы указываете путь, которого нет.
В том же describe посмотрите строку:
Image Pull Secrets: my-regcred
Если там пусто, Kubernetes даже не пытается использовать секрет при pull.
Шаг 2. Откуда Kubernetes берёт credentials: Pod vs ServiceAccount
Есть два нормальных способа передать учётные данные для private registry:
Явно в манифесте Deployment/Pod через
spec.template.spec.imagePullSecrets.Неявно через ServiceAccount: добавляете
imagePullSecretsв ServiceAccount, и все Pod’ы с этим SA смогут тянуть приватные образы без дублирования в каждом деплое.
Если вы ожидаете, что секрет «подхватится сам», но он просто создан в namespace и ни к чему не привязан, будет ErrImagePull и 401 Unauthorized.
Проверьте, какой ServiceAccount использует Pod:
kubectl get pod -n NAMESPACE POD_NAME -o jsonpath='{.spec.serviceAccountName}'; echo
Если вывод пустой, используется default. Посмотрите его:
kubectl get sa -n NAMESPACE default -o yaml
Ищите секцию imagePullSecrets. Если её нет, credentials через SA не подтягиваются.
Если вы держите кластер на отдельных виртуальных машинах, удобно выносить рабочие ноды в VDS: так проще контролировать сеть до реестра, TLS и доступы, а диагностика pull-ошибок обычно быстрее и прозрачнее.

Шаг 3. Проверить секрет: тип, namespace, корректность dockerconfigjson
Самые частые причины 401 в практике: секрет не того типа, секрет создан не в том namespace, неправильный ключ в data, или в нём указан не тот сервер в auths.
Проверьте секрет:
kubectl get secret -n NAMESPACE SECRET_NAME -o yaml
Для секрета реестра ожидаем:
type: kubernetes.io/dockerconfigjsonв
dataесть ключ.dockerconfigjson
Если secret создан как Opaque или вы положили ключ config.json вместо .dockerconfigjson, kubelet не сможет использовать его как registry credentials.
Чтобы убедиться, что внутри валидный JSON, можно декодировать (делайте это только в защищённой среде, потому что это чувствительные данные):
kubectl get secret -n NAMESPACE SECRET_NAME -o jsonpath='{.data..dockerconfigjson}' | base64 -d; echo
Внутри должен быть объект формата Docker config, минимально:
{
"auths": {
"registry.example.com": {
"username": "user",
"password": "pass",
"auth": "dXNlcjpwYXNz"
}
}
}
Критично, чтобы ключ в auths совпадал с тем, как Kubernetes обращается к реестру в имени образа. Если image указан как registry.example.com/team/app:tag, то credentials должны быть для registry.example.com. Если вы положили https://registry.example.com или добавили путь registry.example.com/team, kubelet не сопоставит и пойдёт без авторизации, получив 401 Unauthorized.
Шаг 4. Правильное создание imagePullSecrets (рекомендуемый способ)
Самый надёжный способ — создать secret командой kubectl create secret docker-registry: она гарантирует правильный тип kubernetes.io/dockerconfigjson и корректную структуру.
kubectl create secret docker-registry regcred -n NAMESPACE --docker-server=registry.example.com --docker-username=USER --docker-password=PASS --docker-email=devops@example.com
Если реестр — Harbor, часто удобнее использовать robot account и токен, а не пароль пользователя. Тогда --docker-username будет именем robot’а (например, robot$ci), а --docker-password — его secret.
Если вы хотите глубже закрыть тему TLS и ошибок, которые маскируются под «проблемы с реестром», полезно держать под рукой отдельную шпаргалку: TLS и авторизация для private registry: типовые ошибки и диагностика.
Шаг 5. Привязка секрета: в Deployment и/или в ServiceAccount
Вариант A: указать imagePullSecrets в Deployment
Самый предсказуемый вариант — прописать секрет прямо в манифесте. Пример фрагмента:
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
namespace: NAMESPACE
spec:
replicas: 1
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
imagePullSecrets:
- name: regcred
containers:
- name: app
image: registry.example.com/team/app:1.2.3
Плюс этого подхода: секрет явно «едет» вместе с конкретным workload, меньше сюрпризов при переносе манифестов между namespace.
Вариант B: добавить imagePullSecrets в ServiceAccount
Если в namespace много деплоев и все тянут из одного private registry, удобнее настроить ServiceAccount. Для default это можно сделать так:
kubectl patch serviceaccount -n NAMESPACE default -p '{"imagePullSecrets": [{"name": "regcred"}]}'
После этого новые Pod’ы с ServiceAccount default будут тянуть образы с этим секретом. Застрявшие Pod’ы проще пересоздать (например, через kubectl rollout restart у Deployment).
Шаг 6. Harbor auth: типовые грабли
Для Harbor чаще всего «стреляют» такие моменты:
Неправильный endpoint в секрете. Нужен домен реестра (например,
harbor.example.com), без протокола и без пути.Robot account без pull-доступа к проекту/репозиторию (тут чаще будет
403, но при особенностях прокси/настроек может выглядеть и как401).SSO/LDAP пользователь с протухшим паролем/токеном. Для CI/K8s стабильнее robot account.
Проект приватный: на вашем ноутбуке pull «проходит», потому что Docker уже залогинен; на ноде Kubernetes этих credentials нет без secret.
Полезная проверка: теми же учётными данными попробуйте вручную сделать pull на любой машине с Docker/nerdctl/containerd. Если вручную не тянется, Kubernetes тоже не вытянет.
Если реестр доступен по HTTPS и у вас собственный CA или нестандартная цепочка, проверьте сертификаты заранее. С корректно выпущенным сертификатом (например, через SSL-сертификаты) меньше шансов упереться в ошибки вида x509, которые часто путают с авторизацией.

Шаг 7. «Секрет есть, но всё равно 401»: короткий чек-лист
1) Secret в другом namespace
Secrets — строго namespace-scoped. Если Deployment в prod, а secret создан в default, kubelet его не увидит. Проверка:
kubectl get secret -n NAMESPACE
2) Pod использует не тот ServiceAccount
Вы могли пропатчить default, но Pod использует, например, app-sa. Проверьте:
kubectl get pod -n NAMESPACE POD_NAME -o jsonpath='{.spec.serviceAccountName}'; echo
3) Неверный ключ в auths (server mismatch)
Сравните, что указано в image и что лежит в .dockerconfigjson:
image:
registry.example.com/team/app:tagauths: должен быть ключregistry.example.com
Любые расхождения по поддомену, порту и протоколу ломают сопоставление.
4) Порт указан в одном месте, а в другом нет
Если тянете как registry.example.com:5443/team/app:tag, то и в auths ключ должен быть registry.example.com:5443.
5) Нода ходит в реестр через прокси/зеркало/редирект
Иногда сеть подменяет endpoint или включён reverse proxy, который меняет host. В итоге kubelet обращается к одному хосту, а secret содержит другой.
6) Нестандартная схема авторизации
Kubernetes ожидает стандартный Docker config и совместимость с Docker Registry API v2 (включая token service). Если у вас нестандартная прослойка, проверьте её совместимость, иначе kubelet может уходить в pull без валидных credentials.
Шаг 8. Минимальный набор команд «починить сейчас»
В большинстве случаев достаточно такой последовательности: создать или обновить secret, привязать к ServiceAccount, затем пересоздать Pod’ы.
kubectl create secret docker-registry regcred -n NAMESPACE --docker-server=registry.example.com --docker-username=USER --docker-password=PASS --docker-email=devops@example.com
kubectl patch serviceaccount -n NAMESPACE default -p '{"imagePullSecrets": [{"name": "regcred"}]}'
kubectl rollout restart deployment -n NAMESPACE DEPLOYMENT_NAME
Шаг 9. Как отличить auth-проблему от TLS/CA и DNS
Иногда всё пытаются «лечить» imagePullSecrets, хотя ошибка другого класса. Быстрые ориентиры по тексту событий:
TLS/CA: фразы про
x509,certificate signed by unknown authority,tls: failed to verify certificate.DNS/сеть:
no such host,i/o timeout,connection refused.Auth: явный
401 Unauthorizedилиauthentication required.
Шаг 10. Практика: безопасное хранение и ротация credentials
Чтобы ErrImagePull не всплывал «раз в пару месяцев», когда кто-то поменял пароль или протух токен:
Для CI/Kubernetes используйте robot/service accounts в реестре, а не персональные логины.
Давайте минимальные права: только pull на нужные репозитории.
Документируйте соответствие: какой namespace тянет из какого registry и каким secret.
Планируйте ротацию: обновили secret, сделали rolling restart нужных workload.
Если namespace’ов много, помогает единый стандарт: отдельный ServiceAccount на приложение, один registry secret на namespace, плюс явное указание
imagePullSecretsв чарте/манифесте там, где важна переносимость.
Итоги
ErrImagePull и ImagePullBackOff с 401 Unauthorized почти всегда сводятся к тому, что kubelet не нашёл или не смог применить корректные credentials для private registry. Начните с kubectl describe pod, проверьте ServiceAccount, тип секрета kubernetes.io/dockerconfigjson, ключ .dockerconfigjson и точное совпадение адреса реестра в auths с тем, что указано в image. Затем привяжите secret к Pod или ServiceAccount и пересоздайте Pod’ы.
Если после исправления auth всё равно не тянется, смотрите в сторону TLS (CA) и сетевых проблем: они проявляются другими сообщениями, но часто всплывают «следом».


