Skip to content

Latest commit

 

History

History
332 lines (276 loc) · 17 KB

File metadata and controls

332 lines (276 loc) · 17 KB

RBAC and Elevation

Audience: Kernel hackers, shell-command authors, Win32 thunk authors

Execution context: Kernel — broker runs in the calling task's context, prompts under the compositor lock

Maturity: v0 — broker + CLI prompt + grace cache shipping; GUI overlay path reserved; Argon2id alongside PBKDF2; persistence pending

What problem this solves

Before this slice: every privileged action is either "your AuthRole is Admin (allow)" or "it isn't (DENY)." There is no path between those — a non-admin who needs to install a package must log out, log in as admin, run the command, log out, log back in. That friction is the same friction that makes desktop Linux feel hostile, and it produces the exact UX pressure that gets users to disable security entirely.

This page documents the elevation broker (the in-session "prompt me for my password to do this one thing") and the RBAC roles model (named bundles of kCap* bits — what each role gets by default, what each role can elevate to).

Two orthogonal axes

Pre-existing — DO NOT confuse:

  • AuthRole (kernel/security/auth.h) is a fixed 3-level enum: Guest / User / Admin. Decided at login. The account row carries it. Determines who can log in and what their baseline shell command set is.
  • Process capability authority (kernel/proc/process.{h,cpp}) combines durable CapSet bits, temporary broker leases, and a monotonic ceiling. The cap gate reads an effective snapshot on every privileged syscall.

This slice adds:

  • Role (kernel/security/rbac.h) — a named bundle of kCap* bits with an optional per-cap grace duration override. A row in the role table, not a field on the account. An AuthRole::User can be a member of multiple Roles; the broker decides which one to elevate to based on the cap requested.
Concept Owned by Set when? Used for?
AuthRole account row account create who can log in, baseline shell auth
capability authority Process struct spawn + broker/voluntary drop every privileged syscall
Role global role table role registration broker: "which caps does X grant?"

Decisions (from the design discussion)

  1. Identity model: multi-user-with-named-roles data model ships now; UX defaults to single-user (one account, no login prompt past first boot). Multi-user is a config flip. Avoids the cost of retrofitting uid/role fields later.
  2. Grace leases: per-process, 5 min default, per-cap override in role policy. The Process deadline is authoritative; the fixed grace table stores prompt-suppression metadata only. A zero duration cannot back ambient authority and fails closed with NoLease; kCapFsWrite = 30 min is the canonical long-grace example.
  3. CLI trusted path: reuse LoginFeedKey. Add LoginMode::Elevate alongside Tty / Gui — kernel-trusted keystroke routing already exists, no new input path needed.
  4. GUI trusted path: small modal drawn under the compositor lock. Same discipline as LoginStart (compositor lock + framebuffer primitives). No Secure Attention Key yet — reserved as future work when a real attack model demands it (see Future work below).
  5. Win32 UAC mapping: NtAdjustPrivilegesToken translates mapped privileges into kernel operations. Disable clears live authority, SE_PRIVILEGE_REMOVED permanently lowers the Process ceiling, and enable-on-miss requests a positive-duration broker lease. ACL and integrity-level probes remain cosmetic facades.
  6. Password hashing: Argon2id (memory-hard, 64 MiB / t=3 / p=1) alongside PBKDF2. New password sets use Argon2id; PBKDF2 records migrate lazily on next successful verify.
  7. Per-binary always-allow ("never ask for git push again"): NO. Only per-cap grace duration is overridable. Forever-allow is what a role grants by default, not a one-off knob.

Flow: explicit elevation request → broker → lease

shell `elevate FsWrite` or mapped Win32 token enable
  → BrokerRequestElevation(proc, kCapFsWrite)
     ├─ ceiling missing bit → CeilingDenied
     ├─ effective Process snapshot already has bit → Granted
     ├─ live cache row → re-publish its original Process lease
     ├─ role policy denies → Denied
     ├─ zero grace / no monotonic clock → NoLease
     ├─ trusted password prompt fails → Cancelled / BadPassword
     └─ prompt succeeds
          ├─ install generation-tagged Process lease with absolute deadline
          ├─ publish prompt-suppression cache metadata
          └─ Granted
  → next privileged syscall consumes ProcessCapsSnapshot

An ordinary syscall denial does not open an ambient prompt. The caller must use an explicit trusted elevation surface first.

What's wired up today (v0.1)

