<!-- nornir:class: reasoning -->
# Service-user creation → skidbladnir's pure-Rust `useradd` replacement (exposed)
**dup-audit P1 (finding #4).** nornir shelled `Command::new("useradd") --system …`
(`nornir/src/bin/nornir.rs:7481`) to mint its dedicated service user. The
constellation law is: service/user management lives in **skidbladnir** (the
service layer), and there is NO shell-out (zero-shell charter). skidbladnir
already had the 100%-Rust replacement — it just wasn't reachable from a consumer.
## What was already there (private)
`systemd.rs` grows the dedicated service user as part of `install_systemd`: it
allocates the lowest free system id in `[100, 1000)` and appends matching
`/etc/group` + `/etc/passwd` + a locked `/etc/shadow` row — no `useradd`
subprocess. That logic lived in a private `ensure_system_user` /
`create_system_user_in`, callable only from inside the crate.
## What this change does — expose the seam
Three symbols in `skidbladnir::systemd` are now **public** so nornir (and holger,
dwarfs) consume them instead of hand-rolling `useradd` + `chown`:
| symbol | shape | role |
|---|---|---|
| `ensure_system_user(user, home)` | `-> Result<(u32, u32)>` | the `useradd --system --shell /usr/sbin/nologin` replacement: idempotent, creates `home`, chowns it, returns the `(uid, gid)` |
| `create_system_user_in(passwd, group, shadow, user, home)` | `-> Result<(u32, u32)>` | the file-targeted core (drive an alternate root / test fixture; unit-tested against temp files) |
| `chown_recursive(path, uid, gid)` | `-> Result<()>` | libc `chown` tree-walk so a consumer can re-own extra state dirs to the fresh user (kills the `chown` twin too) |
`ensure_system_user` now **returns `(uid, gid)`** (was `()`) so a caller can reuse
the ids for additional chowns without re-parsing `/etc/passwd`. A `#[cfg(not(unix))]`
stub mirrors `install_systemd` (clear linux-only error).
## Consumer migration (the twin-kill)
nornir's `ensure_system_user` (the `useradd` shell-out), its `passwd_ids`
re-parse and its `chown_recursive` twin collapse to:
```rust
let (uid, gid) = skidbladnir::systemd::ensure_system_user(user, NORNIR_HOME)?;
```
That deletes the `Command::new("useradd")` deploy-subprocess entirely. (Landed
separately in the nornir repo; skidbladnir's job is to expose the seam — done
here.)
## Tests
`tests/public_api.rs` pins the new public surface from a consumer's view
(no live `/etc`, no root): `create_system_user_in` against a temp fixture root
proves the id-allocation + idempotence contract, and `ensure_system_user` /
`chown_recursive` are asserted reachable as public `fn` items. The in-crate
`create_system_user_allocates_consistent_id_and_is_idempotent` unit test still
covers the allocation math.