Validating identity the way a fleet needs it
Status: design note, 2026-10-27. The FreeIPA check landed in
#236, the GDM login in
#237 and the Samba AD
check in #238. FreeIPA
run 40450471210 and AD run 40539508281 are the runs quoted below.
This is a test rig, not a product capability: it proves a machine can join a directory automatically and repeatedly. DeskOS does not enrol machines. Enrollment is reserved in the Plan and not implemented (architecture.md), and keeping running machines in a state belongs to configuration management. Nothing here ships in an image, and no DeskOS command joins a machine.
Any distribution can join a directory once, by hand, with a script. A fleet needs more, and this is what the repository checks:
- the machine joins without a person at the console, before the greeter;
- credentials are not in the artifact, so an image cannot leak a realm;
- the result is asserted end to end, not assumed from an exit code;
- the proof is repeatable and attached to a run;
- a machine belongs to one realm, and a check cannot reuse another realm's disk.
The shape
Two workflows, one per realm, never one for both:
| workflow | realm | cadence |
|---|---|---|
identity-freeipa.yml | FreeIPA (lab.test) | daily 04:23 UTC, dispatch, identity-only paths |
identity-ad.yml | Samba AD (ad.lab.test) | daily 04:47 UTC, dispatch, identity-only paths |
Neither is a pull-request check: each builds a container, a bootc image and a
QCOW2, and boots a VM; a newer push cancels the run in flight. Each owns its
workspace, image tag and disk (/mnt/deskos-ipa-work, /mnt/deskos-ad-work).
A disk that joined one realm keeps krb5.conf, sssd.conf, the machine keytab,
the default realm and authselect state, so a second realm is a schema conflict;
the driver refuses it if realm list is not empty.
Nothing secret enters the image
The image carries the client packages and nothing else. At boot the harness passes systemd credentials through SMBIOS:
type=11,value=io.systemd.credential.binary:<name>=<base64>
Those become tmpfiles.d entries, extra units and the join's EnvironmentFile.
The admin password exists only as a root-only join.env written at boot, from a
value generated in the job and masked in the log. Realm, principal and password
are runtime inputs; deskosctl render never sees them, which keeps the
compiler deterministic and the artifact shareable.
Ordering is the whole trick
The join unit is Before=gdm.service and WantedBy=graphical.target: the
account has to exist when the greeter starts, because the check logs in as that
account, not a local stand-in.
It is not ordered against network-online.target: that target can fail, the
join never starts, GDM waits and the machine sits at Plymouth. The wait for the
link happens inside the script (nm-online -t 80), bounded, and the unit carries
TimeoutStartSec=420.
For AD, sssd starts before the guest knows the realm and comes up offline. The
check restarts it once realm DNS is in place, pins ad_server, and waits for
sssctl domain-status to say Online.
What is asserted, and how
The session runs a system-wide autostart probe as whoever logged in, and a root unit copies its results and the journal to the serial port:
join_ok=yes,join_sssd=active, and the user appears ingetent passwd;session_useris the directory user, for ADqa@ad.lab.test;- the home did not exist before the login (
join_home_before=missing) and is owned by the uid the directory resolved afterwards, throughoddjob-mkhomedir; oddjobdis active.
FreeIPA run 40450471210 and AD run 40539508281 report exactly that. The AD
run also shows sssctl domain-status online with dc.ad.lab.test as domain
controller and global catalog, and the DESKOS-QA$ and host/DESKOS-QA
principals in the keytab.
A watchdog timer writes the join status, the state of gdm and the join unit,
and the failed units to the serial before powering off when no session appears.
That report found the access_provider mistake behind a silent fifteen-minute
hang.
Why this is not a unit test
DNS, Kerberos, sssd discovery, PAM's session phase, the greeter and the home
policy exist only in a running machine. A unit test can tell you the Plan is
well-formed, not that the account resolves, that PAM admits it, or that the home
appears. The fixture is small: a throwaway realm, one user, one machine, no
published artifact, no support claim.
Out of scope: how a machine is enrolled
Enrolment is the organization's business: a configuration-management run, a provisioning template, a first-boot hook or a per-host one-time credential. Core compiles a baseline and never runs shell, so none is shown or recommended here.
A real enrolment needs a credential for that machine alone (never a shared
secret in an image or ISO), a name with forward and reverse DNS, and a place
to run the join. The fixture uses a throwaway realm, a job-generated admin
password and --force, because its FreeIPA has no DNS.
What this does not prove
- Not Windows AD. Joining the Samba DC runs the same Kerberos, LDAP, DNS and
sssd-adpath as a Microsoft Windows AD domain for the client. It is not the same product (no Group Policy, no Windows replication) and is a stand-in for testing only. - A check, not a recommendation. Which directory a fleet runs is the
organization's decision. FreeIPA fits Linux better because it models Unix
identity, host-based access and
sudopolicy natively; AD is supported because organizations already have it. - Not a trust yet. FreeIPA and AD need both realms reachable at once, with DNS delegation; that is the next step.
- Not an interactive password login. The check autologs the directory user, which exercises PAM's session phase and the home policy; no password is typed.
- Not a production directory. Throwaway realm, one account, no hardening.
Reusing the pattern
The same shape (disposable service in a container, booted image, credentials at runtime, serial report, watchdog) is what the next fleet properties need: an AD trust with DNS delegation, a realm outage with cached and emergency accounts, and signature enforcement against a local registry. None changes the compiler.