# monero A Monero full node (`monerod`) on Debian 13, bare metal, systemd, no Docker. Runs on `monerod-box` (Proxmox VMID 2400), reachable from Feather over the WAN. ## Why the published binary and not a build from source The intent was to build from source and check the result against the published binary checksums. That is not achievable for the current release, and it is worth writing down why so nobody re-litigates it: - `contrib/guix`, the reproducible-build system that produces a byte-identical tarball, **exists only on monero `master`**. At tag `v0.18.5.1` `contrib/` contains `gitian`, not `guix`. - Reproducing v0.18.5.1 therefore means **Gitian**, whose README requires an Ubuntu 18.04 host (EOL 2023), LXC or Docker, and `python-cheetah` (Python 2). - A plain `make release-static` is **not** reproducible. It would give us a self-built binary whose hash matches nothing - not upstream's, not anyone else's - so it satisfies neither half of "build it ourselves AND check the checksum". So the role installs the published binary and proves its provenance instead. That chain is real, and it is the strongest claim available for this release: 1. binaryFate's public key is **vendored** at `files/binaryfate.asc`. A trust root fetched over the network at the moment you need it is not a trust root. Its fingerprint is asserted after import anyway (`81AC591FE9C4B65C5806AFC3F0AF4D462A0BDF92`), so a swapped file is caught. 2. `hashes.txt` is downloaded and its clearsigned signature verified. The assertion is on gpg's `VALIDSIG ` status line, **not** on the string "Good signature" - `bitcoin_knots` greps for the latter, which passes for *any* key in the keyring. 3. The verified plaintext is written out by `gpg --decrypt`, and the tarball's SHA256 is looked up in *that*, not in the raw clearsigned file. 4. The lookup requires the hash **and** the filename on one line, so a hash belonging to a different artefact cannot satisfy it. `monero_sha256` in `defaults/` is a second, in-git assertion passed to `get_url`. It is not the security control - step 4 is - but it means a version bump cannot happen without the checksum moving in the same diff. **Bumping the version: change `monero_version` and `monero_sha256` together.** ### The forward path The published `monero-linux-x64-v0.18.5.1.tar.bz2` unpacks to `monero-x86_64-linux-gnu-v0.18.5.1/` - that is the Guix `DISTNAME`, renamed at publish time. So once v0.19 ships with Guix on the release branch, `sha256(guix output) == hashes.txt entry` holds exactly, and a `guix` install method can be added here without changing anything else. ## The data disk The blockchain is on a **dedicated 500 GB disk** (`scsi1`), not on the 10 GB root filesystem. `tasks/storage.yml` creates the filesystem and the fstab entry. This is done in code on purpose. `knots-box` mounts `/mnt/knots_data` (~875 GB of chain) entirely by hand, and that mount appears nowhere in Ansible - a rebuild of that host would come up with the chain silently missing. Three things there are load-bearing: - **The whole device, no partition table.** One disk, one job; a partition table is just another layer that can disagree with itself. - **The device name is read once, ever.** Each run first looks for a filesystem labelled `monero-data`; if it exists, `monero_data_device` is not consulted and the mount goes by that filesystem's UUID. Only a blank disk is found by name, and that name is the Proxmox slot as published under `/dev/disk/by-id` (`...drive-scsi1`), fixed before the VM boots and kept across `qm disk move`. Never `/dev/sdX`: the kernel assigns those in probe order, and on 2026-10-04 a reboot swapped `sda` and `sdb`, leaving the old `/dev/sdb` value naming the root disk. The root-disk guard caught it - but a routine run should never have been reading the name in the first place. - **`RequiresMountsFor=` in the unit.** `nofail` in fstab means a missing disk does not drop a headless VM into an emergency shell - but without `RequiresMountsFor` monerod would then start, find an empty directory, and begin writing a ~300 GB chain onto a 10 GB root disk. With it, the unit refuses to start and `infra/401` reports it within five minutes. The mount-point task and the post-mount ownership task deliberately use the **same** owner and mode. Writing the obvious `root:root 0755` on the first made the two disagree, and every run flipped the directory back and forth. ## Exposure: one HTTPS vhost, no new open ports - **P2P is outbound only.** Nothing forwards 18080 to this box. The node dials out, syncs fully and serves us; it just does not relay for strangers. `no-igd=1` follows - with nothing forwarded there is nothing for UPnP to do except announce us to the router. - **Two RPC servers, not one.** - unrestricted on `127.0.0.1:18081` - administration and the health check. - restricted on `:18089` - the only surface anything off-box touches. This is `rpc-restricted-bind-port`, deliberately **not** `restricted-rpc=1`. That flag restricts the loopback server too, and then we could not ask our own node anything useful. Verified live: the restricted port reports `restricted=true` and blanks `version` and `start_time`. - **`rpc-login` is HTTP Digest** (`contrib/epee/src/http_auth.cpp`), and `core_rpc_server::init` hands the same login to *both* servers. Both ports answer an unauthenticated request with 401. - **Caddy on the edge host terminates TLS and nothing else.** No `caddy_site_basic_auth`: Caddy's Basic challenge would collide with the Digest challenge Feather expects. monerod's own auth is the authentication. ### No UFW rule, and that is correct Traffic arriving over the tailnet **bypasses UFW entirely**. Verified on `mempool-box`: UFW is active with default-deny-incoming and `22/tcp` as its only allow rule, yet the edge host reaches its frontend port - because `iptables -S INPUT` puts `-A INPUT -j ts-input` ahead of every `ufw-*` chain. Adding a UFW allow here would be cargo cult. ## Monitoring Four Gatus endpoints, all verified passing: | Endpoint | Source | Answers | |---|---|---| | `liveness/monerod_box_local` | `infra/400` | is the host up | | `disk/monerod_box_local` | `infra/400` | is it running out of space | | `services/monerod-box-local/monerod` | `infra/401` via `monitored_services` | is the unit active | | `probe/monerod` | this role + `infra/403` | does monerod actually answer RPC | **The probe deliberately does not check `synchronized`.** A full initial sync takes days. A check requiring `synchronized: true` would report DOWN for every one of those days, which is not a monitor - it is an alarm with the batteries taken out. Answering RPC is the health question; height and sync state go in the message so they are visible without being alertable. The timer carries **no `Requires=monerod.service`**. Requires on a timer stops the timer when the watched unit stops - "if the thing I am watching goes down, stop watching it" - and leaves nothing to re-arm it. Fulcrum's timer carried it and sat `active`/`enabled` for seven months without firing once. `bitcoin_knots` still carries it; do not copy that. `Enable and restart the monerod health check timer` reports `changed` on every run. That is intentional and matches `datum_gateway`: `state: restarted` is how a timer gets re-armed, and `started` is a no-op on an already-active timer. ## Backups: none, deliberately The blockchain is re-syncable public data and there is no wallet on this node, so a backup job would move hundreds of GB around to protect nothing. This follows the `phoenixd` precedent (Plan 5, Model C) of recording an exclusion as a decision rather than leaving it to look like an oversight. ## Known constraint: RAM The VM has **2 GB and no swap**. Mid-sync, monerod's RSS was already 1,333 MiB of 1,973 MiB with ~100 MiB free. No OOM kill has happened yet, but the working set grows with the database and there is no swap to absorb a spike. `out-peers` and `in-peers` are held down (16/8) partly for this reason. **8 GB is the recommendation.** Bumping it means editing `memory_mb` in `tofu/nodito/vms.auto.tfvars` and resizing the VM in the Proxmox UI - the vm resource sets `ignore_changes = all`, so tofu will not do it for you.