containerd is an industry-standard container runtime that manages the complete container lifecycle. As a low-level runtime component used by Docker, Kubernetes, and other orchestrators, containerd security requires hardening the daemon configuration, securing the Unix socket, enforcing image trust policies, and applying runtime security controls. This guide covers securing containerd installations, registry configuration, runtime hardening, and integration with orchestrators.
Protect socket permissions:
# Check socket permissions
ls -la /run/containerd/containerd.sock
# Should be: srw-rw---- root root
# Set proper permissions
sudo chmod 660 /run/containerd/containerd.sock
sudo chown root:root /run/containerd/containerd.sock
# Configure in containerd config
# /etc/containerd/config.toml
[grpc]
address = "/run/containerd/containerd.sock"
uid = 0
gid = 0
max_recv_message_size = 16777216
max_send_message_size = 16777216
Restrict socket access:
# Create containerd group (optional)
sudo groupadd containerd
# Add only trusted users
sudo usermod -aG containerd admin-user
# Verify group membership
getent group containerd
# Remove unnecessary users
sudo gpasswd -d username containerd
Audit socket access:
# Monitor socket access with auditd
sudo auditctl -w /run/containerd/containerd.sock -p rwxa -k containerd_socket
# View audit logs
sudo ausearch -k containerd_socket -i
# Check for unauthorized access attempts
sudo grep "containerd.sock" /var/log/audit/audit.log
Run containerd with minimal privileges:
# containerd must run as root, but limit capabilities
# Create systemd override
sudo systemctl edit containerd
# Add security hardening
[Service]
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_SYS_ADMIN CAP_SYS_PTRACE
AmbientCapabilities=
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
Reload and restart:
sudo systemctl daemon-reload
sudo systemctl restart containerd
# Verify hardening
systemctl show containerd | grep -E "CapabilityBoundingSet|NoNewPrivileges|ProtectSystem"
Monitor containerd process:
# Check process status
ps aux | grep containerd
# Check open file descriptors
ls -la /proc/$(pgrep containerd)/fd
# Check network connections
sudo ss -tulpn | grep containerd
Never mount containerd socket in containers:
# BAD - Don't do this in Kubernetes
apiVersion: v1
kind: Pod
metadata:
name: bad-pod
spec:
containers:
- name: app
image: nginx
volumeMounts:
- name: containerd-sock
mountPath: /run/containerd/containerd.sock # ⚠️ Security risk!
volumes:
- name: containerd-sock
hostPath:
path: /run/containerd/containerd.sock
# GOOD - Remove socket mount
apiVersion: v1
kind: Pod
metadata:
name: good-pod
spec:
containers:
- name: app
image: nginx
Audit for socket mounts:
# Check for pods mounting containerd socket
kubectl get pods --all-namespaces -o json | \
jq '.items[] | select(.spec.volumes[]?.hostPath?.path | contains("containerd.sock")) | {namespace: .metadata.namespace, name: .metadata.name}'
# Check Docker containers
docker ps --format '{{.ID}} {{.Mounts}}' | grep containerd.sock
Edit containerd configuration:
# /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".registry]
# Configure registry mirrors
[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
endpoint = ["https://registry-1.docker.io"]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."quay.io"]
endpoint = ["https://quay.io"]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."ghcr.io"]
endpoint = ["https://ghcr.io"]
# Configure registry authentication
[plugins."io.containerd.grpc.v1.cri".registry.configs]
[plugins."io.containerd.grpc.v1.cri".registry.configs."registry.example.com".auth]
username = "registry-user"
password = "registry-password"
auth = ""
identitytoken = ""
# Block insecure registries
[plugins."io.containerd.grpc.v1.cri".registry]
insecure_registries = []
Configure hosts directory:
# Create hosts directory
sudo mkdir -p /etc/containerd/certs.d
# Create default hosts configuration
sudo mkdir -p /etc/containerd/certs.d/docker.io
# /etc/containerd/certs.d/docker.io/hosts.toml
server = "https://registry-1.docker.io"
[host."https://mirror.gcr.io"]
capabilities = ["pull", "resolve"]
override_path = "/library"
[host."https://registry-1.docker.io"]
capabilities = ["pull", "resolve"]
Enforce TLS in containerd:
# /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".registry]
# Disable plain HTTP
insecure_registries = []
# Configure TLS for specific registries
[plugins."io.containerd.grpc.v1.cri".registry.configs]
[plugins."io.containerd.grpc.v1.cri".registry.configs."registry.example.com".tls]
ca_file = "/etc/containerd/certs.d/registry.example.com/ca.crt"
cert_file = "/etc/containerd/certs.d/registry.example.com/client.crt"
key_file = "/etc/containerd/certs.d/registry.example.com/client.key"
insecure_skip_verify = false
Configure CA certificates:
# Create certificate directory
sudo mkdir -p /etc/containerd/certs.d/registry.example.com
# Copy CA certificate
sudo cp ca.crt /etc/containerd/certs.d/registry.example.com/
# Set permissions
sudo chmod 644 /etc/containerd/certs.d/registry.example.com/ca.crt
# Restart containerd
sudo systemctl restart containerd
Never use latest in production:
# BAD - Don't use latest
apiVersion: v1
kind: Pod
metadata:
name: bad-pod
spec:
containers:
- name: app
image: nginx:latest
# GOOD - Pin to version
apiVersion: v1
kind: Pod
metadata:
name: good-pod
spec:
containers:
- name: app
image: nginx:1.25.3
# BEST - Pin to digest
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
containers:
- name: app
image: nginx@sha256:abc123def456...
Get image digest:
# Using ctr
sudo ctr image pull docker.io/library/nginx:1.25
sudo ctr image ls | grep nginx
# Using crictl
crictl pull docker.io/library/nginx:1.25
crictl images | grep nginx
# Get digest
skopeo inspect docker://docker.io/library/nginx:1.25 | jq -r '.Digest'
Create image pull secret:
# Create secret for private registry
kubectl create secret docker-registry regcred \
--docker-server=https://registry.example.com \
--docker-username=user \
--docker-password=password \
--docker-email=user@example.com \
-n default
Reference in pod spec:
apiVersion: v1
kind: Pod
metadata:
name: private-image-pod
spec:
containers:
- name: app
image: registry.example.com/myapp:1.0
imagePullSecrets:
- name: regcred
Configure containerd registry auth:
# Create auth configuration
sudo mkdir -p /etc/containerd/certs.d/registry.example.com
# /etc/containerd/certs.d/registry.example.com/hosts.toml
server = "https://registry.example.com"
[host."https://registry.example.com"]
capabilities = ["pull", "resolve", "push"]
[host."https://registry.example.com".header]
Authorization = "Basic $(echo -n 'user:password' | base64)"
Select secure runtime:
# /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".containerd]
default_runtime_name = "runc"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
runtime_type = "io.containerd.runc.v2"
runtime_engine = ""
runtime_root = ""
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true
BinaryName = "/usr/sbin/runc"
Use crun for enhanced security:
# Install crun
sudo apt install crun # Debian/Ubuntu
sudo dnf install crun # RHEL/Fedora
# Configure containerd to use crun
# /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.crun]
runtime_type = "io.containerd.crun.v1"
runtime_engine = ""
runtime_root = ""
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.crun.options]
BinaryName = "/usr/bin/crun"
# Set as default
[plugins."io.containerd.grpc.v1.cri".containerd]
default_runtime_name = "crun"
Configure default seccomp:
# /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".containerd]
default_runtime_name = "runc"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
NoNewPrivileges = true
Create custom seccomp profile:
// /etc/containerd/seccomp/default.json
{
"defaultAction": "SCMP_ACT_ERRNO",
"archMap": [
{
"architecture": "SCMP_ARCH_X86_64",
"subArchitectures": ["SCMP_ARCH_X86", "SCMP_ARCH_X32"]
}
],
"syscalls": [
{
"names": ["accept", "accept4", "access", "alarm", "bind", "brk", "capget", "capset"],
"action": "SCMP_ACT_ALLOW"
}
]
}
Apply seccomp in Kubernetes:
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp:1.0
securityContext:
seccompProfile:
type: RuntimeDefault
Load AppArmor profile:
# Check AppArmor status
sudo aa-status
# Load containerd profile
sudo apparmor_parser -r /etc/apparmor.d/docker
# Verify profile is loaded
sudo aa-status | grep docker
Create custom AppArmor profile:
# /etc/apparmor.d/containerd-custom
#include <tunables/global>
profile containerd-custom flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
network inet tcp,
network inet udp,
network inet icmp,
deny network raw,
/usr/bin/containerd ix,
/var/lib/containerd/** rw,
/run/containerd/** rw,
}
# Load profile
sudo apparmor_parser -r /etc/apparmor.d/containerd-custom
Apply in Kubernetes:
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
annotations:
container.apparmor.security.beta.kubernetes.io/app: "runtime/default"
spec:
containers:
- name: app
image: myapp:1.0
Configure SELinux in containerd:
# /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SELinux = true
Verify SELinux status:
# Check SELinux status
getenforce
# Check containerd context
ps -eZ | grep containerd
# Check for SELinux denials
ausearch -m avc -ts recent | grep containerd
Configure default capabilities:
# /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
NoNewPrivileges = true
[plugins."io.containerd.grpc.v1.cri".containerd]
[plugins."io.containerd.grpc.v1.cri".containerd.default_runtime]
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.default_runtime.options]
BinaryName = "/usr/sbin/runc"
NoNewPrivileges = true
Drop capabilities in Kubernetes:
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
containers:
- name: app
image: myapp:1.0
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
Configure in Kubernetes:
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
containers:
- name: app
image: myapp:1.0
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
- name: run
mountPath: /run
volumes:
- name: tmp
emptyDir: {}
- name: run
emptyDir: {}
Configure containerd logging:
# /etc/containerd/config.toml
[debug]
level = "info"
format = "json"
[metrics]
address = ""
grpc_histogram = false
Configure log rotation:
# Create logrotate configuration
sudo cat > /etc/logrotate.d/containerd << EOF
/var/log/containerd/*.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
create 640 root root
postrotate
systemctl kill -s HUP containerd
endscript
}
EOF
Configure Prometheus metrics:
# /etc/containerd/config.toml
[metrics]
address = "127.0.0.1:1338"
grpc_histogram = true
Install containerd exporter:
# Download containerd exporter
wget https://github.com/containerd/containerd/releases/latest/download/containerd-1.7.0-linux-amd64.tar.gz
tar xzf containerd-1.7.0-linux-amd64.tar.gz
sudo cp bin/containerd-exporter /usr/local/bin/
# Run exporter
containerd-exporter --address=/run/containerd/containerd.sock
# Or run as systemd service
sudo systemctl enable containerd-exporter
sudo systemctl start containerd-exporter
Create security alerts:
# prometheus-rules.yaml
groups:
- name: containerd-security
rules:
- alert: ContainerdPrivilegedContainer
expr: containerd_containers_privileged > 0
for: 5m
labels:
severity: warning
annotations:
summary: "Privileged container detected"
- alert: ContainerdContainerRoot
expr: containerd_containers_running_as_root > 0
for: 10m
labels:
severity: warning
annotations:
summary: "Container running as root"
List containers:
# Using ctr
sudo ctr containers list
# Using crictl
crictl ps
crictl ps -a
# Check container details
sudo ctr containers info <container-id>
crictl inspect <container-id>
Monitor for security events:
# Watch container events
sudo ctr events
# Check container logs
crictl logs <container-id>
# Check container stats
crictl stats
# Watch for privileged containers
sudo ctr containers list | while read id; do
sudo ctr containers info $id | grep -q "privileged" && echo "Privileged: $id"
done
Check configuration:
# View current configuration
sudo containerd config dump
# Check for security issues
sudo containerd config dump | grep -E "insecure|privileged|seccomp|selinux"
# Verify runtime configuration
sudo ctr version
Audit image pulls:
# View containerd logs
sudo journalctl -u containerd -f
# Filter for image pulls
sudo journalctl -u containerd | grep -i "pull"
# Check for insecure registry access
sudo journalctl -u containerd | grep -i "insecure"
| Control | Status | Notes |
|---|---|---|
| Socket protected | ☐ | 660 permissions, root only |
| Process hardened | ☐ | Systemd hardening enabled |
| No socket sharing | ☐ | Not mounted in containers |
| Trusted registries only | ☐ | Insecure registries disabled |
| TLS enforced | ☐ | All registry connections |
| Images pinned | ☐ | No latest tags |
| Seccomp enabled | ☐ | Default or custom profile |
| AppArmor enabled | ☐ | Default or custom profile |
| SELinux enabled | ☐ | If available |
| Capabilities dropped | ☐ | Drop ALL, add minimum |
| Read-only root filesystem | ☐ | Where possible |
| Logging configured | ☐ | JSON format, rotation |
| Metrics enabled | ☐ | Prometheus integration |
| Audit logging enabled | ☐ | Security events tracked |
latest tag - Pin versions/digestsCheck containerd security status:
# Check containerd version
containerd --version
# Check runc version
runc --version
# Check socket permissions
stat -c "%a %U:%G" /run/containerd/containerd.sock
# View configuration
sudo containerd config dump
# List containers
sudo ctr containers list
crictl ps
# Check container security context
sudo ctr containers info <container-id> | jq '.spec.linux.security_context'
Audit containerd configuration:
# Check registry configuration
sudo containerd config dump | grep -A10 "registry"
# Check for insecure registries
sudo containerd config dump | grep "insecure"
# Check runtime configuration
sudo containerd config dump | grep -A20 "runtimes"
# Check seccomp/selinux settings
sudo containerd config dump | grep -E "seccomp|selinux"
# Verify systemd hardening
systemctl show containerd | grep -E "CapabilityBoundingSet|NoNewPrivileges|ProtectSystem"