Skip to main content

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 Calypso 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 even 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 simply 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.

3. The register

provisioning/uid/uid-map.csv is the source of truth, one row per number:

uid,gid,nasname,email,note
1004,1004,firstname.lastname,firstname.lastname@hevs.ch,
10003,10003,someone.wholeft,,
25301,25301,,,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. nasname is the directory under /volume1/calypso_homes/homes; empty means the number owns files somewhere other than the NAS.

uid-alloc.py beside it:

ModeWhat
--seedOnce, 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
--verifyCompares every row against the NAS and reports each discrepancy
--check-ansibleLists 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:

gitKeycloak attribute
Append-only, never silently reassignedvisible in the diffnothing prevents it
"Who got this number, and when"git logno attribute history
Reviewed before it takes effectthe diffa click in a console
Survives losing the machineyesno — the realm export omits users, so the numbers would live only in CT 110
Readable while everything is downyesno

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 disease this page exists to cure.

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.

RangeWhoSize
1000–1023Legacy staff — closed, no new allocationseight accounts
1024–1099Reserved — Synology DSM local accountsnever issue
1100–9999Teachers & staff8 900
10000–39999ISC students30 000
40000–49999Other students (exchange, external, other programmes)10 000
50000–59999Project & shared group accounts10 000
60000+Off-limitssee 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 actively 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.

A number is burned by owning files, not by being rostered

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, and in exactly the way this whole design exists to prevent.

So the seed burns every UID that owns a home, rostered or not, plus the ones that own files only on the Calypso nodes. 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/calypso_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: the NAS is the terrain, the register is the map.

5. What reads it

ConsumerState
Ansible, on the ISC³ rackreads it since 2026-08-05 — its number wins over any id: in conf/users
create-home.shreads it
carnaval-lab-vm.shstill stats the NAS home, which stays correct for the same reason the seed does
The research machinesdeliberately 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.