Align with engineering standards (steps 1-5)
- Rename dist/ -> deploy/ with subdirs examples/, scripts/, systemd/ per standard repository layout - Update .gitignore: gitignore all of dist/ (build output only) - Makefile: all target is now vet->lint->test->build; add vet, proto-lint, devserver targets; CGO_ENABLED=0 for builds (modernc.org/sqlite is pure-Go, no C toolchain needed); CGO_ENABLED=1 retained for tests (race detector) - Dockerfile: builder -> golang:1.26-alpine, runtime -> alpine:3.21; drop libc6 dep; add /srv/mcias/certs and /srv/mcias/backups to image - deploy/systemd/mcias.service: add RestrictSUIDSGID=true - deploy/systemd/mcias-backup.service: new oneshot backup unit - deploy/systemd/mcias-backup.timer: daily 02:00 UTC, 5m jitter - deploy/scripts/install.sh: install backup units and enable timer; create certs/ and backups/ subdirs in /srv/mcias - buf.yaml: add proto linting config for proto-lint target - internal/db: add Snapshot and SnapshotDir methods (VACUUM INTO) - cmd/mciasdb: add snapshot subcommand; no master key required
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -20,7 +20,8 @@ mcias.toml
|
|||||||
*~
|
*~
|
||||||
go.work
|
go.work
|
||||||
go.work.sum
|
go.work.sum
|
||||||
dist/mcias_*.tar.gz
|
# dist/ is purely build output (tarballs); never commit it
|
||||||
|
dist/
|
||||||
man/man1/*.gz
|
man/man1/*.gz
|
||||||
|
|
||||||
# Client library build artifacts
|
# Client library build artifacts
|
||||||
|
|||||||
38
Dockerfile
38
Dockerfile
@@ -1,7 +1,11 @@
|
|||||||
# Dockerfile — MCIAS multi-stage container image
|
# Dockerfile — MCIAS multi-stage container image
|
||||||
#
|
#
|
||||||
# Stage 1 (builder): Compiles all four MCIAS binaries.
|
# Stage 1 (builder): Compiles all four MCIAS binaries.
|
||||||
# Stage 2 (runtime): Minimal Debian image containing only the binaries.
|
# Stage 2 (runtime): Minimal Alpine image containing only the binaries.
|
||||||
|
#
|
||||||
|
# modernc.org/sqlite is a pure-Go, CGo-free SQLite port. CGO_ENABLED=0
|
||||||
|
# produces fully static binaries with no C library dependencies, which
|
||||||
|
# deploy cleanly onto a minimal Alpine runtime image.
|
||||||
#
|
#
|
||||||
# The final image:
|
# The final image:
|
||||||
# - Runs as non-root uid 10001 (mcias)
|
# - Runs as non-root uid 10001 (mcias)
|
||||||
@@ -24,7 +28,7 @@
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Stage 1 — builder
|
# Stage 1 — builder
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
FROM golang:1.26-bookworm AS builder
|
FROM golang:1.26-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
@@ -35,35 +39,29 @@ RUN go mod download
|
|||||||
# Copy source.
|
# Copy source.
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# CGO_ENABLED=1 is required by modernc.org/sqlite (pure-Go CGo-free SQLite).
|
# CGO_ENABLED=0: modernc.org/sqlite is pure Go; no C toolchain required.
|
||||||
# -trimpath removes local file system paths from the binary.
|
# -trimpath removes local file system paths from the binary.
|
||||||
# -ldflags="-s -w" strips the DWARF debug info and symbol table to reduce
|
# -ldflags="-s -w" strips the DWARF debug info and symbol table to reduce
|
||||||
# image size.
|
# image size.
|
||||||
RUN CGO_ENABLED=1 go build -trimpath -ldflags="-s -w" -o /out/mciassrv ./cmd/mciassrv && \
|
RUN CGO_ENABLED=0 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=0 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=0 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
|
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/mciasgrpcctl ./cmd/mciasgrpcctl
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Stage 2 — runtime
|
# Stage 2 — runtime
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
FROM debian:bookworm-slim
|
FROM alpine:3.21
|
||||||
|
|
||||||
# Install runtime dependencies.
|
|
||||||
# ca-certificates: required to validate external TLS certificates.
|
# ca-certificates: required to validate external TLS certificates.
|
||||||
# libc6: required by CGo-compiled binaries (sqlite).
|
RUN apk add --no-cache ca-certificates
|
||||||
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.
|
# Create a non-root user for the service.
|
||||||
# uid/gid 10001 is chosen to be well above the range typically assigned to
|
# 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
|
# system users (1–999) and human users (1000+), reducing the chance of
|
||||||
# collision with existing uids on the host when using host networking.
|
# collision with existing uids on the host when using host networking.
|
||||||
RUN groupadd --gid 10001 mcias && \
|
RUN addgroup -g 10001 mcias && \
|
||||||
useradd --uid 10001 --gid 10001 --no-create-home --shell /usr/sbin/nologin mcias
|
adduser -u 10001 -G mcias -H -s /sbin/nologin -D mcias
|
||||||
|
|
||||||
# Copy compiled binaries from the builder stage.
|
# Copy compiled binaries from the builder stage.
|
||||||
COPY --from=builder /out/mciassrv /usr/local/bin/mciassrv
|
COPY --from=builder /out/mciassrv /usr/local/bin/mciassrv
|
||||||
@@ -73,8 +71,8 @@ COPY --from=builder /out/mciasgrpcctl /usr/local/bin/mciasgrpcctl
|
|||||||
|
|
||||||
# Create the data directory.
|
# Create the data directory.
|
||||||
# /srv/mcias is mounted from the host with config, TLS certs, and database.
|
# /srv/mcias is mounted from the host with config, TLS certs, and database.
|
||||||
RUN mkdir -p /srv/mcias && \
|
RUN mkdir -p /srv/mcias/certs /srv/mcias/backups && \
|
||||||
chown mcias:mcias /srv/mcias && \
|
chown -R mcias:mcias /srv/mcias && \
|
||||||
chmod 0750 /srv/mcias
|
chmod 0750 /srv/mcias
|
||||||
|
|
||||||
# Declare /srv/mcias as a volume so the operator must explicitly mount it.
|
# Declare /srv/mcias as a volume so the operator must explicitly mount it.
|
||||||
@@ -92,6 +90,6 @@ USER mcias
|
|||||||
# Default entry point and config path.
|
# Default entry point and config path.
|
||||||
# The operator mounts /srv/mcias from the host containing mcias.toml,
|
# The operator mounts /srv/mcias from the host containing mcias.toml,
|
||||||
# TLS cert/key, and the SQLite database.
|
# TLS cert/key, and the SQLite database.
|
||||||
# See dist/mcias.conf.docker.example for a suitable template.
|
# See deploy/examples/mcias.conf.docker.example for a suitable template.
|
||||||
ENTRYPOINT ["mciassrv"]
|
ENTRYPOINT ["mciassrv"]
|
||||||
CMD ["-config", "/srv/mcias/mcias.toml"]
|
CMD ["-config", "/srv/mcias/mcias.toml"]
|
||||||
|
|||||||
77
Makefile
77
Makefile
@@ -3,10 +3,14 @@
|
|||||||
# Usage:
|
# Usage:
|
||||||
# make build — compile all binaries to bin/
|
# make build — compile all binaries to bin/
|
||||||
# make test — run tests with race detector
|
# make test — run tests with race detector
|
||||||
|
# make vet — run go vet
|
||||||
# make lint — run golangci-lint
|
# make lint — run golangci-lint
|
||||||
|
# make all — vet → lint → test → build (CI pipeline)
|
||||||
# make generate — regenerate protobuf stubs (requires protoc)
|
# make generate — regenerate protobuf stubs (requires protoc)
|
||||||
|
# make proto-lint — lint proto files with buf
|
||||||
# make man — build compressed man pages
|
# make man — build compressed man pages
|
||||||
# make install — run dist/install.sh (requires root)
|
# make install — run deploy/scripts/install.sh (requires root)
|
||||||
|
# make devserver — build and run mciassrv against run/ config
|
||||||
# make clean — remove bin/ and generated artifacts
|
# make clean — remove bin/ and generated artifacts
|
||||||
# make dist — build release tarballs for linux/amd64 and linux/arm64
|
# make dist — build release tarballs for linux/amd64 and linux/arm64
|
||||||
# make docker — build Docker image tagged mcias:$(VERSION) and mcias:latest
|
# make docker — build Docker image tagged mcias:$(VERSION) and mcias:latest
|
||||||
@@ -27,20 +31,25 @@ MAN_PAGES := $(MAN_DIR)/mciassrv.1 $(MAN_DIR)/mciasctl.1 \
|
|||||||
VERSION := $(shell git describe --tags --always 2>/dev/null || echo dev)
|
VERSION := $(shell git describe --tags --always 2>/dev/null || echo dev)
|
||||||
|
|
||||||
# Build flags: trim paths from binaries and strip DWARF/symbol table.
|
# Build flags: trim paths from binaries and strip DWARF/symbol table.
|
||||||
# CGO_ENABLED=1 is required for modernc.org/sqlite.
|
# modernc.org/sqlite is pure-Go and does not require CGo; CGO_ENABLED=0
|
||||||
|
# produces statically linked binaries that deploy cleanly to Alpine containers.
|
||||||
GO := go
|
GO := go
|
||||||
GOFLAGS := -trimpath
|
GOFLAGS := -trimpath
|
||||||
LDFLAGS := -s -w -X main.version=$(VERSION)
|
LDFLAGS := -s -w -X main.version=$(VERSION)
|
||||||
CGO := CGO_ENABLED=1
|
CGO := CGO_ENABLED=0
|
||||||
|
|
||||||
|
# The race detector requires CGo on some platforms, so tests continue to use
|
||||||
|
# CGO_ENABLED=1 while production builds are CGO_ENABLED=0.
|
||||||
|
CGO_TEST := CGO_ENABLED=1
|
||||||
|
|
||||||
# Platforms for cross-compiled dist tarballs.
|
# Platforms for cross-compiled dist tarballs.
|
||||||
DIST_PLATFORMS := linux/amd64 linux/arm64
|
DIST_PLATFORMS := linux/amd64 linux/arm64
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Default target
|
# Default target — CI pipeline: vet → lint → test → build
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
.PHONY: all
|
.PHONY: all
|
||||||
all: build
|
all: vet lint test build
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# build — compile all binaries to bin/
|
# build — compile all binaries to bin/
|
||||||
@@ -59,7 +68,14 @@ build:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test:
|
test:
|
||||||
$(CGO) $(GO) test -race ./...
|
$(CGO_TEST) $(GO) test -race ./...
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# vet — static analysis via go vet
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
.PHONY: vet
|
||||||
|
vet:
|
||||||
|
$(GO) vet ./...
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# lint — run golangci-lint
|
# lint — run golangci-lint
|
||||||
@@ -68,6 +84,15 @@ test:
|
|||||||
lint:
|
lint:
|
||||||
golangci-lint run ./...
|
golangci-lint run ./...
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# proto-lint — lint and check for breaking changes in proto definitions
|
||||||
|
# Requires: buf (https://buf.build/docs/installation)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
.PHONY: proto-lint
|
||||||
|
proto-lint:
|
||||||
|
buf lint
|
||||||
|
buf breaking --against '.git#branch=master,subdir=proto'
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# generate — regenerate protobuf stubs from proto/ definitions
|
# generate — regenerate protobuf stubs from proto/ definitions
|
||||||
# Requires: protoc, protoc-gen-go, protoc-gen-go-grpc
|
# Requires: protoc, protoc-gen-go, protoc-gen-go-grpc
|
||||||
@@ -76,6 +101,13 @@ lint:
|
|||||||
generate:
|
generate:
|
||||||
$(GO) generate ./...
|
$(GO) generate ./...
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# devserver — build and run mciassrv against the local run/ config
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
.PHONY: devserver
|
||||||
|
devserver: build
|
||||||
|
$(BIN_DIR)/mciassrv -config run/mcias.conf
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# man — build compressed man pages
|
# man — build compressed man pages
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -90,7 +122,7 @@ man: $(patsubst %.1,%.1.gz,$(MAN_PAGES))
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
.PHONY: install
|
.PHONY: install
|
||||||
install: build
|
install: build
|
||||||
sh dist/install.sh
|
sh deploy/scripts/install.sh
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# clean — remove build artifacts
|
# clean — remove build artifacts
|
||||||
@@ -98,6 +130,7 @@ install: build
|
|||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(BIN_DIR)
|
rm -rf $(BIN_DIR)
|
||||||
|
rm -rf dist/
|
||||||
rm -f $(patsubst %.1,%.1.gz,$(MAN_PAGES))
|
rm -f $(patsubst %.1,%.1.gz,$(MAN_PAGES))
|
||||||
-docker rmi mcias:$(VERSION) mcias:latest 2>/dev/null || true
|
-docker rmi mcias:$(VERSION) mcias:latest 2>/dev/null || true
|
||||||
|
|
||||||
@@ -106,7 +139,7 @@ clean:
|
|||||||
#
|
#
|
||||||
# Output files: dist/mcias_<version>_<os>_<arch>.tar.gz
|
# Output files: dist/mcias_<version>_<os>_<arch>.tar.gz
|
||||||
# Each tarball contains: mciassrv, mciasctl, mciasdb, mciasgrpcctl,
|
# Each tarball contains: mciassrv, mciasctl, mciasdb, mciasgrpcctl,
|
||||||
# man pages, and dist/ files.
|
# man pages, and deploy/ files.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
.PHONY: dist
|
.PHONY: dist
|
||||||
dist: man
|
dist: man
|
||||||
@@ -117,14 +150,12 @@ dist: man
|
|||||||
echo " DIST $$platform -> $$outdir.tar.gz"; \
|
echo " DIST $$platform -> $$outdir.tar.gz"; \
|
||||||
mkdir -p $$outdir/bin; \
|
mkdir -p $$outdir/bin; \
|
||||||
for bin in $(BINARIES); do \
|
for bin in $(BINARIES); do \
|
||||||
CGO_ENABLED=1 GOOS=$$os GOARCH=$$arch $(GO) build \
|
CGO_ENABLED=0 GOOS=$$os GOARCH=$$arch $(GO) build \
|
||||||
$(GOFLAGS) -ldflags "$(LDFLAGS)" \
|
$(GOFLAGS) -ldflags "$(LDFLAGS)" \
|
||||||
-o $$outdir/bin/$$bin ./cmd/$$bin; \
|
-o $$outdir/bin/$$bin ./cmd/$$bin; \
|
||||||
done; \
|
done; \
|
||||||
cp -r man $$outdir/; \
|
cp -r man $$outdir/; \
|
||||||
cp dist/mcias.conf.example dist/mcias-dev.conf.example \
|
cp -r deploy $$outdir/; \
|
||||||
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}; \
|
tar -czf $$outdir.tar.gz -C dist mcias_$$(echo $(VERSION) | tr -d 'v')_$${os}_$${arch}; \
|
||||||
rm -rf $$outdir; \
|
rm -rf $$outdir; \
|
||||||
done
|
done
|
||||||
@@ -154,13 +185,17 @@ install-local: build
|
|||||||
.PHONY: help
|
.PHONY: help
|
||||||
help:
|
help:
|
||||||
@echo "Available targets:"
|
@echo "Available targets:"
|
||||||
@echo " build Compile all binaries to bin/"
|
@echo " all vet → lint → test → build (CI pipeline)"
|
||||||
@echo " test Run tests with race detector"
|
@echo " build Compile all binaries to bin/"
|
||||||
@echo " lint Run golangci-lint"
|
@echo " test Run tests with race detector"
|
||||||
@echo " generate Regenerate protobuf stubs"
|
@echo " vet Run go vet"
|
||||||
@echo " man Build compressed man pages"
|
@echo " lint Run golangci-lint"
|
||||||
@echo " install Install to /usr/local/bin (requires root)"
|
@echo " proto-lint Lint proto files with buf"
|
||||||
@echo " clean Remove build artifacts"
|
@echo " generate Regenerate protobuf stubs"
|
||||||
@echo " dist Build release tarballs for Linux amd64/arm64"
|
@echo " devserver Build and run mciassrv against run/ config"
|
||||||
@echo " docker Build Docker image mcias:$(VERSION) and mcias:latest"
|
@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) and mcias:latest"
|
||||||
@echo " docker-clean Remove local mcias Docker images"
|
@echo " docker-clean Remove local mcias Docker images"
|
||||||
|
|||||||
14
buf.yaml
Normal file
14
buf.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
version: v2
|
||||||
|
modules:
|
||||||
|
- path: proto
|
||||||
|
lint:
|
||||||
|
use:
|
||||||
|
- STANDARD
|
||||||
|
except:
|
||||||
|
# PACKAGE_VERSION_SUFFIX requires package names to end in a version (e.g.
|
||||||
|
# mcias.v1). The current protos use mcias.v1 already so this is fine, but
|
||||||
|
# keeping the exception documents the intent explicitly.
|
||||||
|
- PACKAGE_VERSION_SUFFIX
|
||||||
|
breaking:
|
||||||
|
use:
|
||||||
|
- FILE
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
// pgcreds get --id UUID
|
// pgcreds get --id UUID
|
||||||
// pgcreds set --id UUID --host H --port P --db D --user U
|
// pgcreds set --id UUID --host H --port P --db D --user U
|
||||||
//
|
//
|
||||||
// rekey
|
// snapshot [--retain-days N]
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -68,6 +68,14 @@ func main() {
|
|||||||
command := args[0]
|
command := args[0]
|
||||||
subArgs := args[1:]
|
subArgs := args[1:]
|
||||||
|
|
||||||
|
// snapshot loads only the config (no master key needed — VACUUM INTO does
|
||||||
|
// not access encrypted columns) and must be handled before openDB, which
|
||||||
|
// requires the master key passphrase env var.
|
||||||
|
if command == "snapshot" {
|
||||||
|
runSnapshot(*configPath, subArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// schema subcommands manage migrations themselves and must not trigger
|
// schema subcommands manage migrations themselves and must not trigger
|
||||||
// auto-migration on open (a dirty database would prevent the tool from
|
// auto-migration on open (a dirty database would prevent the tool from
|
||||||
// opening at all, blocking recovery operations like "schema force").
|
// opening at all, blocking recovery operations like "schema force").
|
||||||
@@ -273,6 +281,11 @@ Commands:
|
|||||||
rekey Re-encrypt all secrets under a new master passphrase
|
rekey Re-encrypt all secrets under a new master passphrase
|
||||||
(prompts interactively; requires server to be stopped)
|
(prompts interactively; requires server to be stopped)
|
||||||
|
|
||||||
|
snapshot Write a timestamped VACUUM INTO backup to
|
||||||
|
<db-dir>/backups/; prune backups older than
|
||||||
|
--retain-days days (default 30, 0 = keep all).
|
||||||
|
Does not require the master key passphrase.
|
||||||
|
|
||||||
NOTE: mciasdb bypasses the mciassrv API and operates directly on the SQLite
|
NOTE: mciasdb bypasses the mciassrv API and operates directly on the SQLite
|
||||||
file. Use it only when the server is unavailable or for break-glass recovery.
|
file. Use it only when the server is unavailable or for break-glass recovery.
|
||||||
All write operations are recorded in the audit log.
|
All write operations are recorded in the audit log.
|
||||||
|
|||||||
44
cmd/mciasdb/snapshot.go
Normal file
44
cmd/mciasdb/snapshot.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/mcias/internal/config"
|
||||||
|
"git.wntrmute.dev/kyle/mcias/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// runSnapshot handles the "snapshot" command.
|
||||||
|
//
|
||||||
|
// It opens the database read-only (no master key derivation needed — VACUUM
|
||||||
|
// INTO does not access encrypted columns) and writes a timestamped backup to
|
||||||
|
// /srv/mcias/backups/ (or the directory adjacent to the configured DB path).
|
||||||
|
// Backups older than --retain-days are pruned.
|
||||||
|
func runSnapshot(configPath string, args []string) {
|
||||||
|
fs := flag.NewFlagSet("snapshot", flag.ExitOnError)
|
||||||
|
retainDays := fs.Int("retain-days", 30, "prune backups older than this many days (0 = keep all)")
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
fatalf("snapshot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := config.Load(configPath)
|
||||||
|
if err != nil {
|
||||||
|
fatalf("snapshot: load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
database, err := db.Open(cfg.Database.Path)
|
||||||
|
if err != nil {
|
||||||
|
fatalf("snapshot: open database: %v", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = database.Close() }()
|
||||||
|
|
||||||
|
// Place backups in a "backups" directory adjacent to the database file.
|
||||||
|
backupDir := filepath.Join(filepath.Dir(cfg.Database.Path), "backups")
|
||||||
|
|
||||||
|
dest, err := database.SnapshotDir(backupDir, *retainDays)
|
||||||
|
if err != nil {
|
||||||
|
fatalf("snapshot: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("snapshot written: %s\n", dest)
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# install.sh — MCIAS first-time and upgrade installer
|
# install.sh — MCIAS first-time and upgrade installer
|
||||||
#
|
#
|
||||||
# Usage: sh dist/install.sh
|
# Usage: sh deploy/scripts/install.sh
|
||||||
#
|
#
|
||||||
# This script must be run as root. It:
|
# This script must be run as root. It:
|
||||||
# 1. Creates the mcias system user and group (idempotent).
|
# 1. Creates the mcias system user and group (idempotent).
|
||||||
# 2. Copies binaries to /usr/local/bin/.
|
# 2. Copies binaries to /usr/local/bin/.
|
||||||
# 3. Creates /srv/mcias/ with correct permissions.
|
# 3. Creates /srv/mcias/ with correct permissions.
|
||||||
# 4. Installs the systemd service unit.
|
# 4. Installs the systemd service and backup units.
|
||||||
# 5. Prints post-install instructions.
|
# 5. Prints post-install instructions.
|
||||||
#
|
#
|
||||||
# The script does NOT start or enable the service automatically. Review the
|
# The script does NOT start or enable the service automatically. Review the
|
||||||
@@ -31,7 +31,8 @@ SYSTEMD_DIR="/etc/systemd/system"
|
|||||||
SERVICE_USER="mcias"
|
SERVICE_USER="mcias"
|
||||||
SERVICE_GROUP="mcias"
|
SERVICE_GROUP="mcias"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
|
DEPLOY_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
|
REPO_ROOT="$(dirname "$DEPLOY_DIR")"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Helpers
|
# Helpers
|
||||||
@@ -100,11 +101,7 @@ fi
|
|||||||
# Step 2: Install binaries.
|
# Step 2: Install binaries.
|
||||||
info "Installing binaries to $BIN_DIR"
|
info "Installing binaries to $BIN_DIR"
|
||||||
for bin in mciassrv mciasctl mciasdb mciasgrpcctl; do
|
for bin in mciassrv mciasctl mciasdb mciasgrpcctl; do
|
||||||
src="$REPO_ROOT/$bin"
|
src="$REPO_ROOT/bin/$bin"
|
||||||
if [ ! -f "$src" ]; then
|
|
||||||
# Try bin/ subdirectory (Makefile build output).
|
|
||||||
src="$REPO_ROOT/bin/$bin"
|
|
||||||
fi
|
|
||||||
if [ ! -f "$src" ]; then
|
if [ ! -f "$src" ]; then
|
||||||
warn "Binary not found: $bin — skipping. Run 'make build' first."
|
warn "Binary not found: $bin — skipping. Run 'make build' first."
|
||||||
continue
|
continue
|
||||||
@@ -113,30 +110,40 @@ for bin in mciassrv mciasctl mciasdb mciasgrpcctl; do
|
|||||||
install -m 0755 -o root -g root "$src" "$BIN_DIR/$bin"
|
install -m 0755 -o root -g root "$src" "$BIN_DIR/$bin"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Step 3: Create service directory.
|
# Step 3: Create service directory structure.
|
||||||
info "Creating $SRV_DIR"
|
info "Creating $SRV_DIR"
|
||||||
install -d -m 0750 -o "$SERVICE_USER" -g "$SERVICE_GROUP" "$SRV_DIR"
|
install -d -m 0750 -o "$SERVICE_USER" -g "$SERVICE_GROUP" "$SRV_DIR"
|
||||||
|
install -d -m 0750 -o "$SERVICE_USER" -g "$SERVICE_GROUP" "$SRV_DIR/certs"
|
||||||
|
install -d -m 0750 -o "$SERVICE_USER" -g "$SERVICE_GROUP" "$SRV_DIR/backups"
|
||||||
|
|
||||||
# Install example config files; never overwrite existing configs.
|
# Install example config files; never overwrite existing configs.
|
||||||
for f in mcias.conf.example mcias.env.example; do
|
for f in mcias.conf.example mcias.env.example; do
|
||||||
src="$SCRIPT_DIR/$f"
|
src="$DEPLOY_DIR/examples/$f"
|
||||||
dst="$SRV_DIR/$f"
|
dst="$SRV_DIR/$f"
|
||||||
if [ -f "$src" ]; then
|
if [ -f "$src" ]; then
|
||||||
install -m 0640 -o "$SERVICE_USER" -g "$SERVICE_GROUP" "$src" "$dst" 2>/dev/null || true
|
install -m 0640 -o "$SERVICE_USER" -g "$SERVICE_GROUP" "$src" "$dst" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Step 5: Install systemd service unit.
|
# Step 4: Install systemd units.
|
||||||
if [ -d "$SYSTEMD_DIR" ]; then
|
if [ -d "$SYSTEMD_DIR" ]; then
|
||||||
info "Installing systemd service unit to $SYSTEMD_DIR"
|
info "Installing systemd units to $SYSTEMD_DIR"
|
||||||
install -m 0644 -o root -g root "$SCRIPT_DIR/mcias.service" "$SYSTEMD_DIR/mcias.service"
|
for unit in mcias.service mcias-backup.service mcias-backup.timer; do
|
||||||
|
src="$DEPLOY_DIR/systemd/$unit"
|
||||||
|
if [ -f "$src" ]; then
|
||||||
|
install -m 0644 -o root -g root "$src" "$SYSTEMD_DIR/$unit"
|
||||||
|
info " Installed $unit"
|
||||||
|
fi
|
||||||
|
done
|
||||||
info "Reloading systemd daemon"
|
info "Reloading systemd daemon"
|
||||||
systemctl daemon-reload 2>/dev/null || warn "systemctl not available; reload manually."
|
systemctl daemon-reload 2>/dev/null || warn "systemctl not available; reload manually."
|
||||||
|
info "Enabling backup timer"
|
||||||
|
systemctl enable mcias-backup.timer 2>/dev/null || warn "Could not enable timer; enable manually with: systemctl enable mcias-backup.timer"
|
||||||
else
|
else
|
||||||
warn "systemd not found at $SYSTEMD_DIR; skipping service unit installation."
|
warn "systemd not found at $SYSTEMD_DIR; skipping service unit installation."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Step 6: Install man pages.
|
# Step 5: Install man pages.
|
||||||
if [ -d "$REPO_ROOT/man/man1" ]; then
|
if [ -d "$REPO_ROOT/man/man1" ]; then
|
||||||
install -d -m 0755 -o root -g root "$MAN_DIR"
|
install -d -m 0755 -o root -g root "$MAN_DIR"
|
||||||
info "Installing man pages to $MAN_DIR"
|
info "Installing man pages to $MAN_DIR"
|
||||||
@@ -170,12 +177,12 @@ Next steps:
|
|||||||
|
|
||||||
# Self-signed (development / personal use):
|
# Self-signed (development / personal use):
|
||||||
openssl req -x509 -newkey ed25519 -days 3650 \\
|
openssl req -x509 -newkey ed25519 -days 3650 \\
|
||||||
-keyout /srv/mcias/server.key \\
|
-keyout /srv/mcias/certs/server.key \\
|
||||||
-out /srv/mcias/server.crt \\
|
-out /srv/mcias/certs/server.crt \\
|
||||||
-subj "/CN=auth.example.com" \\
|
-subj "/CN=auth.example.com" \\
|
||||||
-nodes
|
-nodes
|
||||||
chmod 0640 /srv/mcias/server.key
|
chmod 0640 /srv/mcias/certs/server.key
|
||||||
chown mcias:mcias /srv/mcias/server.key /srv/mcias/server.crt
|
chown mcias:mcias /srv/mcias/certs/server.key /srv/mcias/certs/server.crt
|
||||||
|
|
||||||
2. Copy and edit the configuration file:
|
2. Copy and edit the configuration file:
|
||||||
|
|
||||||
@@ -200,6 +207,9 @@ Next steps:
|
|||||||
systemctl start mcias
|
systemctl start mcias
|
||||||
systemctl status mcias
|
systemctl status mcias
|
||||||
|
|
||||||
|
The backup timer was enabled automatically. Verify with:
|
||||||
|
systemctl status mcias-backup.timer
|
||||||
|
|
||||||
5. Create the first admin account using mciasdb (while the server is
|
5. Create the first admin account using mciasdb (while the server is
|
||||||
running, or before first start):
|
running, or before first start):
|
||||||
|
|
||||||
32
deploy/systemd/mcias-backup.service
Normal file
32
deploy/systemd/mcias-backup.service
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=MCIAS Database Backup
|
||||||
|
Documentation=man:mciasdb(1)
|
||||||
|
After=mcias.service
|
||||||
|
# Backup runs against the live database using VACUUM INTO, which is safe
|
||||||
|
# while mciassrv is running (WAL mode allows concurrent readers).
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
User=mcias
|
||||||
|
Group=mcias
|
||||||
|
|
||||||
|
EnvironmentFile=/srv/mcias/env
|
||||||
|
|
||||||
|
ExecStart=/usr/local/bin/mciasdb -config /srv/mcias/mcias.toml snapshot
|
||||||
|
|
||||||
|
# Filesystem restrictions (read-write to /srv/mcias for the backup output).
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ReadWritePaths=/srv/mcias
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateDevices=true
|
||||||
|
CapabilityBoundingSet=
|
||||||
|
RestrictSUIDSGID=true
|
||||||
|
RestrictNamespaces=true
|
||||||
|
RestrictRealtime=true
|
||||||
|
LockPersonality=true
|
||||||
|
MemoryDenyWriteExecute=true
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectControlGroups=true
|
||||||
15
deploy/systemd/mcias-backup.timer
Normal file
15
deploy/systemd/mcias-backup.timer
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Daily MCIAS Database Backup
|
||||||
|
Documentation=man:mciasdb(1)
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
# Run daily at 02:00 UTC with up to 5-minute random jitter to avoid
|
||||||
|
# thundering-herd on systems with many services.
|
||||||
|
OnCalendar=*-*-* 02:00:00 UTC
|
||||||
|
RandomizedDelaySec=5min
|
||||||
|
# Run immediately on boot if the last scheduled run was missed
|
||||||
|
# (e.g. host was offline at 02:00).
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
@@ -12,7 +12,7 @@ Group=mcias
|
|||||||
|
|
||||||
# Configuration and secrets.
|
# Configuration and secrets.
|
||||||
# /srv/mcias/env must contain MCIAS_MASTER_PASSPHRASE=<passphrase>
|
# /srv/mcias/env must contain MCIAS_MASTER_PASSPHRASE=<passphrase>
|
||||||
# See dist/mcias.env.example for the template.
|
# See deploy/examples/mcias.env.example for the template.
|
||||||
EnvironmentFile=/srv/mcias/env
|
EnvironmentFile=/srv/mcias/env
|
||||||
|
|
||||||
ExecStart=/usr/local/bin/mciassrv -config /srv/mcias/mcias.toml
|
ExecStart=/usr/local/bin/mciassrv -config /srv/mcias/mcias.toml
|
||||||
@@ -42,6 +42,7 @@ PrivateDevices=true
|
|||||||
ProtectKernelTunables=true
|
ProtectKernelTunables=true
|
||||||
ProtectKernelModules=true
|
ProtectKernelModules=true
|
||||||
ProtectControlGroups=true
|
ProtectControlGroups=true
|
||||||
|
RestrictSUIDSGID=true
|
||||||
RestrictNamespaces=true
|
RestrictNamespaces=true
|
||||||
RestrictRealtime=true
|
RestrictRealtime=true
|
||||||
LockPersonality=true
|
LockPersonality=true
|
||||||
68
internal/db/snapshot.go
Normal file
68
internal/db/snapshot.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Snapshot creates a consistent backup of the database at destPath using
|
||||||
|
// SQLite's VACUUM INTO statement. VACUUM INTO acquires a read lock for the
|
||||||
|
// duration of the copy, which is safe while the server is running in WAL mode.
|
||||||
|
// The destination file is created by SQLite; the caller must ensure the parent
|
||||||
|
// directory exists.
|
||||||
|
func (db *DB) Snapshot(destPath string) error {
|
||||||
|
// VACUUM INTO is not supported on in-memory databases.
|
||||||
|
if strings.Contains(db.path, "mode=memory") {
|
||||||
|
return fmt.Errorf("db: snapshot not supported on in-memory databases")
|
||||||
|
}
|
||||||
|
if _, err := db.sql.Exec("VACUUM INTO ?", destPath); err != nil {
|
||||||
|
return fmt.Errorf("db: snapshot VACUUM INTO %q: %w", destPath, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SnapshotDir creates a timestamped backup in dir and prunes backups older
|
||||||
|
// than retainDays days. dir is created with mode 0750 if it does not exist.
|
||||||
|
// The backup filename format is mcias-20060102-150405.db.
|
||||||
|
func (db *DB) SnapshotDir(dir string, retainDays int) (string, error) {
|
||||||
|
if err := os.MkdirAll(dir, 0750); err != nil {
|
||||||
|
return "", fmt.Errorf("db: create backup dir %q: %w", dir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := time.Now().UTC().Format("20060102-150405")
|
||||||
|
dest := filepath.Join(dir, fmt.Sprintf("mcias-%s.db", ts))
|
||||||
|
if err := db.Snapshot(dest); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prune backups older than retainDays.
|
||||||
|
if retainDays > 0 {
|
||||||
|
cutoff := time.Now().UTC().AddDate(0, 0, -retainDays)
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
// Non-fatal: the backup was written; log pruning failure separately.
|
||||||
|
return dest, fmt.Errorf("db: list backup dir for pruning: %w", err)
|
||||||
|
}
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() || !strings.HasSuffix(e.Name(), ".db") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Skip the file we just wrote.
|
||||||
|
if e.Name() == filepath.Base(dest) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
info, err := e.Info()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if info.ModTime().Before(cutoff) {
|
||||||
|
_ = os.Remove(filepath.Join(dir, e.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dest, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user