Table of contents

How To Secure Docker Images With Encryption Through Containers

Docker image encryption with containerd

Docker has exploded into popularity over the past decade for DevOps. Its massive adoption makes it the first choice for container-based orchestration and a central focus of Docker container security efforts across the industry. Its massive adoption rates make it the first choice for container-based orchestration. It is used by small implementations and large-scale enterprises to launch and deploy applications using Linux containers — which in a nutshell is a form of OS-level virtualization.

In contrast to a VM, a container just contains the required files — a structure that introduces new container vulnerability and configuration risks that security teams must account for. Docker is open-sourced and is a container engine that uses Linux Kernel features to create containers on top of an operating system. This means that it is easy for a developer to efficiently shift an application over from a laptop to a test environment.

Docker by design is small, lightweight, portable, fast to launch, highly scalable, and great for continuous integration (CI) and continuous deployment (CD). But how secure is it, especially when containers are deployed at scale with tools like Kubernetes and CI/CD systems that require consistent container security best practices?

By default, Docker container images are unencrypted — and they often include sensitive data such as API keys, certificates, and configuration files. Without encryption or regular docker image scanning, these assets remain exposed to attackers who gain access to your registry or host. This means that if a malicious user gains access to the container, they can retrieve secrets, manipulate code, or exploit docker container vulnerabilities. This blog post is part of a series about container security.

How do we prevent this? The easiest solution is to encrypt your Docker containers.

How to encrypt a Docker container image

For this tutorial, we will use containerd to encrypt your Docker image — a method that strengthens the foundation of docker image security by ensuring data remains protected even at rest. What is containerd?  containerd is an industry-standard for container runtimes that is available as a daemon for Linux and Windows and is designed to be embedded into a larger system. It 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.

To start using containerd, you will need Go 1.9.x or above on your Linux host. To install containerd, you can do so using the wget command or go directly to the download page. The current latest version is 1.5.2 and here is the command for installing the binaries for containerd.

wget https://github.com/containerd/containerd/releases/download/v1.5.2/containerd-1.5.2-linux-amd64.tar.gz
 tar xvf containerd-1.5.2-linux-amd64.tar.gz
Once the installation process is completed, a new containerd-1.5.2 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 only Docker registry version 2.7.1 and above supports encrypted OCI images — an important detail when maintaining secure registries and applying 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.

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

How To Secure Docker Images With Encryption Through Containers - 8 inA

Automated Dependency Management Made Simple

Learn why automating dependency updates is crucial for software security and efficiency. Discovertools like to streamline the process.

Read more
How To Secure Docker Images With Encryption Through Containers - 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
How To Secure Docker Images With Encryption Through Containers - 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