Trusted Sources Only – Guardrails Against Unapproved Images

Duration: ~15 minutes

Overview

Limit which registries the cluster may pull from, then confirm what you ran by registry host and sha256 digest.

Try a deployment from an untrusted source, block it with an allow-list, then deploy a trusted UBI image and verify host plus digest provenance.

Why it matters

Pulls from arbitrary registries mean unknown builders and unvetted contents. An allow-list keeps traffic on registries you trust and cuts a lot of avoidable CVE noise from random public tags.

What does it solve

  • Accidental pulls from arbitrary public defaults

  • Image provenance ambiguity

  • Higher vulnerability noise baseline

  • Reproducibility issues with :latest

Your Mission

Supply-chain attackers ship malware through any registry that will take a tag. Your mission: put an allow-list on the door, prove an untrusted image is denied, then run only a trusted UBI workload you can fingerprint by host and digest.

Click each step only if you need a hint.

Mark the namespace as under registry lockdown
oc new-project 101-07-i-trusted
oc label namespace 101-07-i-trusted trusted-registry-enforce=true --overwrite
Install the allow-list (cluster-admin) — close arbitrary pulls
oc apply -f - <<'EOF'
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: allow-trusted-registries-pods
spec:
  failurePolicy: Fail
  matchConstraints:
    matchPolicy: Equivalent
    resourceRules:
    - apiGroups: [""]
      apiVersions: ["v1"]
      operations: ["CREATE","UPDATE"]
      resources: [pods]
  validations:
  - expression: "object.spec.containers.all(c,\n        c.image.startsWith('registry.access.redhat.com') ||\n        c.image.startsWith('registry.redhat.io') ||\n        c.image.startsWith('quay.io') ||\n        c.image.startsWith('image-registry.openshift-image-registry.svc')\n     )"
    message: "Pod rejected: only Red Hat registries, Quay.io, or internal registry images allowed."
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: trusted-images-only-pods
spec:
  policyName: allow-trusted-registries-pods
  validationActions: ["Deny"]
  matchResources:
    namespaceSelector:
      matchLabels:
        trusted-registry-enforce: "true"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: allow-trusted-registries-deployments
spec:
  failurePolicy: Fail
  matchConstraints:
    matchPolicy: Equivalent
    resourceRules:
    - apiGroups: ["apps"]
      apiVersions: ["v1"]
      operations: ["CREATE","UPDATE"]
      resources: [deployments]
  validations:
  - expression: "object.spec.template.spec.containers.all(c,\n        c.image.startsWith('registry.access.redhat.com') ||\n        c.image.startsWith('registry.redhat.io') ||\n        c.image.startsWith('quay.io') ||\n        c.image.startsWith('image-registry.openshift-image-registry.svc')\n     )"
    message: "Deployment rejected: only Red Hat registries, Quay.io, or internal registry images allowed."
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: trusted-images-only-deployments
spec:
  policyName: allow-trusted-registries-deployments
  validationActions: ["Deny"]
  matchResources:
    namespaceSelector:
      matchLabels:
        trusted-registry-enforce: "true"
EOF
Launch the untrusted payload — expect Deny
oc -n 101-07-i-trusted create deployment bad --image=docker.io/library/nginx:latest || true
Deploy only from an approved registry
oc -n 101-07-i-trusted create deployment good --image=registry.access.redhat.com/ubi9/ubi:9.6 -- sleep infinity
oc -n 101-07-i-trusted wait --for=condition=Available deployment/good --timeout=60s
Fingerprint provenance — host + digest

Prove what actually runs so a floated :latest tag cannot quietly swap in attacker content.

oc -n 101-07-i-trusted describe pod -l app=good | grep -E 'Image:|Image ID:'

What to look for:

  • Image: trusted registry host (registry.access.redhat.com, registry.redhat.io, quay.io, or your internal registry)

  • Image ID: immutable sha256 digest (tags can move; digests cannot)

If both are correct, you have a basic provenance check: approved source and exact, verifiable content.

Example (UBI 9.6):

  • Image: registry.redhat.io/ubi9/ubi:9.6 (trusted source)

  • Image ID: …@sha256:dbc1e98d14a022542e45b5f22e0206d3f86b5bdf237b58ee7170c9ddd1b3a283 (immutable digest)

Cleanup
oc delete project 101-07-i-trusted --wait=false
oc delete validatingadmissionpolicybinding trusted-images-only-pods || true
oc delete validatingadmissionpolicybinding trusted-images-only-deployments || true
oc delete validatingadmissionpolicy allow-trusted-registries-pods || true
oc delete validatingadmissionpolicy allow-trusted-registries-deployments || true

Debrief

You marked a namespace for registry lockdown, blocked an untrusted pull, allowed an approved source, and fingerprinted host plus digest for provenance.

What breaks without this:

  • Arbitrary registries → unknown base images and supply-chain risk

  • Floating tags like :latest → non-reproducible, mutable runtime

  • No allow-list → policy gaps between teams and clusters

Controls that matter: trusted-registry allow-lists (admission), digest pins for important workloads, progressive labels for opt-in, and later signature verification.

Quick facts: mirror public images into a registry you control; treat :latest as a demo habit, not production practice.

giphy

Cleanup

Before moving to the next module, run the lab cleanup script to reset transient resources from this module.

cd ~/openshift-security-roadshow
bash setup/lab-cleanup.sh --module 101-07