A wedged explorer produced 83 Signal messages in six hours. Nothing about that
was a monitoring success: the checks were right, the cadence was wrong.
monerod-box is a Monero node in the middle of a multi-week initial sync on a
rotating disk. A 5-minute poll tells you nothing a daily one does not, and when
something breaks it turns one fault into a message every five minutes until
somebody silences the phone - which is the failure mode that makes people stop
reading alerts entirely.
So both Monero probes move to OnCalendar:
monerod *-*-* 05:00:00 RandomizedDelaySec=900
xmrblocks *-*-* 05:30:00 RandomizedDelaySec=900
systemd units (infra/401 per-host timer) *-*-* 05:15:00
30 minutes apart on purpose: both read the same LMDB, and the explorer's check
renders a full page out of it - a cold one measured 68 seconds. Running them
together would have each paying the other's cache misses.
OnCalendar rather than OnUnitActiveSec: an interval timer re-bases on every
restart, so "daily" silently becomes "a day after whenever it last came up".
── The window and the cadence are ONE decision ─────────────────────────────
The Gatus heartbeat moves with them, to 26h. Not 24h: the window must EXCEED
the push interval, or a check reporting exactly on schedule races its own
deadline and alerts on jitter alone. With a 900s randomized delay the true
worst case is ~24h15m, so 26h leaves real headroom.
The same logic drops the pulled public-site checks to a 24h interval - and
their failure-threshold from 3 to 1 with it. Three failures at 5m is 15 minutes
of tolerance; three at 24h would be THREE DAYS before anyone heard. A daily
check has no room for a blip, so it alerts on the first.
── Scoping ────────────────────────────────────────────────────────────────
infra/401's timer is ONE PER HOST and iterates every unit in
monitored_services, so slowing it slows all of them. That is safe here only
because monerod-box runs nothing but Monero units; the knobs are therefore in
its host_vars rather than the role, and both 401 and 403 now take a per-host
(resp. per-probe) override with the old 11m as the default. Every other host
is untouched.
Host liveness and disk are deliberately NOT slowed. They answer "is the box
alive", which is still wanted within minutes.
The role-level OnCalendar defaults are prefixed (monero_healthcheck_on_calendar,
xmrblocks_healthcheck_on_calendar) because the generic `healthcheck` role owns
the unprefixed name, and an unprefixed default here would shadow it for any
play that includes both.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|---|---|---|
| .. | ||
| defaults | ||
| files | ||
| handlers | ||
| tasks | ||
| templates | ||
| README.md | ||
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 moneromaster. At tagv0.18.5.1contrib/containsgitian, notguix.- 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-staticis 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:
- 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. hashes.txtis downloaded and its clearsigned signature verified. The assertion is on gpg'sVALIDSIG <fingerprint>status line, not on the string "Good signature" -bitcoin_knotsgreps for the latter, which passes for any key in the keyring.- 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. - 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.
- Everything after mkfs goes by UUID.
/dev/sdXordering is decided by the kernel at boot. This estate has been bitten eight times by an identifier written down once whose thing then moved;monero_data_deviceis used only to create the filesystem. RequiresMountsFor=in the unit.nofailin fstab means a missing disk does not drop a headless VM into an emergency shell - but withoutRequiresMountsFormonerod 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 andinfra/401reports 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=1follows - 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 notrestricted-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 reportsrestricted=trueand blanksversionandstart_time. - unrestricted on
-
rpc-loginis HTTP Digest (contrib/epee/src/http_auth.cpp), andcore_rpc_server::inithands 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.