aboutsummaryrefslogtreecommitdiff
path: root/README.md
blob: 420356c8ef97af0466e7c87aadbbdd60b50caee5 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
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.