feat: add TOTP multifactor authentication
This commit is contained in:
+81
-3
@@ -98,9 +98,81 @@ remove the old dotenv private keys and `SCHEDULER_TOKEN_PRODUCTION` from Gitea,
|
||||
and delete local and hosted dotenv key material. Removing encrypted files from
|
||||
the current Git tree does not revoke values stored in Git history.
|
||||
|
||||
The staging container disables default-user seeding. On a brand-new staging
|
||||
database, the existing bootstrap policy grants Admin to the first real account
|
||||
that signs in instead of writing generated credentials into deployment logs.
|
||||
## Authentication bootstrap and MFA secrets
|
||||
|
||||
Set the required app-only `MFA_TOTP_ENCRYPTION_KEYS` secret under `/app` in the
|
||||
`dev`, `staging`, and `prod` Infisical environments before starting each app.
|
||||
The value contains one to three comma-separated, unique
|
||||
`key-id:canonical-base64url-32-byte-key` entries. Each key is a random 32-byte
|
||||
value encoded as canonical Base64URL. The first entry is the active write key;
|
||||
following entries are retained decryption keys for older TOTP factors. Do not
|
||||
put this secret under `/shared/scheduler`, in a Gitea variable, in the image, or
|
||||
in a deployment log. A missing, malformed, duplicate, or overlong keyring fails
|
||||
app runtime validation.
|
||||
|
||||
Rotate the keyring without losing access to existing factors:
|
||||
|
||||
1. Generate a new random 32-byte key in the approved secret-management process
|
||||
and assign it a new key ID. Do not copy the value into a document or shell
|
||||
history.
|
||||
2. Prepend the new `key-id` entry and retain the old entries in Infisical.
|
||||
3. Deploy or restart the app, then verify an existing TOTP sign-in and a new
|
||||
enrollment. A successful verification rewraps that factor under the first
|
||||
key.
|
||||
4. Retain each old key until every factor encrypted with it has been rewrapped
|
||||
and verified. Never remove old entries merely because new enrollments use the
|
||||
new key; losing a required old key prevents decryption of those factors.
|
||||
|
||||
TOTP enrollment issues ten recovery codes and displays them once. The database
|
||||
stores only their domain-separated hashes. The account owner must save the codes
|
||||
in an offline password manager or another protected offline record; do not put
|
||||
their values in Infisical, logs, tickets, backups, audit snapshots, or this
|
||||
documentation. Each code works once. Regenerating codes immediately revokes all
|
||||
unused previous codes and displays a replacement set once. Disabling TOTP also
|
||||
revokes its recovery codes.
|
||||
|
||||
Sign-in never grants administrator authority automatically. Before offline
|
||||
elevation, the target administrator must have a verified email, an active TOTP
|
||||
factor, and at least one unused recovery code. The target enrolls TOTP at
|
||||
`/account/security` while the app is running. Then stop every app and scheduler
|
||||
service that can access the database and run the environment-specific command.
|
||||
The command requires that no active full administrator already exists, assigns
|
||||
the canonical administrator role, revokes the target's sessions, and records
|
||||
`role_management / administrator.bootstrapped`.
|
||||
|
||||
For local development:
|
||||
|
||||
```sh
|
||||
npm run admin:bootstrap -- <verified-email>
|
||||
```
|
||||
|
||||
For staging, from `/opt/shd-build-stage`:
|
||||
|
||||
```sh
|
||||
docker compose --env-file .env.deploy -f compose.staging.yml stop scheduler app
|
||||
docker compose --env-file .env.deploy -f compose.staging.yml run --rm --no-deps \
|
||||
app node scripts/admin-bootstrap.mjs <verified-email>
|
||||
docker compose --env-file .env.deploy -f compose.staging.yml up -d app scheduler
|
||||
```
|
||||
|
||||
For production, from `/opt/shd-build-production`:
|
||||
|
||||
```sh
|
||||
docker compose --env-file .env.deploy -f compose.production.yml stop scheduler app
|
||||
docker compose --env-file .env.deploy -f compose.production.yml run --rm --no-deps \
|
||||
app node scripts/admin-bootstrap.mjs <verified-email>
|
||||
docker compose --env-file .env.deploy -f compose.production.yml up -d app scheduler
|
||||
```
|
||||
|
||||
The script requires exactly one active, unblocked account with a verified email,
|
||||
an active TOTP factor, and an unused recovery code. It exits without changing
|
||||
roles when an active full administrator already exists. The Compose commands
|
||||
restart their services; restart the local development server after its command.
|
||||
The target must sign in again because its old sessions were revoked.
|
||||
|
||||
The staging container disables default-user seeding. A first sign-in never
|
||||
receives Admin; use the offline flow above after a new database or an initial
|
||||
data bootstrap.
|
||||
|
||||
## Gitea Actions runner
|
||||
|
||||
@@ -388,6 +460,12 @@ import. The importer verifies the bundle target, creates a production safety
|
||||
backup, and refuses to replace a production database containing users, builds,
|
||||
audit events, or proposals. Remove the remote host copy after verification.
|
||||
|
||||
If the imported database has no active full administrator, complete the offline
|
||||
administrator flow with the production Compose command in
|
||||
[Authentication bootstrap and MFA secrets](#authentication-bootstrap-and-mfa-secrets)
|
||||
before reopening the site. Do not rely on the first production sign-in for
|
||||
elevation.
|
||||
|
||||
### Promote one News article
|
||||
|
||||
Export a locally authored News article by ID or slug while the development
|
||||
|
||||
Reference in New Issue
Block a user