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:
|
# The final image:
|
||||||
# - Runs as non-root uid 10001 (mcias)
|
# - Runs as non-root uid 10001 (mcias)
|
||||||
# - Exposes port 8443 (REST/TLS) and 9443 (gRPC/TLS)
|
# - 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
|
# - Does NOT contain the Go toolchain, source code, or build cache
|
||||||
#
|
#
|
||||||
# Build:
|
# Build:
|
||||||
@@ -15,8 +15,7 @@
|
|||||||
# Run:
|
# Run:
|
||||||
# docker run -d \
|
# docker run -d \
|
||||||
# --name mcias \
|
# --name mcias \
|
||||||
# -v /path/to/config:/etc/mcias:ro \
|
# -v /srv/mcias:/srv/mcias \
|
||||||
# -v mcias-data:/data \
|
|
||||||
# -e MCIAS_MASTER_PASSPHRASE=your-passphrase \
|
# -e MCIAS_MASTER_PASSPHRASE=your-passphrase \
|
||||||
# -p 8443:8443 \
|
# -p 8443:8443 \
|
||||||
# -p 9443:9443 \
|
# -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/mciasdb /usr/local/bin/mciasdb
|
||||||
COPY --from=builder /out/mciasgrpcctl /usr/local/bin/mciasgrpcctl
|
COPY --from=builder /out/mciasgrpcctl /usr/local/bin/mciasgrpcctl
|
||||||
|
|
||||||
# Create the config and data directories.
|
# Create the data directory.
|
||||||
# /etc/mcias is mounted read-only by the operator with the config file,
|
# /srv/mcias is mounted from the host with config, TLS certs, and database.
|
||||||
# TLS cert, and TLS key.
|
RUN mkdir -p /srv/mcias && \
|
||||||
# /data is the SQLite database mount point.
|
chown mcias:mcias /srv/mcias && \
|
||||||
RUN mkdir -p /etc/mcias /data && \
|
chmod 0750 /srv/mcias
|
||||||
chown mcias:mcias /data && \
|
|
||||||
chmod 0750 /data
|
|
||||||
|
|
||||||
# Declare /data as a volume so the operator must explicitly mount it.
|
# Declare /srv/mcias as a volume so the operator must explicitly mount it.
|
||||||
# The SQLite database must persist across container restarts.
|
# Contains the config file, TLS cert/key, and SQLite database.
|
||||||
VOLUME /data
|
VOLUME /srv/mcias
|
||||||
|
|
||||||
# REST/TLS port and gRPC/TLS port. These are documentation only; the actual
|
# 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.
|
# ports are set in the config file. Override by mounting a different config.
|
||||||
@@ -93,7 +90,8 @@ EXPOSE 9443
|
|||||||
USER mcias
|
USER mcias
|
||||||
|
|
||||||
# Default entry point and config path.
|
# 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.
|
# See dist/mcias.conf.docker.example for a suitable template.
|
||||||
ENTRYPOINT ["mciassrv"]
|
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
|
.PHONY: docker
|
||||||
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
|
# 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
|
## Master Key Rotation
|
||||||
|
|
||||||
> This operation is not yet automated. Until a rotation command is
|
> 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]
|
[master_key]
|
||||||
passphrase_env = "MCIAS_MASTER_PASSPHRASE"
|
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 ...
|
# Set it with: docker run -e MCIAS_MASTER_PASSPHRASE=your-passphrase ...
|
||||||
# or with a Docker secret / Kubernetes secret.
|
# or with a Docker secret / Kubernetes secret.
|
||||||
passphrase_env = "MCIAS_MASTER_PASSPHRASE"
|
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.
|
# Uncomment and comment out passphrase_env to switch modes.
|
||||||
# keyfile = "/srv/mcias/master.key"
|
# 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/base32"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
qrcode "github.com/skip2/go-qrcode"
|
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"})
|
u.renderTOTPSection(w, r, ProfileData{TOTPError: "internal error"})
|
||||||
return
|
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.
|
// Issue enrollment nonce for the confirm step.
|
||||||
nonce, err := u.issueTOTPEnrollNonce(acct.ID)
|
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"})
|
u.renderTOTPSection(w, r, ProfileData{TOTPError: "internal error"})
|
||||||
return
|
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)
|
newNonce, nonceErr := u.issueTOTPEnrollNonce(acct.ID)
|
||||||
if nonceErr != nil {
|
if nonceErr != nil {
|
||||||
|
|||||||
@@ -931,11 +931,11 @@ type ProfileData struct { //nolint:govet // fieldalignment: readability over ali
|
|||||||
WebAuthnEnabled bool
|
WebAuthnEnabled bool
|
||||||
// TOTP enrollment fields (populated only during enrollment flow).
|
// TOTP enrollment fields (populated only during enrollment flow).
|
||||||
TOTPEnabled bool
|
TOTPEnabled bool
|
||||||
TOTPSecret string // base32-encoded; shown once during enrollment
|
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
|
TOTPEnrollNonce string // single-use nonce for confirm step
|
||||||
TOTPError string // enrollment-specific error message
|
TOTPError string // enrollment-specific error message
|
||||||
TOTPSuccess string // success flash after confirmation
|
TOTPSuccess string // success flash after confirmation
|
||||||
}
|
}
|
||||||
|
|
||||||
// PGCredsData is the view model for the "My PG Credentials" list page.
|
// PGCredsData is the view model for the "My PG Credentials" list page.
|
||||||
|
|||||||
@@ -60,6 +60,12 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 style="font-size:1rem;font-weight:600;margin-bottom:1rem">Passkeys</h2>
|
<h2 style="font-size:1rem;font-weight:600;margin-bottom:1rem">Passkeys</h2>
|
||||||
{{template "webauthn_credentials" .}}
|
{{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>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|||||||
Reference in New Issue
Block a user