Documentation menu

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 setupUse
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 ACRGive the scanner its own credential, per registry below
Both, or several registriesBoth 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.

bash
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:

bash
kubectl get sa -n <namespace> -o jsonpath='{range .items[*]}{.metadata.name}: {.imagePullSecrets[*].name}{"\n"}{end}'
bash
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.

bash
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-registry

Make 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 —

bash
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.

ecr-token-refresh.yaml
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: /work

The 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:

bash
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-registry

The 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:

bash
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-registry

A 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-serverCredential
Docker Hubhttps://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 Registryghcr.ioUsername + a personal access token (classic) with read:packages
GitLabregistry.gitlab.com or your own hostA deploy token with read_registry
Harbor, Quay, ArtifactoryYour registry hostA 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:

config.json
{
  "auths": {
    "<registry>.azurecr.io": { "auth": "<base64 of appId:password>" },
    "ghcr.io": { "auth": "<base64 of username:token>" }
  }
}
bash
printf '%s' '<username>:<password>' | base64    # one per registry

kubectl create secret generic stackradar-registry \
    --namespace stackradar \
    --type=kubernetes.io/dockerconfigjson \
    --from-file=.dockerconfigjson=config.json

Don'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 via podAnnotations."azure.workload.identity/client-id", for an identity with AcrPull.
This path is not yet verified end to end. The chart renders what each cloud documents and the injected credentials reach the scanner's environment, but the image ships no cloud credential helpers, and whether the registry client picks the credentials up on its own has not been confirmed on a real cluster of each kind. The static credentials above are the path known to work. Exact commands, and where to tell us what you find, are in the chart README.

Checking that it worked

  1. The helm upgrade restarts 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 with kubectl rollout restart deployment -n stackradar -l app.kubernetes.io/name=stackradar-scanner.
  2. Watch the cluster page. The “Registry authentication failed” row for that host disappears as its images upload, and scanned coverage climbs.
  3. Still failing? The scanner log names the image and the registry's answer: kubectl logs -n stackradar -l app.kubernetes.io/name=stackradar-scanner. A 401 or 403 from 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.
Not to be confused with the chart's 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