genpolicy: match sandbox name by regex

`allow_interactive_exec` requires a sandbox-name annotation, however
this is only added for pods by genpolicy. Other pod-generating resources
have unpredictable sandbox names.

This patch instead uses a regex for the sandbox name in genpolicy, based
on the specified metadata and following Kubernetes' naming logic. The
generated regex is then used in the policy to correctly match the
sandbox name.

Fixes: #11823

Signed-off-by: Charlotte Hartmann Paludo <git@charlotteharludo.com>
Co-authored-by: Paul Meyer <katexochen0@gmail.com>
Co-authored-by: Markus Rudy <mr@edgeless.systems>
This commit is contained in:
Charlotte Hartmann Paludo
2025-09-19 10:53:59 +02:00
committed by Charlotte
parent a27009012c
commit 2cea32cc23
16 changed files with 472 additions and 30 deletions

View File

@@ -150,7 +150,7 @@ allow_create_container_input if {
allow_namespace(p_namespace, i_namespace) = add_namespace if {
p_namespace == i_namespace
add_namespace := null
add_namespace := state_allows("namespace", i_namespace)
print("allow_namespace 1: input namespace matches policy data")
}

View File

@@ -80,7 +80,12 @@ impl yaml::K8sResource for CronJob {
}
fn get_sandbox_name(&self) -> Option<String> {
None
// CronJob name - time[min]
// https://github.com/kubernetes/kubernetes/blob/b35c5c0a301d326fdfa353943fca077778544ac6/pkg/controller/cronjob/cronjob_controllerv2.go#L672
let cronjob_name = yaml::name_regex_from_meta(&self.metadata);
let job_name = cronjob_name.map(|prefix| format!("{prefix}-[0-9]+"));
// Pod name now derives from the generated job name.
job_name.map(job::pod_name_regex)
}
fn get_namespace(&self) -> Option<String> {

View File

@@ -82,7 +82,9 @@ impl yaml::K8sResource for DaemonSet {
}
fn get_sandbox_name(&self) -> Option<String> {
None
// https://github.com/kubernetes/kubernetes/blob/b35c5c0a301d326fdfa353943fca077778544ac6/pkg/controller/daemon/daemon_controller.go#L1045
let suffix = yaml::GENERATE_NAME_SUFFIX_REGEX;
yaml::name_regex_from_meta(&self.metadata).map(|prefix| format!("{prefix}-{suffix}"))
}
fn get_namespace(&self) -> Option<String> {

View File

@@ -80,7 +80,11 @@ impl yaml::K8sResource for Deployment {
}
fn get_sandbox_name(&self) -> Option<String> {
None
// Deployment name - pod template hash - suffix
// https://github.com/kubernetes/kubernetes/blob/b35c5c0a301d326fdfa353943fca077778544ac6/pkg/controller/deployment/sync.go#L201
let suffix = yaml::GENERATE_NAME_SUFFIX_REGEX;
yaml::name_regex_from_meta(&self.metadata)
.map(|prefix| format!("{prefix}-{suffix}-{suffix}"))
}
fn get_namespace(&self) -> Option<String> {

View File

@@ -54,7 +54,8 @@ impl yaml::K8sResource for Job {
}
fn get_sandbox_name(&self) -> Option<String> {
None
let job_name = yaml::name_regex_from_meta(&self.metadata);
job_name.map(pod_name_regex)
}
fn get_namespace(&self) -> Option<String> {
@@ -123,3 +124,13 @@ impl yaml::K8sResource for Job {
yaml::get_sysctls(&self.spec.template.spec.securityContext)
}
}
pub fn pod_name_regex(job_name: String) -> String {
// Job name - optional index - generateNameSuffix
// https://github.com/kubernetes/kubernetes/blob/b35c5c0a301d326fdfa353943fca077778544ac6/pkg/controller/job/job_controller.go#L1767
// https://github.com/kubernetes/kubernetes/blob/b35c5c0a301d326fdfa353943fca077778544ac6/pkg/controller/job/indexed_job_utils.go#L501
// Very long job names are handled incorrectly (job_name would need to be truncated, but
// index/suffix len are unknown here)
let suffix = yaml::GENERATE_NAME_SUFFIX_REGEX;
format!("{job_name}(-[0-9]+)?-{suffix}")
}

View File

@@ -16,7 +16,7 @@ pub struct ObjectMeta {
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
generateName: Option<String>,
pub generateName: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
labels: Option<BTreeMap<String, String>>,
@@ -32,17 +32,6 @@ pub struct ObjectMeta {
}
impl ObjectMeta {
pub fn get_name(&self) -> String {
if let Some(name) = &self.name {
format!("^{}$", regex::escape(name))
} else if let Some(generateName) = &self.generateName {
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names
format!("^{}[a-z0-9.-]*[a-z0-9]$", regex::escape(generateName))
} else {
String::new()
}
}
pub fn get_namespace(&self) -> Option<String> {
self.namespace.as_ref().cloned()
}

View File

@@ -857,11 +857,7 @@ impl yaml::K8sResource for Pod {
}
fn get_sandbox_name(&self) -> Option<String> {
let name = self.metadata.get_name();
if !name.is_empty() {
return Some(name);
}
panic!("No pod name.");
yaml::name_regex_from_meta(&self.metadata)
}
fn get_namespace(&self) -> Option<String> {

View File

@@ -960,7 +960,7 @@ fn get_container_annotations(
if let Some(name) = resource.get_sandbox_name() {
annotations
.entry("io.kubernetes.cri.sandbox-name".to_string())
.or_insert(name);
.or_insert(format!("^{name}$"));
}
if !is_pause_container {

View File

@@ -52,7 +52,9 @@ impl yaml::K8sResource for ReplicaSet {
}
fn get_sandbox_name(&self) -> Option<String> {
None
// https://github.com/kubernetes/kubernetes/blob/b35c5c0a301d326fdfa353943fca077778544ac6/pkg/controller/controller_utils.go#L541
let suffix = yaml::GENERATE_NAME_SUFFIX_REGEX;
yaml::name_regex_from_meta(&self.metadata).map(|prefix| format!("{prefix}-{suffix}"))
}
fn get_namespace(&self) -> Option<String> {

View File

@@ -54,7 +54,10 @@ impl yaml::K8sResource for ReplicationController {
}
fn get_sandbox_name(&self) -> Option<String> {
None
// https://github.com/kubernetes/kubernetes/blob/b35c5c0a301d326fdfa353943fca077778544ac6/pkg/controller/controller_utils.go#L541
// https://github.com/kubernetes/kubernetes/blob/b35c5c0a301d326fdfa353943fca077778544ac6/pkg/controller/replication/replication_controller.go#L47-L50
let suffix = yaml::GENERATE_NAME_SUFFIX_REGEX;
yaml::name_regex_from_meta(&self.metadata).map(|prefix| format!("{prefix}-{suffix}"))
}
fn get_namespace(&self) -> Option<String> {

View File

@@ -102,7 +102,8 @@ impl yaml::K8sResource for StatefulSet {
}
fn get_sandbox_name(&self) -> Option<String> {
None
// https://github.com/kubernetes/kubernetes/blob/b35c5c0a301d326fdfa353943fca077778544ac6/pkg/controller/statefulset/stateful_set_utils.go#L113
yaml::name_regex_from_meta(&self.metadata).map(|prefix| format!("{prefix}-[0-9]+"))
}
fn get_namespace(&self) -> Option<String> {

View File

@@ -14,6 +14,7 @@ use crate::job;
use crate::list;
use crate::mount_and_storage;
use crate::no_policy;
use crate::obj_meta::ObjectMeta;
use crate::pod;
use crate::policy;
use crate::replica_set;
@@ -442,3 +443,18 @@ pub fn get_sysctls(security_context: &Option<pod::PodSecurityContext>) -> Vec<po
}
vec![]
}
/// Constructs a non-anchored regex for an object according to k8s naming conventions:
/// 1. If the name field is set, return that literally.
/// 2. If name is unset but generateName is set, return regex that matches generateName and a random suffix.
/// 3. Otherwise, return None. This object is not considered valid by the k8s API server!
pub fn name_regex_from_meta(meta: &ObjectMeta) -> Option<String> {
meta.name.clone().or_else(|| {
meta.generateName
.as_ref()
.map(|p| format!("{}{}", regex::escape(p), GENERATE_NAME_SUFFIX_REGEX))
})
}
// https://github.com/kubernetes/kubernetes/blob/b35c5c0a301d326fdfa353943fca077778544ac6/staging/src/k8s.io/apimachinery/pkg/util/rand/rand.go#L81-L83
pub const GENERATE_NAME_SUFFIX_REGEX: &str = "[bcdfghjklmnpqrstvwxz2456789]+";

View File

@@ -285,6 +285,11 @@ mod tests {
runtests("state/execprocess").await;
}
#[tokio::test]
async fn test_state_exec_process_deployment() {
runtests("state/execprocessdeployment").await;
}
#[tokio::test]
async fn test_create_container_security_context() {
runtests("createcontainer/security_context/runas").await;

View File

@@ -1,18 +1,18 @@
[
{
"description": "generated name with valid prefix (dummyxyz)",
"description": "generated name with valid prefix (dummysfx)",
"allowed": true,
"request": {
"type": "CreateContainer",
"OCI": {
"Version": "1.1.0",
"Annotations": {
"io.kubernetes.cri.sandbox-name": "dummyxyz",
"io.kubernetes.cri.sandbox-name": "dummysfx",
"io.kubernetes.cri.sandbox-namespace": "default",
"io.kubernetes.cri.container-type": "sandbox",
"io.katacontainers.pkg.oci.container_type": "pod_sandbox",
"nerdctl/network-namespace": "/var/run/netns/cni-00000000-0000-0000-0000-000000000000",
"io.kubernetes.cri.sandbox-log-directory": "/var/log/pods/default_dummyxyz_00000000-0000-0000-0000-000000000000",
"io.kubernetes.cri.sandbox-log-directory": "/var/log/pods/default_dummysfx_00000000-0000-0000-0000-000000000000",
"io.katacontainers.pkg.oci.bundle_path": "/run/containerd/io.containerd.runtime.v2.task/k8s.io/bundle-id",
"io.kubernetes.cri.sandbox-id": "0000000000000000000000000000000000000000000000000000000000000000"
},
@@ -271,4 +271,4 @@
}
}
}
]
]

View File

@@ -0,0 +1,25 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: execprocessnamed
spec:
selector:
matchLabels:
app: execprocessnamed
template:
metadata:
labels:
app: execprocessnamed
spec:
runtimeClassName: kata
containers:
- name: execprocessnamed
image: "quay.io/prometheus/busybox:latest"
command:
- sleep
- "3600"
readinessProbe:
exec:
command:
- /bin/true

View File

@@ -0,0 +1,383 @@
[
{
"allowed": true,
"description": "create container request for deployment's only container",
"request": {
"OCI": {
"Annotations": {
"io.katacontainers.pkg.oci.bundle_path": "/run/containerd/io.containerd.runtime.v2.task/k8s.io/de22f4859da69a058e9d7f162d8e27244919c24d3ff2262e46cf56f695a34b5d",
"io.katacontainers.pkg.oci.container_type": "pod_container",
"io.kubernetes.cri.container-name": "execprocessnamed",
"io.kubernetes.cri.container-type": "container",
"io.kubernetes.cri.image-name": "quay.io/prometheus/busybox:latest",
"io.kubernetes.cri.sandbox-id": "c069a0850eb7894c6499133e2c6b5ccd543ebf3c32966a7c87512c9d0322061f",
"io.kubernetes.cri.sandbox-name": "execprocessnamed-7c56d66754-pq9qp",
"io.kubernetes.cri.sandbox-namespace": "default",
"io.kubernetes.cri.sandbox-uid": "e6a10dbc-f413-427c-b59c-cdbd2dcb5003"
},
"Hooks": null,
"Hostname": "",
"Linux": {
"CgroupsPath": "kubepods-besteffort-pode6a10dbc_f413_427c_b59c_cdbd2dcb5003.slice:cri-containerd:de22f4859da69a058e9d7f162d8e27244919c24d3ff2262e46cf56f695a34b5d",
"Devices": [],
"GIDMappings": [],
"IntelRdt": null,
"MaskedPaths": [
"/proc/asound",
"/proc/acpi",
"/proc/kcore",
"/proc/keys",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/proc/scsi",
"/sys/firmware",
"/sys/devices/virtual/powercap"
],
"MountLabel": "",
"Namespaces": [
{
"Path": "",
"Type": "ipc"
},
{
"Path": "",
"Type": "uts"
},
{
"Path": "",
"Type": "mount"
}
],
"ReadonlyPaths": [
"/proc/bus",
"/proc/fs",
"/proc/irq",
"/proc/sys",
"/proc/sysrq-trigger"
],
"Resources": {
"BlockIO": null,
"CPU": {
"Cpus": "",
"Mems": "",
"Period": 100000,
"Quota": 0,
"RealtimePeriod": 0,
"RealtimeRuntime": 0,
"Shares": 2
},
"Devices": [],
"HugepageLimits": [],
"Memory": {
"DisableOOMKiller": false,
"Kernel": 0,
"KernelTCP": 0,
"Limit": 0,
"Reservation": 0,
"Swap": 0,
"Swappiness": 0
},
"Network": null,
"Pids": null
},
"RootfsPropagation": "",
"Seccomp": null,
"Sysctl": {},
"UIDMappings": []
},
"Mounts": [
{
"destination": "/proc",
"options": [
"nosuid",
"noexec",
"nodev"
],
"source": "proc",
"type_": "proc"
},
{
"destination": "/dev",
"options": [
"nosuid",
"strictatime",
"mode=755",
"size=65536k"
],
"source": "tmpfs",
"type_": "tmpfs"
},
{
"destination": "/dev/pts",
"options": [
"nosuid",
"noexec",
"newinstance",
"ptmxmode=0666",
"mode=0620",
"gid=5"
],
"source": "devpts",
"type_": "devpts"
},
{
"destination": "/dev/mqueue",
"options": [
"nosuid",
"noexec",
"nodev"
],
"source": "mqueue",
"type_": "mqueue"
},
{
"destination": "/sys",
"options": [
"nosuid",
"noexec",
"nodev",
"ro"
],
"source": "sysfs",
"type_": "sysfs"
},
{
"destination": "/sys/fs/cgroup",
"options": [
"nosuid",
"noexec",
"nodev",
"relatime",
"ro"
],
"source": "cgroup",
"type_": "cgroup"
},
{
"destination": "/etc/hosts",
"options": [
"rbind",
"rprivate",
"rw"
],
"source": "/run/kata-containers/shared/containers/de22f4859da69a058e9d7f162d8e27244919c24d3ff2262e46cf56f695a34b5d-caaca2830e5ed2bd-hosts",
"type_": "bind"
},
{
"destination": "/dev/termination-log",
"options": [
"rbind",
"rprivate",
"rw"
],
"source": "/run/kata-containers/shared/containers/de22f4859da69a058e9d7f162d8e27244919c24d3ff2262e46cf56f695a34b5d-5e23a5cd6aabdf40-termination-log",
"type_": "bind"
},
{
"destination": "/etc/hostname",
"options": [
"rbind",
"rprivate",
"rw"
],
"source": "/run/kata-containers/shared/containers/de22f4859da69a058e9d7f162d8e27244919c24d3ff2262e46cf56f695a34b5d-d828ac9e9fc033a1-hostname",
"type_": "bind"
},
{
"destination": "/etc/resolv.conf",
"options": [
"rbind",
"rprivate",
"rw"
],
"source": "/run/kata-containers/shared/containers/de22f4859da69a058e9d7f162d8e27244919c24d3ff2262e46cf56f695a34b5d-c06f189b601baf96-resolv.conf",
"type_": "bind"
},
{
"destination": "/dev/shm",
"options": [
"rbind"
],
"source": "/run/kata-containers/sandbox/shm",
"type_": "bind"
},
{
"destination": "/var/run/secrets/kubernetes.io/serviceaccount",
"options": [
"rbind",
"rprivate",
"ro"
],
"source": "/run/kata-containers/shared/containers/de22f4859da69a058e9d7f162d8e27244919c24d3ff2262e46cf56f695a34b5d-708e23948ffe9cd6-serviceaccount",
"type_": "bind"
}
],
"Process": {
"ApparmorProfile": "cri-containerd.apparmor.d",
"Args": [
"sleep",
"3600"
],
"Capabilities": {
"Ambient": [],
"Bounding": [
"CAP_CHOWN",
"CAP_DAC_OVERRIDE",
"CAP_FSETID",
"CAP_FOWNER",
"CAP_MKNOD",
"CAP_NET_RAW",
"CAP_SETGID",
"CAP_SETUID",
"CAP_SETFCAP",
"CAP_SETPCAP",
"CAP_NET_BIND_SERVICE",
"CAP_SYS_CHROOT",
"CAP_KILL",
"CAP_AUDIT_WRITE"
],
"Effective": [
"CAP_CHOWN",
"CAP_DAC_OVERRIDE",
"CAP_FSETID",
"CAP_FOWNER",
"CAP_MKNOD",
"CAP_NET_RAW",
"CAP_SETGID",
"CAP_SETUID",
"CAP_SETFCAP",
"CAP_SETPCAP",
"CAP_NET_BIND_SERVICE",
"CAP_SYS_CHROOT",
"CAP_KILL",
"CAP_AUDIT_WRITE"
],
"Inheritable": [],
"Permitted": [
"CAP_CHOWN",
"CAP_DAC_OVERRIDE",
"CAP_FSETID",
"CAP_FOWNER",
"CAP_MKNOD",
"CAP_NET_RAW",
"CAP_SETGID",
"CAP_SETUID",
"CAP_SETFCAP",
"CAP_SETPCAP",
"CAP_NET_BIND_SERVICE",
"CAP_SYS_CHROOT",
"CAP_KILL",
"CAP_AUDIT_WRITE"
]
},
"ConsoleSize": null,
"Cwd": "/",
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"HOSTNAME=execprocessnamed-7c56d66754-pq9qp",
"KUBERNETES_PORT_443_TCP_PROTO=tcp",
"KUBERNETES_PORT_443_TCP_PORT=443",
"KUBERNETES_PORT_443_TCP_ADDR=10.0.0.1",
"KUBERNETES_SERVICE_HOST=10.0.0.1",
"KUBERNETES_SERVICE_PORT=443",
"KUBERNETES_SERVICE_PORT_HTTPS=443",
"KUBERNETES_PORT=tcp://10.0.0.1:443",
"KUBERNETES_PORT_443_TCP=tcp://10.0.0.1:443"
],
"NoNewPrivileges": false,
"OOMScoreAdj": 1000,
"Rlimits": [],
"SelinuxLabel": "",
"Terminal": false,
"User": {
"AdditionalGids": [
0
],
"GID": 0,
"UID": 0,
"Username": ""
}
},
"Root": {
"Path": "/run/kata-containers/de22f4859da69a058e9d7f162d8e27244919c24d3ff2262e46cf56f695a34b5d/rootfs",
"Readonly": false
},
"Solaris": null,
"Version": "1.1.0",
"Windows": null
},
"container_id": "de22f4859da69a058e9d7f162d8e27244919c24d3ff2262e46cf56f695a34b5d",
"devices": [],
"exec_id": "de22f4859da69a058e9d7f162d8e27244919c24d3ff2262e46cf56f695a34b5d",
"sandbox_pidns": false,
"shared_mounts": [],
"stderr_port": 0,
"stdin_port": 0,
"stdout_port": 0,
"storages": [
{
"driver": "image_guest_pull",
"driver_options": [
"image_guest_pull={\"metadata\":{\"io.katacontainers.pkg.oci.bundle_path\":\"/run/containerd/io.containerd.runtime.v2.task/k8s.io/de22f4859da69a058e9d7f162d8e27244919c24d3ff2262e46cf56f695a34b5d\",\"io.katacontainers.pkg.oci.container_type\":\"pod_container\",\"io.kubernetes.cri.container-name\":\"execprocessnamed\",\"io.kubernetes.cri.container-type\":\"container\",\"io.kubernetes.cri.image-name\":\"quay.io/prometheus/busybox:latest\",\"io.kubernetes.cri.sandbox-id\":\"c069a0850eb7894c6499133e2c6b5ccd543ebf3c32966a7c87512c9d0322061f\",\"io.kubernetes.cri.sandbox-name\":\"execprocessnamed-7c56d66754-pq9qp\",\"io.kubernetes.cri.sandbox-namespace\":\"default\",\"io.kubernetes.cri.sandbox-uid\":\"e6a10dbc-f413-427c-b59c-cdbd2dcb5003\"}}"
],
"fs_group": null,
"fstype": "overlay",
"mount_point": "/run/kata-containers/de22f4859da69a058e9d7f162d8e27244919c24d3ff2262e46cf56f695a34b5d/rootfs",
"options": [],
"source": "quay.io/prometheus/busybox:latest"
}
],
"string_user": null,
"type": "CreateContainer"
}
},
{
"allowed": true,
"description": "test exec process in deployment's container with correct args",
"request": {
"container_id": "de22f4859da69a058e9d7f162d8e27244919c24d3ff2262e46cf56f695a34b5d",
"exec_id": "cc5d171e-57fa-4b0d-995c-af55301e92e9",
"process": {
"ApparmorProfile": "",
"Args": [
"/bin/true"
],
"Capabilities": null,
"ConsoleSize": null,
"Cwd": "/",
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"HOSTNAME=execprocessnamed-7c56d66754-pq9qp",
"KUBERNETES_PORT_443_TCP_PROTO=tcp",
"KUBERNETES_PORT_443_TCP_PORT=443",
"KUBERNETES_PORT_443_TCP_ADDR=10.0.0.1",
"KUBERNETES_SERVICE_HOST=10.0.0.1",
"KUBERNETES_SERVICE_PORT=443",
"KUBERNETES_SERVICE_PORT_HTTPS=443",
"KUBERNETES_PORT=tcp://10.0.0.1:443",
"KUBERNETES_PORT_443_TCP=tcp://10.0.0.1:443"
],
"NoNewPrivileges": false,
"OOMScoreAdj": 0,
"Rlimits": [],
"SelinuxLabel": "",
"Terminal": false,
"User": {
"AdditionalGids": [
0
],
"GID": 0,
"UID": 0,
"Username": ""
}
},
"stderr_port": 0,
"stdin_port": 0,
"stdout_port": 0,
"string_user": null,
"type": "ExecProcess"
}
}
]