From 718a79f8de2a55f2ab83cac7e8595cd3916ed486 Mon Sep 17 00:00:00 2001 From: Karan Jayachandra Date: Sat, 18 Jul 2026 22:32:19 +0200 Subject: Cleaned up the config and the docs --- CLAUDE.md | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 CLAUDE.md (limited to 'CLAUDE.md') diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..35a6923 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,120 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +A flake-based, modular NixOS configuration for a single personal server ("eurovm") running on +Hetzner Cloud in Europe. There is no application code here — this repo *is* the infrastructure: +editing a `.nix` file and deploying is the equivalent of shipping a change. + +## Commands + +There is no local build/test/lint step in the traditional sense — the workflow is evaluate -> deploy +against the live host. Nix itself must be installed locally with flakes enabled (`nix --version`). + +Check the flake evaluates without deploying: +```bash +nix flake check +``` + +Deploy a change (from an x86_64-linux machine): +```bash +nixos-rebuild switch --flake .#eurovm \ + --target-host admin@ \ + --use-remote-sudo +``` + +Deploy from a non-x86_64-linux machine (e.g. Apple Silicon) — the server builds its own closure: +```bash +nixos-rebuild switch --flake .#eurovm \ + --target-host admin@ \ + --build-host admin@ \ + --use-remote-sudo +``` + +First-time provisioning of a fresh VM (partitions the disk via disko, installs NixOS): +```bash +nix run github:nix-community/nixos-anywhere -- \ + --flake .#eurovm \ + --build-on remote \ + root@ +``` + +Edit encrypted secrets (re-encrypts on save): +```bash +sops secrets/secrets.yaml +``` + +Add a new sops recipient (e.g. after rotating a key or adding a new machine) and re-encrypt: +```bash +sops updatekeys secrets/secrets.yaml +``` + +Full first-deploy / bootstrap sequence (age key setup, DNS, secrets, provisioning, per-service +first-login steps) is documented step by step in `README.md` — follow it rather than re-deriving +the sequence from the module files. + +## Architecture + +**Single flake, single host.** `flake.nix` defines exactly one `nixosConfigurations.eurovm` +target, pinned to `system = "x86_64-linux"` (switch to `aarch64-linux"` for an ARM Hetzner +instance). Inputs are `nixpkgs` (pinned to `nixos-26.05`), `disko`, and `sops-nix`. + +**Host assembly (`hosts/eurovm/`)** wires together hardware profile, disk layout, and every +service module: +- `default.nix` — imports everything, sets hostname, the `admin` user + its authorized SSH keys, + passwordless sudo for wheel, `system.stateVersion`. +- `hardware.nix` — Hetzner virtio/qemu-guest profile, GRUB (BIOS boot), network via DHCP on eth0. +- `disko.nix` — declarative disk partitioning consumed by `nixos-anywhere` on first install: a + 1MiB BIOS-boot partition, a 512MiB ext4 `/boot`, and the rest as ext4 `/`. + +**Service modules (`modules/`)** are one file per concern, each self-contained and imported by +`hosts/eurovm/default.nix`: +- `common.nix` — SSH hardening (key-only, no root login), firewall (22/80/443/53 only), timezone, + nix settings (flakes, auto-gc, auto-optimise-store), base packages, disabled autoUpgrade. +- `sops.nix` — sops-nix wiring. The host's own age key is derived at boot from + `/etc/ssh/ssh_host_ed25519_key`, so no separate host key material needs to be provisioned. + Declares which secrets exist, their owning user/group, and file mode. +- `caddy.nix` — the only thing that binds to 80/443. Reverse-proxies each subdomain to a + service's localhost port. All application ports (3000, 5006, 8080, 8085/8086) are bound to + `127.0.0.1` and never exposed directly — Caddy is the sole public HTTP(S) entry point and + handles Let's Encrypt automatically. +- `adguard.nix` — AdGuard Home; DNS on `0.0.0.0:53`, web UI on `127.0.0.1:3000`. Runs with + `mutableSettings = false` (fully declarative config), which creates a specific problem: the + module's own `ExecStartPre` overwrites the state file with a config containing a literal + `REPLACED_AT_ACTIVATION` placeholder on every start. A second `ExecStartPre`, ordered with + `lib.mkAfter`, substitutes in the real bcrypt hash from the sops secret immediately before + `ExecStart`. It also uses a static (non-`DynamicUser`) system user, because sops-nix chowns the + secret to that user during activation while the service is stopped — a `DynamicUser` doesn't + exist at that point. If you touch this module, preserve that ordering; it's the whole point of + the file. +- `miniflux.nix` — RSS reader; admin account seeded from a sops credentials file (`ADMIN_USERNAME` + / `ADMIN_PASSWORD` format) on first run. Uses the NixOS module's own PostgreSQL integration. +- `actual.nix` — Actual Budget, run as a Podman OCI container (`virtualisation.oci-containers`) + rather than a native NixOS module, because none exists for this nixpkgs version. No secrets + involved; the server password is set interactively on first browser visit. +- `cgit.nix` — git hosting. Three-way access split: web browsing is public/unauthenticated via + cgit, `git clone`/`pull` over HTTPS is public/read-only via `git-http-backend` + fcgiwrap, and + `git push` is SSH-only through a dedicated `git` system user with its own authorized keys. Both + cgit and git-http-backend actually run behind a minimal localhost-only nginx instance, which + Caddy then proxies to — nginx exists here purely as an implementation detail for cgit/fcgiwrap, + not as the public-facing server. + +**Secrets (`secrets/secrets.yaml` + `.sops.yaml`)** — encrypted with sops using age keys. Three +recipients: two personal keys (`einstein`, `galileo` — the user's two machines) plus the host's +own key (`eurovm`, derived from its SSH host key), so the file can be decrypted both from a +laptop and by the running server. When adding a new secret, declare it in `modules/sops.nix` +(owner/group/mode/restartUnits) before referencing its `config.sops.secrets."".path` in a +service module. + +## Conventions to preserve + +- Every module takes only the NixOS module arguments it actually uses (`{ config, pkgs, lib, ... }` + etc.) — don't pad the argument set. +- New services follow the same shape: bind the app to `127.0.0.1:`, add a `virtualHosts` + entry in `caddy.nix`, and if the service needs a persistent secret, declare it in + `modules/sops.nix` first. +- Comments in modules explain *why* a workaround exists (e.g. the adguard password-injection + dance, the nginx shim in cgit) — keep that context when editing those files rather than + stripping it. -- cgit v1.3.1