CRI-O is a lightweight container runtime specifically designed for Kubernetes. It implements the Kubernetes Container Runtime Interface (CRI) and provides a secure, production-ready runtime optimized for Kubernetes workloads. CRI-O security requires hardening the runtime configuration, securing the CRI socket, enforcing image trust policies, and applying runtime security controls. This guide covers securing CRI-O installations, image management, runtime hardening, and Kubernetes integration.
Protect CRI-O socket permissions:
# Check socket permissions
ls -la /var/run/crio/crio.sock
# Should be: srw-rw---- root root
# Set proper permissions
sudo chmod 660 /var/run/crio/crio.sock
sudo chown root:root /var/run/crio/crio.sock
# Or configure in crio.conf
# /etc/crio/crio.conf
[crio.runtime]
stream_enable_tls = true
stream_tls_cert = "/etc/crio/server.crt"
stream_tls_key = "/etc/crio/server.key"
stream_tls_ca = "/etc/crio/ca.crt"
Restrict socket access:
# Only kubelet and root should access CRI-O socket
# Verify kubelet configuration
cat /etc/kubernetes/kubelet.conf | grep -i socket
# Check who can access socket
getent group | grep -E "docker|crio|kubelet"
# Remove unnecessary users from groups
sudo gpasswd -d username docker
sudo gpasswd -d username crio
Audit socket access:
# Monitor socket access with auditd
sudo auditctl -w /var/run/crio/crio.sock -p rwxa -k crio_socket
# View audit logs
sudo ausearch -k crio_socket -i
# Check for unauthorized access
sudo grep "crio.sock" /var/log/audit/audit.log
Configure kubelet authentication:
# /var/lib/kubelet/config.yaml
authentication:
anonymous:
enabled: false
webhook:
enabled: true
cacheTTL: 2m0s
x509:
clientCAFile: /etc/kubernetes/pki/ca.crt
authorization:
mode: Webhook
webhook:
cacheAuthorizedTTL: 5m0s
cacheUnauthorizedTTL: 30s
Restrict kubelet read-only port:
# /var/lib/kubelet/config.yaml
readOnlyPort: 0 # Disable read-only port
# Or bind to localhost only
readOnlyPort: 10255
address: 127.0.0.1
Enable kubelet TLS:
# /var/lib/kubelet/config.yaml
tlsCertFile: /etc/kubernetes/pki/kubelet.crt
tlsPrivateKeyFile: /etc/kubernetes/pki/kubelet.key
rotateCertificates: true
serverTLSBootstrap: true
Restart kubelet:
sudo systemctl daemon-reload
sudo systemctl restart kubelet
Configure kernel parameters:
# /etc/sysctl.d/99-kubernetes.conf
# Disable IP forwarding if not needed
net.ipv4.ip_forward = 1 # Required for pod networking
# Enable bridge netfilter
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
# Apply settings
sudo sysctl --system
Configure ulimits:
# /etc/security/limits.d/crio.conf
# CRI-O limits
crio soft nofile 65536
crio hard nofile 65536
# Kubelet limits
kubelet soft nofile 65536
kubelet hard nofile 65536
Edit CRI-O registry configuration:
# /etc/crio/crio.conf
[crio.image]
default_transport = "docker://"
# Trusted registries only
[crio.image.registries]
registries = [
"docker.io",
"quay.io",
"registry.k8s.io",
"registry.redhat.io",
"ghcr.io"
]
# Block insecure registries
[crio.image.insecure_registries]
registries = []
# Configure registry mirrors
[crio.image.registries."docker.io"]
prefix = "docker.io"
location = "mirror.gcr.io"
Configure containers registry configuration:
# /etc/containers/registries.conf
[registries.search]
registries = [
'docker.io',
'quay.io',
'registry.k8s.io'
]
[registries.insecure]
registries = []
[registries.block]
registries = [
'untrusted-registry.example.com'
]
Configure signature policy:
// /etc/containers/policy.json
{
"default": [
{
"type": "reject"
}
],
"transports": {
"docker": {
"docker.io": [
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "/etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release"
}
],
"quay.io": [
{
"type": "sigstoreSigned",
"keyPath": "/etc/containers/openshift/signing-key.pub",
"signedIdentity": {
"type": "matchRepository"
}
}
],
"registry.k8s.io": [
{
"type": "insecureAcceptAnything"
}
]
},
"docker-daemon": {
"": [
{
"type": "insecureAcceptAnything"
}
]
}
}
}
Configure signature verification in CRI-O:
# /etc/crio/crio.conf
[crio.image]
signature_policy = "/etc/containers/policy.json"
signature_policy_dir = "/etc/containers/policy.d"
Create signature verification key:
# Create directory for keys
sudo mkdir -p /etc/containers/policy.d
# Import GPG key
gpg --import /path/to/trusted-key.gpg
# Export key for containers
gpg --export trusted-key@example.com > /etc/containers/policy.d/trusted-key.gpg
Never use latest tag in Kubernetes:
# BAD - Don't use latest
apiVersion: v1
kind: Pod
metadata:
name: bad-pod
spec:
containers:
- name: app
image: myapp:latest
# GOOD - Pin to version
apiVersion: v1
kind: Pod
metadata:
name: good-pod
spec:
containers:
- name: app
image: myapp:1.2.3
# BEST - Pin to digest
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
containers:
- name: app
image: myapp@sha256:abc123def456...
Configure image pull policy:
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
image: myapp:1.2.3
imagePullPolicy: IfNotPresent # Or Always for latest security patches
Integrate Trivy in CI/CD:
# .github/workflows/security-scan.yml
name: Image Security Scan
on: [push, pull_request]
jobs:
trivy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Run Trivy
run: trivy image --exit-code 1 --severity CRITICAL,HIGH myapp:${{ github.sha }}
- name: Push to registry
run: docker push registry.example.com/myapp:${{ github.sha }}
Use admission controller for image scanning:
# Kyverno policy to require image scanning
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-image-scan
spec:
validationFailureAction: Enforce
background: true
rules:
- name: check-image-scan
match:
resources:
kinds:
- Pod
validate:
message: "Images must be scanned before deployment"
deny:
conditions:
- key: "{{ request.object.metadata.annotations.\"image-scanned\" || '' }}"
operator: NotEquals
value: "true"
Use CRI-O image status:
# Check image status with crictl
crictl images
# Inspect image
crictl inspecti <image-id>
# Remove unused images
crictl rmi --prune
Enable default seccomp in CRI-O:
# /etc/crio/crio.conf
[crio.runtime]
seccomp_profile = "/etc/crio/seccomp.json"
seccomp_use_default = true
Create custom seccomp profile:
// /etc/crio/seccomp.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
Configure SELinux in CRI-O:
# /etc/crio/crio.conf
[crio.runtime]
selinux = true
selinux_opt = ["type", "container_t"]
Verify SELinux status:
# Check SELinux status
getenforce
# Check CRI-O SELinux context
ps -eZ | grep crio
# Check for SELinux denials
ausearch -m avc -ts recent | grep crio
Create custom SELinux policy:
# Generate policy from audit logs
ausearch -m avc -ts recent | audit2allow -M crio-custom
# Install policy
semodule -i crio-custom.pp
Configure Pod Security Admission:
# Namespace with restricted policy
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: latest
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: latest
Use OPA Gatekeeper to deny privileged:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sPSPPrivilegedContainer
metadata:
name: deny-privileged
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
Check for privileged containers:
# Find privileged pods
kubectl get pods --all-namespaces -o json | \
jq '.items[] | select(.spec.containers[].securityContext.privileged == true) | {namespace: .metadata.namespace, name: .metadata.name}'
Configure default capabilities in CRI-O:
# /etc/crio/crio.conf
[crio.runtime]
default_capabilities = [
"CHOWN",
"DAC_OVERRIDE",
"FSETID",
"FOWNER",
"SETGID",
"SETUID",
"SETPCAP",
"NET_BIND_SERVICE",
"KILL"
]
# Drop dangerous capabilities
default_drop_capabilities = [
"SYS_ADMIN",
"NET_ADMIN",
"SYS_PTRACE",
"SYS_MODULE",
"SYS_RAWIO",
"SYS_BOOT",
"MKNOD"
]
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: cache
mountPath: /var/cache
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
Configure CRI-O logging:
# /etc/crio/crio.conf
[crio.runtime]
log_level = "info"
log_filter = "info"
[crio.runtime.logging]
log_format = "text" # Or json
log_to_journald = true
Configure container log rotation:
# /etc/crio/crio.conf
[crio.runtime]
log_size_max = 10485760 # 10MB
log_to_journald = true
Configure audit logging:
# /etc/crio/crio.conf
[crio.runtime]
audit = true
audit_file = "/var/log/crio/audit.log"
Monitor CRI-O events:
# View CRI-O logs
sudo journalctl -u crio -f
# View audit logs
sudo tail -f /var/log/crio/audit.log
# Filter for security events
sudo grep -E "privileged|capabilities|seccomp" /var/log/crio/audit.log
Check running containers:
# List containers with crictl
crictl ps
# List all containers
crictl ps -a
# Check container logs
crictl logs <container-id>
# Check container stats
crictl stats
Monitor for security events:
# Watch for privileged containers
crictl ps -o json | jq '.containers[] | select(.info.config.privileged == true)'
# Watch for containers running as root
crictl ps -o json | jq '.containers[] | select(.info.config.user == "0" or .info.config.user == "root")'
# Check for containers with host network
crictl ps -o json | jq '.containers[] | select(.info.config.linux.security_context.namespace_options.network == 2)'
Configure Prometheus metrics:
# /etc/crio/crio.conf
[crio.metrics]
enable_metrics = true
metrics_port = 9090
metrics_socket = "/var/run/crio/crio-metrics.sock"
Create security alerts:
# prometheus-rules.yaml
groups:
- name: crio-security
rules:
- alert: CrioPrivilegedContainer
expr: crio_containers_privileged > 0
for: 5m
labels:
severity: warning
annotations:
summary: "Privileged container detected"
- alert: CrioContainerRoot
expr: crio_containers_running_as_root > 0
for: 10m
labels:
severity: warning
annotations:
summary: "Container running as root"
| Control | Status | Notes |
|---|---|---|
| CRI-O socket protected | ☐ | 660 permissions, root only |
| Kubelet secured | ☐ | Auth enabled, TLS, read-only disabled |
| Trusted registries only | ☐ | registries.conf configured |
| Image signature verification | ☐ | policy.json configured |
| Image versions pinned | ☐ | No latest tags |
| Image scanning enabled | ☐ | Trivy in CI/CD |
| Seccomp enabled | ☐ | Default or custom profile |
| SELinux enabled | ☐ | Confinement active |
| Privileged containers blocked | ☐ | PSA or Gatekeeper |
| Capabilities dropped | ☐ | Drop ALL, add minimum |
| Read-only root filesystem | ☐ | Where possible |
| Logging configured | ☐ | Rotation enabled |
| Metrics enabled | ☐ | Prometheus integration |
| Audit logging enabled | ☐ | Security events tracked |
latest tag - Pin versions/digestsCheck CRI-O security status:
# Check CRI-O version
crio --version
# Check CRI-O configuration
crio config --defaults | grep -E "seccomp|selinux|capabilities|registries"
# Check socket permissions
stat -c "%a %U:%G" /var/run/crio/crio.sock
# List running containers
crictl ps
# Check container security context
crictl inspect <container-id> | jq '.info.config.linux.security_context'
Audit CRI-O configuration:
# Check registry configuration
cat /etc/containers/registries.conf | grep -v "^#" | grep -v "^$"
# Check signature policy
cat /etc/containers/policy.json | jq
# Check seccomp profile
cat /etc/crio/seccomp.json | jq '.defaultAction'
# Check SELinux status
getenforce
# Check for security issues
crictl ps -o json | jq '.containers[] | {id: .id, privileged: .info.config.privileged, user: .info.config.user}'