containerd runs the containers on most Kubernetes nodes, and anything that can reach its socket can create, inspect and exec into every container on the host, so the runtime's configuration is part of the node's privileged access surface. Since Kubernetes removed dockershim in v1.24, many clusters talk to containerd directly through its CRI plugin, and the hardening work has moved into /etc/containerd/config.toml, the registry host files and the runtime handlers. This guide covers socket protection, the CRI settings that matter for security, registry trust and image verification, sandboxed runtime handlers, logging and a rollout order, with exact keys from the containerd documentation. It is written for platform, node and security engineers running containerd under Kubernetes or standalone with ctr or nerdctl. Concepts common to every runtime are in the Container Runtime Security Guide; this page goes deeper on containerd itself.
This is an independent guide from NHI Mgmt Group, based on the containerd project's public documentation as of November 2026. It is not sponsored or reviewed by the containerd project. Product features change, so confirm settings against the containerd project's current documentation.
Key takeaways
- The containerd socket is root on the node.
/run/containerd/containerd.sockis owned by UID 0 and GID 0 by default; keep it that way and never mount it into workloads. - containerd namespaces are not a boundary. The project describes them as an administrative construct that clients can switch trivially, so any socket client reaches the
k8s.ionamespace and every other. - Know your CRI version block. containerd 2.x uses
io.containerd.cri.v1.runtimeandio.containerd.cri.v1.images; 1.x usedio.containerd.grpc.v1.cri. Settings in the wrong block do nothing. - Registry trust lives in
hosts.toml. Setconfig_pathto/etc/containerd/certs.d, keepskip_verify = false, and never give a public mirror theresolvecapability. - Add a sandboxed runtime for untrusted code. Define a gVisor (
io.containerd.runsc.v1) or Kata (io.containerd.kata.v2) handler and select it per pod with a Kubernetes RuntimeClass.
How identity and access work in containerd
containerd has no user accounts. Access is decided by who can open its Unix sockets, which are protected by file ownership. The ops documentation lists the defaults:
| Interface | Default | What a client can do | Guidance |
|---|---|---|---|
| gRPC API socket | /run/containerd/containerd.sock, uid = 0, gid = 0 | Full control of images, containers, tasks and snapshots in every namespace | Keep root-only; do not set a shared group |
| Debug socket | /run/containerd/debug.sock, uid = 0, gid = 0 | Daemon diagnostics | Keep root-only; leave unset where not needed |
| Metrics | 127.0.0.1:1234, path /v1/metrics | Read Prometheus metrics | Keep on loopback or scrape through a node agent |
| CRI streaming server | stream_server_address = '127.0.0.1', stream_server_port = '0' | Serves exec, attach and port-forward streams | Keep on loopback; enable TLS if it must move |
| CRI TCP service | disable_tcp_service = true | CRI over TCP | Leave disabled |
Clients include the kubelet (through the CRI plugin), ctr, nerdctl, crictl and any agent given the socket. Each client picks a containerd namespace on every call, for example with ctr -n or CONTAINERD_NAMESPACE. Because namespaces are not a security feature, separation between tenants must come from Kubernetes RBAC, admission control and node isolation, not from containerd namespaces.
Rootless containerd
containerd can run as an unprivileged user with RootlessKit setting up user, mount and optional network namespaces. The containerd-rootless-setuptool.sh install script from the nerdctl project is the documented easy path, and the socket then sits under the user's runtime directory, such as /run/user/1001/containerd/containerd.sock. Requirements to plan for: the overlayfs snapshotter in a user namespace needs kernel 5.11 or later (Ubuntu and Debian kernels excepted), fuse-overlayfs works from kernel 4.18, and cgroup control needs cgroup v2 with systemd. Rootless mode suits build hosts and developer machines where users need a runtime without root.
Admin roles and privileged access
Three groups hold privileged access to a containerd node, and each needs its own control.
- Node administrators: root on the node means control of containerd and every container. Route node access through a PAM or bastion workflow with session recording, and prefer immutable node images so changes come through the pipeline. See the Linux Server Hardening Guide.
- Configuration owners: whoever edits
config.toml, the files in/etc/containerd/certs.dor the verifierbin_dircan change which images run and how. Manage these through configuration management with review, and monitor them with file integrity monitoring. - Socket holders: monitoring, logging and security agents often request the socket. Each is effectively root on the node. Inventory them, prefer agents that read through the CRI with documented minimal calls, and keep the list short.
The root (/var/lib/containerd) and state (/run/containerd) directories are internal to containerd; the ops documentation says they should not be tampered with and notes that external tools watching them have caused errors. Protect them with root-only permissions and do not mount them into containers.
Privileged containers and host devices
For each runtime handler, privileged_without_host_devices defaults to false, which means a privileged pod receives host devices. Set it to true on handlers where privileged pods should not see /dev from the host. Block privileged pods outside system namespaces with Pod Security Admission. See the Kubernetes Admission Control Guide.
Integrations, API tokens and service accounts
containerd itself holds few secrets, but the credentials it uses to pull images and the clients it serves are machine identities that need ownership.
Registry hosts and credentials
Point the images plugin at a hosts directory: in containerd 2.x set config_path = "/etc/containerd/certs.d" under [plugins."io.containerd.cri.v1.images".registry]. Each registry gets a directory containing hosts.toml, and changes apply without a restart.
hosts.toml field | Purpose | Secure setting |
|---|---|---|
server | Fallback upstream for the namespace | Your approved registry or pull-through cache |
capabilities | pull, resolve, push | Public mirrors: pull only. Private mirrors: pull and resolve. The docs state a public mirror should never be trusted to resolve tags |
ca | CA file or files for the registry | Your internal CA for private registries |
client | Client certificate and key pairs | Use for mTLS to private registries |
skip_verify | Skips chain and hostname checks | false (default); docs limit true to testing |
header | Custom request headers | Avoid static tokens here; they sit in plain files |
_default directory | Fallback for unmatched registries | Point at your mirror to keep pulls inside approved sources |
In Kubernetes, prefer image pull secrets per namespace, or a kubelet credential provider that issues short-lived registry tokens, over static credentials on the node. Our recommendation: any static node-level registry credential should be a read-only robot account, rotated at least every 90 days. See the Container Registry Security Guide.
Image verification
containerd can call external verifiers before an image is pulled. Enable the [plugins."io.containerd.image-verifier.v1.bindir"] plugin with:
bin_dir: directory of verifier executables. If it is missing or empty, verification does not block pulls, so confirm it is populated.max_verifiers: how many verifiers to call (negative for no limit); verifiers are sorted by name.per_verifier_timeout: time limit per verifier, such as"10s".
Each verifier receives -name, -digest and -stdin-media-type plus the OCI descriptor on standard input. Exit code 0 allows the pull; anything else blocks it, and all verifiers must allow (logical AND). A verifier error or timeout also blocks. A typical verifier wraps signature checks such as Sigstore cosign. Confirm in the documentation for your containerd version which pull paths call the verifier, and keep cluster-level signature enforcement at admission as well. See the Sigstore Guide.
Encrypted images
The images plugin supports image decryption, with image_decryption.key_model = 'node' in the 2.x defaults; leaving the key out disables decryption. Where you use encrypted images, the node's decryption keys become secrets to protect and rotate like any other.
Logging and detection
- Daemon logs: under systemd, containerd logs to the journal. Forward
journalctl -u containerdto the SIEM. Raise the log level only for troubleshooting. - Events:
ctr eventsstreams task and container events; agents use the same API to record create, start, exec and delete. - Kubernetes audit:
pods/exec,pods/attachand privileged pod creation are best caught in the API server audit log. See the Kubernetes Audit Log Detection Guide. - Host audit: watch connections to
/run/containerd/containerd.sockfrom processes other than the kubelet and known agents, and changes to/etc/containerd/, with auditd or eBPF sensors. See the Linux Logging and auditd for Defenders Guide.
| Behaviour | Signal | ATT&CK |
|---|---|---|
| Commands run in containers through the runtime, bypassing the API server | ctr, nerdctl or crictl exec processes on a node | T1609 |
| Containers started outside Kubernetes on a node | Tasks in namespaces other than k8s.io | T1610 |
| Container granted host access | Privileged create, host PID or host path mounts of / | T1611 |
| Socket read for secrets or environment | Unknown process opening the containerd socket | T1552.007 |
| Image from an unapproved source | Verifier denials, pulls bypassing your mirror | T1525 |
Metrics that show the controls work: nodes with a non-root socket group or socket mounted into pods (target zero); nodes with any skip_verify = true (target zero); share of untrusted workloads on a sandboxed RuntimeClass; verifier denials per week and their resolution; days from a containerd or runc release to fleet rollout.
Hardening baseline
CRI runtime settings (containerd 2.x)
Key (io.containerd.cri.v1.runtime) | Documented default | Recommended | Reason |
|---|---|---|---|
enable_selinux | false | true on SELinux hosts | Applies SELinux labels to containers |
disable_apparmor | false | false | Keeps AppArmor profiles active on AppArmor hosts |
restrict_oom_score_adj | false | true unless rootless | Stops containers lowering their OOM score below containerd's |
disable_proc_mount | false | false | Keeps default masked /proc paths |
device_ownership_from_security_context | false | true where non-root pods use devices | Gives devices the pod's UID and GID rather than root |
ignore_image_defined_volumes | false | Consider true | Stops images creating volumes the pod spec did not declare |
enable_unprivileged_ports | true | Keep, with NetworkPolicy | Lets non-root containers bind low ports without NET_BIND_SERVICE |
runc.options.SystemdCgroup | Confirm for your version | true on systemd hosts | Matches the kubelet's systemd cgroup driver |
Sandboxed runtime handlers
Alongside the default runc handler (runtime_type = 'io.containerd.runc.v2'), define handlers such as runtimes.gvisor with runtime_type = "io.containerd.runsc.v1" or runtimes.kata with runtime_type = "io.containerd.kata.v2". In Kubernetes, a RuntimeClass (stable since v1.20) whose handler names that runtime lets pods opt in with spec.runtimeClassName, trading some overhead for stronger isolation. Our recommendation: run multi-tenant, CI build and AI agent code execution workloads on a sandboxed class, and keep node pools for them separate. See the AI Agent Sandboxing and Code Execution Guide.
Host baseline
- Keep socket
uidandgidat 0 and directories/var/lib/containerdand/run/containerdroot-only. - Keep metrics on
127.0.0.1and the CRI stream server on loopback; if streaming must listen elsewhere, setenable_tls_streaming = truewithx509_key_pair_streamingfiles. - Pin the sandbox (pause) image through
pinned_images.sandboxto an image in your own registry. - Keep containerd, its shims and runc on supported releases, apply runtime security updates promptly, and follow containerd's security advisories.
Rollout order
- Inventory nodes by containerd version, config block (2.x or 1.x), runtime handlers and every process or pod with socket access.
- Remove socket mounts from workloads and replace agents that need them with CRI-scoped alternatives where possible.
- Move registry settings to
certs.dwithhosts.toml, removeskip_verify, and route_defaultto your mirror. - Apply the CRI runtime baseline to a canary node pool, run conformance and application tests, then promote by pool.
- Add the sandboxed handler and RuntimeClass on a dedicated pool; move untrusted workloads.
- Enable the image verifier on canary nodes, review denials, then enforce fleet-wide. Roll back by redeploying the previous node image or
config.tomland restarting containerd; withKillMode=process, running containers keep running during the restart.
Common misconfigurations
- Socket mounted into a pod. Build tools, log shippers or dashboards given
containerd.sockhold root on the node. Fix: remove the mount; use rootless build tools or CRI-scoped agents. - Settings in the 1.x block on a 2.x install. Hardening appears present but is ignored. Fix: migrate the configuration and check the effective config with
containerd config dump. skip_verify = trueleft from a lab. Fix: install the registry CA throughcaand keep verification on.- A public mirror allowed to resolve. Tag resolution then depends on that mirror. Fix:
capabilities = ["pull"]for public mirrors and pin images by digest. - Treating containerd namespaces as tenancy. Fix: separate tenants with Kubernetes controls and node pools.
- Untrusted code on
runcalongside sensitive workloads. Fix: sandboxed RuntimeClass and dedicated nodes.
| Control area | NIST SP 800-53 Rev 5 | CIS Controls v8.1 | ISO/IEC 27001:2022 Annex A |
|---|---|---|---|
| Socket and node privileged access | AC-3, AC-6 | 6 Access Control Management | A.8.2, A.8.18 |
| CRI and runtime configuration | CM-6, CM-7, SC-39 | 4 Secure Configuration | A.8.9 |
| Registry trust and image verification | SI-7, SR-11, SC-8 | 2 Inventory and Control of Software Assets | A.8.19, A.8.24 |
| Registry credentials | IA-5 | 5 Account Management | A.5.17 |
| Runtime logging | AU-2, AU-12 | 8 Audit Log Management | A.8.15, A.8.16 |
Practitioner checklist
- Confirm the containerd socket and debug socket are owned by root with no shared group, and no pod mounts them.
- List every agent with socket access and record an owner and justification for each.
- Check that hardening keys sit in the correct block for your containerd version and verify with
containerd config dump. - Configure
/etc/containerd/certs.dwithhosts.toml,skip_verify = falseand pull-only public mirrors. - Enable SELinux or AppArmor integration,
restrict_oom_score_adjandprivileged_without_host_deviceswhere appropriate. - Define a gVisor or Kata handler and a RuntimeClass for untrusted workloads on dedicated nodes.
- Enable the image verifier plugin with a populated
bin_dir, plus admission-time signature checks. - Keep metrics and the CRI stream server on loopback and the CRI TCP service disabled.
- Forward containerd journal logs and alert on runtime CLI use and unknown socket clients.
- Track containerd and runc security advisories and patch node images on a set cadence.
Standards and references
- containerd: CRI Plugin Config Guide
- containerd: Administration (ops) Guide
- containerd: Registry Configuration (hosts.toml)
- containerd: Image Verification
- containerd: Rootless Mode
- containerd: Namespaces
- Kubernetes: Runtime Class
- containerd: Security Policy and Advisories
Related NHI Mgmt Group resources: Container Runtime Security Guide · Kubernetes Security Guide · Kubernetes Security Checklist · Container Image Security Guide