Private registry authentication
Give the StackRadar scanner pull access to private images: reuse imagePullSecrets, or mount a credential for Amazon ECR, Google Artifact Registry, Azure ACR, Docker Hub or GHCR.
The scanner pulls every image it scans itself, from the registry, rather than reading layers off the node. So for any workload running from a private registry it needs credentials of its own — the node's IAM role, an attached ACR or a GKE node service account authenticate the kubelet, not the scanner.
This is what the cluster page's coverage card means when it shows “Registry authentication failed” naming a registry host: the registry rejected the scanner's pull, and those images will never get an SBOM until it has credentials — waiting does not fix it. (A “rate-limited, retrying” row is different: that one heals on its own.) Once credentials are in place the next scan uploads and the error clears by itself; there is nothing to reset.
Which method to use
| Your setup | Use |
|---|---|
Pods declare imagePullSecrets (Docker Hub, GHCR, Harbor, GitLab, DigitalOcean…) | Reuse those Secrets — nothing new to mint |
| Pods carry no pull Secret because the node authenticates — the usual setup with ECR, Artifact Registry and ACR | Give the scanner its own credential, per registry below |
| Both, or several registries | Both methods combine; one Secret can hold several registries |
Reusing the pull Secrets your workloads already have
When a pod declares imagePullSecrets, the scanner can read those Secrets in the pod's own namespace and use them to pull that pod's images. It is off by default, because it costs one permission — secrets get in the scanner's ClusterRole — and the chart will not grant that on every Secret in the cluster. Turning it on means naming the pull Secrets it may read, which the chart translates into an RBAC resourceNames restriction; leave the list empty and the install fails rather than rendering a cluster-wide grant.
Find the Secret names on the pods that pull from the private registry. Ask the pods, not the Deployment or CronJob: when the Secret is attached to a ServiceAccount (DigitalOcean's registry integration does this), Kubernetes copies it onto each pod at creation and the controller's template stays empty.
kubectl get pods -n <namespace> -o jsonpath='{range .items[*]}{.metadata.name}: {.spec.imagePullSecrets[*].name}{"\n"}{end}'If the pods are gone — a finished Job — read the ServiceAccounts in that namespace instead:
kubectl get sa -n <namespace> -o jsonpath='{range .items[*]}{.metadata.name}: {.imagePullSecrets[*].name}{"\n"}{end}'helm upgrade stackradar-scanner oci://ghcr.io/lockdep/charts/stackradar-scanner \
--namespace stackradar --reuse-values \
--version <installed-version> \
--set-string scanner.resolveImagePullSecrets=true \
--set 'scanner.imagePullSecretNames={regcred,ghcr-creds}'Two things to know before you set it. Names match in every namespace — a Secret called regcred anywhere in the cluster becomes readable — so use distinct names if that distinction matters to you. And a Secret left off the list is simply denied: images that needed it fall back to an anonymous pull, and the scanner logs a warning naming the Secret and its namespace rather than failing the sweep.
Giving the scanner its own credential
dockerConfigSecret names a standard kubernetes.io/dockerconfigjson Secret in the scanner's namespace, mounted read-only. It covers every pull that a pod's own imagePullSecrets did not; where both apply to the same registry, the workload's credential wins.
kubectl create secret docker-registry stackradar-registry \
--namespace stackradar \
--docker-server=<registry-host> \
--docker-username=<username> \
--docker-password=<password>
helm upgrade stackradar-scanner oci://ghcr.io/lockdep/charts/stackradar-scanner \
--namespace stackradar --reuse-values \
--version <installed-version> \
--set dockerConfigSecret=stackradar-registryMake the credential read-only and scope it to the repositories you want scanned. The scanner reads the mounted file at the start of every scan, so a Secret you rotate later is picked up within a minute or so, without restarting the pod. What each registry wants for <registry-host>, username and password follows.
Amazon ECR
ECR has no long-lived registry password: the username is AWS and the password is a token that expires after 12 hours. Create the Secret once by hand —
kubectl create secret docker-registry stackradar-registry \
--namespace stackradar \
--docker-server=<account>.dkr.ecr.<region>.amazonaws.com \
--docker-username=AWS \
--docker-password="$(aws ecr get-login-password --region <region>)"
helm upgrade stackradar-scanner oci://ghcr.io/lockdep/charts/stackradar-scanner \
--namespace stackradar --reuse-values \
--version <installed-version> \
--set dockerConfigSecret=stackradar-registry— and keep it fresh with a CronJob. The one below gets its AWS credentials from IRSA or EKS Pod Identity, asks ECR for a new token every six hours, and patches that one Secret; its Role can read and patch stackradar-registry and nothing else, which is why the Secret has to exist first.
apiVersion: v1
kind: ServiceAccount
metadata:
name: ecr-token-refresh
namespace: stackradar
annotations:
# IRSA: a role with AmazonEC2ContainerRegistryReadOnly. With EKS Pod
# Identity, drop the annotation and create a pod identity association.
eks.amazonaws.com/role-arn: arn:aws:iam::<account>:role/<role>
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: ecr-token-refresh
namespace: stackradar
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["stackradar-registry"]
verbs: ["get", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ecr-token-refresh
namespace: stackradar
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: ecr-token-refresh
subjects:
- kind: ServiceAccount
name: ecr-token-refresh
namespace: stackradar
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: ecr-token-refresh
namespace: stackradar
spec:
schedule: "0 */6 * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 1
jobTemplate:
spec:
template:
spec:
serviceAccountName: ecr-token-refresh
restartPolicy: OnFailure
volumes:
- name: work
emptyDir: {}
initContainers:
# Asks ECR for a token and writes the Secret manifest.
- name: token
image: public.ecr.aws/aws-cli/aws-cli:latest
command: ["/bin/sh", "-ec"]
args:
- |
auth=$(printf 'AWS:%s' "$(aws ecr get-login-password --region "$REGION")" | base64 | tr -d '\n')
config=$(printf '{"auths":{"%s":{"auth":"%s"}}}' "$REGISTRY" "$auth" | base64 | tr -d '\n')
printf '{"apiVersion":"v1","kind":"Secret","type":"kubernetes.io/dockerconfigjson","metadata":{"name":"stackradar-registry","namespace":"stackradar"},"data":{".dockerconfigjson":"%s"}}' "$config" > /work/secret.json
env:
- name: REGION
value: <region>
- name: REGISTRY
value: <account>.dkr.ecr.<region>.amazonaws.com
volumeMounts:
- name: work
mountPath: /work
containers:
# Applies it. The image holds kubectl and nothing else.
- name: apply
image: registry.k8s.io/kubectl:v1.34.0
args: ["apply", "-f", "/work/secret.json"]
volumeMounts:
- name: work
mountPath: /workThe IAM role needs ecr:GetAuthorizationToken plus read access to the repositories — the managed AmazonEC2ContainerRegistryReadOnly policy covers both. Check the first run with kubectl create job --from=cronjob/ecr-token-refresh ecr-refresh-now -n stackradar. Treat the manifest as a starting point: pin the image tags you have vetted, and add a pod securityContext if the namespace enforces the restricted Pod Security Standard.
Google Artifact Registry
Create a Google service account with the roles/artifactregistry.reader role on the repositories to scan, and a JSON key for it. The username is the literal _json_key and the password is the key file's contents:
kubectl create secret docker-registry stackradar-registry \
--namespace stackradar \
--docker-server=<location>-docker.pkg.dev \
--docker-username=_json_key \
--docker-password="$(cat key.json)"
helm upgrade stackradar-scanner oci://ghcr.io/lockdep/charts/stackradar-scanner \
--namespace stackradar --reuse-values \
--version <installed-version> \
--set dockerConfigSecret=stackradar-registryThe host is per location (europe-west1-docker.pkg.dev, us-docker.pkg.dev…); images in more than one location need one entry each, as do legacy gcr.io hosts. If your organization blocks service account keys (iam.disableServiceAccountKeyCreation), this path is closed — see workload identity.
Azure Container Registry
az aks update --attach-acr grants the kubelet identity, not the scanner. Give the scanner a service principal with the AcrPull role on the registry; its app ID is the username and its secret the password:
ACR_ID=$(az acr show --name <registry> --query id --output tsv)
az ad sp create-for-rbac --name stackradar-scanner \
--scopes "$ACR_ID" --role acrpull \
--query "{username: appId, password: password}"
kubectl create secret docker-registry stackradar-registry \
--namespace stackradar \
--docker-server=<registry>.azurecr.io \
--docker-username=<appId> \
--docker-password=<password>
helm upgrade stackradar-scanner oci://ghcr.io/lockdep/charts/stackradar-scanner \
--namespace stackradar --reuse-values \
--version <installed-version> \
--set dockerConfigSecret=stackradar-registryA service principal secret expires — after one year by default — so put the renewal in your calendar, or the coverage card will remind you with an authentication failure. An ACR repository-scoped token works the same way (token name as username) when you want to limit the scanner to specific repositories.
Docker Hub, GHCR and others
| Registry | --docker-server | Credential |
|---|---|---|
| Docker Hub | https://index.docker.io/v1/ | Username + a read-only access token. Worth setting even for public images: an authenticated pull has a far higher rate limit than an anonymous one |
| GitHub Container Registry | ghcr.io | Username + a personal access token (classic) with read:packages |
| GitLab | registry.gitlab.com or your own host | A deploy token with read_registry |
| Harbor, Quay, Artifactory | Your registry host | A robot or service account with pull-only access |
If the pods that run these images already carry a pull Secret, reusing it is less to maintain than a second credential.
Several registries in one Secret
dockerConfigSecret takes one Secret, and a Docker config holds as many registries as you like. Write the file yourself — each auth is username:password, base64-encoded — and create the Secret from it:
{
"auths": {
"<registry>.azurecr.io": { "auth": "<base64 of appId:password>" },
"ghcr.io": { "auth": "<base64 of username:token>" }
}
}printf '%s' '<username>:<password>' | base64 # one per registry
kubectl create secret generic stackradar-registry \
--namespace stackradar \
--type=kubernetes.io/dockerconfigjson \
--from-file=.dockerconfigjson=config.jsonDon't copy ~/.docker/config.json from a workstation: Docker Desktop keeps credentials in the OS keychain, and the file it leaves behind names a credential helper the scanner image does not have.
Cloud workload identity
On EKS, GKE and AKS the idiomatic alternative to a static credential is giving the scanner's ServiceAccount a cloud identity with read access to your registry, so the pod receives short-lived credentials from the cloud instead. The chart exposes the hooks each cloud's mechanism expects:
- EKS (IRSA) —
serviceAccount.annotations."eks.amazonaws.com/role-arn", pointing at a role with ECR read access. - GKE (Workload Identity) —
serviceAccount.annotations."iam.gke.io/gcp-service-account", bound to a Google service account with Artifact Registry reader. - AKS (Azure Workload Identity) —
podLabels."azure.workload.identity/use": "true"and the client ID viapodAnnotations."azure.workload.identity/client-id", for an identity withAcrPull.
Checking that it worked
- The
helm upgraderestarts the scanner, and a fresh scanner tries every running image again. If you only changed the contents of an existing Secret, nothing restarts — failed images are retried on the next sweep (every six hours by default), or right away withkubectl rollout restart deployment -n stackradar -l app.kubernetes.io/name=stackradar-scanner. - Watch the cluster page. The “Registry authentication failed” row for that host disappears as its images upload, and scanned coverage climbs.
- Still failing? The scanner log names the image and the registry's answer:
kubectl logs -n stackradar -l app.kubernetes.io/name=stackradar-scanner. A401or403from the registry with a Secret in place almost always means the host in the Secret does not match the host in the image reference exactly, or the credential lacks pull rights on that repository.
imagePullSecrets value, which is how the kubelet pulls the scanner's own image when you have mirrored it into a private registry. That one is about starting the scanner; everything on this page is about what the scanner can scan once it is running.Next steps
- Restricted networks & mirroringRun the StackRadar scanner behind restricted egress: chart and image mirroring, proxies and custom CAs, the one endpoint to allow, and what leaves the network.
- TroubleshootingFixes for common StackRadar scanner issues: 401/403 API errors, a cluster that never appears in the dashboard, Helm install failures, and missing SBOMs.