Table of contents

Docker image security: how to encrypt Docker images with containerd

Docker image encryption with containerd

Docker remains the default way teams package and ship applications, which makes Docker image security a core part of any container program. By default, Docker images are unencrypted, and they often contain sensitive data: API keys, certificates, and configuration files. Anyone who gains access to your registry or host can read all of it.

Encryption closes that gap. This tutorial shows how to encrypt a Docker image with containerd, the industry-standard container runtime, and the imgcrypt extension, then push the encrypted image to a registry and run it with your private key. This post is part of our series on container security.

Prerequisites

  • A Linux host with a recent Go toolchain installed
  • containerd installed (the examples below work on the 1.x and 2.x lines; use the latest release from the containerd downloads page)
  • The imgcrypt subproject, which provides the ctr-enc command line tool used throughout this tutorial
  • openssl for key generation

Why Docker image security requires encryption

Unlike a virtual machine, a container image is just a stack of filesystem layers, and every layer is readable by anyone who can pull the image. That structure introduces risks that scanning alone does not solve:

  • Secrets baked into layers (API keys, certificates, credentials) are exposed to anyone with registry access, even in "deleted" layers.
  • A compromised or misconfigured registry leaks your application code and configuration wholesale, giving attackers a head start on exploiting docker container vulnerabilities.
  • Regulated workloads often require data at rest to be encrypted, and image layers are data at rest.

Encryption complements, but does not replace, regular docker image scanning: scan images for vulnerable components first, then encrypt them so their contents stay protected in storage and transit.

How to encrypt a Docker container image

For this tutorial, we will use containerd to encrypt your Docker image, a method that keeps image contents protected even at rest. What is containerd? containerd is an industry-standard container runtime, available as a daemon for Linux and Windows, that manages the complete container lifecycle of its host system, from image transfer and storage to runtime execution and networking, complementing orchestration-level kubernetes security controls. The ctr-enc tool used throughout this tutorial comes from imgcrypt, a containerd subproject that adds encryption and decryption support for OCI images. Install it before continuing.

To start using containerd, you will need a recent Go toolchain on your Linux host (check the go.mod of the containerd release you install for the minimum version). Grab the latest release from the containerd download page. The commands below show the install pattern; substitute the current version number for VERSION:

wget https://github.com/containerd/containerd/releases/download/vVERSION/containerd-VERSION-linux-amd64.tar.gz
 tar xvf containerd-VERSION-linux-amd64.tar.gz
Once the installation process is completed, a new containerd-VERSION directory will be created. The daemon uses the configuration file located at /etc/containerd/config.toml and looks something like this:
 subreaper = true
 oom_score = -999
 ​
 [debug]
        level = "debug"
 ​
 [metrics]
        address = "127.0.0.1:1338"
 ​
 [plugins.linux]

If a configuration file doesn't exist, you can generate a default one using the following command:

containerd config default > /etc/containerd/config.toml

To connect to containerd, create a new main.go file and import containerd as a root package that contains the client. Here is the sample code:

package main
​
import (
  "log"
​
  "github.com/containerd/containerd"
)
​
func main() {
  if err := redisExample(); err != nil {
  log.Fatal(err)
  }
}
​
func redisExample() error {
  client, err := containerd.New("/run/containerd/containerd.sock")
  if err != nil {
  return err
  }
  defer client.Close()
  return nil
}

The above will create a new client with a default containerd socket path. To change this, create a context for calls to client methods.

ctx := namespaces.WithNamespace(context.Background(), "example")

Now it's time to pull in the redis image from DockerHub.

image, err := client.Pull(ctx, "docker.io/library/redis:alpine", containerd.WithPullUnpack)
  if err != nil {
  return err
  }

Here is the entire main.go code you need in one space:

package main
​
import (
        "context"
        "log"
​
        "github.com/containerd/containerd"
        "github.com/containerd/containerd/namespaces"
)
​
func main() {
        if err := redisExample(); err != nil {
                log.Fatal(err)
        }
}
​
func redisExample() error {
        client, err := containerd.New("/run/containerd/containerd.sock")
        if err != nil {
                return err
        }
        defer client.Close()
​
        ctx := namespaces.WithNamespace(context.Background(), "example")
        image, err := client.Pull(ctx, "docker.io/library/redis:alpine", containerd.WithPullUnpack)
        if err != nil {
                return err
        }
        log.Printf("Successfully pulled %s image\n", image.Name())
​
        return nil
}

Now you can build your main.go

go build main.go

If you run sudo ./main, you will get the following returned result (or something similar):

2021/06/02 17:43:21 Successfully pulled docker.io/library/redis:alpine image

Now that we have containerd working, how exactly do we encrypt a Docker image?

First, we need to generate some keys. Here are the commands for generating RSA keys with openssl.

$ openssl genrsa --out mykey.pem
Generating RSA private key, 2048 bit long modulus (2 primes)
...............................................++++
............................+++++
e is 65537 (0x010001)
$ openssl rsa -in mykey.pem -pubout -out mypubkey.pem
writing RSA key

Let's pull in an image so that we can encrypt it.

$ sudo ctr-enc images pull --all-platforms docker.io/library/bash:latest
[... truncated ...]

To view your encryption information on the image, you can use the ctr-enc image layerinfo command. As we haven't encrypted our image yet, here is what it can look like:

