DEV Community

ZèD
ZèD

Posted on Edited on

Configuring GitHub Container Registry with Kubernetes for Private Images

Configuring GitHub Container Registry with Kubernetes for private images

Private container images represent standard production deployment artifacts. When container images reside in GitHub Container Registry (GHCR) at ghcr.io, Kubernetes requires valid registry credentials to authenticate image pull requests during Pod scheduling.

Configuring declarative image pull Secrets and ServiceAccounts enables automated, decoupled authentication against GitHub Container Registry without manual node-level logins.

The problem and production context

Kubernetes cluster nodes pull container images anonymously by default. When referencing private registry artifacts, anonymous requests fail.

  • Failure scenario: A Deployment references a private image at ghcr.io/my-org/private-api:1.0.0. During pod scheduling, the kubelet issues an unauthenticated pull request to ghcr.io. The registry responds with HTTP 401 Unauthorized, causing the Pod to enter ErrImagePull and loop indefinitely in ImagePullBackOff.
  • Why default approaches fall short: Executing manual docker login commands on individual cluster nodes violates declarative infrastructure principles, fails on dynamically autoscaled node groups, and introduces configuration drift. Inlining raw registry secrets directly inside every workload manifest duplicates credential references and exposes base64-encoded tokens in source control.
  • Production impact: Workloads fail to boot during deployments or node drain events, deployment pipelines stall, and hardcoded registry credentials leaked into Git repositories compromise container registry access.

Declarative credential management provides key operational advantages:

  • Restricts application container images from public access.
  • Enables automated private image pulls directly from ghcr.io.
  • Eliminates manual node-level Docker logins.
  • Decouples credential management from workload manifests using ServiceAccounts.
  • Integrates cleanly with GitOps workflows, Helm charts, and Kustomize overlays.

Mental model and core concepts

Pulling private container images from GHCR requires understanding registry URI formatting, Docker credential structures, and Kubernetes identity inheritance.

1. GitHub Container Registry image naming and permissions

Private GHCR image references follow a standardized naming convention:

ghcr.io/OWNER/IMAGE_NAME:TAG
Enter fullscreen mode Exit fullscreen mode

Example:

ghcr.io/my-org/private-api:1.0.0
Enter fullscreen mode Exit fullscreen mode

Pulling private images requires authentication with a personal access token (classic or fine-grained) or a GitHub Actions token configured with at least the read:packages permission scope. When credentials are absent or invalid, the Pod terminates with:

ImagePullBackOff
ErrImagePull
unauthorized
Enter fullscreen mode Exit fullscreen mode

2. Registry credential Secret structure

Kubernetes authenticates against private container registries using Secrets of type kubernetes.io/dockerconfigjson. Prior to base64 encoding, the Docker configuration payload contains:

