𝔩𝔢𝔩𝕠𝔭𝔢𝔷
Theme

Homelab

Image Digest Pinning

Pinning Container Images by Digest: Preventing Upstream Tampering

Overview

This article replaces mutable image tags with immutable SHA256 digests across all HelmRelease values and sidecar containers. A tag can be re-pushed upstream with different content; a digest can't.

Before You Begin

Prerequisites

What We're Pinning

AppCurrent ImageType
Plex1.42.2.10156-f737b826cHelm values tag
FileBrowserfilebrowser/filebrowser:v2.31.2Sidecar (postRenderers)
playit-agentghcr.io/playit-cloud/playit-agent:latestSidecar (postRenderers)

Why Digests Over Tags

Tags are mutable pointers1 — the same tag can point to different content over time:

Day 1: plex:1.42.2 → sha256:abc123  (legitimate)
Day 2: plex:1.42.2 → sha256:def456  (compromised)

Your pod restarts and pulls def456 without warning. Digests are immutable:

plex@sha256:abc123 → always the same content
Note

The playit-agent:latest tag is the most urgent — :latest changes with every release.

How to Update Digests

When upgrading an image version, look up the new digest and update both the tag comment and digest. This is a manual process that replaces checking for tag updates.

Look Up Digests

Install: crane

crane2 is a tool for interacting with container registries without pulling full images.

brew install crane

Lookup: All Current Digests

crane digest docker.io/plexinc/pms-docker:1.42.2.10156-f737b826c
crane digest docker.io/filebrowser/filebrowser:v2.31.2
crane digest ghcr.io/playit-cloud/playit-agent:latest

Note each digest (format: sha256:...).

Pin Plex Image

HelmRelease: Pin Plex Digest

k8s/apps/plex/helmrelease.yaml:

# ... existing HelmRelease header ...
values:
    image:
        tag: "1.42.2.10156-f737b826c@sha256:<PLEX-DIGEST>"

    # ... existing extraEnv, service, pms ...
Note

The tag@sha256: format preserves readability while enforcing the digest. Kubernetes pulls by digest when both are present3.

Pin Sidecar Images

HelmRelease: Pin FileBrowser Digest

In the postRenderers section of k8s/apps/plex/helmrelease.yaml:

- op: add
  path: /spec/template/spec/containers/-
  value:
      name: filebrowser
      image: filebrowser/filebrowser:v2.31.2@sha256:<FILEBROWSER-DIGEST>
      # ... existing args, ports, volumeMounts, resources ...

HelmRelease: Pin playit-agent Digest (Minecraft)

k8s/apps/minecraft/helmrelease.yaml:

- op: add
  path: /spec/template/spec/containers/-
  value:
      name: playit-agent
      image: ghcr.io/playit-cloud/playit-agent:latest@sha256:<PLAYIT-DIGEST>
      # ... existing env ...

HelmRelease: Pin playit-agent Digest (Factorio)

k8s/apps/factorio/helmrelease.yaml:

- op: add
  path: /spec/template/spec/containers/-
  value:
      name: playit-agent
      image: ghcr.io/playit-cloud/playit-agent:latest@sha256:<PLAYIT-DIGEST>
      # ... existing env ...

Deploy Digest Pinning

Git: Commit Changes

cd ~/homelab
git add k8s/apps/plex/helmrelease.yaml k8s/apps/minecraft/helmrelease.yaml k8s/apps/factorio/helmrelease.yaml
git commit -m "security(apps): pin all container images by digest"
git push

Flux: Reconcile

flux reconcile source git flux-system && flux reconcile kustomization sync

Verify Digest Pinning

Verify: Pods Running

kubectl get pods -n plex
kubectl get pods -n minecraft
kubectl get pods -n factorio

Expected: All pods running with same container count as before.

Verify: Images Use Digests

kubectl get pod -n plex plex-plex-media-server-0 -o jsonpath='{.status.containerStatuses[*].imageID}'

Expected: Output contains sha256: digests matching what you pinned.

Addendum: Completing the Sweep

A July 2026 audit found the app HelmReleases carrying bare tags — the digests this article specifies hadn't survived into the running manifests, and the Factorio playit sidecar had regressed to :latest, the exact case flagged above as most urgent. Version bumps are how it happens: a tag gets updated, the digest doesn't come along, and nothing fails — the manifests keep working, just unpinned.

Two states complete the sweep:

  • v1.13.3 restores digest pins across every app image: Plex, FileBrowser, both playit sidecars, and the Factorio server image.
  • v1.13.4 catches the invisible one: the Minecraft server image never appeared in the HelmRelease at all — it came from the chart's default, which was :latest. Charts can pull images your values never mention.

Audit what's actually running, not what your values say:

kubectl get pods -A -o jsonpath='{range .items[*].status.containerStatuses[*]}{.imageID}{"
"}{end}' | grep -cv sha256

Expected: 0 - every running container resolves to a digest.

Note

When bumping an image version, look up and update the digest in the same edit. A tag change without a digest change is the drift starting over.

Next Steps

With every image pinned by digest, the remaining supply-chain gap is who can change what deploys - close it with a durable identity and a deploy gate.

See: GPG Identity

Resources

Footnotes

  1. Docker, "Content trust in Docker," docs.docker.com. Accessed: Apr. 13, 2026. [Online]. Available: https://docs.docker.com/engine/security/trust/

  2. Google, "crane - a tool for interacting with remote images and registries," github.com. Accessed: Apr. 13, 2026. [Online]. Available: https://github.com/google/go-containerregistry/tree/main/cmd/crane

  3. Kubernetes, "Images - Using image digests," kubernetes.io. Accessed: Apr. 13, 2026. [Online]. Available: https://kubernetes.io/docs/concepts/containers/images/#image-pull-policy

Previous
Secure Node Re-image