diff options
| author | Karan Jayachandra <karan.jayachandra@nxp.com> | 2026-07-17 14:34:18 +0200 |
|---|---|---|
| committer | Karan Jayachandra <karan.jayachandra@nxp.com> | 2026-07-17 14:34:18 +0200 |
| commit | 0d0788cd4ee378fba19e47a2c5d5527c196b65e0 (patch) | |
| tree | 0fbd0e2644e94e0227005947004541fd4ff4d53e /README.md | |
Initial commit
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 372 |
1 files changed, 372 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..420356c --- /dev/null +++ b/README.md @@ -0,0 +1,372 @@ +# karanj.com EU Server - NixOS on Hetzner Cloud + +A flake-based, modular NixOS configuration for a personal server running in Europe. + +## Services + +| URL | Service | +|-----|---------| +| https://dns.karanj.com | AdGuard Home (DNS ad-blocker + resolver) | +| https://rss.karanj.com | Miniflux (RSS/Atom reader) | +| https://budget.karanj.com | Actual Budget (personal finance) | +| https://git.karanj.com | cgit (git repository browser) | + +All services are reverse-proxied by **Caddy** with automatic TLS via Let's Encrypt. +AdGuard also listens directly on **port 53 (UDP + TCP)** for DNS. + +--- + +## Repository Layout + +``` +flake.nix top-level flake; single host "eurovm" +.sops.yaml sops-nix age recipient configuration +secrets/secrets.yaml sops-encrypted secrets (miniflux + adguard creds) +hosts/eurovm/ + default.nix host assembly, users, SSH keys + hardware.nix Hetzner Cloud virtio/qemu-guest profile + disko.nix disk partitioning for nixos-anywhere +modules/ + common.nix SSH hardening, firewall, timezone, nix settings + sops.nix sops-nix wiring (age key from SSH host key) + caddy.nix reverse proxy + HTTPS virtual hosts + adguard.nix AdGuard Home DNS + web UI + miniflux.nix Miniflux + PostgreSQL + actual.nix Actual Budget + cgit.nix cgit + fcgiwrap + git push user +``` + +--- + +## Bootstrap: First Deploy + +### 1. Create the Hetzner Cloud VM + +- Log in to https://console.hetzner.cloud +- Create a new server: **CX22** (2 vCPU / 4 GB RAM), location **Nuremberg** or **Falkenstein** +- Base image: **Debian 12** (nixos-anywhere will replace it) +- Add your SSH public keys to the Hetzner project so root access works during install +- Note the assigned **public IPv4 address** + +### 2. Create DNS records + +At your DNS provider (for karanj.com), create **A records** pointing at the server IP: + +``` +dns.karanj.com A <server-ipv4> +rss.karanj.com A <server-ipv4> +budget.karanj.com A <server-ipv4> +git.karanj.com A <server-ipv4> +``` + +Or a single wildcard: `*.karanj.com A <server-ipv4>` + +DNS must resolve **before** the first `nixos-rebuild` so Caddy can obtain TLS certificates. + +### 3. Set up sops-nix secrets + +**3a. Convert your SSH public keys to age format:** + +On your local machine (Einstein): +```bash +cat ~/.ssh/id_ed25519.pub | ssh-to-age +# -> age1xxxx... (copy this) +``` + +Repeat on Galileo (or use the public key directly): +```bash +echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH+qnLTnorv+I2rSSfGjNiCuX/W5AxoNgAdu+cTOyKzW Galileo" | ssh-to-age +# -> age1yyyy... (copy this) +``` + +**3b. Update `.sops.yaml`:** + +Replace the placeholder values: +```yaml +keys: + - &einstein age1xxxx... # output from step 3a (Einstein) + - &galileo age1yyyy... # output from step 3a (Galileo) +``` + +**3c. Generate secrets:** + +AdGuard password hash (uses Apache htpasswd bcrypt format): +```bash +# Install apache2-utils if needed: apt install apache2-utils +htpasswd -nB admin +# Enter password when prompted; copy everything AFTER "admin:" +# Example output: admin:$2y$05$abc123... +# Put only the hash part: $2y$05$abc123... +``` + +Miniflux admin password: choose any strong password. + +**3d. Fill in and encrypt secrets.yaml:** +```bash +# Edit the plaintext file first +vim secrets/secrets.yaml +# Set ADMIN_PASSWORD and password_hash to real values + +# Then encrypt it in place +sops --encrypt --in-place secrets/secrets.yaml +``` + +The file is now safe to commit. To edit it later: `sops secrets/secrets.yaml` + +### 4. Install NixOS with nixos-anywhere + +```bash +# From your local machine (requires nix with flakes enabled) +nix run github:nix-community/nixos-anywhere -- \ + --flake .#eurovm \ + root@<server-ipv4> +``` + +nixos-anywhere will: +1. Copy the flake to the server +2. Run disko to partition the disk +3. Install NixOS +4. Reboot into the new system + +### 5. Add the server host key as a sops recipient + +After the server reboots, grab its SSH host key and convert it to age: +```bash +ssh-keyscan <server-ipv4> | grep ed25519 | ssh-to-age +# -> age1zzzz... +``` + +Update `.sops.yaml` - uncomment the `eurovm` key and fill in the age value: +```yaml + - &eurovm age1zzzz... +``` + +And update the `creation_rules` section to include `*eurovm`. + +Re-encrypt secrets with the new recipient: +```bash +sops updatekeys secrets/secrets.yaml +``` + +Commit and push, then redeploy: +```bash +nixos-rebuild switch --flake .#eurovm --target-host admin@<server-ipv4> --use-remote-sudo +``` + +--- + +## Day-to-Day Operations + +### Deploy updates +```bash +nixos-rebuild switch --flake .#eurovm --target-host admin@<server-ipv4> --use-remote-sudo +``` + +### Edit secrets +```bash +sops secrets/secrets.yaml +# Save and exit; sops re-encrypts automatically +# Then redeploy to apply +``` + +### Create a new git repository +```bash +ssh git@<server-ipv4> init --bare /srv/git/myrepo.git +# On your local machine: +git remote add origin git@<server-ipv4>:/srv/git/myrepo.git +git push -u origin main +# It appears automatically at https://git.karanj.com +``` + +### Clone a public repository +```bash +git clone https://git.karanj.com/myrepo.git +``` + +--- + +## First Access - Per App + +### AdGuard Home (https://dns.karanj.com) + +The admin account is pre-created from the sops secret during deployment. + +- **Username:** `admin` +- **Password:** whatever you set as the plaintext before encrypting `secrets.yaml` + +Log in, then go to **Settings -> General** to verify DNS is working. +To use the server as your DNS resolver, point your device's DNS to `<server-ipv4>`. + +If you need to reset the password: +```bash +# On the server, edit /var/lib/AdGuardHome/AdGuardHome.yaml +# Replace the users[].password bcrypt hash with a new one +# Then: systemctl restart adguardhome +``` + +### Miniflux (https://rss.karanj.com) + +The admin account is pre-created from the sops secret on first startup. + +- **Username:** `admin` (or whatever you set as `ADMIN_USERNAME` in secrets.yaml) +- **Password:** whatever you set as `ADMIN_PASSWORD` in secrets.yaml + +After logging in, go to **Settings -> Users** to change the password or +add additional accounts. + +To reset the password from the server: +```bash +sudo -u miniflux miniflux -reset-password +``` + +### Actual Budget (https://budget.karanj.com) + +No pre-configuration needed. On first visit: + +1. Open https://budget.karanj.com in your browser +2. The app will display a **"Create server password"** prompt +3. Enter a strong password - this is what all your devices use to sync +4. Click **OK** and the app is ready to use + +To reset the password, delete `/var/lib/actual/server-files/account.json` on the +server and restart the service: `systemctl restart actual` + +### cgit (https://git.karanj.com) + +No login required - the web interface is fully public and read-only. + +- **Browse:** open https://git.karanj.com in any browser +- **Clone/Pull:** `git clone https://git.karanj.com/<repo>.git` +- **Push:** SSH only - `git push git@<server-ipv4>:/srv/git/<repo>.git` + +### Caddy / TLS + +TLS certificates are obtained automatically from Let's Encrypt on first startup +(registered to me@karanj.com). No manual action needed. Certificates auto-renew. + +Caddy logs: `journalctl -u caddy -f` + +--- + +## Next Steps + +These are the actions to take immediately after cloning this repo, in order. + +### Step 1 - Fill in `.sops.yaml` with your age keys + +On **Einstein**: +```bash +cat ~/.ssh/id_ed25519.pub | ssh-to-age +``` + +On **Galileo** (or derive from the public key directly): +```bash +echo "ssh-to-age AAAAC3NzaC1lZDI1NTE5AAAAIH+qnLTnorv+I2rSSfGjNiCuX/W5AxoNgAdu+cTOyKzW Galileo" | ssh-to-age +``` + +Paste both `age1...` values into `.sops.yaml` replacing the `REPLACE_WITH_...` placeholders. + +### Step 2 - Set real passwords in `secrets/secrets.yaml` + +Generate the AdGuard bcrypt hash: +```bash +htpasswd -nB admin +# Copy only the hash part (after "admin:") +``` + +Then edit the secrets file and fill in real values: +```bash +vim secrets/secrets.yaml +# Set ADMIN_PASSWORD (Miniflux) and password_hash (AdGuard) +``` + +Encrypt it: +```bash +sops --encrypt --in-place secrets/secrets.yaml +``` + +Commit both `.sops.yaml` and the encrypted `secrets/secrets.yaml`. + +### Step 3 - Create the Hetzner Cloud VM + +1. Go to https://console.hetzner.cloud and create a new project (or use an existing one) +2. Add both your SSH public keys to the project under **Security -> SSH Keys** +3. Create a server with the following spec: + - **Minimum:** CAX11 (2 ARM vCPU / 4 GB RAM / 40 GB disk, ~3.79 EUR/month) - all services work fine on ARM64 + - **Alternative:** CX22 (2 x86 vCPU / 4 GB RAM / 40 GB disk, ~4.35 EUR/month) - use if you need x86 compatibility later + - Location: **Nuremberg** or **Falkenstein** + - Image: **Debian 12** (nixos-anywhere replaces it) +4. Note the **public IPv4 address** + +### Step 4 - Point DNS at the server + +At your DNS registrar for karanj.com, add: +``` +dns.karanj.com A <server-ipv4> +rss.karanj.com A <server-ipv4> +budget.karanj.com A <server-ipv4> +git.karanj.com A <server-ipv4> +``` + +Wait for propagation (usually a few minutes with most registrars). +Verify: `dig dns.karanj.com +short` should return your server IP. + +### Step 5 - Install NixOS with nixos-anywhere + +Make sure you have Nix with flakes enabled locally, then: +```bash +nix run github:nix-community/nixos-anywhere -- \ + --flake .#eurovm \ + root@<server-ipv4> +``` + +The install takes 5-10 minutes. The server reboots into NixOS at the end. + +### Step 6 - Add the server host key to sops + +After the server is up: +```bash +ssh-keyscan <server-ipv4> | grep ed25519 | ssh-to-age +# -> age1zzzz... +``` + +Uncomment and fill in the `eurovm` key in `.sops.yaml`, add `*eurovm` to the +`creation_rules` age list, then re-encrypt: +```bash +sops updatekeys secrets/secrets.yaml +``` + +### Step 7 - Final deploy + +Push the updated `.sops.yaml` and re-encrypted `secrets/secrets.yaml`, then run: +```bash +nixos-rebuild switch --flake .#eurovm --target-host admin@<server-ipv4> --use-remote-sudo +``` + +### Step 8 - First login to each service + +| Service | URL | Action | +|---------|-----|--------| +| AdGuard Home | https://dns.karanj.com | Log in with `admin` + your sops password | +| Miniflux | https://rss.karanj.com | Log in with `admin` + your sops password | +| Actual Budget | https://budget.karanj.com | Set server password in browser on first visit | +| cgit | https://git.karanj.com | No login needed - public read-only | + +### Step 9 - Update `common.nix` flake URL (optional) + +If you push this repo to GitHub/Forgejo/cgit, update the `autoUpgrade.flake` line +in `modules/common.nix` and set `enable = true` to get automatic OS updates. + +--- + +## Firewall Summary + +| Port | Protocol | Purpose | +|------|----------|---------| +| 22 | TCP | SSH (admin + git push) | +| 80 | TCP | HTTP (Caddy redirects to HTTPS) | +| 443 | TCP | HTTPS (all web services) | +| 53 | TCP + UDP | DNS (AdGuard Home) | + +All other ports are closed. App-level ports (3000, 5006, 8080, 8086) are +bound to 127.0.0.1 and never exposed directly. |