{
  "auths": {
    "ghcr.io": {
      "username": "YOUR_GITHUB_USERNAME",
      "password": "YOUR_GITHUB_TOKEN",
      "email": "you@example.com",
      "auth": "BASE64_USERNAME_COLON_TOKEN"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The auth value represents the base64-encoded string of USERNAME:TOKEN:

base64(YOUR_GITHUB_USERNAME:YOUR_GITHUB_TOKEN)
Enter fullscreen mode Exit fullscreen mode

The entire JSON structure is base64-encoded and embedded in the .dockerconfigjson field of the Secret manifest. The Secret must reside in the exact namespace of the workloads consuming it.

3. ServiceAccount-based credential decoupling

Rather than defining imagePullSecrets across every Deployment, StatefulSet, or Job manifest, attaching the Secret to a ServiceAccount enables automatic credential inheritance. When a Pod specifies serviceAccountName, the kubelet automatically inherits the referenced image pull secrets during container initialization.

4. Direct workload credential binding

As an alternative to ServiceAccount binding, individual Pod specifications can declare imagePullSecrets directly. While functional for single-manifest setups, it couples workload specifications to namespace secret names and requires repetitive declarations across services.

5. GitOps secret management pipelines

Raw Kubernetes Secrets use base64 serialization, not cryptographic encryption. Committing raw .dockerconfigjson manifests to version control leaks access tokens. Production GitOps workflows synchronize credentials from external vaults using operators:

Secret Manager
  -> ExternalSecret
  -> Kubernetes Secret
  -> ServiceAccount
  -> Deployment
Enter fullscreen mode Exit fullscreen mode

Dedicated tools such as External Secrets Operator, Sealed Secrets, Mozilla SOPS, or HashiCorp Vault manage the lifecycle, rotation, and injection of registry secrets.

Production implementation

The following declarative manifests provide configurations for namespaces, Secrets, ServiceAccounts, Deployments, and ExternalSecret integrations.

Complete declarative configuration defining the namespace, registry Secret, ServiceAccount, and private container Deployment:

apiVersion: v1
kind: Namespace
metadata:
  name: demo

---
apiVersion: v1
kind: Secret
metadata:
  name: ghcr-secret
  namespace: demo
type: kubernetes.io/dockerconfigjson
data:
  .dockerconfigjson: BASE64_DOCKER_CONFIG_JSON

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-service-account
  namespace: demo
imagePullSecrets:
  - name: ghcr-secret

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: private-api
  namespace: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: private-api
  template:
    metadata:
      labels:
        app: private-api
    spec:
      serviceAccountName: app-service-account
      containers:
        - name: private-api
          image: ghcr.io/my-org/private-api:1.0.0
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080
Enter fullscreen mode Exit fullscreen mode

Production configuration referencing a versioned image path (ghcr.io/octocat/order-service:2026.05.14) in a dedicated namespace:

apiVersion: v1
kind: Namespace
metadata:
  name: production

---
apiVersion: v1
kind: Secret
metadata:
  name: ghcr-secret
  namespace: production
type: kubernetes.io/dockerconfigjson
data:
  .dockerconfigjson: BASE64_DOCKER_CONFIG_JSON

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: order-service-sa
  namespace: production
imagePullSecrets:
  - name: ghcr-secret

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
  namespace: production
spec:
  replicas: 2
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      serviceAccountName: order-service-sa
      containers:
        - name: order-service
          image: ghcr.io/octocat/order-service:2026.05.14
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080
Enter fullscreen mode Exit fullscreen mode

Direct imagePullSecrets reference in the Pod specification for standalone workloads:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: private-api
  namespace: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: private-api
  template:
    metadata:
      labels:
        app: private-api
    spec:
      imagePullSecrets:
        - name: ghcr-secret
      containers:
        - name: private-api
          image: ghcr.io/my-org/private-api:1.0.0
Enter fullscreen mode Exit fullscreen mode

Automated secret synchronization using External Secrets Operator:

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: ghcr-secret
  namespace: demo
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: cluster-secret-store
    kind: ClusterSecretStore
  target:
    name: ghcr-secret
    creationPolicy: Owner
    template:
      type: kubernetes.io/dockerconfigjson
      data:
        .dockerconfigjson: |
          {
            "auths": {
              "ghcr.io": {
                "username": "{{ .githubUsername }}",
                "password": "{{ .githubToken }}",
                "email": "you@example.com",
                "auth": "{{ printf "%s:%s" .githubUsername .githubToken | b64enc }}"
              }
            }
          }
  data:
    - secretKey: githubUsername
      remoteRef:
        key: github-username
    - secretKey: githubToken
      remoteRef:
        key: github-token
Enter fullscreen mode Exit fullscreen mode

Architectural trade-offs and edge cases

Managing private image authentication across Kubernetes environments involves operational trade-offs regarding credential scope and secret distribution.

  • Latency versus consistency: Attaching image pull secrets to ServiceAccounts centralizes credential maintenance and eliminates manifest duplication. ServiceAccount secret inheritance applies at Pod creation time; updating a Secret does not invalidate already running images on nodes until a restart or tag re-evaluation occurs.
  • Failure recovery: If a GitHub personal access token expires or is revoked, kubelet nodes cannot pull images when scheduling new Pods or scaling Deployments. Clusters should configure monitoring on ImagePullBackOff events and automate token rotations using external secret operators.
  • Scale limitations: Kubernetes Secrets are namespace-scoped. Sharing registry credentials across hundreds of isolated tenant namespaces requires automated secret replication controllers (such as Reflector or External Secrets Operator) or node-level credential providers, as Secrets cannot be cross-referenced across namespace boundaries.

Common anti-patterns and gotchas

  • Cross-namespace Secret referencing: Creating ghcr-secret in the default namespace and attempting to reference it from workloads in demo fails because Kubernetes Secrets are strictly namespace-scoped.
  • Insufficient token scope: Generating a GitHub personal access token without the read:packages permission scope causes authentication to fail with HTTP 401 Unauthorized.
  • Unconfigured package repository visibility: Failing to grant the token access to the specific organization or repository package settings blocks pull operations even with valid user credentials.
  • Committing base64 Secret manifests to Git: Storing base64-encoded Secret YAMLs in version control exposes plaintext GitHub tokens to anyone with repository read access.
  • Divergent ServiceAccount mappings: Specifying imagePullSecrets on a Deployment while assigning a ServiceAccount that references a missing Secret causes pull failures.
  • Malformed registry image URIs: Misspelling the registry domain (ghcr.io), organization name, or repository slug produces immediate image resolution errors.
  • Mutable tag references: Relying on mutable tags like :latest instead of immutable, versioned tags or sha256 digests leads to non-deterministic deployments across cluster nodes.

Implementation checklist

  1. Generate a GitHub personal access token or deploy token configured with the read:packages scope.
  2. Grant the token read permissions within the target GitHub organization or repository settings.
  3. Construct the Docker configuration JSON with base64-encoded username:token auth strings.
  4. Deploy the kubernetes.io/dockerconfigjson Secret into the destination Kubernetes namespace.
  5. Create a ServiceAccount referencing the registry Secret under imagePullSecrets.
  6. Configure workload Deployments to declare serviceAccountName pointing to the authenticated ServiceAccount.
  7. Integrate an external secret manager such as External Secrets Operator or Sealed Secrets to automate credential rotation.
  8. Define dedicated ServiceAccounts per workload or namespace according to the principle of least privilege.
  9. Automate image digest pinning and tag promotion through your CI/CD delivery pipeline.
  10. Establish an automated rotation policy for GitHub personal access tokens and deploy keys.
  11. Configure alerting and monitoring for ImagePullBackOff and ErrImagePull events.

Top comments (0)