From 0b37fde1557ab508081c4b1886f860747e6b4f01 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Mon, 16 Mar 2026 18:57:06 -0700 Subject: [PATCH] Add WebAuthn config; Docker single-mount - Add [webauthn] section to all config examples - Add active WebAuthn config to run/mcias.conf - Update Dockerfile to use /srv/mcias single mount - Add WebAuthn and TOTP sections to RUNBOOK.md - Fix TOTP QR display (template.URL type) - Add --force-rm to docker build in Makefile Co-Authored-By: Claude Opus 4.6 --- Dockerfile | 28 ++++++----- Makefile | 2 +- RUNBOOK.md | 77 +++++++++++++++++++++++++++++++ dist/mcias-dev.conf.example | 7 +++ dist/mcias.conf.docker.example | 11 +++++ dist/mcias.conf.example | 21 +++++++++ internal/ui/handlers_totp.go | 5 +- internal/ui/ui.go | 10 ++-- web/templates/account_detail.html | 6 +++ 9 files changed, 144 insertions(+), 23 deletions(-) diff --git a/Dockerfile b/Dockerfile index eb279b9..5877f41 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ # The final image: # - Runs as non-root uid 10001 (mcias) # - Exposes port 8443 (REST/TLS) and 9443 (gRPC/TLS) -# - Declares VOLUME /data for the SQLite database +# - Declares VOLUME /srv/mcias for config, TLS, and database # - Does NOT contain the Go toolchain, source code, or build cache # # Build: @@ -15,8 +15,7 @@ # Run: # docker run -d \ # --name mcias \ -# -v /path/to/config:/etc/mcias:ro \ -# -v mcias-data:/data \ +# -v /srv/mcias:/srv/mcias \ # -e MCIAS_MASTER_PASSPHRASE=your-passphrase \ # -p 8443:8443 \ # -p 9443:9443 \ @@ -72,17 +71,15 @@ COPY --from=builder /out/mciasctl /usr/local/bin/mciasctl COPY --from=builder /out/mciasdb /usr/local/bin/mciasdb COPY --from=builder /out/mciasgrpcctl /usr/local/bin/mciasgrpcctl -# Create the config and data directories. -# /etc/mcias is mounted read-only by the operator with the config file, -# TLS cert, and TLS key. -# /data is the SQLite database mount point. -RUN mkdir -p /etc/mcias /data && \ - chown mcias:mcias /data && \ - chmod 0750 /data +# Create the data directory. +# /srv/mcias is mounted from the host with config, TLS certs, and database. +RUN mkdir -p /srv/mcias && \ + chown mcias:mcias /srv/mcias && \ + chmod 0750 /srv/mcias -# Declare /data as a volume so the operator must explicitly mount it. -# The SQLite database must persist across container restarts. -VOLUME /data +# Declare /srv/mcias as a volume so the operator must explicitly mount it. +# Contains the config file, TLS cert/key, and SQLite database. +VOLUME /srv/mcias # REST/TLS port and gRPC/TLS port. These are documentation only; the actual # ports are set in the config file. Override by mounting a different config. @@ -93,7 +90,8 @@ EXPOSE 9443 USER mcias # Default entry point and config path. -# The operator mounts /etc/mcias/mcias.conf from the host or a volume. +# The operator mounts /srv/mcias from the host containing mcias.toml, +# TLS cert/key, and the SQLite database. # See dist/mcias.conf.docker.example for a suitable template. ENTRYPOINT ["mciassrv"] -CMD ["-config", "/etc/mcias/mcias.conf"] +CMD ["-config", "/srv/mcias/mcias.toml"] diff --git a/Makefile b/Makefile index 1727fcd..437d4da 100644 --- a/Makefile +++ b/Makefile @@ -134,7 +134,7 @@ dist: man # --------------------------------------------------------------------------- .PHONY: docker docker: - docker build -t mcias:$(VERSION) -t mcias:latest . + docker build --force-rm -t mcias:$(VERSION) -t mcias:latest . # --------------------------------------------------------------------------- # docker-clean — remove local mcias Docker images diff --git a/RUNBOOK.md b/RUNBOOK.md index 74c3fdd..73453b7 100644 --- a/RUNBOOK.md +++ b/RUNBOOK.md @@ -322,6 +322,83 @@ mciasdb $CONF audit query --json --- +## WebAuthn / Passkey Configuration + +WebAuthn enables passwordless passkey login and hardware security key 2FA. +It is **disabled by default** — to enable it, add a `[webauthn]` section to +`mcias.toml` with the relying party ID and origin. + +### Enable WebAuthn + +Add to `/srv/mcias/mcias.toml`: + +```toml +[webauthn] +rp_id = "auth.example.com" +rp_origin = "https://auth.example.com" +display_name = "MCIAS" +``` + +- **`rp_id`** — The domain name (no scheme or port). Must match the domain + users see in their browser address bar. +- **`rp_origin`** — The full HTTPS origin. Include the port if non-standard + (e.g., `https://localhost:8443` for development). +- **`display_name`** — Shown to users during browser passkey prompts. Defaults + to "MCIAS" if omitted. + +Restart the server after changing the config: + +```sh +systemctl restart mcias +``` + +Once enabled, the **Passkeys** section appears on the user's Profile page +(self-service enrollment) and on the admin Account Detail page (credential +management). + +### Passkey enrollment + +Passkey enrollment is self-service only. Users add passkeys from their +**Profile → Passkeys** section. Admins can view and remove passkeys from +the Account Detail page but cannot enroll on behalf of users (passkey +registration requires the authenticator device to be present). + +### Disable WebAuthn + +Remove or comment out the `[webauthn]` section and restart. Existing +credentials remain in the database but are unused. Passkey UI sections +will be hidden. + +### Remove all passkeys for an account (break-glass) + +```sh +mciasdb --config /srv/mcias/mcias.toml account reset-webauthn --id +``` + +--- + +## TOTP Two-Factor Authentication + +TOTP enrollment is self-service via the **Profile → Two-Factor Authentication** +section. Users enter their current password to begin enrollment, scan the QR +code with an authenticator app, and confirm with a 6-digit code. + +### Admin: Remove TOTP for an account + +From the web UI: navigate to the account's detail page and click **Remove** +next to the TOTP status. + +From the CLI: + +```sh +mciasdb --config /srv/mcias/mcias.toml account reset-totp --id +``` + +This clears the TOTP secret and disables the 2FA requirement. The user can +re-enroll from their Profile page. + +--- + ## Master Key Rotation > This operation is not yet automated. Until a rotation command is diff --git a/dist/mcias-dev.conf.example b/dist/mcias-dev.conf.example index bce69a6..a4bc6fe 100644 --- a/dist/mcias-dev.conf.example +++ b/dist/mcias-dev.conf.example @@ -41,3 +41,10 @@ threads = 4 [master_key] passphrase_env = "MCIAS_MASTER_PASSPHRASE" + +# WebAuthn — passkey authentication for local development. +# rp_origin includes the non-standard port since we're not behind a proxy. +[webauthn] +rp_id = "localhost" +rp_origin = "https://localhost:8443" +display_name = "MCIAS (dev)" diff --git a/dist/mcias.conf.docker.example b/dist/mcias.conf.docker.example index 1ebfa12..9e800b7 100644 --- a/dist/mcias.conf.docker.example +++ b/dist/mcias.conf.docker.example @@ -48,3 +48,14 @@ threads = 4 # Set it with: docker run -e MCIAS_MASTER_PASSPHRASE=your-passphrase ... # or with a Docker secret / Kubernetes secret. passphrase_env = "MCIAS_MASTER_PASSPHRASE" + +# --------------------------------------------------------------------------- +# [webauthn] — FIDO2/WebAuthn passkey authentication (OPTIONAL) +# --------------------------------------------------------------------------- +# Uncomment to enable passwordless passkey login. Set rp_id to your domain +# and rp_origin to the full HTTPS origin users access in their browser. +# +# [webauthn] +# rp_id = "auth.example.com" +# rp_origin = "https://auth.example.com" +# display_name = "MCIAS" diff --git a/dist/mcias.conf.example b/dist/mcias.conf.example index 2002f01..e08e8bb 100644 --- a/dist/mcias.conf.example +++ b/dist/mcias.conf.example @@ -123,3 +123,24 @@ passphrase_env = "MCIAS_MASTER_PASSPHRASE" # # Uncomment and comment out passphrase_env to switch modes. # keyfile = "/srv/mcias/master.key" + +# --------------------------------------------------------------------------- +# [webauthn] — FIDO2/WebAuthn passkey authentication (OPTIONAL) +# --------------------------------------------------------------------------- +# Enables passwordless passkey login and hardware security key 2FA. +# If this section is omitted or rp_id/rp_origin are empty, WebAuthn is +# disabled and passkey options will not appear in the UI. +# +# [webauthn] +# +# REQUIRED (if enabling). The Relying Party ID — typically the domain name +# (without port or scheme). Must match the domain users see in their browser. +# rp_id = "auth.example.com" +# +# REQUIRED (if enabling). The Relying Party Origin — the full origin URL +# including scheme. Must be HTTPS. Include the port if non-standard (not 443). +# rp_origin = "https://auth.example.com" +# +# OPTIONAL. Display name shown to users during passkey registration prompts. +# Default: "MCIAS". +# display_name = "MCIAS" diff --git a/internal/ui/handlers_totp.go b/internal/ui/handlers_totp.go index 631cd28..bfe398a 100644 --- a/internal/ui/handlers_totp.go +++ b/internal/ui/handlers_totp.go @@ -4,6 +4,7 @@ import ( "encoding/base32" "encoding/base64" "fmt" + "html/template" "net/http" qrcode "github.com/skip2/go-qrcode" @@ -108,7 +109,7 @@ func (u *UIServer) handleTOTPEnrollStart(w http.ResponseWriter, r *http.Request) u.renderTOTPSection(w, r, ProfileData{TOTPError: "internal error"}) return } - qrDataURI := "data:image/png;base64," + base64.StdEncoding.EncodeToString(png) + qrDataURI := template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(png)) //nolint:gosec // G203: trusted server-generated data URI // Issue enrollment nonce for the confirm step. nonce, err := u.issueTOTPEnrollNonce(acct.ID) @@ -224,7 +225,7 @@ func (u *UIServer) reissueTOTPEnrollQR(w http.ResponseWriter, r *http.Request, a u.renderTOTPSection(w, r, ProfileData{TOTPError: "internal error"}) return } - qrDataURI := "data:image/png;base64," + base64.StdEncoding.EncodeToString(png) + qrDataURI := template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(png)) //nolint:gosec // G203: trusted server-generated data URI newNonce, nonceErr := u.issueTOTPEnrollNonce(acct.ID) if nonceErr != nil { diff --git a/internal/ui/ui.go b/internal/ui/ui.go index a7064d6..da8002b 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -931,11 +931,11 @@ type ProfileData struct { //nolint:govet // fieldalignment: readability over ali WebAuthnEnabled bool // TOTP enrollment fields (populated only during enrollment flow). TOTPEnabled bool - TOTPSecret string // base32-encoded; shown once during enrollment - TOTPQR string // data:image/png;base64,... QR code - TOTPEnrollNonce string // single-use nonce for confirm step - TOTPError string // enrollment-specific error message - TOTPSuccess string // success flash after confirmation + TOTPSecret string // base32-encoded; shown once during enrollment + TOTPQR template.URL // data:image/png;base64,... QR code; template.URL bypasses URL escaping + TOTPEnrollNonce string // single-use nonce for confirm step + TOTPError string // enrollment-specific error message + TOTPSuccess string // success flash after confirmation } // PGCredsData is the view model for the "My PG Credentials" list page. diff --git a/web/templates/account_detail.html b/web/templates/account_detail.html index a852770..775579b 100644 --- a/web/templates/account_detail.html +++ b/web/templates/account_detail.html @@ -60,6 +60,12 @@

Passkeys

{{template "webauthn_credentials" .}} + {{if eq (string .Account.AccountType) "human"}} +

+ Passkey enrollment is self-service. The account holder can add passkeys from their + Profile page. +

+ {{end}}
{{end}}