[C3] Control-plane credential & impersonation model (masquerade as any user)

closed
#3541d85 opened by agent Aug 24

Goal

Answer with code + ADR: what credential does the control plane hold, exactly what can it do on behalf of arbitrary users, and how? This is the masquerade question. Two distinct needs must be separated and tested independently:

  1. Provisioning authority — Registry/set on Account/Domain objects (server-side admin operations; no user context needed).
  2. User-context operations — JMAP calls that must execute as a specific user (initial mailbox setup, applying com.sovrn.mail.settings like vacation/forwarding via user-scope JMAP methods, support/debugging). Determine the correct Stalwart mechanism: Permission::Impersonate vs master-user impersonation separator (crates/common/src/auth/authentication.rs) vs issuing short-lived per-user credentials.

Context: docs/02-identity-and-auth.md §4/§6, docs/03-provisioning.md §6, docs/05-stalwart-integration.md §2–§3, docs/08-security-compliance.md §1/§3.

Tasks

  • [ ] Provision least-privilege service role in harness: SysAccountCreate/Update/Destroy, SysDomainCreate, ActionReloadSettings, Impersonate; verify each permission is individually required (negative tests)
  • [ ] Empirically test Impersonate: user-context JMAP call (e.g., VacationResponse/set) executed as another principal via service account; document exact request shape
  • [ ] Test master-user separator alternative; compare semantics (audit trail, scope, cache behaviour)
  • [ ] Secret-store interface (internal/store/secrets): file provider for dev; KMS interface stub; service credential never in env/config files
  • [ ] Audit-trail check: what does Stalwart log for impersonated calls (attribution of actor vs subject)
  • [ ] ADR docs/adr/0001-impersonation-model.md: chosen mechanism, least-privilege permission list, rotation story

Acceptance criteria

  • Negative tests prove the role cannot: create tenants, change settings without Action phase, read other tenants’ objects
  • One user-context operation demonstrably executes as an arbitrary user with correct attribution in logs
  • ADR merged; design docs 02/05 updated if findings contradict them

Risks this should surface

  • Impersonation may not work over Registry/JMAP the way source reading suggests — or may carry hidden audit/licensing implications
  • Permission granularity gaps forcing broader grants than acceptable (escalate to design-doc revision)

2 Comments

agent 345b461 Aug 24

Verified mechanics (pre-ADR evidence) — 2026-08-24

Traced in source @ rev 2add611e and partially exercised live during C1 (see ccd3c37 comment c7cfd93). The ADR this issue produces should start from these facts, not hypotheses.

Credential inventory (three distinct secrets)

# Secret Nature Notes
1 Recovery/fallback admin (STALWART_RECOVERY_ADMIN=user:pass) Env-only; never stored in registry Authenticates via Basic ⇒ AccessToken::new_admin() (crates/common/src/auth/authentication.rs:90–127). Works in bootstrap mode AND normal mode; works even when the registry/data store is broken (break-glass). Dev harness persists it to /data/service-secret chmod 600 (nix/scripts/serve-stalwart.sh).
2 Bootstrap-created admin@<domain> Real Account object, UserRoles::Admin, random 16-char alphanumeric secret hashed into registry Created automatically by x:Bootstrap/set when directory is internal; credentials returned ONLY in that response’s updated callback (crates/jmap/src/registry/mapping/bootstrap.rs:476–505). Our harness captures them to /data/admin-credentials.txt.
3 Sovrn service account (this issue creates it) Dedicated Account w/ scoped custom role incl. Impersonate Day-to-day operator credential; must be vaulted (KMS), audited, rotated.

Masquerade mechanics — VERIFIED

  • Separator is %, not *: UsernameParts::new splits on % (authentication.rs:565–596). Wire format: <master>%<user>@<domain> e.g. sovrn-admin%[email protected].
  • Authentication uses the master’s own secret; on success an access token is minted as the target (account_id_from_email(target) → access_token(...)).
  • Enforcement: token.assert_has_permissions(&[Permission::Impersonate, Permission::Authenticate]) (authentication.rs:261–263). Admin role carries Impersonate by default; our scoped role must include it deliberately.
  • App passwords cannot impersonate — explicit rejection (authentication.rs:145–152).
  • External-directory caveat (important for our OIDC-backed mode): the generic Basic-auth path with a master separator authenticates the target’s address against the target’s directory using the master’s secret (authentication.rs ~174–180) — wrong-shaped for OIDC-backed accounts and would fail. The recovery-admin path bypasses directories entirely (pure registry lookup, line ~90 branch) and keeps working regardless of backing store. ⇒ recovery admin is the guaranteed masquerade channel for OIDC-backed accounts.
  • Post-check: alias logins require AuthenticateWithAlias on the token (~line 253).

What actually needs masquerade

  • No masquerade: provisioning itself — Registry/set of Domain/Account/quotas/aliases is server-side administration under the service account.
  • Masquerade required: user-context JMAP operations driven by com.sovrn.mail.settings records (vacation via VacationResponse/set, forwarding/sieve edits, initial folder setup if done in user context).

Open questions for the ADR

  1. Can a service account even hold local password credentials once its domain points at the OpenId directory backend? Source says external-directory accounts’ credentials become read-only inside Stalwart (crates/jmap/src/registry/mapping/principal.rs:102). Likely answer: keep the service account on an internal-directory domain, or use the recovery admin as the operator credential in OIDC-mode deployments. Must be tested empirically.
  2. Audit attribution of impersonated calls — what exactly Stalwart logs (events show Details = master_address; confirm full request-level audit availability in OSS).
  3. Negative tests still required per issue body: role lacking Sys* perms fails; Impersonate-less master separator fails; app-password impersonation attempt fails.
  4. Per-environment decision: recovery-admin-as-operator (simple, break-glass capable) vs dedicated service account (least privilege). Recommend: dev = recovery admin (already wired); prod = dedicated service account + vaulted recovery admin for break-glass only.
BT 3b5b491 Aug 26

Re-scoped: impersonation dropped — 2026-08-26

Design session decided the control plane does NOT need masquerade for the v1 mail-flow. Grounds:

  • Provisioning (Account/Domain/tenant/alias/quota/app-password) is Registry admin under the service credential — no impersonation.
  • Webmail authenticates via Sovrn-minted OIDC bearer tokens (tokenissuer → Stalwart OpenId directory), not per-call masquerade. Direction committed: Sovrn owns the login UX; the hosted webmail is a fork adapted to receive a Sovrn token and call Stalwart JMAP directly.
  • App passwords cover legacy IMAP/JMAP clients.

The only remaining impersonation use case — applying per-user settings (vacation/forwarding/sieve) that lack a Registry admin surface — is deferred. Reopen (or file a new issue) if com.sovrn.mail.settings application becomes a requirement.

Pre-ADR source findings stay valid and preserved here for that future case: - Separator is %, wire format % (NOT % as an earlier note claimed — UsernameParts::new fills account from text before % and master_user from text after, authentication.rs:566-599). - Enforcement: assert_has_permissions([Impersonate, Authenticate]); app passwords cannot impersonate. - Audit event logs AccountName = target, Details = master.

Work continues under C4 (a589d34), C5 (c1c7e19), C6 (0d541aa).