$ sudo ctr-enc images layerinfo --platform linux/amd64 docker.io/library/bash:latest
    #                                                                   DIGEST     PLATFORM     SIZE   ENCRYPTION   RECIPIENTS
    0   sha256:9e57c4ce12a330de1631e554b498a125e564ced155ebdd1c7764eb871cbd9609   linux/amd64   2789544                         
    1   sha256:5ee01fd661d4ec8478c5096b983326b44e4fc8bd7f98209b9e840291be9b15c0   linux/amd64   3174231                         
    2   sha256:735cfbca546415867c7b55f357dc15e45973e7d285c2b3b783bd2b2b8ea52def3   linux/amd64       125

Now it's time to encrypt our Docker image. To do this by using the ctr-enc images encrypt command. This will encrypt the existing image to a new tag. ctr-enc images encrypt takes five arguments.

The first argument is --recipient jwe:mypubkey.pem. This portion of the command tells containerd that we want to encrypt the image using the public key mypubkey.pem. It is prefixed with jwe: to indicate that the encryption scheme is JSON web encryption scheme.

The second argument is --platform linux/amd64. This flag tells containerd to only encrypt the linux/amd64 image.

The third argument is docker.io/library/bash:latest, which points to the image we want to encrypt.

The fourth argument is bash.enc:latest, which is the tag of the encrypted image to be created.

And finally, you can also decide which layer you want to encrypt using the --layer tag. This argument is optional and can be omitted if you want to encrypt the entire image and not just parts of the image.

Here is an example of how to use it with our public key.

$ sudo ctr-enc images encrypt --recipient jwe:mypubkey.pem --platform linux/amd64 docker.io/library/bash:latest bash.enc:latest
Encrypting docker.io/library/bash:latest to bash.enc:latest

To push your encrypted image to the registry, you can just use sudo docker run. Note that Docker registry 2.7.1 and later supports encrypted OCI images; any current registry release qualifies. Keep registries hardened and apply kubernetes security best practices across deployments.

Here is the full command for it:

$ sudo docker run -d -p 5000:5000 --restart=always --name registry registry:2.7.1

You can now tag and push the image, and then delete the local copy using the following command:

$ sudo ctr-enc images tag bash.enc:latest localhost:5000/bash.enc:latest
$ sudo ctr-enc images push localhost:5000/bash.enc:latest
$ sudo ctr-enc images rm localhost:5000/bash.enc:latest bash.enc:latest
$ sudo ctr-enc images pull localhost:5000/bash.enc:latest

Before pushing encrypted images, teams should perform a container vulnerability assessment and update Docker images to ensure they contain the latest patched dependencies.

Pulling and encrypting images like this should be part of a consistent container scan routine that checks images for vulnerabilities before they are encrypted and pushed to registries.

Now if we attempt to run the encrypted container, the image will fail if the keys for the encrypted image are not provided. You can pass in the keys using the --key flag. Here is an example of how to do so:

$ sudo ctr-enc run --rm --key mykey.pem localhost:5000/bash.enc:latest test echo 'It works!'
It works!

Encrypting Docker images doesn’t replace traditional scanning. It should run alongside regular container security scanning to identify risks before images are encrypted. That is basically it for encrypting a Docker image, pushing it to a registry, and running the decrypted image.

Docker image encryption FAQs

Does encrypting a Docker image replace vulnerability scanning?

No. Encryption protects image contents from unauthorized reading; scanning finds vulnerable and outdated components inside the image. Run both: scan first, then encrypt.

Can Kubernetes run encrypted images?

Yes. containerd supports image decryption in Kubernetes clusters; keys are configured on the nodes so the kubelet can pull and decrypt authorized images. See the containerd CRI decryption documentation for setup.

Which layers should I encrypt?

Encrypt the layers containing sensitive data at minimum, using the layer flag shown above. Encrypting the entire image is simpler to manage and is the safer default.

Where to from here?

When it comes to security, using the default settings is one of the biggest risks that any production-level application can experience. Encryption is one methodology for securing your Docker environment. Other essential practices include enforcing resource limits, implementing Docker Bench Security checks, and integrating continuous container security best practices to monitor host, daemon, and image configurations.

Another standard protocol is to never run a container as a root user — a key rule also reflected in kubernetes security best practices, which restrict privilege escalation and enforce least-privilege policies. If you do not specify a user when starting a container, it defaults the user set in the image -- which is often the root user.

Always scan and rebuild images to include security patches, leveraging docker image scanning to detect outdated components and update Docker images efficiently across your pipeline. You can also enable Docker Content Trust (DCT), which uses digital signatures to validate the integrity of images pulled from remote Docker registries. Combined with orchestration-layer kubernetes security, runtime Container security tools, and layered Docker container security strategies, encryption through containerd delivers strong, modern protection for containerized workloads.

Recent resources

Docker image security: how to encrypt Docker images with containerd - 5 inB

npm vs Yarn in 2026: Which package manager should you choose?

npm vs Yarn in 2026: speed, security, and workspaces compared.

Read more
Docker image security: how to encrypt Docker images with containerd - 5 Tools for Managing Dependency Updates

What is LDAP Injection? Types, Examples and How to Prevent It

Learn what LDAP Injection is, its types, examples, and how to prevent it. Secure your applications against LDAP attacks.

Read more
Docker image security: how to encrypt Docker images with containerd - How to Use Dependency Injection in Java Tutorial with

How to Use Dependency Injection in Java: Tutorial with Examples

Learn how to use Dependency Injection in Java with this comprehensive tutorial. Discover its benefits, types, and practical examples.

Read more