id-RFC-016 — Kodix as a Koder-ID-joined OS: Koder ID login at the greeter (passkey-first, offline-capable, desktop SSO)
| Field | Value |
|---|---|
| Status | Ratified (2026-06-08 — owner answered all §9 questions) |
| Author(s) | Rodrigo (with Claude as scribe) |
| Target modules | infra/linux/kodix (bootlogin chain), `infralinuxkolide (greeter, #048), servicesfoundation/id (offline assertion + device-join + OS session token), new pam_koder + koder-id-agent` |
| Related | kolide#048 (native greeter over greetd — the foundation + item 4 PAM Koder ID), id-RFC-003 (authentication service), id-RFC-004 (OAuthOIDC), id-RFC-005 (session service), id-RFC-012 (account models), id-RFC-014a (emergency-kit recovery), `specsauthpasskeys.kmd (WebAuthn-only #212–214, deployed), specsauthoauth-flow.kmd, policiesmulti-tenant-by-default.kmd, policies/self-hosted-first.kmd` |
| Origin | Owner question (2026-06-08): "o login do Kodix pode ser integrado ao Koder ID já na tela de login, como o Windows permite?" — promotes the "extensão futura via RFC" flagged in kolide#048's Nota to a ratifiable decision |
1. Summary
Make Kodix a Koder-ID-joined operating system: the login screen (the Kolide native greeter, kolide-shell --greeter, kolide#048) authenticates the user against Koder ID — passkey-first (WebAuthn-only, already deployed: /v1/auth/webauthn-only/{start,verify}, #212–214) with a password fallback. The same identity you already use for every Koder app becomes the OS login identity, exactly as Windows lets you sign in with a Microsoft account / Entra ID.
Three properties make this better than the Windows analogue:
- Desktop SSO — logging into the OS mints a Koder ID session token
that flows to the session, so every Koder app opens already authenticated (no per-app "Sign in"). This is the killer feature beyond Windows: one OS login = the whole desktop is authed.
- Passkey-first OS login — phishing-resistant, passwordless, against
your own self-hosted IdP (
id.koder.dev), not a vendor cloud. Windows Hello is local-only; a Koder ID passkey works on any Kodix machine. - Self-hosted — the IdP is Koder's (Flipping Point /
self-hosted-first).
The hard part — and the reason this is an RFC, not a ticket — is doing it without making the machine un-usable when the network or Koder ID is down: offline login, first-login provisioning, and local-account coexistence must be designed up front.
2. Problem / motivation
- Koder ID is already the identity for every Koder app. The OS (Kodix)
is the last layer outside that loop — today a Kodix login is a plain local Unix account, disconnected from the Stack identity.
kolide#048builds a native greeter overgreetd, and its item 4already specifies "login Koder ID via a
pam_kodermodule"; its Nota says "passkey-first / device-flow pré-sessão = extensão futura via RFC". This RFC is that document.- Without a designed approach, naïve "OS login = call the Koder ID API" would
lock the owner out of their own laptop during any outage. That is unacceptable and is precisely what Windows solved with cached domain credentials. We must match that bar.
3. Goals / non-goals
Goals
- G1 — Sign into Kodix with a Koder ID (passkey-first, password fallback) at
the greeter.
- G2 — Offline login: a previously-successful Koder ID user can log in with
the network/IdP unreachable.
- G3 — First-login provisioning: a Koder ID's first successful login
creates/maps a local Unix account (uid, home, skel) — like a Windows profile.
- G4 — Desktop SSO: the session carries a Koder ID token usable by apps
(no per-app re-auth).
- G5 — Device-join / seat policy: control which Koder IDs may log into a
given machine (allow-list / org-join — the "domain-join" knob).
- G6 — Local-account coexistence + recovery: a local admin always exists;
the machine is never bricked if Koder ID is permanently unreachable.
- G7 — Reuse-first: greetd stays the manager; Koder ID stays the IdP; no
reinvented auth (
reuse-first.kmd#5).
Non-goals (this RFC)
- N1 — Replacing greetd with a Koder display-manager (rejected, see §6 alt D;
consistent with
kolide#048's ratified caminho B). - N2 — Full MDM / fleet device-management console (future; this RFC is the
per-device login contract that an MDM would later orchestrate).
- N3 — Disk-encryption unlock with the passkey (FDE/TPM binding) — adjacent,
deferred to a follow-up (see §9 Q4).
- N4 — Mobile/Android device login (Kodix is the desktop OS here).
4. Background
- Boot/login chain (today, kodix#020 / kolide#048):
greetd(manager,owns VT/seat + PAM) →
gtkgreet(generic UI) →kolide-session.kolide#048replacesgtkgreetwithkolide-shell --greeter(Koder-native UI) — manager stays greetd, mirroring GDM (GDMmanager, gnome-shellgreeter). - Koder ID passkey-first (deployed):
specs/auth/passkeys.kmd+ engine212#213#214 —
POST /v1/auth/webauthn-only/start(resolves identifier →challenge) and
/verify(assertion → session). Enrollment is live (POST /v1/me/passkeys/register/{begin,finish}). The RPID is the Koder ID origin; a credential is bound to its RPID. - PAM model: greetd drives a PAM conversation; a stacked PAM module decides
success/failure and can populate the session environment. This is the seam where
pam_koderlives — the greeter only renders the conversation.
5. Design
5.1 Components (and where each decision lives)
┌─ greeter (kolide-shell --greeter, kolide#048) ── renders the PAM conversation,
│ shows passkey/password UI
├─ greetd (manager) ───────────────────────────── owns the seat, runs PAM
├─ pam_koder.so (NEW) ─────────────────────────── the PAM module: talks Koder ID
│ ├─ online: POST /v1/auth/webauthn-only/{start,verify} (passkey) or
│ │ the password grant; on success caches the verifier (§5.3)
│ ├─ offline: verifies a cached passkey assertion / cached password hash
│ ├─ provisioning: maps Koder ID → local uid (§5.4) on first success
│ └─ device-join: enforces the seat allow-list (§5.5)
├─ koder-id-agent (NEW, session) ───────────────── holds the OS session token,
│ exposes it to apps (keyring/D-Bus) for desktop SSO (§5.6)
└─ local credential cache (NEW, root-only) ─────── cached pubkeys + pw hashes +
device-join policy (§5.3/5.5)The only new IdP-side capability is an offline-verifiable assertion contract (§5.3) + a device-join grant (§5.5) + OS-session-token issuance (§5.6). Everything else reuses greetd + the deployed Koder ID.
5.2 Online login (happy path)
- Greeter calls
pam_kodervia greetd's PAM conversation with the enteredidentifier (emailusername, resolved per `specsidentity/login-resolution.kmd`).
- Passkey-first:
pam_kodercalls/v1/auth/webauthn-only/start→ gets achallenge; the greeter drives the local authenticator (platform passkey / security key);
pam_koderPOSTs the assertion to/verify→ a Koder ID session on success. (Password grant is the fallback factor.) - On success
pam_koder: (a) provisions/maps the local user (§5.4), (b)caches the verifier for offline (§5.3), (c) hands the OS session token to
koder-id-agent(§5.6), (d) returnsPAM_SUCCESS. greetd launcheskolide-session.
5.3 Offline login (the keystone)
The machine MUST log a previously-seen user in with no network. Per factor:
- Passkey (preferred): WebAuthn is challenge–response; the public key
lives server-side but can be cached locally at first online login (root-only store). Offline,
pam_kodergenerates the challenge itself, drives the authenticator, and verifies the assertion signature against the cached pubkey + checks the signature counter monotonicity. No IdP round-trip needed — this is genuinely offline and still phishing-resistant. (Trade-off: revocation can't propagate while offline → bounded by a max-offline-age TTL after which online re-validation is forced, mirroring Windows cached-logon count/age. Default30 daysfor personal images (owner-ratified 2026-06-08, Q2); fleet images use a shorter TTL. Past the TTL the next login requires the IdP; if still unreachable, the local-admin fallback (§5.7) applies.) - Password fallback: cache an Argon2id hash of the password at online
login; verify offline. Same TTL.
- Cache store:
/var/lib/koder-id-cache/(root 0700), one record perprovisioned Koder ID:
{koder_id, local_uid, cred_pubkeys[], sign_counter, pw_argon2, last_online, max_offline_age}. Never stores a usable secret in the clear; the pubkey + hash are not reversible.
5.4 First-login provisioning (Koder ID → local account)
On the first successful login of a Koder ID not yet on the machine:
- Allocate a local uid in a reserved range (e.g. 60000+), create
$HOMEfrom/etc/skel, set up the keyring. - Record
koder_id ↔ local_uidin the cache (§5.3) so subsequent logins(online or offline) resolve to the same account.
- The Unix username = the Koder ID handle (owner-ratified 2026-06-08, Q3),
POSIX-sanitized: lowercased, restricted to
[a-z0-9_-], never leading with a digit, length-clamped (perpolicies/username-allocation.kmdconstraints). On collision with an existing local account, append a numeric suffix (rodrigo→rodrigo2); the stablekoder_id ↔ local_uidmap (§5.3) is the source of truth, so the suffixed name is permanent for that id. Display name + avatar come from the Koder ID profile (the pretty name lives in the UI, the POSIX login stays clean).
5.5 Device-join / seat policy
Who may log into this machine (the "domain-join" knob):
claim-on-first-login(TOFU — personal device default, owner-ratified2026-06-08, Q1): the device ships unclaimed; the first Koder ID to log in successfully becomes the device owner and is auto-added to the allow-list; thereafter the device is locked (default-deny) and any new identity needs the owner's grant. Mirrors the macOS/Windows OOBE pattern ("first account = admin", then closed) and the TOFU model (SSH/Signal). This resolves both the security hole of permanent
open(a stranger's Koder ID could provision on your already-claimed laptop) and the bootstrap chicken-and- egg of a pre-seededallow-list(who seeds it before first login?).allow-list/ org-join (fleet/corporate default): only enumeratedKoder IDs / an org (per
kompass-RFC-001) may log in — the "joined to org X" mode. Fleet images ship the list pre-seeded, skipping the TOFU window. Enforced bypam_koderbefore provisioning.open(opt-in, lab/kiosk only): any Koder ID provisions, always. NOT adefault — carries a permanent security hole; offered only for throwaway/shared lab machines that explicitly want it.
- Policy stored locally (signed by an enrolment token; the owner-claim is
recorded the same way) so it's tamper-evident and works offline. Multi-tenant-by-default: the seat is scoped to its allowed identities.
5.6 Desktop SSO (the differentiator)
koder-id-agent(a user-session daemon, started bykolide-session) holdsthe OS session token minted at login and brokers it to apps:
- Apps obtain a token via the agent (D-Buskeyring), so KoderAuth
koder_kitfinds an existing session → no per-app sign-in.
- The token is an OAuth/OIDC session bound to this device session
(
id-RFC-005session service); scoped + refreshable; revoked on logout.
- Apps obtain a token via the agent (D-Buskeyring), so KoderAuth
- This is what makes the top-bar account badge (
kolide#044right side) aconsequence (it reflects the OS-logged-in Koder ID), dissolving the
topbar-placement.kmdR1 vs GNOME-parity conflict.
5.7 Local-account coexistence + recovery (never brick the box)
- A local admin account always exists (set at install,
dev-default-passwordrules for dev images) and can log in with zero Koder ID dependency.
id-RFC-014aemergency-kit recovery applies if a Koder ID itself is lost.- If device-join policy is
allow-listand the IdP is permanently gone, thelocal admin + a documented break-glass restores access.
6. Alternatives considered (the /k-arch deliberation)
- A — Status quo (local Unix accounts only). Rejected: leaves the OS
outside the Stack identity; no SSO; owner manages a separate OS password.
- B — Koder ID via PAM, online-only. Rejected as the final design (but is
a valid first slice): a network/IdP outage locks the user out of their own machine — fails G2/G6, the exact failure mode Windows engineered around.
- * — Koder-ID-joined OS: PAM + offline cache + first-login provisioning +
desktop SSO + device-join (this RFC). Chosen.*Most complete + most correct long-term (Quality > Speed,
stack-principles §2): closes the identity loop, ships the SSO differentiator, and stays usable offline. Higher up-front effort (offline assertion verification, agent, cache) — accepted. - D — Build a Koder display-manager (replace greetd). Rejected (also per
kolide#048caminho B): greetd is hardened, security-critical plumbing with ~zero differentiation to reinvent;self-hosted-firsthas no login-manager pair pressuring a replacement. All the value is in the greeter + PAM module, which C delivers without owning the seat manager.
Trade-off recorded: C accepts real implementation cost (offline WebAuthn verification + the session agent + the cache) to avoid shipping a "v1 with limitations" (B) that would brick the machine offline — the structurally better form is achievable, so it wins (Rule 13 / D12 root-cause: the real requirement is usable OS login, not online OS login).
7. Security considerations
- Offline ≠ weaker: cached-pubkey assertion verification keeps passkeys
phishing-resistant offline; the only relaxation is revocation latency, bounded by
max-offline-age(force online re-validation past the TTL). - Cache is root-only, non-reversible (pubkeys + Argon2id hashes); compromise
of the cache does not yield a usable credential.
- Sign-counter monotonicity check detects cloned authenticators offline.
- Device-join policy is signed (enrolment token) → tamper-evident offline.
- Token blast radius: the OS session token is device-session-scoped
(
id-RFC-005), refreshable, and revoked on logout/lock; apps get it via the agent, never from disk. - PII / auth-data retention for the cache follows
policies/identity-data-retention.kmd.
8. Implementation slices (phased — Rule 15 cat-A)
- S1 —
pam_koderonline passkey-first (builds onkolide#048item048d): PAM module calls
/v1/auth/webauthn-only/{start,verify}+ password fallback; greeter renders it. Online-only (alt B as a stepping stone, not the endpoint). Headless-testable: the pure protocol/cache logic split out like the 048a greetd parser. - S2 — first-login provisioning (§5.4): Koder ID → local uidhomeskel +
cache record.
- S3 — offline login (§5.3): cache pubkeys + Argon2id hash at S1 success;
offline assertion + password verification;
max-offline-ageTTL. - S4 —
koder-id-agent+ desktop SSO (§5.6): mint + broker the OS sessiontoken; KoderAuth/
koder_kitconsume an existing session (closes thekolide#044right-side account question). - S5 — device-join / seat policy (§5.5):
open|allow-list| org-join. - S6 — Kodix wiring + boot drill (
kolide#048item 048e neighbourhood):greetd config, package-lists, install-time local-admin, VM boot/login drill.
9. Open questions (owner)
- Q1 — Default device-join mode — RESOLVED (owner, 2026-06-08):
claim-on-first-login(TOFU) for personal images — the first Koder ID claims the device + auto-allow-lists itself; locked thereafter (owner grants new ids). Fleet images ship pre-seededallow-list/org-join. Rawopenis opt-in labkiosk only (not a default). See §5.5. *(Deliberated via `k-arch: rawopen= permanent security hole; rawallow-list` = bootstrap chicken- and-egg; TOFU resolves both — Rule 13 / stack-principles §2.)* - Q2 —
max-offline-age— RESOLVED (owner, 2026-06-08):30 daysforpersonal images (fleet images shorter). Balances usable-offline (≈1 month of no-network travel) against revocation latency; past the TTL the next login needs the IdP, else the local-admin fallback (§5.7). See §5.3.
- Q3 — Username derivation — *ESOLVED (owner, 2026-06-08): the Koder ID
handle* POSIX-sanitized (
[a-z0-9_-], lowercased, no leading digit); collision → numeric suffix, fixed permanently via thekoder_id ↔ local_uidmap. See §5.4. - Q4 — FDE/TPM binding (N3) — *ESOLVED (owner, 2026-06-08): YES, as a
follow-up RFC (not in this one). Seal the LUKS key in the TPM + gate disk-unlock on the passkey/presence so data-at-rest is tied to identity (BitLocker+Hello / FileVault parity). Tracked as a planned follow-up; the hook is recorded here, the design lands in its own RFC (adjacent to Kodix #078–082 full-live-system). Stays a non-goal of this RFC (N3). → Designed + Ratified as
id-RFC-017(2026-06-25; alt C — TPM-sealed LUKS, measured-boot + TPM-PIN presence, signed-policy for AB survival). It also closes the §5.3§5.5 at-rest custody hole*found while implementing S2 (the offline cache + device-join allow-list need at-rest integrity, not justroot 0700);id-RFC-017provides that via TPM-sealed FDE. Its §9 design questions are all owner-resolved → implementation may begin atid-RFC-017S1. - Q5 — Numbering/ownership — *ESOLVED (owner, 2026-06-08): keep it all in
id-RFC-016.*The OS login is "another consumer of Koder ID" (like OAuth / id-RFC-004 has many); a single document is easier to read/reference and the title makes the Kodix/greeter consumer explicit. No split.
All §9 questions resolved (2026-06-08) → this RFC is RATIFIED. Implementation may begin at slice S1 (
pam_koderonline passkey-first), which is alsokolide#048item 4. S2–S6 follow per §8.
10. References
kolide#048— native greeter over greetd (foundation; item 4 = pam_koder).specs/auth/passkeys.kmd; Koder ID engine #212#213#214 (passkey-first,deployed).
id-RFC-003(authentication),id-RFC-004(OAuth/OIDC),id-RFC-005(session),
id-RFC-012(account models),id-RFC-014a(emergency-kit).kompass-RFC-001(identity & org unification — the org/device-join basis).policies/multi-tenant-by-default.kmd,policies/self-hosted-first.kmd,policies/reuse-first.kmd,policies/stack-principles.kmd §2.- Windows prior art: Microsoft-account / Entra ID OS sign-in + cached domain
logon (the offline-login bar this RFC matches and the SSO it exceeds).
Changelog
- 2026-06-08 — Draft v0.1. Motivated by the owner's question on integrating
Kodix login with Koder ID at the login screen. Promotes
kolide#048's "extensão futura via RFC" Nota. Statusdraftpending owner answers to §9. - 2026-06-08 — Q1 resolved (owner, via
/k-archdeliberation):claim-on-first-login(TOFU) is the personal-image device-join default; §5.5 rewritten accordingly (fleet = pre-seeded allow-list/org; rawopendemoted to opt-in lab/kiosk). Q2–Q5 still open. - 2026-06-08 — Q2 resolved (owner):
max-offline-agedefault = 30 daysfor personal images (fleet shorter). §5.3 + §9 updated.
- 2026-06-08 — Q3 resolved (owner): Unix username = the Koder ID handle
(POSIX-sanitized; collision → numeric suffix). §5.4 + §9 updated.
- 2026-06-08 — Q4 resolved (owner): FDE/TPM disk-unlock binding pursued as a
follow-up RFC (stays N3 / non-goal of this RFC). §9 updated.
- 2026-06-08 — Q5 resolved (owner): keep the whole thing in
id-RFC-016(nosplit). All §9 questions answered → status promoted Draft → Ratified. Implementation may start at slice S1 (
pam_koder, =kolide#048item 4). - 2026-06-25 — Q4 follow-up drafted: the FDE/TPM binding lands as
id-RFC-017(Draft). It also closes the §5.3/§5.5 at-rest custody hole surfaced during S2 implementation (offline cache + device-join allow-list need at-rest integrity via TPM-sealed FDE, not justroot 0700). Q4 note updated to link it.