genpolicy: Integrate /etc/passwd from OCI container when setting GIDs

The GID used for the running process in an OCI container is a function of
1. The securityContext.runAsGroup specified in a pod yaml, 2. The UID:GID mapping in
/etc/passwd, if present in the container image layers, 3. Zero, even if
the userstr specifies a GID.

Make our policy engine align with this behavior by:
1. At the registry level, always obtain the GID from the /etc/passwd
   file if present. Ignore GIDs specified in the userstr encoded in the
OCI container.
2. After an update to UID due to securityContexts, perform one final check against
   the /etc/passwd file if present. The GID used for the running
process is the mapping in this file from UID->GID.
3. Override everything above with the GID of the securityContext
   configuration if provided

Signed-off-by: Cameron Baird <cameronbaird@microsoft.com>
This commit is contained in:
Cameron Baird 2025-03-27 23:23:06 +00:00
parent c13d7796ee
commit eb2c7f4150
12 changed files with 100 additions and 17 deletions

View File

@ -34,6 +34,13 @@
"io.katacontainers.pkg.oci.bundle_path": "/run/containerd/io.containerd.runtime.v2.task/k8s.io/$(bundle-id)"
},
"Process": {
"NoNewPrivileges": true,
"User": {
"UID": 65535,
"GID": 65535,
"AdditionalGids": [],
"Username": ""
},
"Args": [
"/pause"
]

View File

@ -148,10 +148,11 @@ impl yaml::K8sResource for CronJob {
false
}
fn get_process_fields(&self, process: &mut policy::KataProcess) {
fn get_process_fields(&self, process: &mut policy::KataProcess, must_check_passwd: &mut bool) {
yaml::get_process_fields(
process,
&self.spec.jobTemplate.spec.template.spec.securityContext,
must_check_passwd,
);
}

View File

@ -148,8 +148,12 @@ impl yaml::K8sResource for DaemonSet {
.or_else(|| Some(String::new()))
}
fn get_process_fields(&self, process: &mut policy::KataProcess) {
yaml::get_process_fields(process, &self.spec.template.spec.securityContext);
fn get_process_fields(&self, process: &mut policy::KataProcess, must_check_passwd: &mut bool) {
yaml::get_process_fields(
process,
&self.spec.template.spec.securityContext,
must_check_passwd,
);
}
fn get_sysctls(&self) -> Vec<pod::Sysctl> {

View File

@ -146,8 +146,12 @@ impl yaml::K8sResource for Deployment {
.or_else(|| Some(String::new()))
}
fn get_process_fields(&self, process: &mut policy::KataProcess) {
yaml::get_process_fields(process, &self.spec.template.spec.securityContext);
fn get_process_fields(&self, process: &mut policy::KataProcess, must_check_passwd: &mut bool) {
yaml::get_process_fields(
process,
&self.spec.template.spec.securityContext,
must_check_passwd,
);
}
fn get_sysctls(&self) -> Vec<pod::Sysctl> {

View File

@ -111,8 +111,12 @@ impl yaml::K8sResource for Job {
false
}
fn get_process_fields(&self, process: &mut policy::KataProcess) {
yaml::get_process_fields(process, &self.spec.template.spec.securityContext);
fn get_process_fields(&self, process: &mut policy::KataProcess, must_check_passwd: &mut bool) {
yaml::get_process_fields(
process,
&self.spec.template.spec.securityContext,
must_check_passwd,
);
}
fn get_sysctls(&self) -> Vec<pod::Sysctl> {

View File

@ -911,8 +911,8 @@ impl yaml::K8sResource for Pod {
.or_else(|| Some(String::new()))
}
fn get_process_fields(&self, process: &mut policy::KataProcess) {
yaml::get_process_fields(process, &self.spec.securityContext);
fn get_process_fields(&self, process: &mut policy::KataProcess, must_check_passwd: &mut bool) {
yaml::get_process_fields(process, &self.spec.securityContext, must_check_passwd);
}
fn get_sysctls(&self) -> Vec<Sysctl> {
@ -970,6 +970,19 @@ impl Container {
if let Some(context) = &self.securityContext {
if let Some(uid) = context.runAsUser {
process.User.UID = uid.try_into().unwrap();
// Changing the UID can break the GID mapping
// if a /etc/passwd file is present.
// The proper GID is determined, in order of preference:
// 1. the securityContext runAsGroup field (applied last in code)
// 2. lacking an explicit runAsGroup, /etc/passwd (get_gid_from_passwd_uid)
// 3. fall back to pod-level GID if there is one (unwrap_or)
//
// This behavior comes from the containerd runtime implementation:
// WithUser https://github.com/containerd/containerd/blob/main/pkg/oci/spec_opts.go#L592
process.User.GID = self
.registry
.get_gid_from_passwd_uid(process.User.UID)
.unwrap_or(process.User.GID);
}
if let Some(gid) = context.runAsGroup {

View File

@ -713,7 +713,17 @@ impl AgentPolicy {
substitute_args_env_variables(&mut process.Args, &process.Env);
c_settings.get_process_fields(&mut process);
resource.get_process_fields(&mut process);
let mut must_check_passwd = false;
resource.get_process_fields(&mut process, &mut must_check_passwd);
// The actual GID of the process run by the CRI
// Depends on the contents of /etc/passwd in the container
if must_check_passwd {
process.User.GID = yaml_container
.registry
.get_gid_from_passwd_uid(process.User.UID)
.unwrap_or(0);
}
yaml_container.get_process_fields(&mut process);
process

View File

@ -356,6 +356,12 @@ impl Container {
debug!("Parsing gid from user[1] = {:?}", user[1]);
process.User.GID = self.parse_group_string(user[1]);
debug!(
"Overriding OCI container GID with UID:GID mapping from /etc/passwd"
);
process.User.GID =
self.get_gid_from_passwd_uid(process.User.UID).unwrap_or(0);
}
} else {
debug!("Parsing uid from image_user = {}", image_user);

View File

@ -109,8 +109,12 @@ impl yaml::K8sResource for ReplicaSet {
false
}
fn get_process_fields(&self, process: &mut policy::KataProcess) {
yaml::get_process_fields(process, &self.spec.template.spec.securityContext);
fn get_process_fields(&self, process: &mut policy::KataProcess, must_check_passwd: &mut bool) {
yaml::get_process_fields(
process,
&self.spec.template.spec.securityContext,
must_check_passwd,
);
}
fn get_sysctls(&self) -> Vec<pod::Sysctl> {

View File

@ -111,8 +111,12 @@ impl yaml::K8sResource for ReplicationController {
false
}
fn get_process_fields(&self, process: &mut policy::KataProcess) {
yaml::get_process_fields(process, &self.spec.template.spec.securityContext);
fn get_process_fields(&self, process: &mut policy::KataProcess, must_check_passwd: &mut bool) {
yaml::get_process_fields(
process,
&self.spec.template.spec.securityContext,
must_check_passwd,
);
}
fn get_sysctls(&self) -> Vec<pod::Sysctl> {

View File

@ -193,8 +193,12 @@ impl yaml::K8sResource for StatefulSet {
.or_else(|| Some(String::new()))
}
fn get_process_fields(&self, process: &mut policy::KataProcess) {
yaml::get_process_fields(process, &self.spec.template.spec.securityContext);
fn get_process_fields(&self, process: &mut policy::KataProcess, must_check_passwd: &mut bool) {
yaml::get_process_fields(
process,
&self.spec.template.spec.securityContext,
must_check_passwd,
);
}
fn get_sysctls(&self) -> Vec<pod::Sysctl> {

View File

@ -96,7 +96,11 @@ pub trait K8sResource {
None
}
fn get_process_fields(&self, _process: &mut policy::KataProcess) {
fn get_process_fields(
&self,
_process: &mut policy::KataProcess,
_must_check_passwd: &mut bool,
) {
// No need to implement support for securityContext or similar fields
// for some of the K8s resource types.
}
@ -386,14 +390,32 @@ fn handle_unused_field(path: &str, silent_unsupported_fields: bool) {
pub fn get_process_fields(
process: &mut policy::KataProcess,
security_context: &Option<pod::PodSecurityContext>,
must_check_passwd: &mut bool,
) {
if let Some(context) = security_context {
if let Some(uid) = context.runAsUser {
process.User.UID = uid.try_into().unwrap();
// Changing the UID can break the GID mapping
// if a /etc/passwd file is present.
// The proper GID is determined, in order of preference:
// 1. the securityContext runAsGroup field (applied last in code)
// 2. lacking an explicit runAsGroup, /etc/passwd
// (parsed in policy::get_container_process())
// 3. lacking an /etc/passwd, 0 (unwrap_or)
//
// This behavior comes from the containerd runtime implementation:
// WithUser https://github.com/containerd/containerd/blob/main/pkg/oci/spec_opts.go#L592
//
// We can't parse the /etc/passwd file here because
// we are in the resource context. Defer execution to outside
// the resource context, in policy::get_container_process()
// IFF the UID is changed by the resource securityContext but not the GID.
*must_check_passwd = true;
}
if let Some(gid) = context.runAsGroup {
process.User.GID = gid.try_into().unwrap();
*must_check_passwd = false;
}
if let Some(allow) = context.allowPrivilegeEscalation {