Skip to main content

Image Security

Many images are public and do not require authorization for the container runtime on each node to pull the image. However, images that actually run an organization's applications should be private and protected against unauthorized access.

Let's look at this deployment as an example (or it could be a pod, it doesn't matter)...

apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: nginx
name: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
resources: {}
status: {}

The container named nginx will use the nginx image, but where does it come from? By default, these images reference the Docker image registry, which is why we can simplify using just the name nginx, as Kubernetes knows it comes from Docker.

If we did this, it would be the same thing.

...
spec:
containers:
- name: nginx
image: docker.io/nginx
resources: {}
...
kubectl create deployment nginx --image docker.io/nginx
deployment.apps/nginx created
➜ files git:(main) ✗ kubectl get deploy
NAME READY UP-TO-DATE AVAILABLE AGE
nginx 1/1 1 1 18s

Let's do a different test for an image in a private repository.

# Authenticating to my docker account
docker login
Authenticating with existing credentials...
WARNING! Your password will be stored unencrypted in /home/david-prata/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded

# Pulling a public image
docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
c57ee5000d61: Pull complete
9b0163235c08: Pull complete
f24a6f652778: Pull complete
9f3589a5fc50: Pull complete
f0bd99a47d4a: Pull complete
398157bc5c51: Pull complete
1ef1c1a36ec2: Pull complete
Digest: sha256:84c52dfd55c467e12ef85cad6a252c0990564f03c4850799bf41dd738738691f
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest

# Creating a new tag, but now referencing it as if it were mine
docker tag nginx:latest davidpuziol/nginx
docker push davidpuziol/nginx
Using default tag: latest
The push refers to repository [docker.io/davidpuziol/nginx] # <<< Look where it's going
f205d290cd76: Mounted from library/nginx
2b28485849ea: Mounted from library/nginx
9f21a390e3f6: Mounted from library/nginx
06536efc503a: Mounted from library/nginx
84e0c9ef07d7: Mounted from library/nginx
83bdf27d9eaa: Mounted from library/nginx
fb1bd2fc5282: Mounted from library/nginx
latest: digest: sha256:6a9af2366105c104e353d16998458d6a15aa5d6db0861ad9ce98538890391950 size: 1778

# Let's logout from docker and try to pull
docker logout
Removing login credentials for https://index.docker.io/v1/

# And we cannot pull because we don't have permission.
docker pull davidpuziol/nginx
Using default tag: latest
Error response from daemon: pull access denied for davidpuziol/nginx, repository does not exist or may require 'docker login': denied: requested access to the resource is denied

This is exactly what will happen when we try to create a deployment with this image. Let's test it.

kubectl create deployment nginx-david --image docker.io/davidpuziol/nginx
deployment.apps/nginx-david created

kubectl get deploy
NAME READY UP-TO-DATE AVAILABLE AGE
nginx 1/1 1 1 7m1s
nginx-david 0/1 1 0 11s

kubectl get pods nginx-david-54ccffddf9-mm5jp
NAME READY STATUS RESTARTS AGE
nginx-david-54ccffddf9-mm5jp 0/1 ImagePullBackOff 0 24s

Looking at the image we have docker.io/davidpuziol/nginx which is the same as docker.io/davidpuziol/nginx:latest

  • docker.io is the registry
  • davidpuziol is the user account
  • nginx is the image name
  • latest is the image tag

To perform the authentication process, we need to pass some login parameters. How do we do this?

Several other registries exist besides docker.io. We have: ECR from AWS, ACR from Azure, GCR, or even repository servers that we create using Harbor, which is widely used in the open source world.

GCR has many public images that are used in the cluster, such as gcr.io/kubernetes-e2e-test-images/dnsutils

Take a look at this registry which has a lot of things used out there besides Docker.

Many Kubernetes images that were in Google's repositories (k8s.gcr.io) are migrating to another repository called registry.k8s.io

kubectl describe pods -n kube-system kube-apiserver-kind-cluster-control-plane | grep Image:
Image: registry.k8s.io/kube-apiserver:v1.29.1