Surface State
Broker prompt loop (TTY) REAL — reads from Ps2KeyboardReadEvent directly
Broker prompt loop (GUI) REAL — kernel-drawn modal under the compositor lock
Grace leases REAL — Process deadline authority + metadata-only fixed cache
Role table + memberships REAL — in-memory; adminroot, guestsandbox seeded
elevate <cap> REAL — single-cap prompt + grant
elevate role <name> REAL — one prompt, grants every cap in the role bundle
elevate off REAL — drops every active grant on the shell pseudo-process
roles / roles me REAL — list all roles / list the active user's memberships
roleadd / roledel REAL — admin-gated membership management
elevations REAL — dump live grace-cache rows
RequireAdmin integration REAL — composes on top of RequireCap(kCapFsWrite)
RequireCap(cap, cmd) per-cap gating REAL — fine-grained gate at shell sites
Win32 NtAdjustPrivilegesToken routing REAL — enable-but-not-held routes to broker via deferred prompt
Blake2b primitive REAL — RFC 7693, KAT-verified at boot (foundation for Argon2id)
Argon2id KDF REAL — RFC 9106 §5.3 KAT-verified at boot; V2 default for new pw
Lazy V1→V2 (PBKDF2→Argon2id) migration REAL — fires on AuthVerify success; boot self-test pins behaviour
ChaCha20-Poly1305 AEAD REAL — RFC 8439 §2.8.2 KAT-verified at boot
DuetSecretsFile envelope REAL — security/persistence.{h,cpp}, round-trip + tamper KATs
Auth snapshot (export / import) REAL — security/auth.{h,cpp}::Auth{Export,Import}Snapshot
RBAC snapshot (export / import) REAL — security/rbac.{h,cpp}::Rbac{Export,Import}Snapshot
On-disk persistence (/system/secrets/) DEFERRED — needs writable system FS slice

Per-cap admin gating (v0.3)

RequireCap(cap, cmd_name) is the fine-grained admin gate. It passes when:

  1. The active session is AuthRole::Admin (admin holds every cap implicitly), OR
  2. The shell pseudo-process holds the specific cap in its effective Process snapshot — i.e. the user ran elevate <cap> and its positive-duration lease has not expired.

A non-admin netop who runs elevate NetAdmin can now run firewall and fwpolicy (gated on kCapNetAdmin) without being root. The same netop running elevate FsWrite does NOT unlock firewall — the cap they elevated for is different from the cap the command requires. Each command picks the most appropriate cap; the default RequireAdmin composes on top of RequireCap(kCapFsWrite) for sites that don't yet have a more specific mapping.

On denial, RequireCap consults the role table for the active user and, if their roles WOULD grant the missing cap, prints a hint telling them which elevate <cap> to run. Without this hint non-discoverable UX trapped users in "denied" loops.

Migrated sites:

  • firewall / fwpolicykCapNetAdmin
  • guard mode advisory|enforce|offkCapDebug
  • Everything else still goes through RequireAdminkCapFsWrite. Sites can migrate one at a time as their natural cap becomes clear.

Deferred-prompt mechanism (v0.2)

Ps2KeyboardReadEvent is single-consumer by contract — concurrent readers race for bytes. A shell-driven elevate works because the shell IS the kbd-reader thread, so the inline prompt loop in BrokerRequestElevation is safe. A Win32 PE syscall runs in a different task and would race the shell.

The broker resolves this by picking the path at call time:

  1. BrokerSetKbdReaderTid(tid) records the kbd-reader's TaskId at bring-up (kernel/core/main.cpp after SchedCreate).
  2. RunPrompt checks CurrentTaskId() == g_kbd_reader_tid. Match → inline TTY/GUI prompt (same as v0). Mismatch → deferred path.
  3. The deferred path posts a request to a single-slot global DeferredSlot, injects a synthetic kKeyNone event to wake the kbd reader, and blocks on a WaitQueue.
  4. The kbd-reader loop calls BrokerKbdReaderPumpDeferred() at the top of every iteration. On a pending slot it runs the prompt UI (safe — the kbd reader IS the legal Ps2KeyboardReadEvent consumer), stores the password in the slot, sets completed, and wakes the waiter.

State is guarded by arch::Cli/Sti only (no spinlock), mirroring the existing Process::StdinRing discipline. Single-flight for v0: a second concurrent deferred request returns false immediately and the caller falls through to the legacy denial branch (NOT_ALL_ASSIGNED for Win32 callers).

File layout

File Owns
kernel/security/rbac.h Role, RolePolicy, RoleId, registry API
kernel/security/rbac.cpp Built-in role definitions, lookup
kernel/security/broker.h BrokerRequest, BrokerOutcome, prompt hooks
kernel/security/broker.cpp Cache + role check + prompt orchestration
kernel/security/grace.h Metadata lookup / lease publish / metadata expiry
kernel/security/grace.cpp Fixed-size prompt-suppression metadata table
kernel/proc/process.{h,cpp} Locked durable caps, leases, deadlines, ceiling
kernel/security/login.{h,cpp} extended with LoginMode::Elevate
kernel/syscall/cap_gate.cpp Effective-snapshot syscall enforcement
kernel/security/password_hash.* extended with Argon2id variant
kernel/shell/shell_security.cpp extended with elevate / roles commands

Built-in roles (v0)

