UID allocation — one register instead of four mirrors
Date: August 2026 · Status: built 2026-08-05 — the register is
provisioning/uid/uid-map.csv, issued and checked by uid-alloc.py. Dated operations on it are on
the history page; open items in the ops todo.
Scope: the numeric UIDs/GIDs that NFS uses to authorise access to the student homes on the
FS2500, and every consumer that has to agree with them.
1. Why
Nothing in the fleet issued a UID. Each consumer rediscovered one from whichever list was nearest —
a node's passwd, the Ansible conf/users/uid-*.yml, a home's ownership on the NAS — and
the lists drifted apart. Measured 2026-08-05: five accounts carried a node number that disagreed
with the owner of their own home, and the nodes did not agree with each other, one of them holding
a different number for the same person than the other two.
The failure is silent. A lab VM built from a wrong number creates the account, mounts the home,
and reports success; the student then cannot read their own ~/nas_home, because the directory is
mode 750 owned by a number nobody assigned them. There is no error to notice — no login fails, no
mount fails, the files are not there.
A variant is just as quiet: where only the uid was renumbered and the gid was left behind, the person
matches their home's group rather than its owner, so 750 lets them read it and not write to it.
Any design that looks a number up at provisioning time inherits this. The number has to be issued once and thereafter only read back.
2. The principle
Allocate, don't discover. One thing issues the number and owns the range; everything else is a reader. A UID, once issued, is permanent and never reused — it is baked into file ownership on the NAS, so a freed number is not free.
One named exception: a student who becomes an assistant. Their student-era material is discarded
once that role ends anyway, so carrying the student number forward into the staff account buys
nothing — and the bands don't overlap, so it couldn't be carried forward as the same number
regardless. The person legitimately holds two numbers by design: the student uid stays with whatever
of their student life is still on disk, and the staff/assistant role gets a fresh number in its own
band. Don't record this as "undecided" in the register —
louis.heredero is the first instance, not a one-off.
3. The register
provisioning/uid/uid-map.csv is the source of truth, one row per number:
uid,gid,name,email,status,note
1004,1004,firstname.lastname,firstname.lastname@hevs.ch,active,
10003,10003,someone.wholeft,,departed,
25301,25301,,,reserved,drifted duplicate on the calypso nodes
An empty email means the number is taken but nobody currently holds it — someone who left whose
home still exists, or an account that owns files elsewhere. It is burned, not free. status is
active/departed/reserved, hand-maintained from liveness checks; name is the directory under
/volume1/isc3_homes/homes for a NAS-seeded row, empty when the number owns files somewhere other
than the NAS.
uid-alloc.py beside it:
| Mode | What |
|---|---|
--seed | Once, from stat on the NAS homes. Refuses to run twice, and flags a home owned by uid 0 rather than importing it |
| (default) | Allocates for anyone in roster.csv without a number, in the band for their role. Prints the plan; writes only with --commit |
--verify | Compares the NAS against the register and reports each discrepancy. A missing home is only reported for a row someone currently holds — most of the register is service, reserved or departed numbers that are not meant to have one |
--check-ansible | Lists the accounts a playbook run would renumber — read it before running one |
create-home.sh makes a home with the number the register holds, and refuses a name the register
does not know, a number nobody on the roster holds, or an existing directory whose ownership
disagrees. It works through calypsomaster, the only host the NAS exports to without root
squashing, and applies no ACL — the homes are plain POSIX 750.
Why a committed file and not Keycloak
Keycloak was the obvious candidate — it already holds the roster and the identities — and it was rejected. What an allocation needs, git gives and an IdP attribute does not:
| git | Keycloak attribute | |
|---|---|---|
| Append-only, never silently reassigned | visible in the diff | nothing prevents it |
| "Who got this number, and when" | git log | no attribute history |
| Reviewed before it takes effect | the diff | a click in a console |
| Survives losing the machine | yes | no — the realm export omits users, so the numbers would live only in CT 110 |
| Readable while everything is down | yes | no |
The last two decide it. Provisioning must work with the directory down, so a committed file had to exist either way — and then the Keycloak copy would have been a second copy of one fact, which is the duplication the register exists to remove.
Keycloak may still become a reader later: an SSSD/LDAP bridge or a SLURM integration wanting
posixAccount semantics would need uidNumber in the token. That is a push from the register, and
the direction is what matters.
The bands
Sized so that "never reuse" stays affordable for the life of the school — the earlier
10000–10500 sketch gave out after 500 students, which is a few years, and its two student bands
overlapped at 10500.
| Range | Who | Size |
|---|---|---|
1000–1023 | Legacy staff — closed, no new allocations | eight accounts |
1024–1099 | Reserved — Synology DSM local accounts | never issue |
1100–9999 | Teachers & staff | 8 900 |
10000–39999 | ISC students | 30 000 |
40000–49999 | Other students (exchange, external, other programmes) | 10 000 |
50000–59999 | Project & shared group accounts | 10 000 |
60000+ | Off-limits | see below |
Why the ceiling is 60000. useradd stops there by default, systemd hands out dynamic users from
61184, nobody is 65534, and SLURM already sits at 64030. Above that, two ranges are
dangerous: Synology package accounts start at 100000 (FileStation 179652, HybridShare 298119 …)
and unprivileged LXC maps subuid from 100000 — a UID up there can collide with a DSM package
account on the very device that stores the homes.
Why 1024–1099 is a hole. DSM allocates its own local accounts from 1024 upward — admin 1024,
guest 1025, then its administrator accounts — and guest is also the anonuid that
root squashing maps to. A person allocated a number in that range
is indistinguishable, to the NAS, from one of its own administrators: ls -l renders their home as
owned by the admin account, and an admin session has owner rights over it. One home sat inside the
range and was renumbered out.
The roster holds who is currently authorized, so it is always smaller than the set of homes: 21 lines against 66 homes as of 2026-08-05, because a departed student keeps their home until it is deliberately removed. An allocator reading only the roster would see 45 numbers as free that already own files, and would hand a future student a number whose directories belong to someone else — silently.
So the seed burns every UID that owns a home, rostered or not, plus the ones that owned files only on the nodes at seeding time. Once the file is complete, "absent from the file" is a sufficient test — which is why the allocator takes the lowest free number in the band rather than working above a high-water mark. Retiring a home frees nothing: the number stays spent.
The seed found this the hard way — it first burned only the numbers with a NAS home, and promptly offered one that a node account already held. What that cost.
Joining a roster line to a home
The seed reads ownership off the NAS, so every rostered person has to be matched to a directory
under /volume1/isc3_homes/homes. The roster keys on the edu-ID email; the homes are named
firstname.lastname. The join is the optional third roster.csv column, nasname, defaulting to
the email local-part — correct for every home that follows the naming, and filled in only where it
does not. Three of the 66 homes need it because they predate the convention, and two of the current
cohort because the directory truncates a double surname while the email keeps it whole.
roster-sync.sh rejects two people claiming the same home, because that would hand them one number
and one home between them.
4. The guarantee
A home directory and a lab-VM account draw their number from the same issuance, so a 750 mount
cannot silently deny. Before the register the two were derived independently and agreed by luck.
The guarantee is only as good as the file matching the disk, so uid-alloc.py --verify is the check
that matters: it compares the register against the ownership the NAS enforces.
5. What reads it
| Consumer | State |
|---|---|
| Ansible, on the ISC³ rack | reads it since 2026-08-05 — its number wins over any id: in conf/users |
create-home.sh | reads it |
carnaval-lab-vm.sh | still stats the NAS home, which stays correct for the same reason the seed does |
| The research machines | deliberately not wired — they share the same conf/users lists with their own numbering |
Why the id: columns survive in conf/users rather than being deleted: the same lists serve the
research machines, where the register is off. Emptying them would leave those machines with no
number at all. Unifying the two namespaces means renumbering and chowning one side, which is a
decision rather than a side effect of a playbook run — ops todo.
dance is a writer even though it isn't a reader. Chacha/Disco don't consult the register when
provisioning, but as of 2026-08-11 every number uid-others.yml (or any other dance-side conf/users
cohort) hands out is still burned into uid-map.csv — reserved, empty email, name set to the
username, note conf/users value for <user>; no live number confirmed. The two namespaces are only
non-overlapping in principle: dance's numbers land inside the rack's own bands (see
bands), so leaving them unburned would let uid-alloc.py hand a rack student a number a
CALC researcher already holds on Chacha/Disco — invisibly, since dance never reads the file to notice
the clash. Burning costs nothing (dance ignores its own reserved rows) and means a future unification
of the two namespaces starts from one map that already knows about every number in use, instead of
having to rediscover dance's half of it first.