Forge
forge is the dedicated personal engineering server. It hosts public forge
services and supporting infrastructure. It is not a general-purpose homelab or
a platform for customer data.
Configuration
Forge uses the stable NixOS package set and is assembled from three layers:
modules/nixos/serverprovides the reusable server baseline;modules/nixos/server/forgeprovides the engineering services; andmachines/forgecontains host identity, hardware, storage and networking.
The server does not inherit workstation Home Manager configuration, desktop theming, impermanence or personal SOPS material. Automatic upgrades are disabled so package-set changes remain explicit.
The baseline provides:
- key-only SSH for the
roelcadministrator account; - a locked root account and disabled root SSH login;
- a default-deny firewall;
- administrative SSH only through WireGuard, with forwarding disabled;
- fail2ban, SMART monitoring, SSD trimming and compressed swap; and
- scheduled Nix garbage collection and store optimisation.
The public interface, enp1s0f0, uses DHCP. The second network interface is
not configured.
Storage
The server boots through legacy BIOS and contains three SATA SSDs. Disko defines:
- a BIOS boot partition on each durable disk, with GRUB installed to both;
- a 1 GiB ext4
/bootmdadm RAID1 array; - an ext4
/mdadm RAID1 array using the remaining durable capacity; and - an independent ext4
/cachefilesystem on the third SSD.
/cache contains reconstructible data and uses nofail; losing it must not
prevent the server from booting. The root and boot arrays contain persistent
state and must remain mirrored.
Disko and nixos-anywhere are destructive. Before reinstalling, compare the
stable device paths in machines/forge/disko.nix with the live
/dev/disk/by-id inventory. The configured kernel compatibility assertion must
also remain satisfied when recreating the mdraid arrays.
Administration Network
WireGuard is the administration boundary:
| Item | Value |
|---|---|
| Server address | 10.77.0.1/24 |
| Workstation peer | 10.77.0.2/32 |
| Public endpoint | vpn.decort.tech:51820 |
| Server private key | /var/lib/wireguard/forge.key |
The VPN DNS record must remain DNS-only in Cloudflare because the standard
proxy does not forward WireGuard. Administrative SSH is available at
roelc@10.77.0.1; the provider KVM is the break-glass path.
The public SSH listener also serves Tangled Git traffic, but AllowUsers
restricts roelc to the WireGuard workstation address and permits only the
Knot's dynamic git account from other networks.
Inspect WireGuard and derive the server's public key with:
sudo wg show wg0
sudo sh -c 'wg pubkey < /var/lib/wireguard/forge.key'
The private key is generated on the host, kept outside the repository and Nix store, copied to the operator's password manager, and included in the encrypted host backup.
Backup and Restore
Restic writes encrypted backups to the 100 GB Scaleway Dedibackup allocation through rclone's FTP backend. The initial backup set contains state that cannot be reconstructed from this repository:
- the WireGuard private key;
- the Nix cache signing key;
- the Radicle node identity key; and
- the SSH host keys; and
- the Tangled Knot database and hosted Git repositories.
Nix store paths, cache contents, logs, metrics and reconstructible Radicle replicas are not included. The Knot is authoritative for repositories assigned to it, so its state is included even though those repositories are public. New stateful services must add their authoritative state explicitly. The backend's 1000-object limit is monitored and constrains future expansion.
SecretSpec resolves the Restic password from the operator's Apple Keychain and provisions it over WireGuard. It is not a runtime dependency of Forge:
secretspec check --profile default --scope forge-backup \
--reason "Provision the forge backup repository password"
secretspec run --profile default --scope forge-backup \
--reason "Provision the forge backup repository password" -- \
provision-forge-backup-secret
The resulting /var/lib/forge-secrets/restic-password file is owned by root
with mode 0400. The backup repository is initialized separately and is never
initialized automatically by the service.
The schedule and retention policy are:
- daily snapshot at 03:00, with up to 30 minutes of randomized delay;
- weekly maintenance on Sunday at 05:30;
- 14 daily, 8 weekly and 6 monthly snapshots; and
- weekly prune followed by a full repository data check.
The state job stops the Knot before Restic reads its SQLite database and bare repositories, then restarts it in the cleanup hook even when the backup fails. This creates one consistent snapshot at the cost of pausing Git hosting for the duration of the nightly state backup.
Inspect or run the jobs with:
ssh roelc@10.77.0.1 systemctl list-timers --all \
restic-backups-forge-state.timer \
restic-backups-forge-maintenance.timer
ssh roelc@10.77.0.1 sudo journalctl \
-u restic-backups-forge-state.service \
-u restic-backups-forge-maintenance.service
ssh roelc@10.77.0.1 sudo systemctl start \
restic-backups-forge-state.service
ssh roelc@10.77.0.1 sudo restic-forge-state snapshots
ssh roelc@10.77.0.1 sudo restic-forge-state check --read-data
A restore test must compare the restored WireGuard identity and SSH host-key fingerprints with the live host. A successful snapshot alone is not sufficient restore evidence.
Monitoring and Alerting
Prometheus retains 30 days of host metrics on the mirrored root filesystem. Prometheus, Alertmanager, the node exporter and the SMART exporter listen only on loopback. Grafana is available to WireGuard peers at:
http://10.77.0.1:3000/d/forge-overview/forge-overview
Grafana provides an anonymous read-only view on the administration network. Its datasource and dashboard are declarative and contain no credentials. Its local database and encryption key remain disposable while that boundary holds.
The alert rules cover:
- exporter and systemd service availability;
- RAID and SMART health;
- root and cache capacity;
- inode exhaustion and OOM kills;
- backup and inventory freshness;
- the Dedibackup object limit; and
- public cache health and signing-key presence;
- Radicle replication health and declared repository policy; and
- Tangled Knot state, listeners, public endpoint and owner identity.
CPU saturation is intentionally not an alert because Nix builds are expected
to use the host fully. Warning and critical capacity thresholds do not overlap.
The rule suite is validated with Prometheus's promtool in CI.
Alertmanager uses a local receiver and has clustering disabled. External alert delivery remains deferred until its authentication and trust boundary is settled. Scaleway's independent server-ping notification covers whole-host and provider-network loss.
Inspect the monitoring stack with:
ssh roelc@10.77.0.1 systemctl status \
prometheus.service \
prometheus-node-exporter.service \
prometheus-smartctl-exporter.service \
alertmanager.service \
grafana.service
ssh roelc@10.77.0.1 curl -fsS http://127.0.0.1:9090/api/v1/alerts
ssh roelc@10.77.0.1 curl -fsS http://127.0.0.1:9090/api/v1/rules
ssh roelc@10.77.0.1 curl -fsS http://127.0.0.1:9093/api/v2/status
Native Nix Cache
cache.decort.tech exposes a signed native Nix binary cache over HTTPS. The
endpoint permits only GET and HEAD; it has no HTTP upload API. Its
Cloudflare record remains DNS-only so the host can terminate TLS and obtain its
ACME certificate.
Cache data lives under /cache/nix and is deliberately excluded from backup.
The public signing key is committed in public-keys.nix. SecretSpec keeps
the private key in the operator's Apple Keychain and provisions it only to:
/var/lib/forge-secrets/nix-cache-private-key
Provision the key over WireGuard with:
secretspec check --profile default --scope forge-cache \
--reason "Provision the forge binary-cache signing key"
secretspec run --profile default --scope forge-cache \
--reason "Provision the forge binary-cache signing key" -- \
provision-forge-cache-key
publish-forge-cache copies an already realized closure to Forge over SSH and
invokes the root-only signing command on the server:
store_path="$(nix build --no-link --print-out-paths .#PACKAGE)"
publish-forge-cache "$store_path"
The nhdsp Zsh alias switches the Darwin configuration and publishes the
activated closure. The existing nhds alias remains the switch-only path.
Clients use the Forge cache as an additional substituter; cache hits remain
platform-specific.
Verify a published closure from a client with:
hello="$(nix build --no-link --print-out-paths nixpkgs#hello)"
publish-forge-cache "$hello"
narinfo="$(basename "$hello" | cut -d- -f1)"
curl -fsSI "https://cache.decort.tech/$narinfo.narinfo"
nix path-info --store https://cache.decort.tech "$hello"
nix store verify --no-contents --recursive "$hello"
GitHub Actions continues to use Cachix. External runners do not receive Forge publication authority. Cache eviction remains an explicit operator action.
Radicle Node
Forge runs a selective Radicle seed node on public TCP port 8776. The node
advertises radicle.decort.tech:8776; the DNS record remains DNS-only because
the Radicle protocol does not traverse the Cloudflare HTTP proxy. The HTTP
gateway and web explorer are not enabled.
The node uses a dedicated identity rather than the workstation maintainer identity:
Node ID: z6Mkv2Vt5s46dasz4m2Ht7rA8nnxDnwXp4Q6enhwt2V1RKMZ
Address: z6Mkv2Vt5s46dasz4m2Ht7rA8nnxDnwXp4Q6enhwt2V1RKMZ@radicle.decort.tech:8776
The public key is committed in public-keys.nix. SecretSpec keeps the
unencrypted private key in the operator's Apple Keychain and provisions it to:
/var/lib/forge-secrets/radicle-node
An unencrypted key is used because the node must restart unattended. The file
is root-owned with mode 0400 and is passed to the confined service as a
systemd credential. Provision it over WireGuard with:
secretspec check --profile default --scope forge-radicle \
--reason "Provision the Forge Radicle node identity"
secretspec run --profile default --scope forge-radicle \
--reason "Provision the Forge Radicle node identity" -- \
provision-forge-radicle-key
The service is skipped when the key is absent instead of entering a restart loop. After provisioning a newly deployed host, start the node and apply the declared repository policy:
ssh roelc@10.77.0.1 sudo systemctl start \
radicle-node.service \
forge-radicle-seed.service \
forge-radicle-metrics.service
The default seeding policy is block. forge-radicle-seed.service grants a
followed policy only to the public repository identifiers declared in the
Radicle module. It currently seeds nixos-config; removing a repository from
that list does not delete its existing replica automatically.
The service has an explicit memory and task budget so public replication cannot
consume the engineering server without bound. Monitoring verifies the node's
administrative interface, each declared repository identity, and its expected
followed policy in addition to process and listener availability.
Inspect the node and its policies with:
ssh roelc@10.77.0.1 sudo rad-system node status
ssh roelc@10.77.0.1 sudo rad-system node config --addresses
ssh roelc@10.77.0.1 sudo rad-system seed
ssh roelc@10.77.0.1 sudo journalctl -u radicle-node.service
From another Radicle node, verify the public path with:
rad node connect \
z6Mkv2Vt5s46dasz4m2Ht7rA8nnxDnwXp4Q6enhwt2V1RKMZ@radicle.decort.tech:8776
rad sync status
The encrypted Forge backup includes the node identity key. Replicated public
repository data under /var/lib/radicle is reconstructible and deliberately
excluded. The configured repository list restores the seeding policy after a
new node starts. A restore must reproduce the same Node ID before the recovered
node is advertised.
To roll back the service, stop the node and remove the module from the Forge composition. Preserve the identity key until the old Node ID and address have been retired deliberately.
Tangled Knot
Forge runs a single-operator Tangled Knot at knot.decort.tech. The Knot hosts
Git data while the public tangled.org AppView and the operator's existing AT
Protocol identity provide discovery and collaboration records. Forge does not
run an AppView, PDS, PLC directory, search service or Spindle in this slice.
The deployment pins Tangled v1.16.1-alpha and uses its upstream NixOS module.
Its network boundaries are:
| Surface | Exposure | Purpose |
|---|---|---|
HTTPS 443 |
Public through Nginx and ACME | Knot HTTP, XRPC and event endpoints |
SSH 22 as git |
Public | Git clone, fetch and push |
SSH 22 as roelc |
WireGuard source 10.77.0.2 only |
Host administration |
HTTP 127.0.0.1:5555 |
Loopback | Nginx upstream |
HTTP 127.0.0.1:5444 |
Loopback | Knot internal API and SSH key lookup |
The Cloudflare record must remain DNS-only. TLS terminates on Forge and Git SSH does not traverse the Cloudflare proxy. Knot ownership is declared as:
did:plc:wrl7x5yocird6ep6472fkm3a
Secure Mode is enabled. Git subprocesses are confined to their repository with Landlock and run under owner-specific virtual UIDs. Forge's kernel is newer than the upstream Linux 5.19 requirement for push-safe Landlock isolation. The service receives only the setuid, setgid and chown capabilities required by that mode and has explicit memory, task and systemd hardening limits. The upstream release emits debug-level logs unconditionally; a per-service burst limit prevents automated public scans from overwhelming journald.
Authoritative state lives under /var/lib/tangled-knot:
knotserver.dbcontains the Knot database and access model;repos/contains the hosted bare repositories; and- the system OpenSSH host key provides continuity for Git clients.
This state is backed up consistently by the daily Forge state job. Restore the directory and SSH host keys before announcing a replacement Knot. The Knot has no additional bootstrap secret in this version; its owner and public service configuration are declarative.
After the service passes its local and public checks, register
knot.decort.tech from the Knot settings page on tangled.org. Registration
publishes the Knot record through the operator's PDS and verifies the public
owner endpoint. Do not register an incomplete or temporary deployment.
Inspect the service with:
ssh roelc@10.77.0.1 systemctl status knot.service
ssh roelc@10.77.0.1 sudo journalctl -u knot.service
curl -fsS https://knot.decort.tech/
curl -fsS https://knot.decort.tech/xrpc/sh.tangled.owner | jq
ssh -T git@knot.decort.tech
Monitoring checks both loopback listeners, the public TLS endpoint, the owner
DID and the on-disk database. The dashboard also reports repository count and
state size. To roll back, remove the Knot module and Nginx virtual host, then
switch the Forge configuration. Preserve /var/lib/tangled-knot and the SSH
host keys until the Knot registration has been retired or moved deliberately.
Build and Deployment
Evaluate the Forge system and Disko derivations from any supported client:
nix eval --raw .#nixosConfigurations.forge.config.system.build.toplevel.drvPath
nix eval --raw .#nixosConfigurations.forge.config.system.build.diskoScript.drvPath
The Apple Silicon workstation delegates the x86-64 build to Forge. Use test
before making a generation persistent:
nix run nixpkgs#nixos-rebuild -- test \
--flake .#forge \
--build-host roelc@10.77.0.1 \
--target-host roelc@10.77.0.1 \
--elevate sudo \
--use-substitutes \
--print-build-logs
After the acceptance checks pass, replace test with switch. CI separately
builds the Forge system, Disko script and Prometheus rule suite.
For disaster recovery, review the live disk identities and backup material
before invoking nixos-anywhere:
nix run .#nixos-anywhere -- \
--flake .#forge \
--build-on-remote \
--target-host roelc@FORGE_ADDRESS
Planned Services
Stateful applications are added as independent, reversible slices:
- OpenBao with identity integration; and
- an optional private Attic evaluation, separate from the native cache.
Each service must define its network exposure, state ownership, backup and restore procedure, monitoring, acceptance checks and rollback path.