Platform security — MFA and impersonation¶
Phase 1b adds multi-factor authentication for platform administrators and account impersonation for support workflows.
Platform admin MFA (Console)¶
Open Account from the sidebar (/console/account) — one page with tabs:
| Tab | Purpose |
|---|---|
| Profile & account | Signed-in user + tenant account settings and quotas |
| Security | MFA, TOTP (QR), passkeys (unlimited, deletable), sessions |
| Members | Who can access this tenant account and their roles |
| Platform users | Global HomeCloud identities (root only) |
| Access keys | API keys for the signed-in user |
| Audit log | Account activity (when permitted) |
Members vs platform users: Members are people in the current account (acc-*). Platform users are global identities before or beyond any single account.
Legacy URLs (/console/account/settings, /console/iam, /console/iam/security) redirect to the matching tab.
Supported second factors:
| Method | Description |
|---|---|
| TOTP | Google Authenticator, 1Password, Authy — scan provisioning_uri or enter secret |
| Passkey (WebAuthn) | Browser/device passkey (Touch ID, Windows Hello, security key) — unlimited per user, each removable |
| Backup codes | One-time codes generated when TOTP is confirmed |
At least one of TOTP or passkey must be active when MFA is enabled.
TOTP enrollment (API)¶
Confirm:
POST /api/v1/auth/mfa/confirm
Authorization: Bearer <access_token>
Content-Type: application/json
{"code": "123456"}
Passkey enrollment (API)¶
POST /api/v1/auth/mfa/webauthn/register/verify
Authorization: Bearer <access_token>
Content-Type: application/json
{"credential": { ... PublicKeyCredential JSON ... }, "nickname": "MacBook"}
Passkeys use WEBAUTHN_RP_ID (defaults to the hostname of CONSOLE_PUBLIC_URL, e.g. console.holab.abrdns.com).
When MFA is required¶
Login, create tenant, and enter-account impersonation accept either mfa_code (TOTP/backup) or mfa_webauthn (assertion JSON):
POST /api/v1/auth/loginPOST /api/v1/platform/accountsPOST /api/v1/platform/impersonation/accounts/{account_id}/enter
Login without a second factor returns 403 with MFA_REQUIRED. The response includes:
mfa_token— short-lived token (5 minutes) from the password step; use it for passkey options and the MFA completion login without re-sending the passwordmethods— configured factors for this user, e.g.["passkey"],["totp"], or bothpasskeys— registered passkey labels (for display before the user clicks Continue with passkey)
{
"error": {
"code": "MFA_REQUIRED",
"message": "Second factor required",
"details": {
"mfa_token": "<token>",
"methods": ["passkey", "totp"],
"passkeys": [{"id": "…", "nickname": "MacBook Touch ID"}]
}
}
}
Passkey login options (no password when mfa_token is present):
POST /api/v1/auth/mfa/webauthn/login/options
Content-Type: application/json
{"mfa_token": "<token from MFA_REQUIRED>"}
Complete login with passkey:
POST /api/v1/auth/login
Content-Type: application/json
{"mfa_token": "<token>", "mfa_webauthn": { ... assertion JSON ... }}
The Console login card transitions inline to step 2 (no popup) when MFA is required. It shows available methods and registered passkey names; the user selects a passkey, confirms on a review card, then clicks Continue to open the browser WebAuthn prompt (no automatic prompt).
TOTP and backup codes use a 6-digit OTP input (individual boxes, paste-friendly). Backup codes can be entered via “Use backup code”.
TOTP enrollment shows a QR code by default; the manual secret is hidden under “Can't scan?”. Passkey registration also shows a confirmation card with the chosen label before the browser prompt.
Check status:
Returns totp_configured, passkeys_count, and registered passkey metadata.
Disable MFA (TOTP code or passkey):
Or {"mfa_webauthn": { ... }}.
PowerShell example — login with MFA¶
$body = @{
username = "root"
password = "password123"
mfa_code = "123456"
} | ConvertTo-Json
Invoke-RestMethod -Method POST -Uri "http://127.0.0.1:8000/api/v1/auth/login" -Body $body -ContentType "application/json"
Bash example — create account with MFA¶
curl -sS -X POST "http://127.0.0.1:8000/api/v1/platform/accounts" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Acme","slug":"acme","owner_email":"owner@example.com","mfa_code":"123456"}'
CLI authentication with MFA¶
The homecloud CLI supports the same MFA factors as the Console.
Terminal (TOTP / backup)¶
homecloud login --username alice
# → Verification code: …
homecloud login --username alice --password '…' --mfa-code 123456
On 403 MFA_REQUIRED with mfa_token, the CLI completes POST /auth/login with {mfa_token, mfa_code}. The same central handler injects mfa_code for step-up calls (create account, impersonation, etc.).
Browser (passkeys / security keys)¶
| Step | API |
|---|---|
| Start | POST /api/v1/auth/cli/session → verification_uri |
| Browser page | /auth/cli?session=… — full Console login including WebAuthn |
| Approve | POST /api/v1/auth/cli/session/{id}/approve (Bearer JWT) |
| Poll | GET /api/v1/auth/cli/session/{id} → one-time access_token |
Sessions live ~10 minutes in Redis, are single-use, and never persist mfa_token or codes on disk.
Account impersonation¶
Platform admins can act inside a tenant account for support. Impersonation:
- Issues a new JWT with
impersonating_account_id - Sets
current_account_idto the target account - Adds response header
X-HomeCloud-Acting-Ason account-scoped requests - Writes audit events
impersonation.startandimpersonation.end
Enter account:
POST /api/v1/platform/impersonation/accounts/{account_id}/enter
Authorization: Bearer <platform_admin_token>
Content-Type: application/json
{"mfa_code": "123456"}
Or {"mfa_webauthn": { ... }}.
Exit impersonation:
In the Console, click Enter account on a platform account row. When MFA is enabled, choose authenticator code or Use passkey. An amber banner shows while impersonation is active.
Legacy homelab inventory¶
After migration (scripts/migrate_legacy_to_platform_account.py), pre-tenant homelab resources live in the tagged platform admin account (settings.legacy_homelab=true). New tenants start empty. Account settings show a read-only notice when viewing the legacy inventory account.
Tenant isolation smoke test¶
Verify a freshly created account has no apps/resources (only bootstrap projects):
Breaking changes¶
- Legacy
GET /api/v1/kubernetes/*andGET /api/v1/storage/*require platform admin; regular users must use account-scoped routes under/api/v1/accounts/{account_id}/kubernetes/*. - Bootstrap MinIO policies use
so:*actions scoped toarn:holab:so:::{short_id}-*— not permissives3:*.