Role Cap bundle Grace override
root every kCap* bit kCapNetAdmin = 30 sec
developer FsRead, FsWrite, SpawnThread, Debug, SerialConsole, Input kCapFsWrite = 30 min
netop Net, NetAdmin, FsRead kCapNetAdmin = 30 sec
auditor FsRead, SerialConsole, Input default 5 min
sandbox none — explicit deny role for untrusted PEs n/a

The role table is in-memory v0; persistence is a follow-up tied to a writable system filesystem (see Persistence below). Adding a role at runtime: RoleRegister(name, cap_mask, grace_overrides) — admin-only via shell.

kCapNetAdmin uses a deliberately short positive lease for the built-in root and netop roles. A zero-duration override is reserved for policies that intentionally deny ambient elevation; it cannot authorize even a momentary capability because the broker fails closed rather than publishing an immediately expired lease.

Anti-spoofing — CLI v0

The CLI prompt is safe by construction because the keyboard reader demultiplexes keystrokes at the input ring level (kernel/core/main.cpp kbd-reader loop). When LoginIsActive() returns true (because the broker called LoginStartElevate()), every keystroke routes through LoginFeedKey directly. No user-mode process sees the password — the path is keyboard driver → input ring → reader thread → login gate (in-kernel). The same path the boot login uses.

A malicious PE could draw a fake prompt to the framebuffer, but nothing it does can read the keystrokes when the gate is active. Worst case: the PE prints "Enter your password" and waits forever for input that never arrives, because the kernel ate the bytes.

Anti-spoofing — GUI v1 (this slice)

The GUI elevation modal is drawn by the broker under the compositor lock, same discipline as the boot login screen. Other windows cannot paint over it (the broker raises a kElevationOverlay z-layer above every app window). Keystrokes still demultiplex through LoginFeedKey.

Anti-spoofing — GUI v2 (deferred)

A future hardening: bind a Secure Attention Key (Ctrl+Alt+Del at the PS/2 driver level) that always shows the kernel-drawn broker prompt, so a paranoid user can force a known-good prompt rather than trusting that the current one is the broker's. The keycode and the broker-prompt syscall shape are reserved now so the future SAK implementation has the seams to plug into.

Win32 facade routing

userland/libs/ntdll/:

  • NtAdjustPrivilegesToken — maps supported LUIDs to kCap*. Disable clears live durable/leased bits, SE_PRIVILEGE_REMOVED lowers the monotonic ceiling, and enable-on-miss calls the broker. PreviousState reports the pre-request effective snapshot.
  • RtlAdjustPrivilege — same.
  • OpenProcessToken / LookupPrivilegeValue — cosmetic facade, no broker call. They return believable handles; the actual gate fires at the next privileged NT syscall.

A mapped token-enable request triggers a real broker prompt drawn by the broker (not the PE's UI), with the calling account's password. If granted, the calling Process receives a temporary lease. Cache eviction does not revoke it; the Process deadline does. Child processes inherit durable caps and the ceiling, never lease bits.

Persistence

Out of scope for this slice — gated on a writable system filesystem. Today the role table and account table both seed from AuthInit() / RbacInit() at boot. A follow-up will add /system/secrets/ (encrypted at rest, TPM-sealed once the TPM driver lands) and an installer-driven first-boot flow that replaces the hardcoded seeding.

Tracked as a GAP marker in kernel/security/rbac.cpp and a row in wiki/reference/Roadmap.md.

Argon2id rollout

password_hash.h grows a tagged-union record:

struct PasswordHashRecord {
    enum Kind : u8 { Pbkdf2 = 0, Argon2id = 1 } kind;
    union {
        Pbkdf2Record   pbkdf2;
        Argon2idRecord argon2id;
    };
};
  • AuthAddUser, AuthChangePassword always write Argon2id.
  • AuthVerify reads the kind tag and runs the matching KDF. On a successful PBKDF2 verify, it re-hashes the supplied plaintext with Argon2id and overwrites the record in place (lazy migration).
  • Wall-clock uniformity is preserved: both code paths run a full derivation; the verify wall-clock no longer reveals which KDF a given account holds because both timings are within the password-derivation envelope the existing decoy path already smooths.

Known Limits / GAPs

  • Role + membership tables are in-memory onlyRbacInit re-seeds the built-in roles and memberships on every boot, so any runtime roleadd / roledel / RoleRegister change is lost on reboot (rbac.cpp:302). Carries a FIX_NOTE_GAP marker so the fix journal flags it at runtime. Persistence is blocked on a writable system FS + the /system/secrets/ layout (see Persistence above) and tracked in wiki/reference/Roadmap.md.
  • On-disk persistence (/system/secrets/) is deferred — the encrypted-at-rest, TPM-sealed envelope and the installer-driven first-boot flow that replaces hardcoded seeding both wait on the same writable-system-FS slice.

Related pages