
Here's a claim I'd defend: on a stock cluster, anyone who can create a pod can run an image from any registry on the internet, and the kubelet will happily go and pull it. There is no trusted-registry list. Type image: some-registry.io/whatever and it runs. That's the default, and it's the thing this post argues you should switch off.
Most people assume the opposite, that a "production" cluster surely won't reach out to a random registry. It will. Kubernetes treats the image field as an instruction, not a request for permission. So the interesting question isn't whether to restrict registries, it's how, and the honest catch is that the built-in tool for it fails open, which means the obvious setup can leave you no safer than when you started.
The default is "pull from anywhere"
When you write image: nginx, Kubernetes quietly expands it to docker.io/library/nginx, the official image on Docker Hub. The library namespace is Docker's shorthand for its own maintained images, and docker.io is the registry it defaults to when you name none. Convenient, and it hides how much trust you're handing over by typing one word.
The problem is that the same shortcut works for anything. Nothing stops a manifest naming a registry you've never heard of:
apiVersion: v1
kind: Pod
metadata:
name: sample-pod
spec:
containers:
- name: sample-app
image: some-registry.io/a-very-vulnerable-image
Apply that and the kubelet pulls it and runs it. If that image is backdoored or riddled with known bugs, you've just given an attacker a running process inside your cluster, and from there the walk to a node or another workload is the same walk I keep coming back to. The point defenders miss is that this isn't an exploit, it's the product working as designed. The image field trusts you, and it trusts anyone who can create a pod exactly as much.
Authentication is not the same as restriction
The first thing teams reach for is a private registry, and it's worth being clear about what that does and doesn't buy you. A private registry keeps your images off the public internet. You log in, push, and give the cluster credentials to pull them back. In Kubernetes those credentials live in a Secret of type docker-registry:
kubectl create secret docker-registry regcred \
--docker-server=private-registry.io \
--docker-username=registry-user \
--docker-password='••••••••' \
--docker-email=you@org.com
Then you point a pod at it with imagePullSecrets, and the kubelet uses those credentials to authenticate the pull:
spec:
containers:
- name: app
image: private-registry.io/apps/internal-app
imagePullSecrets:
- name: regcred
Here's the catch people trip on, and I've done it myself: this controls what you can reach, not what you're allowed to run. Nothing about regcred stops a different pod, in the same namespace or another, from pulling some-registry.io/a-very-vulnerable-image that needs no credentials at all. Private-registry auth answers "can I get my own images", and that's a real and useful thing. It just isn't the allowlist, and it's easy to feel protected because you set up a Secret when you've actually only solved the easy half. That Secret is also per-namespace, so it's forgettable in exactly the way that produces a 3am ImagePullBackOff.
Restriction lives at admission, and there are three ways in
The actual control is an admission controller: code that inspects a pod-creation request after authentication and authorisation, and rejects it if the image isn't from an approved registry. Every create request already runs this gauntlet, so it's the right place to enforce the rule.
Who is making the request
Are they allowed to
Does the image pass policy
Untrusted registry blocked
There are three common ways to put a rule in that third gate. They solve the same problem with very different amounts of glue.
package kubernetes.admission
deny[msg] {
input.request.kind.kind == "Pod"
image := input.request.object.spec.containers[_].image
not startswith(image, "internal-registry.io/")
msg := sprintf("image '%v' is not from a trusted registry", [image])
}@app.route("/validate", methods=["POST"])
def validate():
img = request.json["request"]["object"]["spec"]["containers"][0]["image"]
ok = "internal-registry.io" in img
msg = "" if ok else "only internal-registry.io images are allowed"
return jsonify({"response": {"allowed": ok, "status": {"message": msg}}})
Note it only checks the first container. Real policies must loop every container and initContainer, which is exactly the sort of thing the Rego version handles for you.ImagePolicyWebhook admission plugin that calls out to an external policy server. It works, but it's the most fiddly: an admission config file, a kubeconfig for the webhook, and flags on the API server itself.
--enable-admission-plugins=ImagePolicyWebhook
--admission-control-config-file=/etc/kubernetes/admission-config.yaml
On a managed control plane you often can't set those flags at all, which quietly rules this option out.The evidence that this bites: the fail-open default
Here's the piece that turns this from a tidy how-to into an actual argument. The built-in ImagePolicyWebhook takes a config file, and one field in it decides what happens when the policy server can't be reached:
imagePolicy:
kubeConfigFile: /etc/kubernetes/image-policy-kubeconfig.yaml
allowTTL: 50
denyTTL: 50
retryBackoff: 500
defaultAllow: true # <-- the whole control, undone
defaultAllow: true means: if the webhook server is down or slow, admit the pod anyway. Read that again, because it's the trap. The one moment your policy service is having a bad day is the one moment an untrusted image sails straight through, and nothing in the cluster logs screams about it. You built a wall with a door that opens automatically whenever the guard steps out.
defaultAllow: false and you've swapped a silent security gap for a loud availability one: if the webhook is down, no pods schedule, including the ones restarting your webhook. That's why a self-hosted policy service has to be genuinely highly available, and why "just add an admission webhook" is never just adding an admission webhook.I'll be straight about the limit of my own testing here. I haven't run ImagePolicyWebhook against a live API server, so I'm reading the behaviour off the config semantics and the docs rather than a scar I earned. If you've operated one through an outage, I'd like to know which way you set defaultAllow and whether you regretted it.
The strongest objection, answered
The fair pushback is that this is a lot of moving parts to stop something nobody's actually doing. Who's really deploying a-very-vulnerable-image on purpose?
Almost nobody, on purpose. But that's not the threat. The threat is a typo'd registry name that resolves to a typosquat, a copied Stack Overflow manifest pointing at someone's personal Docker Hub, a compromised CI token that pushes to a place you don't watch, or a supply-chain attacker who only needs one workload to pull from a registry you never sanctioned. Registry allowlisting isn't there to stop a malicious insider typing an evil image. It's there so the cluster mechanically refuses the thousand accidental and opportunistic ways a bad image gets referenced. A control that only works against deliberate abuse isn't worth much; this one works against mistakes, which is where the real risk lives.
Where the claim stops
I'm arguing you should restrict registries. I'm not arguing it makes your images safe, and it's worth marking that line clearly. Allowlisting controls where an image comes from, never what's inside it. Your own trusted registry can host an image with a critical CVE in it, and the allowlist will wave it through because it came from the right place. That's the exact gap I looked at when scanning images for what they carry, and it's why registry policy and vulnerability scanning are two layers, not one.
So if I were setting this up from scratch today, I'd do the cheap things first: pin a small allowlist as a Rego policy through a policy engine I don't have to keep alive myself, and skip the built-in ImagePolicyWebhook unless I had a specific reason and a control plane I fully own. Then wire image scanning in behind it, because the allowlist decides who's allowed in the building and the scanner decides whether they're carrying anything. Neither does the other's job.
That closes the image-trust thread I've been pulling on for three topics now, from what's inside an image, to what a linter sees in the manifest, to where the image is even allowed to come from. Next I want to step back to the cluster's own front door and look at how the API server decides who gets to ask any of this in the first place.
References
- Kubernetes docs: the ImagePolicyWebhook admission controller
- Kubernetes docs: pull an image from a private registry
- Kubernetes docs: images and registry defaults
- OPA Gatekeeper documentation
FAQ
Can Kubernetes pull an image from any registry by default?
Yes. Out of the box, anyone who can create a pod can set the image field to any registry on the internet, and the kubelet will pull it. Kubernetes does not ship an allowlist of trusted registries, so restricting where images come from is something you have to add with an admission controller.
What is the difference between imagePullSecrets and an image policy?
imagePullSecrets give the kubelet credentials to pull from a private registry, so they control what you can reach. An image policy controls what you are allowed to run. One is authentication for your own images, the other is a rule that rejects images from registries you do not trust. You usually want both.
What does defaultAllow do in ImagePolicyWebhook?
defaultAllow decides what happens when the webhook server is unreachable. Set to true, pods are admitted when the policy service is down, so the control fails open and quietly stops enforcing. Set to false it fails closed and blocks pods until the service recovers, which is safer but risks a self-inflicted outage.
Should I use ImagePolicyWebhook or OPA Gatekeeper to restrict registries?
For most clusters OPA Gatekeeper, or a similar policy engine, is easier to run than the built-in ImagePolicyWebhook, which needs an external server, a kubeconfig and API server flags. Gatekeeper installs as a normal admission webhook and you write the registry allowlist as a Rego policy. Either works, Gatekeeper has less moving-part risk.
Does allowlisting registries make my images safe?
No. It controls where images come from, not what is inside them. A trusted registry can still host a vulnerable or backdoored image. Registry allowlisting is one layer, sitting alongside vulnerability scanning and signature verification, not a replacement for either. It narrows the supply, it does not vet it.
Related reading
- Topic 38: A Manifest That Applies Isn't Safe (catching image problems in the pull request)
- Topic 37: Your Scanner Doesn't Delete Anything (why a trusted registry still needs scanning)
- Topic 30: Policy as Code with OPA Gatekeeper (the engine I'd run the allowlist on)
- Topic 27: Admission Controllers (the gate this all hangs off)
- My Kubernetes pentest notes (the registry checks without the argument)
- Browse the whole Kubernetes Journey