Problem – Prometheus Fails to Pull Its Image on an Edge Node
An edge‑computing node that runs a lightweight Kubernetes distribution (e.g., K3s) attempts to start the prometheus pod. The pod remains in ImagePullBackOff and the node reports a registry authentication error combined with intermittent network timeouts.
$ kubectl get pod -n monitoring prometheus-0 -o wide
NAME READY STATUS RESTARTS AGE IP NODE
prometheus-0 0/2 ImagePullBackOff 0 3m 10.42.0.12 edge-node-01
$ kubectl describe pod -n monitoring prometheus-0
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Pulling 2m kubelet, edge-node-01 Pulling image "registry.example.com/prometheus:2.45.0"
Warning Failed 2m kubelet, edge-node-01 Failed to pull image "registry.example.com/prometheus:2.45.0": rpc error: code = Unknown desc = failed to resolve auth config for registry.example.com: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)
Warning BackOff 1m kubelet, edge-node-01 Back-off pulling image "registry.example.com/prometheus:2.45.0"
Root Cause – Combined Auth Token Expiry and Bandwidth Saturation
The failure is not a simple typo in the image name. Two independent conditions interact:
- Registry authentication token expiration: The edge node uses a short‑lived
docker/config.jsontoken (default 1 hour) generated by an external CI pipeline. When the token expires, the registry returns401 Unauthorized. The kubelet logs only surface the generic “failed to resolve auth config”. - Intermittent network bandwidth: The edge node connects to the central registry over a satellite link with < 2 Mbps average throughput and frequent packet loss. When the token is refreshed, the pull request often times out before the registry can respond, causing the
Client.Timeouterror shown above.
Because the kubelet retries with exponential back‑off, the pod stays in ImagePullBackOff even after the token is refreshed, until the back‑off timer expires.
Debug – Step‑by‑Step Investigation
1. Verify the kubelet’s credential source
$ cat /var/lib/kubelet/config.json | jq .auths
{
"registry.example.com": {
"auth": "c2VjcmV0dG9rZW4="
}
}
The auth field is a base64‑encoded username:password pair that expires after one hour.
2. Test registry connectivity manually
$ curl -v -u user:$(echo c2VjcmV0dG9rZW4= | base64 -d) \
https://registry.example.com/v2/_catalog
* Trying 203.0.113.10:443...
* Connected to registry.example.com (203.0.113.10) port 443 (#0)
* TLS handshake ... (omitted)
* Connected to registry.example.com (203.0.113.10) port 443 (#0)
* Operation timed out after 30000 milliseconds with 0 bytes received
The request times out, confirming bandwidth constraints.
3. Check token validity directly
$ docker login registry.example.com -u user -p $(echo c2VjcmV0dG9rZW4= | base64 -d)
Error response from daemon: Get https://registry.example.com/v2/: unauthorized: authentication required
The registry rejects the token, indicating expiration.
4. Inspect node‑level network metrics
$ ss -i state established '( dport = :443 )'
Recv-Q Send-Q Local Address:Port Peer Address:Port
0 0 10.42.0.12:53024 203.0.113.10:443
skmem:(r0,rb87380,t0,tb131072) bbr wscale:7,7 rto:200 rtt:150ms/30ms ato:40ms mss:1460 cwnd:10 ssthresh:20
Round‑trip time spikes (>150 ms) and low congestion window confirm the link is saturated.
Solution – Refresh Credentials and Add a Pull‑Through Cache
1. Deploy a local registry cache on the edge node
Running a lightweight registry:2 instance with proxy mode eliminates the need for the edge node to contact the central registry for every image.
Before (no cache)
| Component | Registry Access |
|---|---|
| Edge kubelet | Direct HTTPS to central registry |
After (cache added)
| Component | Registry Access |
|---|---|
| Edge kubelet | Local HTTP to localhost:5000 (proxy) |
| Local cache | HTTPS to central registry (only on miss) |
# Deploy cache (manifest saved as cache-registry.yaml)
apiVersion: apps/v1
kind: Deployment
metadata:
name: edge-registry-cache
namespace: kube-system
spec:
replicas: 1
selector:
matchLabels:
app: edge-registry-cache
template:
metadata:
labels:
app: edge-registry-cache
spec:
containers:
- name: registry
image: registry:2
env:
- name: REGISTRY_PROXY_REMOTEURL
value: https://registry.example.com
ports:
- containerPort: 5000
---
apiVersion: v1
kind: Service
metadata:
name: edge-registry-cache
namespace: kube-system
spec:
selector:
app: edge-registry-cache
ports:
- protocol: TCP
port: 5000
targetPort: 5000
2. Update the Prometheus image reference to use the cache
# Before (central registry)
image: registry.example.com/prometheus:2.45.0
# After (local cache)
image: localhost:5000/prometheus:2.45.0
3. Automate token rotation with a short‑lived secret
Create a Kubernetes docker-registry secret that is refreshed by a CronJob every 45 minutes.
# cronjob-refresh-registry-secret.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: refresh-registry-secret
namespace: kube-system
spec:
schedule: "*/45 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: refresher
image: alpine:3.18
command: ["/bin/sh", "-c"]
args:
- |
TOKEN=$(curl -s -X POST https://auth.example.com/token -d '{"user":"edge","pass":"$EDGE_PASS"}')
kubectl create secret docker-registry edge-registry \
--docker-server=localhost:5000 \
--docker-username=edge \
--docker-password=$TOKEN \
--dry-run=client -o yaml | kubectl apply -f -
restartPolicy: OnFailure
Verify – Confirm the Pod Starts and Metrics Flow
$ kubectl rollout restart deployment/prometheus -n monitoring
$ kubectl get pod -n monitoring -w
NAME READY STATUS RESTARTS AGE
prometheus-0 2/2 Running 0 30s
Check that the image was pulled from the cache:
$ kubectl logs -n kube-system -l app=edge-registry-cache -c registry
time="2026-10-09T12:34:56Z" level=info msg="GET /v2/prometheus/manifests/2.45.0 from 10.42.0.12:53024"
Validate Prometheus is scraping targets:
$ curl -s http://edge-node-01:9090/api/v1/targets | jq '.data.activeTargets[] | .scrapeUrl'
"http://node-exporter:9100/metrics"
Prevent – Operational Guardrails for Edge Registries
- Cache first strategy: Deploy a pull‑through cache on every edge site. It reduces bandwidth usage and isolates the edge from central registry outages.
- Token lifetime management: Use secrets with
kubectl apply --server-sideto ensure atomic updates and avoid stale tokens. - Health checks: Add a liveness probe to the cache deployment that queries
/v2/_catalogevery 5 minutes; alert on failures. - Network QoS: Prioritize registry traffic (port 443) with a traffic‑shaping rule (e.g.,
tc qdisc add dev eth0 root tbf rate 2mbit burst 32kbit latency 100ms) to guarantee enough headroom for image pulls. - Back‑off tuning: Set
imagePullBackOffback‑off limits in the kubelet config to a maximum of 30 seconds on edge nodes, so a refreshed token is retried sooner.
Related Topic Hub: Observability Troubleshooting Hub
FAQ
- Why does the pod stay in
ImagePullBackOffafter I manually re‑login?
Because the kubelet’s internal back‑off timer continues to delay retries. Restarting the pod or clearing the back‑off (e.g.,kubectl delete pod) forces an immediate pull. - Can I use a public mirror (e.g., Docker Hub) instead of a local cache?
A public mirror works only if the image is publicly available. For private registries you must either proxy through a cache or replicate the image to a public registry. - How do I know if the token is the root cause versus network latency?
Inspect the registry response code. A401 Unauthorizedindicates token issues; a timeout or504 Gateway Timeoutpoints to network problems. Correlate withkubectl logsfrom the cache or registry. - Is it safe to store the registry password in a plain‑text secret?
Kubernetes secrets are base64‑encoded, not encrypted. On edge nodes enableEncryptionConfigurationfor secret data at rest, and restrict secret access via RBAC. - What if the edge node loses connectivity for an extended period?
The local cache will serve any previously pulled layers. Ensure the cache’s storage (e.g.,emptyDiron SSD) is sized to hold all required images for the expected outage duration.