read privateUsers from the closure record at start, and move the data owner option under hive-controller
This commit is contained in:
parent
1fd591d28b
commit
f5a609911c
4 changed files with 208 additions and 97 deletions
|
|
@ -26,6 +26,7 @@ pub(crate) fn handle_write_nspawn_flags(
|
|||
isolation: &NetworkIsolation,
|
||||
load_credentials: &[CredentialMount],
|
||||
hive_facts: &HiveFacts,
|
||||
private_users: Option<bool>,
|
||||
) -> Result<(String, String)> {
|
||||
let agent = validate_container_system_name(container)?;
|
||||
for bind in binds {
|
||||
|
|
@ -52,7 +53,7 @@ pub(crate) fn handle_write_nspawn_flags(
|
|||
});
|
||||
// After the salvage, so the mount points land in the root the start will
|
||||
// snapshot.
|
||||
let userns = crate::userns::activate(container, agent, binds)?;
|
||||
let userns = crate::userns::activate(container, agent, binds, private_users)?;
|
||||
write_nspawn_flags(
|
||||
container,
|
||||
binds,
|
||||
|
|
@ -77,6 +78,7 @@ pub(crate) async fn handle_write_nspawn_flags_blocking(
|
|||
isolation: NetworkIsolation,
|
||||
load_credentials: Vec<CredentialMount>,
|
||||
hive_facts: HiveFacts,
|
||||
private_users: Option<bool>,
|
||||
) -> Result<(String, String)> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
handle_write_nspawn_flags(
|
||||
|
|
@ -85,6 +87,7 @@ pub(crate) async fn handle_write_nspawn_flags_blocking(
|
|||
&isolation,
|
||||
&load_credentials,
|
||||
&hive_facts,
|
||||
private_users,
|
||||
)
|
||||
})
|
||||
.await
|
||||
|
|
@ -1163,7 +1166,7 @@ mod tests {
|
|||
let facts = HiveFacts::default();
|
||||
for name in ["hive-forge", "hive-matrix"] {
|
||||
let errs = [
|
||||
handle_write_nspawn_flags(name, &[], &isolation, &[], &facts).unwrap_err(),
|
||||
handle_write_nspawn_flags(name, &[], &isolation, &[], &facts, None).unwrap_err(),
|
||||
write_resource_limits(name, "4G", "200%", None, None).unwrap_err(),
|
||||
remove_service_dropin(name).unwrap_err(),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -153,11 +153,14 @@ pub(crate) fn remove_legacy_tmpfiles() -> Result<()> {
|
|||
}
|
||||
|
||||
/// `EnsureAgentSocketDir` — create `/run/hive-agent/<name>` if missing.
|
||||
pub(crate) fn ensure_agent_socket_dir(name: &str) -> Result<(String, String)> {
|
||||
pub(crate) fn ensure_agent_socket_dir(
|
||||
name: &str,
|
||||
private_users: Option<bool>,
|
||||
) -> Result<(String, String)> {
|
||||
ensure_socket_dir_in(Path::new(SOCKET_DIR_ROOT), name)?;
|
||||
// A namespaced agent's activation cannot chown the dir through the id
|
||||
// mapping, so the data owner, which is the agent user inside, gets it here.
|
||||
if let Some(owner) = crate::userns::owner_for_agent(name)? {
|
||||
if let Some(owner) = crate::userns::owner_for_agent(name, private_users)? {
|
||||
let path = Path::new(SOCKET_DIR_ROOT).join(name);
|
||||
std::os::unix::fs::lchown(&path, Some(owner), Some(owner))
|
||||
.with_context(|| format!("chown {}", path.display()))?;
|
||||
|
|
|
|||
|
|
@ -391,18 +391,23 @@ async fn exec(
|
|||
isolation,
|
||||
load_credentials,
|
||||
hive_facts,
|
||||
private_users,
|
||||
} => {
|
||||
container_config::handle_write_nspawn_flags_blocking(
|
||||
container,
|
||||
binds,
|
||||
isolation,
|
||||
load_credentials,
|
||||
hive_facts,
|
||||
*hive_facts,
|
||||
private_users,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
PrivRequest::EnsureAgentSocketDir { ref name } => files::ensure_agent_socket_dir(name),
|
||||
PrivRequest::EnsureAgentSocketDir {
|
||||
ref name,
|
||||
private_users,
|
||||
} => files::ensure_agent_socket_dir(name, private_users),
|
||||
|
||||
PrivRequest::WriteResourceLimits {
|
||||
ref container,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
//! Agents that run in their own user namespace.
|
||||
//!
|
||||
//! The operator lists them in `services.hyperhive.userns.agents` and names the
|
||||
//! one host uid:gid that owns their data on disk (`dataOwner`); the unit's
|
||||
//! environment carries both, so only root decides who is namespaced. A listed
|
||||
//! agent starts with `--private-users=pick`, a root idmapped rather than
|
||||
//! chowned, and every bind mounted `owneridmap`: the owner of the bind's
|
||||
//! target inside the container, the agent user, is the data owner on disk.
|
||||
//! An agent's config sets `services.hyperhive.agent.user.privateUsers`; the
|
||||
//! swarm builder records it, hive-c0re reads the record and sends the flag with
|
||||
//! each start. The host's data owner (`dataOwner`, one uid:gid) comes from this
|
||||
//! unit's environment. A namespaced agent starts with `--private-users=pick`, a
|
||||
//! root idmapped rather than chowned, and every bind mounted `owneridmap`: the
|
||||
//! owner of the bind's target inside the container, the agent user, is the data
|
||||
//! owner on disk.
|
||||
//!
|
||||
//! Getting there moves the agent's data once. [`activate`] chowns its `state`,
|
||||
//! `harness`, `work` and `claude` dirs to the data owner, never touching
|
||||
|
|
@ -28,7 +29,6 @@ use rustix::io::Errno;
|
|||
|
||||
use crate::files::SOCKET_DIR_ROOT;
|
||||
|
||||
const AGENTS_ENV: &str = "HIVE_PRIV_USERNS_AGENTS";
|
||||
const DATA_OWNER_ENV: &str = "HIVE_PRIV_USERNS_DATA_OWNER";
|
||||
|
||||
/// One `<owner> <uid> <gid>` file per agent: the data owner its data was moved
|
||||
|
|
@ -49,7 +49,7 @@ pub(crate) struct Env<'a> {
|
|||
pub(crate) socket_dir: &'a Path,
|
||||
pub(crate) marker_root: &'a Path,
|
||||
pub(crate) rootfs: &'a Path,
|
||||
/// The data owner when the agent is listed, else `None`.
|
||||
/// The data owner when the agent is to be namespaced, else `None`.
|
||||
pub(crate) owner: Option<u32>,
|
||||
pub(crate) stopped: bool,
|
||||
/// Lowest uid taken for an agent's own: below it a dir belongs to root or
|
||||
|
|
@ -78,22 +78,26 @@ fn chown_at(dir: BorrowedFd<'_>, name: &CStr, uid: u32, gid: u32) -> std::io::Re
|
|||
)?)
|
||||
}
|
||||
|
||||
/// The data owner for `agent` under the unit's environment: `None` when the
|
||||
/// agent is not listed.
|
||||
/// The data owner when `agent` is to run in a user namespace, else `None`.
|
||||
///
|
||||
/// `requested` is what hive-c0re sent from the closure record. `None` there
|
||||
/// means it could not say, and the agent stays as it is: namespaced if
|
||||
/// hive-priv already `moved` its data.
|
||||
///
|
||||
/// # Errors
|
||||
/// The agent is listed and the data owner is missing or not a plain uid.
|
||||
/// The agent is to be namespaced and the data owner is missing or not a plain
|
||||
/// uid.
|
||||
pub(crate) fn owner_for(
|
||||
agents: Option<&str>,
|
||||
requested: Option<bool>,
|
||||
moved: bool,
|
||||
owner: Option<&str>,
|
||||
agent: &str,
|
||||
) -> Result<Option<u32>> {
|
||||
if !agents.is_some_and(|list| list.split_whitespace().any(|a| a == agent)) {
|
||||
if !requested.unwrap_or(moved) {
|
||||
return Ok(None);
|
||||
}
|
||||
let owner = owner.with_context(|| {
|
||||
format!("{agent} is listed in {AGENTS_ENV}, but {DATA_OWNER_ENV} is not set")
|
||||
})?;
|
||||
let owner = owner
|
||||
.with_context(|| format!("{agent} sets privateUsers, but {DATA_OWNER_ENV} is not set"))?;
|
||||
let owner = owner
|
||||
.trim()
|
||||
.parse()
|
||||
|
|
@ -101,30 +105,37 @@ pub(crate) fn owner_for(
|
|||
Ok(Some(owner))
|
||||
}
|
||||
|
||||
/// [`owner_for`] under the unit's environment.
|
||||
/// [`owner_for`] under the unit's environment and the agent's marker.
|
||||
///
|
||||
/// # Errors
|
||||
/// As [`owner_for`].
|
||||
pub(crate) fn owner_for_agent(agent: &str) -> Result<Option<u32>> {
|
||||
/// As [`owner_for`], or `agent` is not an agent name.
|
||||
pub(crate) fn owner_for_agent(agent: &str, requested: Option<bool>) -> Result<Option<u32>> {
|
||||
crate::shared::validate_agent_name(agent)?;
|
||||
owner_for(
|
||||
std::env::var(AGENTS_ENV).ok().as_deref(),
|
||||
requested,
|
||||
Path::new(MARKER_ROOT).join(agent).exists(),
|
||||
std::env::var(DATA_OWNER_ENV).ok().as_deref(),
|
||||
agent,
|
||||
)
|
||||
}
|
||||
|
||||
/// Make `agent`'s next start of `container` namespaced when it is listed, and
|
||||
/// say whether it is. `binds` are the ones the start will mount.
|
||||
/// Make `agent`'s next start of `container` namespaced when its config asks
|
||||
/// for it, and say whether it is. `binds` are the ones the start will mount.
|
||||
///
|
||||
/// A container that is running, and whose data has not been moved yet, keeps
|
||||
/// its current start: the move waits for a stopped container.
|
||||
///
|
||||
/// # Errors
|
||||
/// The environment is inconsistent, the data owner changed since the data
|
||||
/// moved, the agent was dropped from the list after it moved, or a chown or
|
||||
/// mkdir fails.
|
||||
pub(crate) fn activate(container: &str, agent: &str, binds: &[BindMount]) -> Result<bool> {
|
||||
let owner = owner_for_agent(agent)?;
|
||||
/// moved, the agent stopped asking for a namespace after it moved, or a chown
|
||||
/// or mkdir fails.
|
||||
pub(crate) fn activate(
|
||||
container: &str,
|
||||
agent: &str,
|
||||
binds: &[BindMount],
|
||||
requested: Option<bool>,
|
||||
) -> Result<bool> {
|
||||
let owner = owner_for_agent(agent, requested)?;
|
||||
let rootfs = crate::container_config::container_rootfs(container);
|
||||
let stopped = crate::rootfs::unit_is_stopped(container)?;
|
||||
activate_in(
|
||||
|
|
@ -153,8 +164,8 @@ pub(crate) fn activate_in(env: &Env<'_>, agent: &str, binds: &[BindMount]) -> Re
|
|||
let Some(owner) = env.owner else {
|
||||
if moved.is_some() {
|
||||
bail!(
|
||||
"{agent}: its data was moved to the userns data owner and it is no longer \
|
||||
listed; list it again, or chown its dirs back and remove {}",
|
||||
"{agent}: its data was moved to the userns data owner and its config no longer \
|
||||
sets privateUsers; set it again, or chown its dirs back and remove {}",
|
||||
marker.display()
|
||||
);
|
||||
}
|
||||
|
|
@ -470,14 +481,25 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// The flag decides; with no word from the record the marker does. The
|
||||
/// refusal arms and the `None` arms are each other's controls.
|
||||
#[test]
|
||||
fn only_listed_agents_have_an_owner() {
|
||||
let owner = |agents, owner| owner_for(agents, owner, "a");
|
||||
assert_eq!(owner(Some("b a"), Some("60000")).unwrap(), Some(60000));
|
||||
assert_eq!(owner(Some("b c"), Some("60000")).unwrap(), None);
|
||||
assert_eq!(owner(None, None).unwrap(), None);
|
||||
assert!(owner(Some("a"), None).is_err(), "listed without an owner");
|
||||
assert!(owner(Some("a"), Some("root")).is_err(), "not a uid");
|
||||
fn the_requested_flag_decides_and_without_it_the_marker_does() {
|
||||
let owner = |requested, moved, env| owner_for(requested, moved, env, "a");
|
||||
assert_eq!(
|
||||
owner(Some(true), false, Some("60000")).unwrap(),
|
||||
Some(60000)
|
||||
);
|
||||
assert_eq!(owner(Some(false), false, Some("60000")).unwrap(), None);
|
||||
assert_eq!(owner(Some(false), true, Some("60000")).unwrap(), None);
|
||||
assert_eq!(owner(None, true, Some("60000")).unwrap(), Some(60000));
|
||||
assert_eq!(owner(None, false, Some("60000")).unwrap(), None);
|
||||
assert!(owner(Some(true), false, None).is_err(), "no data owner");
|
||||
assert!(owner(Some(true), false, Some("root")).is_err(), "not a uid");
|
||||
assert!(
|
||||
owner(Some(false), false, None).is_ok(),
|
||||
"not asked, not needed"
|
||||
);
|
||||
}
|
||||
|
||||
/// The move chowns the four data dirs and what is in them and nothing
|
||||
|
|
@ -552,7 +574,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn an_unlisted_agent_is_left_alone_and_a_moved_one_is_refused() {
|
||||
fn an_agent_without_the_flag_is_left_alone_and_a_moved_one_is_refused() {
|
||||
let root = tree();
|
||||
let (answer, seen) = run(&root, None, true, "a", &[]);
|
||||
assert!(!answer.unwrap() && seen.is_empty());
|
||||
|
|
|
|||
Loading…
Reference in a new issue