Skip to content

Commit

Permalink
Extend SELinux support (#1032)
Browse files Browse the repository at this point in the history
* Split selinux context into mount and execve

Split the selinux configuration into the context that is used when the
rootfs is mounted and the context the container process in spawned into.

* Cleanup of squashfs pseudo file generation

* Set security.selinux attribute in dev mounts

The minimal dev mounted with the mount type `dev` shall be labeld
correctly.

* Change android default seclabel to northstar

* Raise mksquashfs version requirement to 4.6

* Pipe pseudofile definition via stdin to mksquashfs

Avoid the usage of a tempfile and use mksquashfs ability to read the
pseudo file definition from stdin.

* Deny unknown fields in selinux manifest configuration

* Check if SELinux is enabled only once at startup

* Mount container root nosuid

* Clippy fixes
  • Loading branch information
flxo authored Oct 19, 2023
1 parent d2c5d0a commit 8cf2da4
Show file tree
Hide file tree
Showing 13 changed files with 269 additions and 135 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ Install build dependencies on Debian based distributions by running
sudo apt-get install build-essential libclang1 squashfs-tools
```

The `squashfs-tools` package is required in version **4.1** or higher.
The `squashfs-tools` package is required in version **4.6** or higher.

Northstar comes with a set of [examples](./examples) that demonstrate most of
the Northstar features. Building the example binaries and packing its
Expand Down Expand Up @@ -184,7 +184,7 @@ sudo apt install help2man libz-dev liblzo2-dev liblz4-dev libzstd-dev
git clone https://github.com/plougher/squashfs-tools.git
cd squashfs-tools/squashfs-tools
git checkout 4.6.1
sudo CONFIG=1 LZO_SUPPORT=1 LZ4_SUPPORT=1 ZSTD_SUPPORT=1 XZ_SUPPORT=1 make -j $(nproc) install
sudo CONFIG=1 LZO_SUPPORT=1 LZ4_SUPPORT=1 ZSTD_SUPPORT=1 XZ_SUPPORT=1 XATTR_SUPPORT=1 make -j $(nproc) install
```

## Configuration
Expand Down
2 changes: 1 addition & 1 deletion android/northstar.rc
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ on post-fs-data
service northstar /system/bin/logwrapper -- /system/bin/northstar --disable-mount-namespace -c /system/etc/northstar.toml
class main
setenv ANDROID_DNS_MODE local
seclabel u:r:native:s0
seclabel u:r:northstar:s0
user system
group system inet shell
devmode
Expand Down
3 changes: 2 additions & 1 deletion examples/inspect/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ rlimits:
hard: 20000
suppl_groups: [src, inet]
selinux:
context: unconfined_u:object_r:user_home_t:s0
mount_context: unconfined_u:object_r:user_home_t:s0
exec: unconfined_u:object_r:inspect_t:s0
3 changes: 2 additions & 1 deletion examples/test-container/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ seccomp:
clone: any # Needed for socket tests.
clone3: any # Needed for socket tests.
selinux:
context: unconfined_u:object_r:user_home_t:s0
mount_context: unconfined_u:object_r:user_home_t:s0
exec: unconfined_u:object_r:test_container_t:s0
sockets:
datagram:
type: datagram
Expand Down
7 changes: 7 additions & 0 deletions northstar-runtime/src/npk/manifest/mount.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,13 @@ impl fmt::Display for MountOption {
#[derive(Default, Clone, Eq, PartialEq, Debug)]
pub struct MountOptions(HashSet<MountOption>);

impl MountOptions {
/// Check if the options container the rw flag.
pub fn is_rw(&self) -> bool {
self.contains(&MountOption::Rw)
}
}

impl std::ops::Deref for MountOptions {
type Target = HashSet<MountOption>;

Expand Down
64 changes: 52 additions & 12 deletions northstar-runtime/src/npk/manifest/selinux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,70 @@ use crate::common::non_nul_string::NonNulString;

/// SELinux configuration
#[derive(Clone, Eq, PartialEq, Debug, Validate, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Selinux {
/// Explicit list of allowed syscalls
/// Default SE label (mount option context=...).
#[validate(custom = "validate_context")]
pub context: NonNulString,
pub mount_context: Option<NonNulString>,
/// SE context for the execve call from init.
#[validate(custom = "validate_context")]
pub exec: Option<NonNulString>,
}

/// Validate selinux settings
fn validate_context(context: &NonNulString) -> Result<(), ValidationError> {
fn validate_context<T: AsRef<str>>(context: T) -> Result<(), ValidationError> {
// Maximum length since at least Linux v3.7
// (https://elixir.bootlin.com/linux/v3.7/source/include/uapi/linux/limits.h)
const XATTR_SIZE_MAX: usize = 65536;

if context.len() >= XATTR_SIZE_MAX {
return Err(ValidationError::new("Selinux context too long"));
if context.as_ref().is_empty() {
return Err(ValidationError::new("SELinux context is empty"));
}

if context.as_ref().len() >= XATTR_SIZE_MAX {
return Err(ValidationError::new("SELinux context too long"));
}

if !context
context
.as_ref()
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == ':' || c == '_')
{
return Err(ValidationError::new(
"Selinux context must consist of alphanumeric ASCII characters, '?' or '_'",
));
}
.then_some(())
.ok_or_else(|| {
ValidationError::new(
"SELinux context must consist of alphanumeric ASCII characters, ':' or '_'",
)
})
}

#[test]
fn validate_valid_context() {
assert!(validate_context("system_u:object_r:container_file_t:s0").is_ok());
}

#[test]
fn validate_context_with_invalid_char() {
assert!(validate_context("system_u:object_r:container_file_t@s0").is_err());
}

#[test]
fn validate_context_with_space() {
assert!(validate_context("system_u:object_r: container_file_ts0").is_err());
}

#[test]
fn validate_invalid_empty_context() {
assert!(validate_context("").is_err());
}

Ok(())
#[test]
fn deserialize_unknown_field() {
serde_json::from_str::<Selinux>(
"{
\"mount_context\": \"system_u:object_r:container_file_t:s0\",
\"exec\": \"system_u:object_r:container_file_t:s0\",
\"unknown\": \"system_u:object_r:container_file_t:s0\"
}",
)
.expect_err("unknown field should not be deserialized");
}
10 changes: 5 additions & 5 deletions northstar-runtime/src/npk/manifest/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ fn invalid_gid_zero() -> Result<()> {
#[test]
fn invalid_selinux_context() -> Result<()> {
let manifest =
"name: hello\nversion: 0.0.0\ninit: /binary\nuid: 1\ngid: 1\nselinux:\n context: fo@o";
"name: hello\nversion: 0.0.0\ninit: /binary\nuid: 1\ngid: 1\nselinux:\n mount_context: fo@o";
assert!(Manifest::from_str(manifest).is_err());
Ok(())
}
Expand Down Expand Up @@ -308,7 +308,7 @@ mounts:
version: '>=1.0.0'
dir: bin/foo
";
Manifest::from_str(manifest).unwrap();
Manifest::from_str(manifest).expect("failed to parse manifest");
}

/// Resource mount with absolute dir
Expand All @@ -322,7 +322,7 @@ mounts:
version: '>=1.0.0'
dir: /bin/foo
";
Manifest::from_str(manifest).unwrap();
Manifest::from_str(manifest).expect("failed to parse manifest");
}

#[test]
Expand All @@ -343,7 +343,7 @@ mounts:
size: 100GB
";
let mountpoint = |s| -> NonNulString { unsafe { NonNulString::from_str_unchecked(s) } };
let manifest = Manifest::from_str(manifest).unwrap();
let manifest = Manifest::from_str(manifest).expect("failed to parse manifest");
assert_eq!(
manifest.mounts.get(&mountpoint("/a")),
Some(&Mount::Tmpfs(Tmpfs { size: 100 }))
Expand Down Expand Up @@ -388,7 +388,7 @@ mounts:
dir: /
options: rw,noexec,nosuid
";
Manifest::from_str(manifest).unwrap();
Manifest::from_str(manifest).expect("failed to parse manifest");
}

const ROUNDTRIP_MANIFEST: &str = "
Expand Down
Loading

0 comments on commit 8cf2da4

Please sign in to comment.