Implement Phase 8: operational artifacts

- Makefile: build/test/lint/generate/man/install/clean/dist/docker;
  CGO_ENABLED=1 throughout; VERSION from git describe --tags --always
- Dockerfile: multi-stage (golang:1.26-bookworm builder ->
  debian:bookworm-slim runtime); non-root uid 10001 (mcias),
  VOLUME /data, EXPOSE 8443/9443; no toolchain in final image
- dist/mcias.service: hardened systemd unit (ProtectSystem=strict,
  ProtectHome, PrivateTmp, NoNewPrivileges, MemoryDenyWriteExecute,
  CapabilityBoundingSet= empty, EnvironmentFile, LimitNOFILE=65536)
- dist/mcias.env.example: passphrase env file template
- dist/mcias.conf.example: fully-commented production TOML config
- dist/mcias-dev.conf.example: local dev config (/tmp, short expiry)
- dist/mcias.conf.docker.example: container config template
- dist/install.sh: POSIX sh idempotent installer; creates mcias
  user/group, installs binaries, /etc/mcias, /var/lib/mcias,
  systemd unit, man pages; prints post-install instructions
- man/man1/mciassrv.1: mdoc synopsis/config/API/signals/files
- man/man1/mciasctl.1: mdoc all subcommands/env/examples
- man/man1/mciasdb.1: mdoc trust model/safety/all subcommands
- man/man1/mciasgrpcctl.1: mdoc gRPC commands/grpcurl example
- README.md: user-facing quick-start, first-run setup, build
  instructions, CLI references, Docker deployment, security notes
