click the screen · press Enter
← back to blog
Kubernetes Journey · Topic 39

By Default, Kubernetes Trusts Every Registry Online

Restricting Kubernetes to trusted registries: an admission controller rejecting a pod that pulls from an untrusted registry while allowing one from the internal registry - Kubernetes Journey

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.

By default Kubernetes pulls a container image from any registry, and an admission controller with a registry allowlist rejects images that do not come from a trusted source
Left is the default: any registry, no questions. Right is what you have to build: a rule at admission that rejects anything off the list.

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.

01
Authentication

Who is making the request

02
Authorisation

Are they allowed to

03
Admission

Does the image pass policy

04
Created / rejected

Untrusted registry blocked

The registry check is the third gate. Authn and authz say who you are and what you may touch; admission judges the request itself.

There are three common ways to put a rule in that third gate. They solve the same problem with very different amounts of glue.

A short Rego policy, run by OPA or Gatekeeper, denies any image not starting with your registry. This is where I'd start: it installs as a normal webhook, no API server surgery.
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])
}
A small validating webhook you write and host yourself. Full control, but now you own a service that has to be up for pods to schedule.
@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.
Kubernetes ships an 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.
Same rule, three amounts of plumbing. All three configs are from my course notes, not stood up on my own cluster, so treat them as the shape rather than a captured run.

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.

The real bind Flip it to 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

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.