43.4 Image Security: Scanning, Signing, and Trusted Registries
Let’s be honest: your container images are the front door to your cluster, and right now, you’re probably leaving the key under the mat. You wouldn’t run curl http://strange-website.com | sudo bash on a production server, but if you’re pulling random, unsigned images from public registries, you’re doing the containerized equivalent. The image is the one artifact that contains everything that will run, from your brilliant application code to a forgotten, vulnerable curl binary from 2014. Securing this isn’t just a good idea; it’s the absolute bedrock. We’ll tackle this in three parts: making sure your images aren’t full of holes (scanning), proving they came from you and not an imposter (signing), and controlling where you get them from (trusted registries).
Why You Can’t Just docker pull and pray
Hope is not a strategy. A public image, even from a reputable maintainer, can have critical vulnerabilities for one simple reason: it was built on a Tuesday. No malice intended; its creator just used the latest base image available that day, which included a library that had a CVE published on Wednesday. You’re now running that CVE. The only way to know is to continuously check. Furthermore, anyone can push an image named nginx/nginx to a public registry. Without a cryptographic signature, you have no way to verify that the image you’re pulling is the one its maintainers actually built and not a tampered-with malicious copy.
Scanning Images: Your Automated Code Auditor
Think of a vulnerability scanner as a brutally honest, hyper-fast security consultant who reads every line of code in your image and cross-references it against a giant database of known flaws (CVEs). You should run this before the image even gets to a registry, ideally in your CI/CD pipeline.
Here’s how you can do a quick local scan with Trivy, a fantastic open-source tool, to see what you’re dealing with:
# Scan a local image you've built
trivy image your-app:latest
# Scan a remote image from a registry without pulling it first
trivy registry python:3.9-slim-buster
# Get *just* the exit code to fail a CI build if critical vulns are found
trivy image --severity CRITICAL,HIGH --exit-code 1 your-app:latest
The output will list vulnerabilities, their severity, and, crucially, the exact library and version they’re in. The best practice? Make this scan a mandatory gate in your pipeline. If the build finds new critical vulnerabilities, the build fails. No exceptions. This creates the beautiful pressure necessary to keep your base images and dependencies updated.
Signing and Verifying Images: The Digital Wax Seal
Scanning tells you what is in the image. Signing tells you who it’s from and that it hasn’t been altered. This is where Cosign comes in. It uses public-key cryptography to attach a signature to your image in the registry.
First, you need a keypair. You can generate one with Cosign:
cosign generate-key-pair
This creates cosign.key (your private key - guard this with your life) and cosign.pub (your public key - you’ll distribute this).
Now, let’s sign an image you’ve pushed to a registry. You’ll need to set the COSIGN_EXPERIMENTAL environment variable to 1 to use the built-in keyless mode with Fulcio (a certificate authority for code signing) for simplicity here, which is great for public projects.
# For a truly robust setup, use your generated private key
cosign sign --key cosign.key your-registry.com/your-app:v1.0.0
# Or, for a quick example using keyless signing (requires login)
export COSIGN_EXPERIMENTAL=1
cosign sign your-registry.com/your-app:v1.0.0
The magic happens when you go to deploy. You can configure Kubernetes to verify these signatures before it’s even allowed to pull the image, using a admission controller like Sigstore’s policy-controller. This is the killer feature. It moves security from a “hopefully the team did it in CI” checklist item to an enforceable platform policy.
Enforcing Trust with Trusted Registries and Admission Control
This is where we tie it all together. You need to enforce two things: that images only come from registries you trust (blocking my-evil-image.net/backdoor:latest) and that they are signed.
First, ditch the default, permissive PodSecurity standards. We’re going to define an ImagePolicy that demands signatures. Here’s a sample manifest for the Sigstore policy-controller:
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
name: require-production-signature
spec:
images:
- glob: "your-registry.com/production/**" # Images in our production repo MUST be signed
- glob: "your-registry.com/staging/**" # Let's be a bit more lax in staging, maybe
authorities:
- key:
data: |
-----BEGIN PUBLIC KEY-----
# Your cosign.pub content goes here
-----END PUBLIC KEY-----
static: "your-registry.com/production/**" # This enforces the signature came from the expected source
But we also need to block all other registries. For that, we use a Kubernetes ValidatingAdmissionWebhook, often bundled with commercial distributions or open-source projects like OPA/Gatekeeper. A simple rule to only allow your corporate registry might look like this in OPA:
package kubernetes.validating.images
deny[msg] {
container := input.review.object.spec.containers[_]
not startswith(container.image, "your-registry.com/")
msg := sprintf("Image '%v' comes from an untrusted registry: %v", [container.image, "your-registry.com"])
}
The combination of these two policies is devastatingly effective: it blocks all unknown registries and, for the one allowed registry, requires cryptographic proof that the image was built by you. This is the gold standard. It turns your Kubernetes API server into a skeptical bouncer who checks both the ID and the fingerprint on the signature. It’s not just best practice; it’s the difference between having a door and having a locked door with a security guard.