Skip to content

Commit

Permalink
use /etc/subuid and /etc/subgid to get auxiliary ranges
Browse files Browse the repository at this point in the history
  • Loading branch information
tofay committed Aug 29, 2023
1 parent 6a9dd8d commit 0f025dc
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 20 deletions.
1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ glob = "0.3.0"
log = "0.4.19"
nix = { version = "0.26.2", features = [
"sched",
"signal",
"user",
], default-features = false }
oci-spec = { version = "0.6.1", features = ["image"], default-features = false }
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ rpmoci builds OCI container images from RPM packages, using [DNF](https://github
rpmoci features:

- **deterministic** rpmoci locks RPM dependencies using the package file/lockfile paradigm of bundler/cargo etc and supports vendoring of RPMs for later rebuilds.
- **unprivileged** rpmoci can build images in environments without docker access, and without root access (this relies on the user being able to create [user namespaces](https://www.man7.org/linux/man-pages/man7/user_namespaces.7.html))
- **unprivileged** rpmoci can build images in environments without access to a container runtime, and without root access (this relies on the user being able to create [user namespaces](https://www.man7.org/linux/man-pages/man7/user_namespaces.7.html))
- **small** rpmoci images are built solely from the RPMs you request, so don't contain unnecessary dependencies.

The design of rpmoci is influenced by [apko](https://github.com/chainguard-dev/apko) and [distroless](https://github.com/GoogleContainerTools/distroless) tooling.
Expand All @@ -19,6 +19,14 @@ rpmoci isn't published to a widely accessible location yet so you'll need build

rpmoci has a runtime dependency on dnf and python-rpm (a dependency of dnf, so shouldn't need to be specified directly).

### Rootless setup
When rpmoci runs as a non-root user it will automatically attempt to setup a user namespace in which to run.
rpmoci maps the user's uid/gid to root in the user namespace.

It also attempts to map the current user's subuid/subgid range into the user namespace, which is required for rpmoci to be able to create containers from RPMs that contain files owned by a non-root user.

If your user doesn't have any subuids/subgids then you'll need to create them per [https://rootlesscontaine.rs/getting-started/common/subuid/](https://rootlesscontaine.rs/getting-started/common/subuid/).

## Getting started
You need to create an rpmoci.toml file. An example is:

Expand Down
116 changes: 98 additions & 18 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
//!
//! You should have received a copy of the GNU General Public License
//! along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::{fs::File, io::Write, os::unix::process::CommandExt, path::PathBuf, process::Command};
use std::{fs::read_to_string, os::unix::process::CommandExt, path::Path, process::Command};

use anyhow::{bail, Context, Result};
use clap::Parser;
use nix::{
sched::CloneFlags,
sys::{signal, wait::wait},
unistd::{close, getgid, getuid, pipe, read},
sys::wait::wait,
unistd::{close, getgid, getuid, pipe, read, Gid, Group, Pid, Uid, User},
};
use rpmoci::write;

Expand Down Expand Up @@ -65,27 +65,15 @@ fn run_in_userns() -> anyhow::Result<()> {
255
}),
stack,
CloneFlags::CLONE_NEWUSER,
Some(signal::SIGCHLD as i32),
CloneFlags::CLONE_NEWUSER | CloneFlags::CLONE_NEWNS,
None,
)
.context("Clone failed")?;

// this parent process sets up user namespace mappings, notifies the child to continue,
// then waits for the child to exit
close(reader)?;
let child_proc = PathBuf::from("/proc").join(child.to_string());
File::create(child_proc.join("setgroups"))
.context("failed to create setgroups file")?
.write_all(b"deny")
.context("failed to write to setgroups file")?;
// map the current uid/gid to root, and create mappings for uids/gids 1-999 as
// RPMs could potentially contain files owned by any of these
// (these additional mappings are the cause of us needing to spawn a child - otherwise
// we could just unshare and configure mappings in the current process)
File::create(child_proc.join("uid_map"))?
.write_all(format!("0 {user_id} 1\n1 100000 999").as_bytes())?;
File::create(child_proc.join("gid_map"))?
.write_all(format!("0 {group_id} 1\n1 100000 999").as_bytes())?;
setup_id_maps(child, user_id, group_id).context("Failed to setup id mappings")?;
close(writer)?;
let status = wait()?;
if let nix::sys::wait::WaitStatus::Exited(_, code) = status {
Expand All @@ -95,6 +83,98 @@ fn run_in_userns() -> anyhow::Result<()> {
}
}

// Represents a range of sub uid/gids
#[derive(Debug)]
struct SubIdRange {
start: usize,
count: usize,
}

fn get_sub_id_ranges(
subid_path: impl AsRef<Path>,
id: &str,
name: Option<&str>,
) -> anyhow::Result<Vec<SubIdRange>> {
let subid_path = subid_path.as_ref();
Ok(read_to_string(subid_path)
.context(format!(
"Failed to read sub id file {}",
subid_path.display()
))?
.lines() // split the string into an iterator of string slices
.filter_map(|line| {
let parts = line.splitn(2, ':').collect::<Vec<_>>();
if parts.len() != 3 {
// Not a valid line
return None;
}
if Some(parts[0]) != name || parts[0] != id {
// Not a line for the desired user/group
return None;
}
if let (Ok(start), Ok(count)) = (parts[1].parse::<usize>(), parts[2].parse::<usize>()) {
Some(SubIdRange { start, count })
} else {
None
}
})
.collect())
}

/// Create new uid/gid mappings for the current user/group
fn setup_id_maps(child: Pid, uid: Uid, gid: Gid) -> anyhow::Result<()> {
let username = User::from_uid(uid).ok().flatten().map(|user| user.name);
let uid_string = uid.to_string();
let subuid_ranges = get_sub_id_ranges("/etc/subuid", &uid_string, username.as_deref())?;

let groupname = Group::from_gid(gid)
.ok()
.flatten()
.map(|group: Group| group.name);
let gid_string = gid.to_string();
let subgid_ranges = get_sub_id_ranges("/etc/subuid", &gid.to_string(), groupname.as_deref())?;

let mut uid_args = vec![
child.to_string(),
"0".to_string(),
uid_string,
"1".to_string(),
];
let mut next_uid = 1;
for range in subuid_ranges {
uid_args.push(next_uid.to_string());
uid_args.push(range.start.to_string());
uid_args.push(range.count.to_string());
next_uid += range.count;
}

let mut gid_args = vec![
child.to_string(),
"0".to_string(),
gid_string,
"1".to_string(),
];
let mut next_gid = 1;
for range in subgid_ranges {
gid_args.push(next_gid.to_string());
gid_args.push(range.start.to_string());
gid_args.push(range.count.to_string());
next_gid += range.count;
}

let status = Command::new("newuidmap").args(uid_args).status()?;
if !status.success() {
bail!("Failed to create uid mappings");
}

let status = Command::new("newgidmap").args(gid_args).status()?;
if !status.success() {
bail!("Failed to create gid mappings");
}

Ok(())
}

fn try_main() -> Result<()> {
let args = rpmoci::cli::Cli::parse();
env_logger::Builder::new()
Expand Down

0 comments on commit 0f025dc

Please sign in to comment.