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 IDpasskey-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:

  1. 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.

  2. 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.

  3. 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#048 builds a native greeter over greetd, and its item 4

    already specifies "login Koder ID via a pam_koder module"; 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#048 replaces gtkgreet with kolide-shell --greeter (Koder-native UI) — manager stays greetd, mirroring GDM (GDMmanager, gnome-shellgreeter).

  • Koder ID passkey-first (deployed): specs/auth/passkeys.kmd + engine

    212#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_koder lives — 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)

  1. Greeter calls pam_koder via greetd's PAM conversation with the entered

    identifier (emailusername, resolved per `specsidentity/login-resolution.kmd`).

  2. Passkey-first: pam_koder calls /v1/auth/webauthn-only/start → gets a

    challenge; the greeter drives the local authenticator (platform passkey / security key); pam_koder POSTs the assertion to /verify → a Koder ID session on success. (Password grant is the fallback factor.)

  3. 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) returns PAM_SUCCESS. greetd launches kolide-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_koder generates 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. Default 30 days for 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 per

    provisioned 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 $HOME from

    /etc/skel, set up the keyring.

  • Record koder_id ↔ local_uid in 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 (per policies/username-allocation.kmd constraints). On collision with an existing local account, append a numeric suffix (rodrigorodrigo2); the stable koder_id ↔ local_uid map (§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-ratified

    2026-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-seeded allow-list (who seeds it before first login?).

  • allow-list / org-join (fleet/corporate default): only enumerated

    Koder 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 by pam_koder before provisioning.

  • open (opt-in, lab/kiosk only): any Koder ID provisions, always. NOT a

    default — 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 by kolide-session) holds

    the OS session token minted at login and brokers it to apps:

    • Apps obtain a token via the agent (D-Buskeyring), so KoderAuthkoder_kit

      finds an existing session → no per-app sign-in.

    • The token is an OAuth/OIDC session bound to this device session

      (id-RFC-005 session service); scoped + refreshable; revoked on logout.

  • This is what makes the top-bar account badge (kolide#044 right side) a

    consequence (it reflects the OS-logged-in Koder ID), dissolving the topbar-placement.kmd R1 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-password

    rules for dev images) and can log in with zero Koder ID dependency.

  • id-RFC-014a emergency-kit recovery applies if a Koder ID itself is lost.
  • If device-join policy is allow-list and the IdP is permanently gone, the

    local 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#048 caminho B): greetd is hardened, security-critical plumbing with ~zero differentiation to reinvent; self-hosted-first has 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)

  1. S1 — pam_koder online passkey-first (builds on kolide#048 item

    048d): 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.

  2. S2 — first-login provisioning (§5.4): Koder ID → local uidhomeskel +

    cache record.

  3. S3 — offline login (§5.3): cache pubkeys + Argon2id hash at S1 success;

    offline assertion + password verification; max-offline-age TTL.

  4. S4 — koder-id-agent + desktop SSO (§5.6): mint + broker the OS session

    token; KoderAuth/koder_kit consume an existing session (closes the kolide#044 right-side account question).

  5. S5 — device-join / seat policy (§5.5): open | allow-list | org-join.
  6. S6 — Kodix wiring + boot drill (kolide#048 item 048e neighbourhood):

    greetd config, package-lists, install-time local-admin, VM boot/login drill.

9. Open questions (owner)

  • Q1 — Default device-join modeRESOLVED (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-seeded allow-list/org-join. Raw open is opt-in labkiosk only (not a default). See §5.5. *(Deliberated via `k-arch: raw open = permanent security hole; raw allow-list` = bootstrap chicken- and-egg; TOFU resolves both — Rule 13 / stack-principles §2.)*

  • Q2 — max-offline-ageRESOLVED (owner, 2026-06-08): 30 days for

    personal 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 the koder_id ↔ local_uid map. 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 just root 0700); id-RFC-017 provides that via TPM-sealed FDE. Its §9 design questions are all owner-resolved → implementation may begin at id-RFC-017 S1.

  • 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_koder online passkey-first), which is also kolide#048 item 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. Status draft pending owner answers to §9.

  • 2026-06-08 — Q1 resolved (owner, via /k-arch deliberation):

    claim-on-first-login (TOFU) is the personal-image device-join default; §5.5 rewritten accordingly (fleet = pre-seeded allow-list/org; raw open demoted to opt-in lab/kiosk). Q2–Q5 still open.

  • 2026-06-08 — Q2 resolved (owner): max-offline-age default = 30 days

    for 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 (no

    split). All §9 questions answered → status promoted Draft → Ratified. Implementation may start at slice S1 (pam_koder, = kolide#048 item 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 just root 0700). Q4 note updated to link it.