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 <noreply@anthropic.com>
This commit is contained in:
28
Dockerfile
28
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"]
|
||||
|
||||
2
Makefile
2
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
|
||||
|
||||
77
RUNBOOK.md
77
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 <UUID>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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 <UUID>
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
7
dist/mcias-dev.conf.example
vendored
7
dist/mcias-dev.conf.example
vendored
@@ -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)"
|
||||
|
||||
11
dist/mcias.conf.docker.example
vendored
11
dist/mcias.conf.docker.example
vendored
@@ -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"
|
||||
|
||||
21
dist/mcias.conf.example
vendored
21
dist/mcias.conf.example
vendored
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -932,7 +932,7 @@ type ProfileData struct { //nolint:govet // fieldalignment: readability over ali
|
||||
// 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
|
||||
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
|
||||
|
||||
@@ -60,6 +60,12 @@
|
||||
<div class="card">
|
||||
<h2 style="font-size:1rem;font-weight:600;margin-bottom:1rem">Passkeys</h2>
|
||||
{{template "webauthn_credentials" .}}
|
||||
{{if eq (string .Account.AccountType) "human"}}
|
||||
<p class="text-muted text-small" style="margin-top:.75rem">
|
||||
Passkey enrollment is self-service. The account holder can add passkeys from their
|
||||
<a href="/profile">Profile</a> page.
|
||||
</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="card">
|
||||
|
||||
Reference in New Issue
Block a user