From 941c71f2d17ba8443d45dab140d44ab8cc34ea14 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Wed, 11 Mar 2026 15:11:36 -0700 Subject: [PATCH] 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 --- .gitignore | 14 +- Dockerfile | 99 ++++++++++++ Makefile | 151 ++++++++++++++++++ PROGRESS.md | 58 +++++-- README.md | 277 +++++++++++++++++++++++++++++---- dist/install.sh | 225 ++++++++++++++++++++++++++ dist/mcias-dev.conf.example | 42 +++++ dist/mcias.conf.docker.example | 48 ++++++ dist/mcias.conf.example | 110 +++++++++++++ dist/mcias.env.example | 17 ++ dist/mcias.service | 51 ++++++ man/man1/mciasctl.1 | 124 +++++++++++++++ man/man1/mciasdb.1 | 203 ++++++++++++++++++++++++ man/man1/mciasgrpcctl.1 | 122 +++++++++++++++ man/man1/mciassrv.1 | 228 +++++++++++++++++++++++++++ 15 files changed, 1715 insertions(+), 54 deletions(-) create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 dist/install.sh create mode 100644 dist/mcias-dev.conf.example create mode 100644 dist/mcias.conf.docker.example create mode 100644 dist/mcias.conf.example create mode 100644 dist/mcias.env.example create mode 100644 dist/mcias.service create mode 100644 man/man1/mciasctl.1 create mode 100644 man/man1/mciasdb.1 create mode 100644 man/man1/mciasgrpcctl.1 create mode 100644 man/man1/mciassrv.1 diff --git a/.gitignore b/.gitignore index 5b2f179..4be2bbb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,32 +1,24 @@ -# Build output (root-level binaries only) /mciassrv /mciasctl /mciasdb /mciasgrpcctl +/bin/ *.exe - -# Database files *.db *.db-wal *.db-shm - -# Test artifacts *.out *.test coverage.html coverage.txt - -# Config files with secrets (keep example configs) mcias.toml - -# Editor artifacts .DS_Store .idea/ .vscode/ *.swp *.swo *~ - -# Go workspace files go.work go.work.sum +dist/mcias_*.tar.gz +man/man1/*.gz diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..eb279b9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,99 @@ +# Dockerfile — MCIAS multi-stage container image +# +# Stage 1 (builder): Compiles all four MCIAS binaries. +# Stage 2 (runtime): Minimal Debian image containing only the binaries. +# +# 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 +# - Does NOT contain the Go toolchain, source code, or build cache +# +# Build: +# docker build -t mcias:$(git describe --tags --always) . +# +# Run: +# docker run -d \ +# --name mcias \ +# -v /path/to/config:/etc/mcias:ro \ +# -v mcias-data:/data \ +# -e MCIAS_MASTER_PASSPHRASE=your-passphrase \ +# -p 8443:8443 \ +# -p 9443:9443 \ +# mcias:latest + +# --------------------------------------------------------------------------- +# Stage 1 — builder +# --------------------------------------------------------------------------- +FROM golang:1.26-bookworm AS builder + +WORKDIR /build + +# Download dependencies first for layer caching. +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source. +COPY . . + +# CGO_ENABLED=1 is required by modernc.org/sqlite (pure-Go CGo-free SQLite). +# -trimpath removes local file system paths from the binary. +# -ldflags="-s -w" strips the DWARF debug info and symbol table to reduce +# image size. +RUN CGO_ENABLED=1 go build -trimpath -ldflags="-s -w" -o /out/mciassrv ./cmd/mciassrv && \ + CGO_ENABLED=1 go build -trimpath -ldflags="-s -w" -o /out/mciasctl ./cmd/mciasctl && \ + CGO_ENABLED=1 go build -trimpath -ldflags="-s -w" -o /out/mciasdb ./cmd/mciasdb && \ + CGO_ENABLED=1 go build -trimpath -ldflags="-s -w" -o /out/mciasgrpcctl ./cmd/mciasgrpcctl + +# --------------------------------------------------------------------------- +# Stage 2 — runtime +# --------------------------------------------------------------------------- +FROM debian:bookworm-slim + +# Install runtime dependencies. +# ca-certificates: required to validate external TLS certificates. +# libc6: required by CGo-compiled binaries (sqlite). +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + ca-certificates \ + libc6 && \ + rm -rf /var/lib/apt/lists/* + +# Create a non-root user for the service. +# uid/gid 10001 is chosen to be well above the range typically assigned to +# system users (1–999) and human users (1000+), reducing the chance of +# collision with existing uids on the host when using host networking. +RUN groupadd --gid 10001 mcias && \ + useradd --uid 10001 --gid 10001 --no-create-home --shell /usr/sbin/nologin mcias + +# Copy compiled binaries from the builder stage. +COPY --from=builder /out/mciassrv /usr/local/bin/mciassrv +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 + +# Declare /data as a volume so the operator must explicitly mount it. +# The SQLite database must persist across container restarts. +VOLUME /data + +# 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. +EXPOSE 8443 +EXPOSE 9443 + +# Run as the non-root mcias user. +USER mcias + +# Default entry point and config path. +# The operator mounts /etc/mcias/mcias.conf from the host or a volume. +# See dist/mcias.conf.docker.example for a suitable template. +ENTRYPOINT ["mciassrv"] +CMD ["-config", "/etc/mcias/mcias.conf"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..860195b --- /dev/null +++ b/Makefile @@ -0,0 +1,151 @@ +# Makefile — MCIAS build, test, lint, and release targets +# +# Usage: +# make build — compile all binaries to bin/ +# make test — run tests with race detector +# make lint — run golangci-lint +# make generate — regenerate protobuf stubs (requires protoc) +# make man — build compressed man pages +# make install — run dist/install.sh (requires root) +# make clean — remove bin/ and generated artifacts +# make dist — build release tarballs for linux/amd64 and linux/arm64 +# make docker — build Docker image tagged mcias:$(VERSION) + +# --------------------------------------------------------------------------- +# Variables +# --------------------------------------------------------------------------- +MODULE := git.wntrmute.dev/kyle/mcias +BINARIES := mciassrv mciasctl mciasdb mciasgrpcctl +BIN_DIR := bin +MAN_DIR := man/man1 +MAN_PAGES := $(MAN_DIR)/mciassrv.1 $(MAN_DIR)/mciasctl.1 \ + $(MAN_DIR)/mciasdb.1 $(MAN_DIR)/mciasgrpcctl.1 + +# Version: prefer git describe; fall back to "dev" when git is unavailable +# or the tree has no tags. +VERSION := $(shell git describe --tags --always 2>/dev/null || echo dev) + +# Build flags: trim paths from binaries and strip DWARF/symbol table. +# CGO_ENABLED=1 is required for modernc.org/sqlite. +GO := go +GOFLAGS := -trimpath +LDFLAGS := -s -w -X main.version=$(VERSION) +CGO := CGO_ENABLED=1 + +# Platforms for cross-compiled dist tarballs. +DIST_PLATFORMS := linux/amd64 linux/arm64 + +# --------------------------------------------------------------------------- +# Default target +# --------------------------------------------------------------------------- +.PHONY: all +all: build + +# --------------------------------------------------------------------------- +# build — compile all binaries to bin/ +# --------------------------------------------------------------------------- +.PHONY: build +build: + @mkdir -p $(BIN_DIR) + @for bin in $(BINARIES); do \ + echo " GO BUILD cmd/$$bin"; \ + $(CGO) $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" \ + -o $(BIN_DIR)/$$bin ./cmd/$$bin; \ + done + +# --------------------------------------------------------------------------- +# test — run all tests with race detector +# --------------------------------------------------------------------------- +.PHONY: test +test: + $(CGO) $(GO) test -race ./... + +# --------------------------------------------------------------------------- +# lint — run golangci-lint +# --------------------------------------------------------------------------- +.PHONY: lint +lint: + golangci-lint run ./... + +# --------------------------------------------------------------------------- +# generate — regenerate protobuf stubs from proto/ definitions +# Requires: protoc, protoc-gen-go, protoc-gen-go-grpc +# --------------------------------------------------------------------------- +.PHONY: generate +generate: + $(GO) generate ./... + +# --------------------------------------------------------------------------- +# man — build compressed man pages +# --------------------------------------------------------------------------- +.PHONY: man +man: $(patsubst %.1,%.1.gz,$(MAN_PAGES)) + +%.1.gz: %.1 + gzip -kf $< + +# --------------------------------------------------------------------------- +# install — run the installation script (requires root) +# --------------------------------------------------------------------------- +.PHONY: install +install: build + sh dist/install.sh + +# --------------------------------------------------------------------------- +# clean — remove build artifacts +# --------------------------------------------------------------------------- +.PHONY: clean +clean: + rm -rf $(BIN_DIR) + rm -f $(patsubst %.1,%.1.gz,$(MAN_PAGES)) + +# --------------------------------------------------------------------------- +# dist — cross-compiled release tarballs for linux/amd64 and linux/arm64 +# +# Output files: dist/mcias___.tar.gz +# Each tarball contains: mciassrv, mciasctl, mciasdb, mciasgrpcctl, +# man pages, and dist/ files. +# --------------------------------------------------------------------------- +.PHONY: dist +dist: man + @for platform in $(DIST_PLATFORMS); do \ + os=$$(echo $$platform | cut -d/ -f1); \ + arch=$$(echo $$platform | cut -d/ -f2); \ + outdir=dist/mcias_$$(echo $(VERSION) | tr -d 'v')_$${os}_$${arch}; \ + echo " DIST $$platform -> $$outdir.tar.gz"; \ + mkdir -p $$outdir/bin; \ + for bin in $(BINARIES); do \ + CGO_ENABLED=1 GOOS=$$os GOARCH=$$arch $(GO) build \ + $(GOFLAGS) -ldflags "$(LDFLAGS)" \ + -o $$outdir/bin/$$bin ./cmd/$$bin; \ + done; \ + cp -r man $$outdir/; \ + cp dist/mcias.conf.example dist/mcias-dev.conf.example \ + dist/mcias.env.example dist/mcias.service \ + dist/install.sh $$outdir/; \ + tar -czf $$outdir.tar.gz -C dist mcias_$$(echo $(VERSION) | tr -d 'v')_$${os}_$${arch}; \ + rm -rf $$outdir; \ + done + +# --------------------------------------------------------------------------- +# docker — build the Docker image +# --------------------------------------------------------------------------- +.PHONY: docker +docker: + docker build -t mcias:$(VERSION) . + +# --------------------------------------------------------------------------- +# Help +# --------------------------------------------------------------------------- +.PHONY: help +help: + @echo "Available targets:" + @echo " build Compile all binaries to bin/" + @echo " test Run tests with race detector" + @echo " lint Run golangci-lint" + @echo " generate Regenerate protobuf stubs" + @echo " man Build compressed man pages" + @echo " install Install to /usr/local/bin (requires root)" + @echo " clean Remove build artifacts" + @echo " dist Build release tarballs for Linux amd64/arm64" + @echo " docker Build Docker image mcias:$(VERSION)" diff --git a/PROGRESS.md b/PROGRESS.md index ac1b2de..12843ed 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -1,16 +1,10 @@ # MCIAS Progress Source of truth for current development state. - --- - -## Current Status: Phase 7 Complete — Phases 8–9 Planned - -137 tests pass with zero race conditions. Phase 7 (gRPC dual-stack) is -complete. Phases 8–9 are designed and documented; implementation not yet started. - -### Completed Phases - +137 tests pass with zero race conditions. Phase 8 (operational artifacts) is +complete. Phase 9 (client libraries) is designed and documented; implementation +not yet started. - [x] Phase 0: Repository bootstrap (go.mod, .gitignore, docs) - [x] Phase 1: Foundational packages (model, config, crypto, db) - [x] Phase 2: Auth core (auth, token, middleware) @@ -19,16 +13,46 @@ complete. Phases 8–9 are designed and documented; implementation not yet start - [x] Phase 5: E2E tests, security hardening, commit - [x] Phase 6: mciasdb — direct SQLite maintenance tool - [x] Phase 7: gRPC interface (alternate transport; dual-stack with REST) - -### Planned Phases - -- [ ] Phase 8: Operational artifacts (systemd unit, man pages, Makefile, install script) +- [x] Phase 8: Operational artifacts (Makefile, Dockerfile, systemd, man pages, install script) - [ ] Phase 9: Client libraries (Go, Rust, Common Lisp, Python) - --- - -## Implementation Log - +**Makefile** +- Targets: build, test, lint, generate, man, install, clean, dist, docker +- build: compiles all four binaries to bin/ with CGO_ENABLED=1 and + -trimpath -ldflags="-s -w" +- dist: cross-compiled tarballs for linux/amd64 and linux/arm64 +- docker: builds image tagged mcias:$(git describe --tags --always) +- VERSION derived from git describe --tags --always +**Dockerfile** (multi-stage) +- Build stage: golang:1.26-bookworm with CGO_ENABLED=1 +- Runtime stage: debian:bookworm-slim with only ca-certificates and libc6; + no Go toolchain, no source, no build cache in final image +- Non-root user mcias (uid/gid 10001) +- EXPOSE 8443 (REST/TLS) and EXPOSE 9443 (gRPC/TLS) +- VOLUME /data for the SQLite database mount point +- ENTRYPOINT ["mciassrv"] CMD ["-config", "/etc/mcias/mcias.conf"] +**dist/ artifacts** +- dist/mcias.service: hardened systemd unit with ProtectSystem=strict, + ProtectHome=true, PrivateTmp=true, NoNewPrivileges=true, + CapabilityBoundingSet= (no capabilities), ReadWritePaths=/var/lib/mcias, + EnvironmentFile=/etc/mcias/env, Restart=on-failure, LimitNOFILE=65536 +- dist/mcias.env.example: passphrase env file template +- dist/mcias.conf.example: fully-commented production TOML config reference +- dist/mcias-dev.conf.example: local dev config (127.0.0.1, short expiry) +- dist/mcias.conf.docker.example: container config template +- dist/install.sh: idempotent POSIX sh installer; creates user/group, + installs binaries, creates /etc/mcias and /var/lib/mcias, installs + systemd unit and man pages; existing configs not overwritten (placed .new) +**man/ pages** (mdoc format) +- man/man1/mciassrv.1: synopsis, options, config, REST API, signals, files +- man/man1/mciasctl.1: all subcommands, env vars, examples +- man/man1/mciasdb.1: trust model warnings, all subcommands, examples +- man/man1/mciasgrpcctl.1: gRPC subcommands, grpcurl examples +**Documentation** +- README.md: replaced dev-workflow notes with user-facing docs; quick-start, + first-run setup, build instructions, CLI references, Docker deployment, + man page index, security notes +- .gitignore: added /bin/, dist/mcias_*.tar.gz, man/man1/*.gz ### 2026-03-11 — Phase 7: gRPC dual-stack **proto/mcias/v1/** diff --git a/README.md b/README.md index 911630e..55d3d43 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,257 @@ -The project notes are in PROJECT.md. +# MCIAS — Metacircular Identity and Access System -Before starting the project, read the project description and develop a -system architecture document (ARCHITECTURE.md). It should describe the -technical design of the system. Then, PROJECT_PLAN.md should be written -that describes how to build this system in discrete steps. +MCIAS is a self-hosted SSO and IAM service for personal projects. +It provides authentication (JWT/Ed25519), account management, TOTP, and +Postgres credential storage over a REST API (HTTPS) and a gRPC API (TLS). -Commit these files to Git after human review. For this project, all git -commits should have a single first line, no more than 55 characters, -that succinctly describes an overview of the changes. As necessary, the -body of the commit message should contain bullets describing the work -that was done. +See [ARCHITECTURE.md](ARCHITECTURE.md) for the technical design and +[PROJECT_PLAN.md](PROJECT_PLAN.md) for the implementation roadmap. -Then, create PROGRESS.md. This should be initialized with the first -steps for the project. +--- -The development process for this should generally be: +## Quick Start -1. Determine the current state of the project. Track the progress in - PROGRESS.md, explicitly stating what was done and next steps. -2. Develop reasonable, discrete next steps to move forward with - actionable acceptance criteria. -3. Complete the next steps. - + You should adversarially check whether any outputs are correct. - + All code units should be thoroughly unit tested. - + All subsystems should be thoroughly integration tested. - + Where appropriate, end-to-end tests to validate the system should - be developed. - + All code changes must pass golangci-lint checks. -4. After each phase, checkpoint your work, committing it to git. +### Option A: Install from source -Repeat this cycle until the system is in the desired end state. +**Prerequisites:** Go 1.26+, a C compiler (required by modernc.org/sqlite). + +```sh +git clone https://git.wntrmute.dev/kyle/mcias +cd mcias +make build # produces bin/mciassrv, other binaries +sudo make install +``` + +### Option B: Docker + +```sh +docker build -t mcias:latest . +# or +make docker # tags mcias: +``` + +See [Deploying with Docker](#deploying-with-docker) below. + +--- + +## First-Run Setup + +### 1. Generate a TLS certificate + +```sh +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. Configure the server + +```sh +cp dist/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 +``` + +Minimum required fields: + +```toml +[server] +listen_addr = "0.0.0.0:8443" +tls_cert = "/etc/mcias/server.crt" +tls_key = "/etc/mcias/server.key" + +[database] +path = "/var/lib/mcias/mcias.db" + +[tokens] +issuer = "https://auth.example.com" + +[master_key] +passphrase_env = "MCIAS_MASTER_PASSPHRASE" +``` + +See `dist/mcias.conf.example` for the fully-commented reference configuration. +For local development, use `dist/mcias-dev.conf.example`. + +### 3. Set the master key passphrase + +```sh +cp dist/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. Create the first admin account + +```sh +export MCIAS_MASTER_PASSPHRASE=your-passphrase + +mciasdb --config /etc/mcias/mcias.conf account create \ + --username admin --type human +mciasdb --config /etc/mcias/mcias.conf account set-password --id +mciasdb --config /etc/mcias/mcias.conf role grant --id --role admin +``` + +### 5. Start the server + +```sh +# systemd +systemctl enable --now mcias + +# manual +MCIAS_MASTER_PASSPHRASE=your-passphrase mciassrv -config /etc/mcias/mcias.conf +``` + +### 6. Verify + +```sh +curl -k https://localhost:8443/v1/health +# {"status":"ok"} +``` + +--- + +## Building + +```sh +make build # compile all binaries to bin/ +make test # go test -race ./... +make lint # golangci-lint run ./... +make generate # regenerate protobuf stubs (requires protoc) +make man # build compressed man pages +make clean # remove bin/ and generated artifacts +make dist # cross-compiled tarballs for linux/amd64 and linux/arm64 +make docker # build Docker image mcias: +``` + +--- + +## Admin CLI (mciasctl) + +```sh +TOKEN=$(curl -sk https://localhost:8443/v1/auth/login \ + -d '{"username":"admin","password":"..."}' | jq -r .token) +export MCIAS_TOKEN=$token + +mciasctl -server https://localhost:8443 account list +mciasctl account create -username alice -password s3cr3t +mciasctl role set -id $UUID -roles admin +mciasctl token issue -id $SYSTEM_UUID +mciasctl pgcreds set -id $UUID -host db.example.com -port 5432 \ + -db mydb -user myuser -password mypassword +``` + +See `man mciasctl` for the full reference. + +--- + +## Database Maintenance Tool (mciasdb) + +```sh +export MCIAS_MASTER_PASSPHRASE=your-passphrase +CONF<½--config /etc/mcias/mcias.conf + +mciasdb $CONF schema verify +mciasdb $CONF account list +mciasdb $CONF account set-password --id $UUID +mciasdb $CONF role grant --id $UUID --role admin +mciasdb $CONF prune tokens +mciasdb $CONF audit tail --n 50 +mciasdb $CONF audit query --account $UUID --json +``` + +See `man mciasdb` for the full reference, including the trust model and +safety warnings. + +--- + +## gRPC Interface + +Enable the gRPC listener in config: + +```toml +[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" +``` + +Using mciasgrpcctl: + +```sh +export MCIAS_TOKEN=$ADMIN_JWT +mciasgrpcctl -server auth.example.com:9443 -cacert /etc/mcias/server.crt health +mciasgrpcctl account list +``` + +Using grpcurl: + +```sh +grpcurl -cacert /etc/mcias/server.crt \ + -H "authorization: Bearer $ADMIN_JWT" \ + auth.example.com:9443 \ + mcias.v1.AdminService/Health +``` + +See `man mciasgrpcctl` and [ARCHITECTURE.md](ARCHITECTURE.md) §17. + +--- + +## Deploying with Docker + +```sh +make docker + +mkdir -p /srv/mcias/config +cp dist/mcias.conf.docker.example /srv/mcias/config/mcias.conf +$EDITOR /srv/mcias/config/mcias.conf + +docker run -d \ + --name mcias \ + -v /srv/mcias/config:/etc/mcias:ro \ + -v mcias-data:/data \ + -e MCIAS_MASTER_PASSPHRASE=your-passphrase \ + -p 8443:8443 \ + -p 9443:9443 \ + mcias:latest + +curl -k https://localhost:8443/v1/health +``` + +The container runs as uid 10001 (mcias) with no capabilities. +TLS termination happens inside the container. +See `dist/mcias.conf.docker.example` for the config template. + +--- + +## Man Pages + +```sh +man mciassrv # server reference +man mciasctl # admin CLI (REST) +man mciasdb # database maintenance tool +man mciasgrpcctl # gRPC admin CLI +``` + +--- + +## Security Notes + +- JWTs signed with Ed25519 (EdDSA); `alg` header validated before processing + to defeat algorithm confusion attacks. +- Passwords hashed with Argon2id at OWASP 2023 minimum parameters. +- All secret material encrypted at rest with AES-256-GCM using the master key. +- Credential fields never appear in any API response. +- TLS 1.2 minimum protocol version. + +See [ARCHITECTURE.md](ARCHITECTURE.md) §2-³3 for the full security model. diff --git a/dist/install.sh b/dist/install.sh new file mode 100644 index 0000000..fac6873 --- /dev/null +++ b/dist/install.sh @@ -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 < + + And grant the admin role: + MCIAS_MASTER_PASSPHRASE=... mciasdb --config /etc/mcias/mcias.conf \\ + role grant --id --role admin + +For full documentation, see: man mciassrv +========================================================================== +EOF diff --git a/dist/mcias-dev.conf.example b/dist/mcias-dev.conf.example new file mode 100644 index 0000000..e077cba --- /dev/null +++ b/dist/mcias-dev.conf.example @@ -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" diff --git a/dist/mcias.conf.docker.example b/dist/mcias.conf.docker.example new file mode 100644 index 0000000..fc862c3 --- /dev/null +++ b/dist/mcias.conf.docker.example @@ -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" diff --git a/dist/mcias.conf.example b/dist/mcias.conf.example new file mode 100644 index 0000000..cca3ded --- /dev/null +++ b/dist/mcias.conf.example @@ -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" diff --git a/dist/mcias.env.example b/dist/mcias.env.example new file mode 100644 index 0000000..1c1f394 --- /dev/null +++ b/dist/mcias.env.example @@ -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 diff --git a/dist/mcias.service b/dist/mcias.service new file mode 100644 index 0000000..ea45d54 --- /dev/null +++ b/dist/mcias.service @@ -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= +# 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 diff --git a/man/man1/mciasctl.1 b/man/man1/mciasctl.1 new file mode 100644 index 0000000..859c6e7 --- /dev/null +++ b/man/man1/mciasctl.1 @@ -0,0 +1,124 @@ +.Dd March 11, 2026 +.Dt MCIASCTL 1 +.Os +.Sh NAME +.Nm mciasctl +.Nd MCIAS admin CLI (REST) +.Sh SYNOPSIS +.Nm +.Op Fl server Ar url +.Op Fl token Ar jwt +.Op Fl cacert Ar path +.Ar command +.Op Ar subcommand +.Op Ar flags +.Sh DESCRIPTION +.Nm +is the administrator command-line interface for MCIAS. +It connects to a running +.Xr mciassrv 1 +instance via the REST API over HTTPS and provides subcommands for managing +accounts, roles, tokens, and Postgres credentials. +.Pp +Authentication is performed using a bearer JWT. +The token must have the +.Qq admin +role for most operations. +Pass the token with the +.Fl token +flag or by setting the +.Ev MCIAS_TOKEN +environment variable. +.Sh OPTIONS +.Bl -tag -width Ds +.It Fl server Ar url +Base URL of the mciassrv instance. +Default: +.Qq https://localhost:8443 . +Can also be set with the +.Ev MCIAS_SERVER +environment variable. +.It Fl token Ar jwt +Bearer token for authentication. +Can also be set with the +.Ev MCIAS_TOKEN +environment variable. +.It Fl cacert Ar path +Path to a CA certificate in PEM format for TLS verification. +Useful when mciassrv uses a self-signed certificate. +If omitted, the system certificate pool is used. +.El +.Sh COMMANDS +.Ss account +.Bl -tag -width Ds +.It Nm Ic account Ic list +Lists all accounts. +Credential fields are never included in the output. +.It Nm Ic account Ic create Fl username Ar name Fl password Ar pass Op Fl type Ar human|system +Creates a new account. +.Fl type +defaults to +.Qq human . +.It Nm Ic account Ic get Fl id Ar uuid +Returns the account with the given UUID. +.It Nm Ic account Ic update Fl id Ar uuid Op Fl status Ar active|inactive +Updates account fields. +Currently only status can be updated. +.It Nm Ic account Ic delete Fl id Ar uuid +Soft-deletes the account and revokes all its tokens. +.El +.Ss role +.Bl -tag -width Ds +.It Nm Ic role Ic list Fl id Ar uuid +Lists the roles assigned to the account. +.It Nm Ic role Ic set Fl id Ar uuid Fl roles Ar role1,role2,... +Replaces the role set for the account with the comma-separated list. +.El +.Ss token +.Bl -tag -width Ds +.It Nm Ic token Ic issue Fl id Ar uuid +Issues a new service token for a system account. +.It Nm Ic token Ic revoke Fl jti Ar jti +Revokes the token with the given JTI. +.El +.Ss pgcreds +.Bl -tag -width Ds +.It Nm Ic pgcreds Ic set Fl id Ar uuid Fl host Ar host Fl port Ar port Fl db Ar db Fl user Ar user Fl password Ar pass +Sets Postgres credentials for the account. +The credentials are encrypted with AES-256-GCM using the server master key. +.It Nm Ic pgcreds Ic get Fl id Ar uuid +Retrieves and prints the Postgres credentials. +The password is included in plaintext; treat the output as sensitive. +.El +.Sh ENVIRONMENT +.Bl -tag -width Ds +.It Ev MCIAS_TOKEN +Bearer token used for authentication when +.Fl token +is not specified. +.It Ev MCIAS_SERVER +Base URL of the mciassrv instance when +.Fl server +is not specified. +.El +.Sh EXAMPLES +List all accounts: +.Bd -literal -offset indent +mciasctl -server https://auth.example.com -token $ADMIN_TOKEN account list +.Ed +.Pp +Create a human account: +.Bd -literal -offset indent +mciasctl account create -username alice -password s3cr3t +.Ed +.Pp +Grant the admin role: +.Bd -literal -offset indent +mciasctl role set -id $UUID -roles admin +.Ed +.Sh EXIT STATUS +.Ex -std +.Sh SEE ALSO +.Xr mciassrv 1 , +.Xr mciasdb 1 , +.Xr mciasgrpcctl 1 diff --git a/man/man1/mciasdb.1 b/man/man1/mciasdb.1 new file mode 100644 index 0000000..8f47276 --- /dev/null +++ b/man/man1/mciasdb.1 @@ -0,0 +1,203 @@ +.Dd March 11, 2026 +.Dt MCIASDB 1 +.Os +.Sh NAME +.Nm mciasdb +.Nd MCIAS database maintenance tool +.Sh SYNOPSIS +.Nm +.Op Fl config Ar path +.Ar command +.Op Ar subcommand +.Op Ar flags +.Sh DESCRIPTION +.Nm +is the MCIAS database maintenance tool. +It operates +.Em directly +on the SQLite database file, bypassing the mciassrv REST API. +.Pp +Use +.Nm +for: +.Bl -bullet -compact +.It +Break-glass recovery when the server is unavailable. +.It +Offline account and role management before first server start. +.It +Schema verification and migration. +.It +Token pruning and audit log inspection. +.El +.Pp +.Nm +requires the same master key configuration as +.Xr mciassrv 1 +to decrypt secrets at rest. +.Sh TRUST MODEL AND SAFETY WARNINGS +.Nm +has +.Em direct write access +to the database and can modify any record without going through the server's +validation logic. +Use it only when necessary. +.Bl -bullet +.It +Run +.Nm +on the same host as the database file. +Never copy the database to an untrusted host. +.It +The master key passphrase (or keyfile) must be available. +Set the environment variable named by +.Sy passphrase_env +in the config, or ensure the keyfile is accessible. +.It +All write operations append an audit log entry tagged +.Qq actor:mciasdb . +.It +Avoid running +.Nm +while mciassrv is under heavy write load; SQLite WAL mode provides some +isolation, but schema migrations acquire an exclusive lock. +.El +.Sh OPTIONS +.Bl -tag -width Ds +.It Fl config Ar path +Path to the TOML configuration file. +Defaults to +.Pa mcias.toml +in the current directory. +.El +.Sh COMMANDS +.Ss schema +.Bl -tag -width Ds +.It Nm Ic schema Ic verify +Reports the current schema version. +Exits 0 if the schema is up to date; exits 1 if migrations are pending. +.It Nm Ic schema Ic migrate +Applies any pending migrations and reports each one applied. +Exits 0 on success. +.El +.Ss account +.Bl -tag -width Ds +.It Nm Ic account Ic list +Prints UUID, username, type, and status for all accounts. +.It Nm Ic account Ic get Fl -id Ar uuid +Prints the account record for the given UUID. +.It Nm Ic account Ic create Fl -username Ar name Fl -type Ar human|system +Inserts a new account row and prints the new UUID. +.It Nm Ic account Ic set-password Fl -id Ar uuid +Prompts for a new password twice (confirmation). +Re-hashes with Argon2id and updates the row. +No +.Fl -password +flag is provided; the password is always read interactively. +.It Nm Ic account Ic set-status Fl -id Ar uuid Fl -status Ar STATUS +Updates the account status. +Valid statuses: +.Qq active , +.Qq inactive , +.Qq deleted . +.It Nm Ic account Ic reset-totp Fl -id Ar uuid +Clears the TOTP secret and disables TOTP for the account. +The user will be able to log in with password only until they re-enrol. +.El +.Ss role +.Bl -tag -width Ds +.It Nm Ic role Ic list Fl -id Ar uuid +Prints the roles assigned to the account. +.It Nm Ic role Ic grant Fl -id Ar uuid Fl -role Ar role +Adds a role to the account. +.It Nm Ic role Ic revoke Fl -id Ar uuid Fl -role Ar role +Removes a role from the account. +.El +.Ss token +.Bl -tag -width Ds +.It Nm Ic token Ic list Fl -id Ar uuid +Prints JTI, issued_at, expires_at, and revoked_at for all tokens associated +with the account. +.It Nm Ic token Ic revoke Fl -jti Ar jti +Sets revoked_at to the current time on the specified token row. +.It Nm Ic token Ic revoke-all Fl -id Ar uuid +Revokes all non-revoked tokens for the account. +A no-op (exits 0) if no active tokens exist. +.El +.Ss prune +.Bl -tag -width Ds +.It Nm Ic prune Ic tokens +Deletes rows from the token_revocation table where expires_at is in the past. +Prints the number of rows removed. +.El +.Ss audit +.Bl -tag -width Ds +.It Nm Ic audit Ic tail Op Fl -n Ar N +Prints the last N audit events, newest last. +Default: 50. +.It Nm Ic audit Ic query Op Fl -account Ar uuid Op Fl -type Ar event_type Op Fl -since Ar timestamp Op Fl -json +Queries the audit log. +Filters are combinable (AND semantics). +.Fl -since +accepts an RFC 3339 timestamp. +.Fl -json +emits newline-delimited JSON instead of tabular output. +.El +.Ss pgcreds +.Bl -tag -width Ds +.It Nm Ic pgcreds Ic get Fl -id Ar uuid +Decrypts and prints the Postgres credentials for the account. +A warning header is printed before the output because the password is +displayed in plaintext. +.It Nm Ic pgcreds Ic set Fl -id Ar uuid Fl -host Ar host Fl -port Ar port Fl -db Ar db Fl -user Ar user +Prompts for the Postgres password interactively (no +.Fl -password +flag). +Encrypts with AES-256-GCM using the master key and stores the record. +.El +.Sh EXAMPLES +Verify schema after upgrade: +.Bd -literal -offset indent +MCIAS_MASTER_PASSPHRASE=... mciasdb --config /etc/mcias/mcias.conf schema verify +.Ed +.Pp +Create an admin account before first server start: +.Bd -literal -offset indent +MCIAS_MASTER_PASSPHRASE=... mciasdb --config /etc/mcias/mcias.conf \\ + account create --username admin --type human +MCIAS_MASTER_PASSPHRASE=... mciasdb --config /etc/mcias/mcias.conf \\ + account set-password --id +MCIAS_MASTER_PASSPHRASE=... mciasdb --config /etc/mcias/mcias.conf \\ + role grant --id --role admin +.Ed +.Pp +Tail the last 20 audit events: +.Bd -literal -offset indent +MCIAS_MASTER_PASSPHRASE=... mciasdb --config /etc/mcias/mcias.conf \\ + audit tail --n 20 +.Ed +.Sh ENVIRONMENT +The environment variable named by +.Sy passphrase_env +in the configuration file must be set to the master key passphrase, unless +.Sy keyfile +is used instead. +.Pp +Example (with default config): +.Bd -literal -offset indent +export MCIAS_MASTER_PASSPHRASE=your-passphrase +mciasdb --config /etc/mcias/mcias.conf account list +.Ed +.Sh EXIT STATUS +.Ex -std +.Sh FILES +.Bl -tag -width Ds +.It Pa /etc/mcias/mcias.conf +Default configuration file location. +.It Pa /var/lib/mcias/mcias.db +SQLite database. +.El +.Sh SEE ALSO +.Xr mciassrv 1 , +.Xr mciasctl 1 , +.Xr mciasgrpcctl 1 diff --git a/man/man1/mciasgrpcctl.1 b/man/man1/mciasgrpcctl.1 new file mode 100644 index 0000000..bb46618 --- /dev/null +++ b/man/man1/mciasgrpcctl.1 @@ -0,0 +1,122 @@ +.Dd March 11, 2026 +.Dt MCIASGRPCCTL 1 +.Os +.Sh NAME +.Nm mciasgrpcctl +.Nd MCIAS gRPC admin CLI +.Sh SYNOPSIS +.Nm +.Op Fl server Ar addr +.Op Fl token Ar jwt +.Op Fl cacert Ar path +.Ar command +.Op Ar subcommand +.Op Ar flags +.Sh DESCRIPTION +.Nm +is the gRPC companion to +.Xr mciasctl 1 . +It connects to the gRPC/TLS listener of a running +.Xr mciassrv 1 +instance and provides subcommands mirroring the REST admin CLI. +.Pp +The gRPC listener must be enabled in the mciassrv configuration +.Pq Sy grpc_addr +for +.Nm +to connect. +.Pp +Authentication is performed using a bearer JWT passed as gRPC metadata. +The token must have the +.Qq admin +role for most operations. +.Sh OPTIONS +.Bl -tag -width Ds +.It Fl server Ar addr +gRPC server address in +.Ar host:port +format. +Default: +.Qq localhost:9443 . +.It Fl token Ar jwt +Bearer token for authentication. +Can also be set with the +.Ev MCIAS_TOKEN +environment variable. +.It Fl cacert Ar path +Path to a CA certificate in PEM format for TLS verification. +Useful when mciassrv uses a self-signed certificate. +.El +.Sh COMMANDS +.Ss Informational (no authentication required) +.Bl -tag -width Ds +.It Nm Ic health +Calls the Health RPC. +Prints +.Qq ok +and exits 0 if the server is healthy. +.It Nm Ic pubkey +Returns the server's Ed25519 public key as a JWK. +.El +.Ss account +.Bl -tag -width Ds +.It Nm Ic account Ic list +Lists all accounts. +.It Nm Ic account Ic create Fl username Ar name Fl password Ar pass Op Fl type Ar human|system +Creates a new account. +.It Nm Ic account Ic get Fl id Ar uuid +Returns the account with the given UUID. +.It Nm Ic account Ic update Fl id Ar uuid Fl status Ar active|inactive +Updates account status. +.It Nm Ic account Ic delete Fl id Ar uuid +Soft-deletes the account and revokes all its tokens. +.El +.Ss role +.Bl -tag -width Ds +.It Nm Ic role Ic list Fl id Ar uuid +Lists roles for the account. +.It Nm Ic role Ic set Fl id Ar uuid Fl roles Ar role1,role2,... +Replaces the role set for the account. +.El +.Ss token +.Bl -tag -width Ds +.It Nm Ic token Ic validate Fl token Ar jwt +Validates the given token and prints its claims. +.It Nm Ic token Ic issue Fl id Ar uuid +Issues a new service token for a system account. +.It Nm Ic token Ic revoke Fl jti Ar jti +Revokes the token with the given JTI. +.El +.Ss pgcreds +.Bl -tag -width Ds +.It Nm Ic pgcreds Ic get Fl id Ar uuid +Returns the Postgres credentials for the account. +.It Nm Ic pgcreds Ic set Fl id Ar uuid Fl host Ar host Op Fl port Ar port Fl db Ar db Fl user Ar user Fl password Ar pass +Sets Postgres credentials for the account. +.El +.Sh ENVIRONMENT +.Bl -tag -width Ds +.It Ev MCIAS_TOKEN +Bearer token used for authentication when +.Fl token +is not specified. +.El +.Sh EXAMPLES +Check server health over gRPC: +.Bd -literal -offset indent +mciasgrpcctl -server auth.example.com:9443 -cacert /etc/mcias/ca.crt health +.Ed +.Pp +Using grpcurl as an alternative client: +.Bd -literal -offset indent +grpcurl -cacert /etc/mcias/ca.crt \\ + -H "authorization: Bearer $TOKEN" \\ + auth.example.com:9443 \\ + mcias.v1.AdminService/Health +.Ed +.Sh EXIT STATUS +.Ex -std +.Sh SEE ALSO +.Xr mciassrv 1 , +.Xr mciasctl 1 , +.Xr mciasdb 1 diff --git a/man/man1/mciassrv.1 b/man/man1/mciassrv.1 new file mode 100644 index 0000000..1cd31c8 --- /dev/null +++ b/man/man1/mciassrv.1 @@ -0,0 +1,228 @@ +.Dd March 11, 2026 +.Dt MCIASSRV 1 +.Os +.Sh NAME +.Nm mciassrv +.Nd MCIAS authentication server +.Sh SYNOPSIS +.Nm +.Fl config +.Ar path +.Sh DESCRIPTION +.Nm +is the MCIAS (Metacircular Identity and Access System) authentication server. +It provides a single-sign-on (SSO) and identity and access management (IAM) +service for personal applications. +.Pp +.Nm +reads a TOML configuration file, derives the AES-256 master encryption key, +loads or generates an Ed25519 signing key, opens the SQLite database, applies +any pending schema migrations, and starts an HTTPS listener. +.Pp +If the +.Sy grpc_addr +field is set in the configuration file, a gRPC/TLS listener is also started +on the specified address. +Both listeners share the same signing key, database connection, and +configuration. +.Sh OPTIONS +.Bl -tag -width Ds +.It Fl config Ar path +Path to the TOML configuration file. +Defaults to +.Pa mcias.toml +in the current directory. +.El +.Sh CONFIGURATION +The configuration file is in TOML format. +See +.Pa /etc/mcias/mcias.conf +(or +.Pa dist/mcias.conf.example +in the source tree) for a fully-commented reference configuration. +.Pp +The following sections are recognised: +.Bl -tag -width Ds +.It Sy [server] +.Bl -tag -width Ds +.It Sy listen_addr +.Pq required +Host and port for the HTTPS REST listener. +Example: +.Qq 0.0.0.0:8443 . +.It Sy grpc_addr +.Pq optional +Host and port for the gRPC/TLS listener. +If omitted, gRPC is disabled. +Example: +.Qq 0.0.0.0:9443 . +.It Sy tls_cert +.Pq required +Path to the TLS certificate file in PEM format. +.It Sy tls_key +.Pq required +Path to the TLS private key file in PEM format. +.El +.It Sy [database] +.Bl -tag -width Ds +.It Sy path +.Pq required +Path to the SQLite database file. +WAL mode and foreign key enforcement are enabled automatically. +.El +.It Sy [tokens] +.Bl -tag -width Ds +.It Sy issuer +.Pq required +Issuer claim embedded in every JWT. +Use the base URL of your MCIAS server. +.It Sy default_expiry +.Pq optional, default 720h +Token expiry for interactive logins. +Go duration string. +.It Sy admin_expiry +.Pq optional, default 8h +Token expiry for tokens with the +.Qq admin +role. +.It Sy service_expiry +.Pq optional, default 8760h +Token expiry for system account tokens. +.El +.It Sy [argon2] +.Bl -tag -width Ds +.It Sy time +.Pq optional, default 3 +Argon2id time cost. +Must be >= 2 (OWASP 2023 minimum). +.It Sy memory +.Pq optional, default 65536 +Argon2id memory cost in KiB. +Must be >= 65536 (64 MB, OWASP 2023 minimum). +.It Sy threads +.Pq optional, default 4 +Argon2id parallelism. +.El +.It Sy [master_key] +Exactly one of the following must be set: +.Bl -tag -width Ds +.It Sy passphrase_env +Name of the environment variable containing the master key passphrase. +The passphrase is read at startup and the environment variable is immediately +cleared. +.It Sy keyfile +Path to a file containing exactly 32 bytes of raw AES-256 key material. +.El +.El +.Sh REST API +.Nm +exposes the following REST endpoints over HTTPS: +.Bl -tag -width Ds +.It GET /v1/health +Returns +.Qq {"status":"ok"} +with HTTP 200. +No authentication required. +.It GET /v1/keys/public +Returns the Ed25519 public key as a JWK. +No authentication required. +.It POST /v1/auth/login +Authenticates a user and issues a JWT. +Body: JSON with +.Sy username , +.Sy password , +and optionally +.Sy totp_code . +Returns +.Sy token +and +.Sy expires_at . +.It POST /v1/auth/logout +Revokes the current JWT. +Requires a valid Bearer token. +Returns HTTP 204. +.It POST /v1/auth/renew +Renews the current JWT, revoking the old one. +Requires a valid Bearer token. +Returns +.Sy token +and +.Sy expires_at . +.It POST /v1/token/validate +Validates a submitted token and returns its claims. +.It DELETE /v1/token/{jti} +Revokes a token by JTI. +Requires admin role. +.It POST /v1/accounts +Creates a new account. +Requires admin role. +.It GET /v1/accounts +Lists all accounts (no credential fields in response). +Requires admin role. +.It GET /v1/accounts/{id} +Returns a single account. +Requires admin role. +.It PATCH /v1/accounts/{id} +Updates an account. +Requires admin role. +.It DELETE /v1/accounts/{id} +Soft-deletes an account and revokes all its tokens. +Requires admin role. +.It GET /v1/accounts/{id}/roles +Returns the role set for an account. +Requires admin role. +.It PUT /v1/accounts/{id}/roles +Replaces the role set for an account. +Requires admin role. +.It POST /v1/auth/totp/enroll +Generates a TOTP secret and returns the otpauth URI. +Requires authentication. +.It POST /v1/auth/totp/confirm +Confirms TOTP enrollment. +Requires authentication. +.It DELETE /v1/auth/totp +Removes TOTP from an account. +Requires admin role. +.It GET /v1/accounts/{id}/pgcreds +Returns the Postgres credentials for an account (decrypted). +Requires admin role. +.It PUT /v1/accounts/{id}/pgcreds +Sets Postgres credentials for an account. +Requires admin role. +.El +.Sh SIGNALS +.Bl -tag -width Ds +.It Dv SIGINT , SIGTERM +Initiate graceful shutdown. +In-flight requests are drained for up to 15 seconds before the server exits. +.El +.Sh EXIT STATUS +.Ex -std +.Sh FILES +.Bl -tag -width Ds +.It Pa /etc/mcias/mcias.conf +Default configuration file location. +.It Pa /etc/mcias/server.crt +TLS certificate (operator-supplied). +.It Pa /etc/mcias/server.key +TLS private key (operator-supplied). +.It Pa /var/lib/mcias/mcias.db +SQLite database. +.It Pa /etc/systemd/system/mcias.service +systemd service unit. +.El +.Sh SEE ALSO +.Xr mciasctl 1 , +.Xr mciasdb 1 , +.Xr mciasgrpcctl 1 +.Sh SECURITY +.Nm +enforces TLS 1.2 as the minimum protocol version. +All JWTs are signed with Ed25519; the +.Sy alg +header is validated before any other processing to defeat algorithm confusion +attacks. +Password hashing uses Argon2id with parameters meeting or exceeding the +OWASP 2023 recommendations. +Credential fields (password hashes, TOTP secrets, Postgres passwords) are +never included in any API response.