- .gitignore: added /bin/, dist/mcias_*.tar.gz, man/man1/*.gz
This commit is contained in:
2026-03-11 15:11:36 -07:00
parent 8f706f10ec
commit 941c71f2d1
15 changed files with 1715 additions and 54 deletions

225
dist/install.sh vendored Normal file
View File

@@ -0,0 +1,225 @@
#!/bin/sh
# install.sh — MCIAS first-time and upgrade installer
#
# Usage: sh dist/install.sh
#
# This script must be run as root. It:
# 1. Creates the mcias system user and group (idempotent).
# 2. Copies binaries to /usr/local/bin/.
# 3. Creates /etc/mcias/ and /var/lib/mcias/ with correct permissions.
# 4. Installs the systemd service unit.
# 5. Prints post-install instructions.
#
# The script does NOT start or enable the service automatically. Review the
# configuration files and set the master key passphrase before starting.
#
# Idempotent: safe to re-run after upgrades. Existing config files are not
# overwritten; new example files are placed alongside them with a .new suffix
# so you can review and merge changes.
#
# POSIX sh compatible — no bash-isms.
set -eu
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
BIN_DIR="/usr/local/bin"
CONF_DIR="/etc/mcias"
DATA_DIR="/var/lib/mcias"
MAN_DIR="/usr/share/man/man1"
SYSTEMD_DIR="/etc/systemd/system"
SERVICE_USER="mcias"
SERVICE_GROUP="mcias"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
info() {
printf '==> %s\n' "$*"
}
warn() {
printf 'WARNING: %s\n' "$*" >&2
}
die() {
printf 'ERROR: %s\n' "$*" >&2
exit 1
}
require_root() {
if [ "$(id -u)" -ne 0 ]; then
die "This script must be run as root."
fi
}
# install_file SRC DST MODE OWNER
# Installs SRC to DST. If DST already exists, installs SRC as DST.new
# so the operator can review changes.
install_file() {
src="$1"
dst="$2"
mode="$3"
owner="$4"
if [ -e "$dst" ]; then
info "Existing file found: $dst (installing as $dst.new)"
install -m "$mode" -o "$owner" "$src" "$dst.new"
else
install -m "$mode" -o "$owner" "$src" "$dst"
fi
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
require_root
# Step 1: Create system user and group.
info "Creating system user and group: $SERVICE_USER"
if getent group "$SERVICE_GROUP" > /dev/null 2>&1; then
info "Group $SERVICE_GROUP already exists — skipping."
else
groupadd --system "$SERVICE_GROUP"
fi
if getent passwd "$SERVICE_USER" > /dev/null 2>&1; then
info "User $SERVICE_USER already exists — skipping."
else
useradd \
--system \
--gid "$SERVICE_GROUP" \
--no-create-home \
--shell /usr/sbin/nologin \
--comment "MCIAS authentication server" \
"$SERVICE_USER"
fi
# Step 2: Install binaries.
info "Installing binaries to $BIN_DIR"
for bin in mciassrv mciasctl mciasdb mciasgrpcctl; do
src="$REPO_ROOT/$bin"
if [ ! -f "$src" ]; then
# Try bin/ subdirectory (Makefile build output).
src="$REPO_ROOT/bin/$bin"
fi
if [ ! -f "$src" ]; then
warn "Binary not found: $bin — skipping. Run 'make build' first."
continue
fi
info " Installing $bin"
install -m 0755 -o root -g root "$src" "$BIN_DIR/$bin"
done
# Step 3: Create configuration directory.
info "Creating $CONF_DIR"
install -d -m 0750 -o root -g "$SERVICE_GROUP" "$CONF_DIR"
# Install example config files; never overwrite existing configs.
for f in mcias.conf.example mcias.env.example; do
src="$SCRIPT_DIR/$f"
dst="$CONF_DIR/$f"
if [ -f "$src" ]; then
install -m 0640 -o root -g "$SERVICE_GROUP" "$src" "$dst" 2>/dev/null || true
fi
done
# Step 4: Create data directory.
info "Creating $DATA_DIR"
install -d -m 0750 -o "$SERVICE_USER" -g "$SERVICE_GROUP" "$DATA_DIR"
# Step 5: Install systemd service unit.
if [ -d "$SYSTEMD_DIR" ]; then
info "Installing systemd service unit to $SYSTEMD_DIR"
install -m 0644 -o root -g root "$SCRIPT_DIR/mcias.service" "$SYSTEMD_DIR/mcias.service"
info "Reloading systemd daemon"
systemctl daemon-reload 2>/dev/null || warn "systemctl not available; reload manually."
else
warn "systemd not found at $SYSTEMD_DIR; skipping service unit installation."
fi
# Step 6: Install man pages.
if [ -d "$REPO_ROOT/man/man1" ]; then
install -d -m 0755 -o root -g root "$MAN_DIR"
info "Installing man pages to $MAN_DIR"
for page in "$REPO_ROOT/man/man1"/*.1.gz; do
[ -f "$page" ] || continue
install -m 0644 -o root -g root "$page" "$MAN_DIR/"
done
# Also install uncompressed pages if no gz versions exist.
for page in "$REPO_ROOT/man/man1"/*.1; do
[ -f "$page" ] || continue
gzname="${MAN_DIR}/$(basename "$page").gz"
if [ ! -f "$gzname" ]; then
gzip -c "$page" > "$gzname"
chmod 0644 "$gzname"
fi
done
fi
# ---------------------------------------------------------------------------
# Post-install instructions
# ---------------------------------------------------------------------------
cat <<EOF
==========================================================================
MCIAS installed successfully.
==========================================================================
Next steps:
1. Generate a TLS certificate and key:
# Self-signed (development / personal use):
openssl req -x509 -newkey ed25519 -days 3650 \\
-keyout /etc/mcias/server.key \\
-out /etc/mcias/server.crt \\
-subj "/CN=auth.example.com" \\
-nodes
chmod 0640 /etc/mcias/server.key
chown root:mcias /etc/mcias/server.key
2. Copy and edit the configuration file:
cp /etc/mcias/mcias.conf.example /etc/mcias/mcias.conf
\$EDITOR /etc/mcias/mcias.conf
chmod 0640 /etc/mcias/mcias.conf
chown root:mcias /etc/mcias/mcias.conf
3. Set the master key passphrase:
cp /etc/mcias/mcias.env.example /etc/mcias/env
\$EDITOR /etc/mcias/env # replace the placeholder passphrase
chmod 0640 /etc/mcias/env
chown root:mcias /etc/mcias/env
IMPORTANT: Back up the passphrase to a secure offline location.
Losing it means losing access to all encrypted data in the database.
4. Enable and start the service:
systemctl enable mcias
systemctl start mcias
systemctl status mcias
5. Create the first admin account using mciasdb (while the server is
running, or before first start):
MCIAS_MASTER_PASSPHRASE=\$(grep MCIAS_MASTER_PASSPHRASE /etc/mcias/env | cut -d= -f2) \\
mciasdb --config /etc/mcias/mcias.conf account create \\
--username admin --type human
Then set a password:
MCIAS_MASTER_PASSPHRASE=... mciasdb --config /etc/mcias/mcias.conf \\
account set-password --id <UUID>
And grant the admin role:
MCIAS_MASTER_PASSPHRASE=... mciasdb --config /etc/mcias/mcias.conf \\
role grant --id <UUID> --role admin
For full documentation, see: man mciassrv
==========================================================================
EOF

42
dist/mcias-dev.conf.example vendored Normal file
View File

@@ -0,0 +1,42 @@
# mcias-dev.conf — Local development configuration for mciassrv
#
# Suitable for running mciassrv on a developer workstation.
# DO NOT use this configuration in production:
# - Tokens expire quickly (for rapid test iteration).
# - The master key passphrase is trivial.
# - TLS paths point to local self-signed certificates.
#
# Generate a self-signed certificate for local development:
# openssl req -x509 -newkey ed25519 -days 365 \
# -keyout /tmp/mcias-dev.key -out /tmp/mcias-dev.crt \
# -subj "/CN=localhost" -nodes
#
# Set the master passphrase:
# export MCIAS_MASTER_PASSPHRASE=devpassphrase
#
# Start the server:
# mciassrv -config /path/to/mcias-dev.conf
[server]
listen_addr = "127.0.0.1:8443"
grpc_addr = "127.0.0.1:9443"
tls_cert = "/tmp/mcias-dev.crt"
tls_key = "/tmp/mcias-dev.key"
[database]
path = "/tmp/mcias-dev.db"
[tokens]
issuer = "https://localhost:8443"
default_expiry = "1h"
admin_expiry = "30m"
service_expiry = "24h"
[argon2]
# OWASP minimums maintained even in dev; do not reduce further.
time = 2
memory = 65536
threads = 4
[master_key]
passphrase_env = "MCIAS_MASTER_PASSPHRASE"

48
dist/mcias.conf.docker.example vendored Normal file
View File

@@ -0,0 +1,48 @@
# mcias.conf.docker.example — Config template for container deployment
#
# Mount this file into the container at /etc/mcias/mcias.conf:
#
# docker run -d \
# --name mcias \
# -v /path/to/mcias.conf:/etc/mcias/mcias.conf:ro \
# -v /path/to/certs:/etc/mcias:ro \
# -v mcias-data:/data \
# -e MCIAS_MASTER_PASSPHRASE=your-passphrase \
# -p 8443:8443 \
# -p 9443:9443 \
# mcias:latest
#
# The container runs as uid 10001 (mcias). Ensure that:
# - /data volume is writable by uid 10001
# - TLS cert and key are readable by uid 10001
#
# TLS: The server performs TLS termination inside the container; there is no
# plain-text mode. Mount your certificate and key under /etc/mcias/.
# For Let's Encrypt certificates, mount the live/ directory read-only.
[server]
listen_addr = "0.0.0.0:8443"
grpc_addr = "0.0.0.0:9443"
tls_cert = "/etc/mcias/server.crt"
tls_key = "/etc/mcias/server.key"
[database]
# VOLUME /data is declared in the Dockerfile; map a named volume here.
path = "/data/mcias.db"
[tokens]
issuer = "https://auth.example.com"
default_expiry = "720h"
admin_expiry = "8h"
service_expiry = "8760h"
[argon2]
time = 3
memory = 65536
threads = 4
[master_key]
# Pass the passphrase via the MCIAS_MASTER_PASSPHRASE environment variable.
# Set it with: docker run -e MCIAS_MASTER_PASSPHRASE=your-passphrase ...
# or with a Docker secret / Kubernetes secret.
passphrase_env = "MCIAS_MASTER_PASSPHRASE"

110
dist/mcias.conf.example vendored Normal file
View File

@@ -0,0 +1,110 @@
# mcias.conf — Reference configuration for mciassrv
#
# Copy this file to /etc/mcias/mcias.conf and adjust the values for your
# deployment. All fields marked REQUIRED must be set before the server will
# start. Fields marked OPTIONAL can be omitted to use defaults.
#
# File permissions: mode 0640, owner root:mcias.
# chmod 0640 /etc/mcias/mcias.conf
# chown root:mcias /etc/mcias/mcias.conf
# ---------------------------------------------------------------------------
# [server] — Network listener configuration
# ---------------------------------------------------------------------------
[server]
# REQUIRED. Address and port for the HTTPS REST listener.
# Format: "host:port". Use "0.0.0.0" to listen on all interfaces.
# Ports > 1024 do not require elevated privileges.
listen_addr = "0.0.0.0:8443"
# OPTIONAL. Address and port for the gRPC/TLS listener.
# If omitted, the gRPC listener is disabled and only REST is served.
# Format: "host:port".
# grpc_addr = "0.0.0.0:9443"
# REQUIRED. Path to the TLS certificate (PEM format).
# Self-signed certificates work fine for personal deployments; for
# public-facing deployments consider a certificate from Let's Encrypt.
tls_cert = "/etc/mcias/server.crt"
# REQUIRED. Path to the TLS private key (PEM format).
# Permissions: mode 0640, owner root:mcias.
tls_key = "/etc/mcias/server.key"
# ---------------------------------------------------------------------------
# [database] — SQLite database
# ---------------------------------------------------------------------------
[database]
# REQUIRED. Path to the SQLite database file.
# The directory must be writable by the mcias user. WAL mode is enabled
# automatically; expect three files: mcias.db, mcias.db-wal, mcias.db-shm.
path = "/var/lib/mcias/mcias.db"
# ---------------------------------------------------------------------------
# [tokens] — JWT issuance policy
# ---------------------------------------------------------------------------
[tokens]
# REQUIRED. Issuer claim embedded in every JWT. Relying parties should
# validate this claim matches the expected value.
# Use the base URL of your MCIAS server (without trailing slash).
issuer = "https://auth.example.com"
# OPTIONAL. Default token expiry for interactive (human) logins.
# Go duration string: "h" hours, "m" minutes, "s" seconds.
# Default: 720h (30 days). Reduce for higher-security deployments.
default_expiry = "720h"
# OPTIONAL. Expiry for admin tokens (tokens with the "admin" role).
# Should be shorter than default_expiry to limit the blast radius of
# a leaked admin credential.
# Default: 8h.
admin_expiry = "8h"
# OPTIONAL. Expiry for system account tokens (machine-to-machine).
# System accounts have no interactive login; their tokens are long-lived.
# Default: 8760h (365 days).
service_expiry = "8760h"
# ---------------------------------------------------------------------------
# [argon2] — Password hashing parameters (Argon2id)
# ---------------------------------------------------------------------------
[argon2]
# OWASP 2023 minimums: time >= 2, memory >= 65536 KiB (64 MB).
# Increasing these values improves resistance to brute-force attacks but
# increases CPU and memory usage at login time.
# OPTIONAL. Time cost (number of passes over memory). Default: 3.
time = 3
# OPTIONAL. Memory cost in KiB. Default: 65536 (64 MB).
memory = 65536
# OPTIONAL. Parallelism (number of threads). Default: 4.
threads = 4
# ---------------------------------------------------------------------------
# [master_key] — AES-256 master key derivation
# ---------------------------------------------------------------------------
[master_key]
# REQUIRED. Exactly ONE of passphrase_env or keyfile must be set.
# Option A: Passphrase mode. The passphrase is read from the named environment
# variable at startup, then cleared. The Argon2id KDF salt is stored in the
# database on first run and reused on subsequent runs so the same passphrase
# always produces the same master key.
#
# Set the passphrase in /etc/mcias/env (loaded by the systemd EnvironmentFile
# directive). See dist/mcias.env.example for the template.
passphrase_env = "MCIAS_MASTER_PASSPHRASE"
# Option B: Key file mode. The file must contain exactly 32 bytes of raw key
# material (AES-256). Generate with: openssl rand -out /etc/mcias/master.key 32
# Permissions: mode 0640, owner root:mcias.
#
# Uncomment and comment out passphrase_env to switch modes.
# keyfile = "/etc/mcias/master.key"

17
dist/mcias.env.example vendored Normal file
View File

@@ -0,0 +1,17 @@
# /etc/mcias/env — Environment file for mciassrv (systemd EnvironmentFile).
#
# This file is loaded by the mcias.service unit before the server starts.
# It must be readable only by root and the mcias service account:
#
# chmod 0640 /etc/mcias/env
# chown root:mcias /etc/mcias/env
#
# SECURITY: This file contains the master key passphrase. Treat it with
# the same care as a private key. Do not commit it to version control.
# Back it up to a secure offline location — losing this passphrase means
# losing access to all encrypted data in the database.
# Master key passphrase. Used to derive the AES-256 master key via Argon2id.
# Choose a long, random passphrase (e.g., generated by `openssl rand -base64 32`).
# This must match the passphrase_env setting in mcias.conf.
MCIAS_MASTER_PASSPHRASE=change-me-to-a-long-random-passphrase

51
dist/mcias.service vendored Normal file
View File

@@ -0,0 +1,51 @@
[Unit]
Description=MCIAS Authentication Server
Documentation=man:mciassrv(1)
After=network.target
# Require network to be available before starting.
# Remove if you bind only to loopback.
[Service]
Type=simple
User=mcias
Group=mcias
# Configuration and secrets.
# /etc/mcias/env must contain MCIAS_MASTER_PASSPHRASE=<passphrase>
# See dist/mcias.env.example for the template.
EnvironmentFile=/etc/mcias/env
ExecStart=/usr/local/bin/mciassrv -config /etc/mcias/mcias.conf
Restart=on-failure
RestartSec=5
# File descriptor limit. mciassrv keeps one fd per open connection plus
# the SQLite WAL files; 65536 is generous headroom for a personal server.
LimitNOFILE=65536
# Sandboxing. mcias does not need capabilities; it listens on ports > 1024.
# If you need port 443 or 8443 on a privileged port (< 1024), either:
# a) use a reverse proxy (recommended), or
# b) grant CAP_NET_BIND_SERVICE with: AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=
# Filesystem restrictions.
# mciassrv reads /etc/mcias (config, TLS cert/key) and writes /var/lib/mcias (DB).
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/mcias
# Additional hardening.
NoNewPrivileges=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictNamespaces=true
RestrictRealtime=true
LockPersonality=true
MemoryDenyWriteExecute=true
[Install]
WantedBy=multi-user.target