Repository navigation
Conversation
A public repository without a license is "all rights reserved": nobody may legally reuse the code. MIT keeps it permissive and, importantly for a tool that edits /etc/shadow, states that it comes without warranty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v1 edited /etc/passwd & co. through predictable files in /tmp and sed on whole lines. A review found that this could hand /etc/shadow to any local user, made /etc/passwd unreadable after the first change, corrupted group memberships (removing "bob" also mangled "bobby"), and could not roll back. v2 keeps v1's goals - one file, no useradd/usermod/passwd, the same console look - and rebuilds the core: * locking compatible with shadow-utils (FILE.lock hard-link protocol) and lckpwdf (flock --fcntl where available), taken before anything is read * the commonio.c commit protocol: temp file in the target directory, owner/mode/SELinux label copied, fsync, FILE- backup, rename(), dir fsync * validation of every line, cross-file consistency, and a blast-radius check that refuses any change the operation did not declare * a journal with pre/post images: dry-run diffs, rollback, crash recovery * entry-level editing in memory (exact-token group membership, no sed) * ID allocation from login.defs ranges, free as UID *and* GID, NSS-checked * passwords: pwquality policy, secrets only via pipes, batch/parallel hashing * lock = '!' plus account expiry (so SSH keys are refused too); unlock never produces an empty password field * writes inside home directories run as the user (setpriv), not as root * joiner/mover/leaver lifecycle: offboard (reversible) before delete * schema-flexible bulk import (CSV any delimiter/encoding, nested JSON) with inspect -> plan -> apply, identity matching and all-or-nothing semantics * temporary passwords with an activation deadline enforced by a sweep and a shadow-expiry backstop; credential slips that self-destruct * CIS-mapped audit, access-review export, password policy management * hash-chained audit log plus structured journald fields * a CLI with documented exit codes; the console runs the same commands shellcheck: 0 findings at every severity. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests/run-in-docker.sh runs the suite in throw-away containers for nine distributions (Debian 12/13, Ubuntu 22.04/24.04, Rocky 9, Alma 9, Fedora 42, RHEL UBI 8/9) with the repository mounted read-only, so nothing on the developer's machine is ever modified and no hypervisor is needed. The import fixtures are deliberately awkward: a semicolon-separated Excel export with a BOM, CRLF line endings, accented names and day-first dates; a nested JSON document as an HR API would return it; and a native manifest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…' writes The live concurrency test (UMC next to useradd and chpasswd) exposed a real race on Debian 12: chpasswd changes passwords through pam_unix, which only honours /etc/.pwd.lock (an fcntl lock), and util-linux 2.38 has no 'flock --fcntl'. pam_unix rewrote /etc/shadow during UMC's commit; UMC's post-commit check caught it, but its automatic rollback would then have reverted chpasswd's change. * lk_pwd now falls back to a tiny python3 or perl helper that holds the same fcntl lock for UMC's lifetime (signals ignored; released if UMC dies) * txn_verify rolls back only when every file is still byte-identical to what UMC wrote; otherwise it checks that UMC's own entries survived and reports instead of destroying the other program's change * a bug found while writing tests: renaming a user declared only one of its two group entries, and the blast-radius check refused the commit - the check doing its job; the declaration is fixed * IDs for new users continue after the highest UID (skipping numbers taken as a GID), closer to useradd * configuration is resolved once in main (the sweep did not clean credential slips because it never resolved credentials_dir) * sandbox-only fault injection (UMC_FAULT) to prove crash recovery in tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* unit: validators, JSON/CSV readers, date and status parsing, transliteration, password generation and policy, CSV formula escaping * integration (--root sandboxes built from Debian- and RHEL-style fixtures): commit protocol, blast radius, invariants, user lifecycle, groups, sudo, bulk import, rollback, crash recovery via SIGKILL, audit-log tampering, onboarding deadlines with a simulated clock, audit, export, policy * live (the container's own /etc): NSS verification, shadow-utils honouring UMC's lock, UMC + useradd + chpasswd in parallel without lost updates Every v1 finding has a named regression test (F-01 ... F-30). tests/run.sh refuses to run outside a container or test VM because the live tests rewrite /etc. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The disk-full evidence run (every amount of free space from 0 to 120 KB) showed two weaknesses, although no run ever left the files inconsistent: * a staging write cut short by a full disk was only caught later, by the blast-radius check, whose message then blamed "a bug in UMC"; every write into a staging file is now checked and reported as "disk full?" * when the disk filled up during the renames, restoring the old files also needed space, so only 'umc recover' could finish the job. Each original is now hard-linked to its backup name before the rename (FILE- for the four databases, a hidden .NAME.umc-pre otherwise); undo is a plain rename() Also: journal entries of transactions killed before their first rename are marked "aborted" instead of lingering as "prepared", and the minimum util-linux version for 'flock --fcntl' is corrected to 2.41 (2.39 and 2.40 do not have it; found by the Fedora 42 matrix run). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* tests/e2e (UMC_E2E=1, VMs only): SELinux labels stay correct on an enforcing host; an SSH key logs in, a UMC lock refuses it while a '!'-only lock would not; journald receives structured records; the sweep timer installs * tests/vagrant/smoke.sh PROVIDER boots each pinned box, runs a ten-second smoke test inside it and prints which boxes work for that hypervisor * the Debian/Ubuntu test images carry an en_US.UTF-8 locale so the evidence scripts can run v1: v1's password rule is an invalid regular expression in the C locale (new finding F-33) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…llels Current bento boxes no longer ship libvirt builds and the generic/* boxes are frozen since 2024, so each VM now names a pinned box per provider via Vagrant's override.vm.box mechanism. The repository is rsync'ed to /opt/umc (no NFS server needed; UMC_SYNC=nfs for live editing), which retires the shared/umc.sh copy. 'vagrant up' without a name starts nothing, so seven VMs are never booted by accident. RHEL registration reads credentials from the environment only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every push runs shellcheck (zero findings required for umc.sh), the test suite in disposable containers for nine distributions, and regenerates the evidence reports, failing if any claim no longer holds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* README: what UMC is for, highlights linked to reproducible evidence, quick start, CLI reference with exit codes, bulk onboarding, safety model, compatibility, comparison with alternatives, honest limitations * docs/DESIGN.md: architecture, commit protocol, locking, validation, journal and recovery, error-handling contract, threat model, trade-offs, and the bugs the safety nets caught during development * docs/SECURITY-ADVISORY-v1.md: v1's security findings (impact, cause, fix, regression test), without exploitation instructions * examples/: umc.conf, roles, access rules, an HR export and a manifest - verified end to end in a sandbox Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e benchmark The first benchmark showed UMC slower than a useradd loop. Profiling, not guessing, found the cause: one small state file per user (onboarding deadline, lock and offboarding records) went through the full commit protocol, about ten processes each. * UMC's own state is now one table per kind (onboarding.tsv, locks.tsv, offboarded.tsv, managed.tsv), kept in memory and written once per transaction * NSS collision checks and post-commit verification use one getent call per batch instead of two per user * home directories of bulk imports are created in parallel (one worker per core, max 8); every failure is still reported by name The parallel path then deadlocked in live mode: a bare 'wait' also waited for the lckpwdf helper coprocess, which by design lives as long as UMC. It now waits for its worker PIDs only; a live test covers it. Result (evidence E-11, 1,000 users with hashed passwords and homes, same container): umc apply 5.2 s, useradd loop + chpasswd 21.5 s, newusers 16.7 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The disk-full evidence run (E-06, now 0-200 KB free) found a real bug: with 136 KB free, the commit completed but recording "state=committed" in the journal failed, unchecked. UMC reported success, and the next run's crash recovery saw "committing" and rolled the finished transaction back. The "committing" transition had the same flaw in the other direction. Every journal state (committing, committed, rolled-back) is now written at journal time, while space is guaranteed, and later only renamed into place. Each transition is checked; UMC may fail, but never reports a success the journal disagrees with. Tests: findings are now traceable - every v1 finding that still applies has a test named after it (F-17, F-29, F-31, F-32, F-16, F-23, F-03 renamed), plus a new F-33 test (the password policy in the C locale). 126 tests pass on Debian 12, Rocky 9 and UBI 8 (bash 4.4). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
poc/run.sh runs each scenario in a throw-away container and writes a report (claim, method, environment, one-line reproduce command, raw output, verdict) to evidence/. CI regenerates them on every push. * E-01 no account data staged in /tmp (v2's defence against F-01) * E-02..E-04 v1 vs v2: file permissions, exact group membership, ID allocation * E-05 400 random SIGKILLs: 0 inconsistent states * E-06 disk full at every point from 0 to 200 KB free: 0 inconsistent outcomes * E-07 UMC + useradd + chpasswd in parallel: 0 lost updates * E-08 idempotency · E-09 byte-exact rollback · E-10 blast-radius check * E-11 1,000 users: umc apply vs useradd loop vs newusers * E-12 audit · E-13 tamper-evident log · E-14 messy HR files * E-15 onboarding deadline with a simulated clock The evidence runs themselves found four real bugs, fixed in earlier commits (see docs/DESIGN.md section 14). v1's security bugs are documented in docs/SECURITY-ADVISORY-v1.md rather than shipped as runnable exploits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* yescrypt hashes are now split across CPU cores like SHA-512 (mkpasswd hashes one password per process, so each chunk is a loop); a test checks with crypt(3) that every password stays paired with its own hash * 'umc doctor', 'umc policy show' and a new audit check (AUD-24, low) report when login.defs' ENCRYPT_METHOD and PAM's pam_unix option differ - Debian 12 ships SHA512 vs yescrypt - and explain that UMC follows login.defs unless hash_method is set in umc.conf (now accepted in any letter case) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* README: "Who needs this" (break-glass accounts, air-gapped/regulated networks, seasonal bulk onboarding, organisations without an IdP, leavers and incidents, golden images, audits) and "What is actually new here" - an honest novelty statement: the techniques are known; the combination for Linux's flat-file account databases, on hosts where the usual tools cannot run, is what the reviewed tools lack * E-11 now compares like with like: the same hashing algorithm on both sides, every run on all cores and pinned to one core, plus a single-user row. The first version compared UMC's SHA-512 with newusers' yescrypt, which overstated the gap. Measured: ~4.5x faster than a useradd loop on 12 cores, 2.4x on one core; newusers is faster with yescrypt on one core; useradd is 17x faster for a single user - all stated in the README * DESIGN.md section 15 explains where the time goes (batching, not language) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
All 15 reports pass; the 9-distribution matrix passes. README and DESIGN quote the published E-11 numbers exactly (generated from the report, not typed by hand). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fter itself tests/vagrant/verify.sh replaces smoke.sh. For each pinned box it boots the VM, runs the whole suite including the VM-only end-to-end tests (SELinux labels, SSH key lock, journald, systemd timer), destroys the VM and removes what the run downloaded: new boxes, their libvirt image volumes and new libvirt networks. Pre-existing boxes, volumes and networks are snapshotted first and never removed; network definitions are saved and restored if anything deletes them. vagrant-libvirt 0.12 deletes a management network it created itself on every destroy (management_network_keep only protects one that already existed), so --keep-network re-defines it, stopped. Exits 1 when any box fails. Writes E-16. Fixes found on the first VM runs: - Vagrantfile: debian13 uses debian/trixie64 (crystax/debian13 never boots); ubuntu2204 on libvirt uses generic/ubuntu2204 - bento/ubuntu-22.04 202502.21.0 is UEFI-only (GPT, empty MBR boot code) and hangs at "Booting from Hard Disk" under the default BIOS firmware; force the KVM driver (that bento box also sets driver "qemu", pure emulation); management_network_keep so a pre-existing vagrant-libvirt network survives. - e2e: offboard before delete - a real SSH login leaves the systemd user manager running for a few seconds and delete rightly refuses. - unit: sniff/translit tests run under UMC's LC_ALL=C like the real code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A transient DNS failure on the host was reported as "the box did not boot". Download errors (catalogue unreachable, host not resolved) now get one more try after 60 s, and are reported as download failures; a box that downloads but does not boot is still not retried. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One run of tests/vagrant/verify.sh libvirt at d570975: Rocky 9 and Alma 9 (SELinux enforcing), Debian 12 and 13, Ubuntu 22.04 and 24.04 - 133 tests each, 0 failed. RHEL 9 needs a subscription and was not run. README compatibility table, limitations and evidence index updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"name: shellcheck (umc.sh: zero findings ...)" contains ": ", which YAML reads as a nested mapping, so GitHub rejected the whole file and no job ever started. Found on the first push of v2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
A rewrite of
umc.shthat keeps v1's goals: one file, nouseradd/usermod/passwd, and the same console look. It makes those goals hold under failure, concurrency and scale. v1 stays available as tagv1.0.Highlights
useradd/PAM). Rollback and crash recovery are included.CHANGELOG.mdanddocs/SECURITY-ADVISORY-v1.md.Evidence
evidence/E-01…E-15: reproducible PoCs covering crash consistency, disk-full, concurrency, rollback, a fair performance benchmark, messy HR files and more.evidence/E-16: six real VMs (Rocky 9 and Alma 9 with SELinux enforcing, Debian 12/13, Ubuntu 22.04/24.04) pass 133 tests each, including SSH, SELinux, journald and systemd end-to-end tests.Review notes
umc.shis ~6,100 lines, shellcheck clean at every severity.tests/vagrant/verify.shboots each pinned box, tests it, and removes everything it downloaded.