agent: Add SELinux support for containers

The kata-agent supports SELinux for containers inside the guest
to comply with the OCI runtime specification.

Fixes: #4812

Signed-off-by: Manabu Sugimoto <Manabu.Sugimoto@sony.com>
This commit is contained in:
Manabu Sugimoto 2022-08-07 18:14:22 +09:00
parent a75f99d20d
commit 9354769286
7 changed files with 190 additions and 7 deletions

10
src/agent/Cargo.lock generated
View File

@ -1764,6 +1764,7 @@ dependencies = [
"tempfile",
"test-utils",
"tokio",
"xattr",
"zbus",
]
@ -2552,6 +2553,15 @@ version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
[[package]]
name = "xattr"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc"
dependencies = [
"libc",
]
[[package]]
name = "zbus"
version = "2.3.2"

View File

@ -35,6 +35,7 @@ inotify = "0.9.2"
libseccomp = { version = "0.3.0", optional = true }
zbus = "2.3.0"
bit-vec= "0.6.3"
xattr = "0.2.3"
[dev-dependencies]
serial_test = "0.5.0"

View File

@ -30,6 +30,7 @@ use crate::log_child;
use crate::process::Process;
#[cfg(feature = "seccomp")]
use crate::seccomp;
use crate::selinux;
use crate::specconv::CreateOpts;
use crate::{mount, validator};
@ -537,6 +538,8 @@ fn do_init_child(cwfd: RawFd) -> Result<()> {
}
}
let selinux_enabled = selinux::is_enabled()?;
sched::unshare(to_new & !CloneFlags::CLONE_NEWUSER)?;
if userns {
@ -638,6 +641,18 @@ fn do_init_child(cwfd: RawFd) -> Result<()> {
capctl::prctl::set_no_new_privs().map_err(|_| anyhow!("cannot set no new privileges"))?;
}
// Set SELinux label
if !oci_process.selinux_label.is_empty() {
if !selinux_enabled {
return Err(anyhow!(
"SELinux label for the process is provided but SELinux is not enabled on the running kernel"
));
}
log_child!(cfd_log, "Set SELinux label to the container process");
selinux::set_exec_label(&oci_process.selinux_label)?;
}
// Log unknown seccomp system calls in advance before the log file descriptor closes.
#[cfg(feature = "seccomp")]
if let Some(ref scmp) = linux.seccomp {

View File

@ -38,6 +38,7 @@ pub mod pipestream;
pub mod process;
#[cfg(feature = "seccomp")]
pub mod seccomp;
pub mod selinux;
pub mod specconv;
pub mod sync;
pub mod sync_with_async;

View File

@ -25,6 +25,7 @@ use std::fs::File;
use std::io::{BufRead, BufReader};
use crate::container::DEFAULT_DEVICES;
use crate::selinux;
use crate::sync::write_count;
use std::string::ToString;
@ -181,6 +182,8 @@ pub fn init_rootfs(
None => flags |= MsFlags::MS_SLAVE,
}
let label = &linux.mount_label;
let root = spec
.root
.as_ref()
@ -244,7 +247,7 @@ pub fn init_rootfs(
}
}
mount_from(cfd_log, m, rootfs, flags, &data, "")?;
mount_from(cfd_log, m, rootfs, flags, &data, label)?;
// bind mount won't change mount options, we need remount to make mount options
// effective.
// first check that we have non-default options required before attempting a
@ -524,7 +527,6 @@ pub fn pivot_rootfs<P: ?Sized + NixPath + std::fmt::Debug>(path: &P) -> Result<(
fn rootfs_parent_mount_private(path: &str) -> Result<()> {
let mount_infos = parse_mount_table(MOUNTINFO_PATH)?;
let mut max_len = 0;
let mut mount_point = String::from("");
let mut options = String::from("");
@ -767,9 +769,9 @@ fn mount_from(
rootfs: &str,
flags: MsFlags,
data: &str,
_label: &str,
label: &str,
) -> Result<()> {
let d = String::from(data);
let mut d = String::from(data);
let dest = secure_join(rootfs, &m.destination);
let src = if m.r#type.as_str() == "bind" {
@ -822,6 +824,37 @@ fn mount_from(
e
})?;
// Set the SELinux context for the mounts
let mut use_xattr = false;
if !label.is_empty() {
if selinux::is_enabled()? {
let device = Path::new(&m.source)
.file_name()
.ok_or_else(|| anyhow!("invalid device source path: {}", &m.source))?
.to_str()
.ok_or_else(|| anyhow!("failed to convert device source path: {}", &m.source))?;
match device {
// SELinux does not support labeling of /proc or /sys
"proc" | "sysfs" => (),
// SELinux does not support mount labeling against /dev/mqueue,
// so we use setxattr instead
"mqueue" => {
use_xattr = true;
}
_ => {
log_child!(cfd_log, "add SELinux mount label to {}", dest.as_str());
selinux::add_mount_label(&mut d, label);
}
}
} else {
log_child!(
cfd_log,
"SELinux label for the mount is provided but SELinux is not enabled on the running kernel"
);
}
}
mount(
Some(src.as_str()),
dest.as_str(),
@ -834,6 +867,10 @@ fn mount_from(
e
})?;
if !label.is_empty() && selinux::is_enabled()? && use_xattr {
xattr::set(dest.as_str(), "security.selinux", label.as_bytes())?;
}
if flags.contains(MsFlags::MS_BIND)
&& flags.intersects(
!(MsFlags::MS_REC

View File

@ -0,0 +1,80 @@
// Copyright 2022 Sony Group Corporation
//
// SPDX-License-Identifier: Apache-2.0
//
use anyhow::{Context, Result};
use nix::unistd::gettid;
use std::fs::{self, OpenOptions};
use std::io::prelude::*;
use std::path::Path;
pub fn is_enabled() -> Result<bool> {
let buf = fs::read_to_string("/proc/mounts")?;
let enabled = buf.contains("selinuxfs");
Ok(enabled)
}
pub fn add_mount_label(data: &mut String, label: &str) {
if data.is_empty() {
let context = format!("context=\"{}\"", label);
data.push_str(&context);
} else {
let context = format!(",context=\"{}\"", label);
data.push_str(&context);
}
}
pub fn set_exec_label(label: &str) -> Result<()> {
let mut attr_path = Path::new("/proc/thread-self/attr/exec").to_path_buf();
if !attr_path.exists() {
// Fall back to the old convention
attr_path = Path::new("/proc/self/task")
.join(gettid().to_string())
.join("attr/exec")
}
let mut file = OpenOptions::new()
.write(true)
.truncate(true)
.open(attr_path)?;
file.write_all(label.as_bytes())
.with_context(|| "failed to apply SELinux label")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_LABEL: &str = "system_u:system_r:unconfined_t:s0";
#[test]
fn test_is_enabled() {
let ret = is_enabled();
assert!(ret.is_ok(), "Expecting Ok, Got {:?}", ret);
}
#[test]
fn test_add_mount_label() {
let mut data = String::new();
add_mount_label(&mut data, TEST_LABEL);
assert_eq!(data, format!("context=\"{}\"", TEST_LABEL));
let mut data = String::from("defaults");
add_mount_label(&mut data, TEST_LABEL);
assert_eq!(data, format!("defaults,context=\"{}\"", TEST_LABEL));
}
#[test]
fn test_set_exec_label() {
let ret = set_exec_label(TEST_LABEL);
if is_enabled().unwrap() {
assert!(ret.is_ok(), "Expecting Ok, Got {:?}", ret);
} else {
assert!(ret.is_err(), "Expecting error, Got {:?}", ret);
}
}
}

View File

@ -6,6 +6,7 @@
use crate::container::Config;
use anyhow::{anyhow, Context, Result};
use oci::{Linux, LinuxIdMapping, LinuxNamespace, Spec};
use regex::Regex;
use std::collections::HashMap;
use std::path::{Component, PathBuf};
@ -86,6 +87,23 @@ fn hostname(oci: &Spec) -> Result<()> {
fn security(oci: &Spec) -> Result<()> {
let linux = get_linux(oci)?;
let label_pattern = r".*_u:.*_r:.*_t:s[0-9]|1[0-5].*";
let label_regex = Regex::new(label_pattern)?;
if let Some(ref process) = oci.process {
if !process.selinux_label.is_empty() && !label_regex.is_match(&process.selinux_label) {
return Err(anyhow!(
"SELinux label for the process is invalid format: {}",
&process.selinux_label
));
}
}
if !linux.mount_label.is_empty() && !label_regex.is_match(&linux.mount_label) {
return Err(anyhow!(
"SELinux label for the mount is invalid format: {}",
&linux.mount_label
));
}
if linux.masked_paths.is_empty() && linux.readonly_paths.is_empty() {
return Ok(());
@ -95,8 +113,6 @@ fn security(oci: &Spec) -> Result<()> {
return Err(anyhow!("Linux namespace does not contain mount"));
}
// don't care about selinux at present
Ok(())
}
@ -285,7 +301,7 @@ pub fn validate(conf: &Config) -> Result<()> {
#[cfg(test)]
mod tests {
use super::*;
use oci::Mount;
use oci::{Mount, Process};
#[test]
fn test_namespace() {
@ -388,6 +404,29 @@ mod tests {
];
spec.linux = Some(linux);
security(&spec).unwrap();
// SELinux
let valid_label = "system_u:system_r:container_t:s0:c123,c456";
let mut process = Process::default();
process.selinux_label = valid_label.to_string();
spec.process = Some(process);
security(&spec).unwrap();
let mut linux = Linux::default();
linux.mount_label = valid_label.to_string();
spec.linux = Some(linux);
security(&spec).unwrap();
let invalid_label = "system_u:system_r:container_t";
let mut process = Process::default();
process.selinux_label = invalid_label.to_string();
spec.process = Some(process);
security(&spec).unwrap_err();
let mut linux = Linux::default();
linux.mount_label = invalid_label.to_string();
spec.linux = Some(linux);
security(&spec).unwrap_err();
}
#[test]