Well, understanding the process that we need to be logged in, the runtime will try to login the same way when obtaining a private image.

To pass the credentials, we first need them inside a secret.

If we were to create a secret that had the registry data, it would be like this:

Create a file with this content and base64 encode it as we will need it.

cat <<EOF | base64 -w 0
{
"auths": {
"docker.io": {
"username": "myuser",
"password": "mypass123",
"email": "my@email123"
}
}
}
EOF
ewogICJhdXRocyI6IHsKICAgICJkb2NrZXIuaW8iOiB7CiAgICAgICJ1c2VybmFtZSI6ICJtZXV1c2VyIiwKICAgICAgInBhc3N3b3JkIjogIm1ldXBhc3MxMjMiLAogICAgICAiZW1haWwiOiAibWV1QGVtYWlsMTIzIgogICAgfQogIH0KfQo=

Create a manifest passing the encoded value. Note that the type of this secret is dockerconfigjson.

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
name: dockercreds
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: ewogICJhdXRocyI6IHsKICAgICJkb2NrZXIuaW8iOiB7CiAgICAgICJ1c2VybmFtZSI6ICJtZXV1c2VyIiwKICAgICAgInBhc3N3b3JkIjogIm1ldXBhc3MxMjMiLAogICAgICAiZW1haWwiOiAibWV1QGVtYWlsMTIzIgogICAgfQogIH0KfQo=
EOF

secret/dockercreds created

Now we can use this secret. Note that this is a Pod specification and not for the containers inside the pod. This secret will work for any image inside this pod. We can also observe that it's a list.

...
spec:
imagePullSecrets
- name: dockercreds
containers:
- name: nginx
image: docker.io/nginx
resources: {}
...

Taking an example I did with real data, look.

kubectl get deployments.apps nginx-david -o yaml
apiVersion: apps/v1
kind: Deployment
metadata:
...
generation: 1
labels:
app: nginx-david
spec:
...
selector:
matchLabels:
app: nginx-david
...
template:
metadata:
creationTimestamp: null
labels:
app: nginx-david
spec:
containers:
- image: docker.io/davidpuziol/nginx:latest
imagePullPolicy: Always
name: nginx
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
imagePullSecrets: #<<< Used
- name: docker-david-secret
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
...

kubectl get pods nginx-david-c8644f94d-nlgch
NAME READY STATUS RESTARTS AGE
nginx-david-c8644f94d-nlgch 1/1 Running 0 15m

kubectl describe pods nginx-david-c8644f94d-nlgch| grep Image:
Image: docker.io/davidpuziol/nginx:latest

Generally, it's not a good practice to create secrets in manifests, so we can use the command.

kubectl create secret docker-registry dockerreq --docker-email [email protected] --docker-username mytest --docker-password pass123 --docker-server docker.io
secret/dockerreq created

kubectl get secrets dockerreq -o yaml
apiVersion: v1
data:
.dockerconfigjson: eyJhdXRocyI6eyJkb2NrZXIuaW8iOnsidXNlcm5hbWUiOiJtZXV0ZXN0IiwicGFzc3dvcmQiOiJwYXNzMTIzIiwiZW1haWwiOiJtZXVAdGVzdC5jb20iLCJhdXRoIjoiYldWMWRHVnpkRHB3WVhOek1USXoifX19
kind: Secret
metadata:
creationTimestamp: "2024-02-12T20:41:04Z"
name: dockerreq
namespace: default
resourceVersion: "479885"
uid: 72a67fab-256e-414c-94c7-bb76badd19a0
type: kubernetes.io/dockerconfigjson

echo eyJhdXRocyI6eyJkb2NrZXIuaW8iOnsidXNlcm5hbWUiOiJtZXV0ZXN0IiwicGFzc3dvcmQiOiJwYXNzMTIzIiwiZW1haWwiOiJtZXVAdGVzdC5jb20iLCJhdXRoIjoiYldWMWRHVnpkRHB3WVhOek1USXoifX19 | base64 --decode
{"auths":{"docker.io":{"username":"mytest","password":"pass123","email":"[email protected]","auth":"bWV1dGVzdDpwYXNzMTIz"}}}%