monero: a full node on its own box, reachable from Feather over the WAN
monerod v0.18.5.1 on monerod-box (VMID 2400), Debian 13, systemd, no Docker.
Syncing at 24% as of this commit, 0 restarts, 0 OOM kills.
── The build-from-source requirement could not be met ──────────────────────
The intent was to build it ourselves and check the result against the published
checksums. That is not achievable for this release, and the role's README says
so at length so nobody re-derives it:
contrib/guix - the reproducible build that yields a byte-identical tarball -
exists ONLY on monero master. At tag v0.18.5.1 contrib/ contains `gitian`,
whose README wants an Ubuntu 18.04 host (EOL 2023) and python-cheetah.
A plain `make release-static` is not reproducible at all, so it produces a
binary whose hash matches nothing - failing both halves of the ask.
So we install the published binary and prove its provenance instead:
1. binaryFate's 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 regardless.
2. hashes.txt is verified, and the assertion is on gpg's `VALIDSIG <fpr>`
status line - NOT on the string "Good signature", which is what
bitcoin_knots greps for and which passes for any key in the keyring.
3. The hash is looked up in the output of `gpg --decrypt`, not in the raw
clearsigned file, so nothing unsigned is ever searched.
4. The lookup requires hash AND filename on one line.
Tested with a positive and a negative control before deploying: the real
tarball matches, a corrupted hash correctly fails. monero_sha256 in defaults/
is a second in-git assertion so a version bump cannot happen without the
checksum moving in the same diff.
When v0.19 ships with guix on the release branch this gets easier, and the
README records why: the published monero-linux-x64 tarball unpacks to
monero-x86_64-linux-gnu-v0.18.5.1/, i.e. it IS the guix DISTNAME renamed, so
sha256(guix output) will equal the hashes.txt entry exactly.
── The 500 GB disk is provisioned in code ──────────────────────────────────
knots-box mounts /mnt/knots_data (~875 GB of chain) entirely by hand; that
mount appears nowhere in Ansible and a rebuild of that host would come up with
the blockchain silently missing. tasks/storage.yml does not repeat it.
Whole device, no partition table. Everything after mkfs goes by UUID, because
/dev/sdX is assigned by the kernel at boot and this estate has been bitten
eight times by an identifier written down once whose thing then moved.
monero_data_device is used ONLY to create the filesystem. Guards refuse to
touch the root disk or to overwrite an unrecognised filesystem, and a final
assert confirms the data directory is not still on /.
fstab gets `nofail` so a missing disk cannot drop a headless VM into an
emergency shell - and the unit gets `RequiresMountsFor=`, without which monerod
would then start, find an empty directory, and write a 300 GB chain onto a
10 GB root disk. With it the unit refuses to start and infra/401 says so within
five minutes.
The mount-point task and the ownership task deliberately share owner and mode.
Writing the obvious root:root 0755 on the first made them disagree and every
run flipped the directory back and forth; caught by re-running --check.
── Exposure: one HTTPS vhost, zero new open ports ──────────────────────────
Two RPC servers, not one: unrestricted on 127.0.0.1:18081 for administration
and the health check, restricted on :18089 for everything else. That is
`rpc-restricted-bind-port` and deliberately NOT `restricted-rpc=1`, which would
restrict the loopback server too. 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. Caddy on vipy does
TLS and routing only - no caddy_site_basic_auth, because a Basic challenge
would collide with the Digest challenge Feather answers. Verified end to end
from the WAN: 401 unauthenticated, get_block_count over /json_rpc when
authenticated, 401 again with a wrong password.
No UFW rule, and that is correct: tailnet traffic bypasses UFW entirely because
`-A INPUT -j ts-input` sits ahead of every ufw-* chain. Confirmed on mempool-box
before relying on it here.
P2P is outbound only. Nothing forwards 18080, so the node dials out, syncs and
serves us without relaying for strangers; no-igd follows from that.
── Monitoring: infra, systemd, public. No backups ──────────────────────────
Six Gatus endpoints, all green: liveness, disk, services/monerod-box-local/
monerod, probe/monerod, dns/monero.contrapeso.xyz, public/monero.
The probe does NOT assert `synchronized`. A full sync takes days and a check
requiring it would report DOWN for every one of them - an alarm with the
batteries taken out. Height and sync state go in the message instead.
The timer carries no `Requires=monerod.service`. Fulcrum's did, and sat
active+enabled for seven months without firing once. public/monero asserts 401,
not 200, so it goes red if rpc-login ever stops being enforced.
No backups, deliberately, following the phoenixd precedent: the chain is
re-syncable public data and there is no wallet on this node.
── Known constraint ────────────────────────────────────────────────────────
The VM has 2 GB and no swap; monerod's RSS is ~1.3 GB mid-sync. It is holding
(0 OOM kills, ~1 GB available, kernel reclaiming cache) but 8 GB is the
recommendation once the initial sync is done. out-peers/in-peers are held at
16/8 partly for this reason. Bumping it is a Proxmox resize plus memory_mb
here, since the vm resource sets ignore_changes = all.
The 500 GB data disk is likewise invisible to tofu - it was attached in the UI
and the vm resource ignores it - so vms.auto.tfvars now carries a comment
saying where it is and what mounts it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 17:50:46 +02:00
|
|
|
# Monero node (monerod) configuration variables.
|
|
|
|
|
# https://docs.getmonero.org/interacting/monerod-reference/
|
|
|
|
|
|
|
|
|
|
# --- Version and artefact ---------------------------------------------------
|
|
|
|
|
# Bump monero_version and monero_sha256 TOGETHER. The sha256 here is a second,
|
|
|
|
|
# in-git assertion; the one that actually matters is the grep against the
|
|
|
|
|
# GPG-signed hashes.txt in install.yml. Having both means a version bump cannot
|
|
|
|
|
# be done silently - the checksum has to move in the same diff.
|
|
|
|
|
monero_version: "0.18.5.1"
|
|
|
|
|
monero_platform: "linux-x64"
|
|
|
|
|
monero_tarball: "monero-{{ monero_platform }}-v{{ monero_version }}.tar.bz2"
|
|
|
|
|
monero_url: "https://downloads.getmonero.org/cli/{{ monero_tarball }}"
|
|
|
|
|
monero_sha256: "22a7dda7b0cb699fdd6b7674c3b4a4465b337cc98a54983523b759e1e7cc9958"
|
|
|
|
|
|
|
|
|
|
# The signed list of checksums for every published Monero artefact.
|
|
|
|
|
monero_hashes_url: "https://www.getmonero.org/downloads/hashes.txt"
|
|
|
|
|
|
|
|
|
|
# binaryFate, the release signing key. The .asc is vendored in files/ rather
|
|
|
|
|
# than fetched at deploy time: a trust root downloaded over the network at the
|
|
|
|
|
# moment you need it is not a trust root. The fingerprint is asserted after
|
|
|
|
|
# import anyway, so a swapped file cannot go unnoticed.
|
|
|
|
|
monero_signing_fingerprint: "81AC591FE9C4B65C5806AFC3F0AF4D462A0BDF92"
|
|
|
|
|
|
|
|
|
|
# --- Service user -----------------------------------------------------------
|
|
|
|
|
monero_user: monero
|
|
|
|
|
monero_group: monero
|
|
|
|
|
|
|
|
|
|
# --- Directories ------------------------------------------------------------
|
|
|
|
|
monero_conf_dir: /etc/monero
|
|
|
|
|
monero_log_dir: /var/log/monero
|
|
|
|
|
monero_bin_dir: /usr/local/bin
|
|
|
|
|
|
|
|
|
|
# The blockchain. This is the mount point of the dedicated 500 GB disk, NOT a
|
|
|
|
|
# directory on the 10 GB root filesystem - see storage.yml.
|
|
|
|
|
monero_data_dir: /var/lib/monero
|
|
|
|
|
|
|
|
|
|
# --- The data disk ----------------------------------------------------------
|
|
|
|
|
# The whole device, no partition table: this disk has one job and a partition
|
|
|
|
|
# table would only add a layer that can disagree with itself.
|
|
|
|
|
#
|
|
|
|
|
# monero_data_device is used ONLY to create the filesystem, once. Everything
|
|
|
|
|
# afterwards - fstab, the mount, the systemd unit - goes by UUID, because
|
|
|
|
|
# /dev/sdX ordering is decided by the kernel at boot and is not a stable name
|
|
|
|
|
# for anything. This estate has been bitten eight times by an identifier that
|
|
|
|
|
# was written down once and then moved.
|
|
|
|
|
monero_data_device: /dev/sdb
|
|
|
|
|
monero_data_fstype: ext4
|
|
|
|
|
# -m 0: the 5% ext4 reserve exists to stop root-owned daemons deadlocking a full
|
|
|
|
|
# root filesystem. On a dedicated data disk it is 25 GB thrown away.
|
|
|
|
|
monero_data_mkfs_opts: "-m 0 -L monero-data"
|
|
|
|
|
|
|
|
|
|
# --- Network ----------------------------------------------------------------
|
|
|
|
|
# P2P is OUTBOUND ONLY. No inbound port is published from the edge host, so the
|
|
|
|
|
# node dials out to peers and never receives. It syncs and serves us completely;
|
|
|
|
|
# it just does not relay for strangers. no-igd follows from that: with nothing
|
|
|
|
|
# forwarded there is nothing for UPnP to do but tell the router about us.
|
|
|
|
|
monero_p2p_port: 18080
|
|
|
|
|
|
|
|
|
|
# Unrestricted RPC, loopback only: administration and the health check.
|
|
|
|
|
monero_rpc_bind_ip: "127.0.0.1"
|
|
|
|
|
monero_rpc_port: 18081
|
|
|
|
|
|
|
|
|
|
# The RESTRICTED RPC is a second server on its own port, reachable over the
|
|
|
|
|
# tailnet and published by Caddy on the edge host. This is deliberately
|
|
|
|
|
# rpc-restricted-bind-port and NOT `restricted-rpc=1` - the latter would
|
|
|
|
|
# restrict the loopback server too, and then we could not ask our own node
|
|
|
|
|
# anything useful.
|
|
|
|
|
#
|
|
|
|
|
# The authoritative value for the live deployment is in
|
|
|
|
|
# host_vars/monerod_box_local/main.yml, which the edge play also reads; the
|
|
|
|
|
# value here is the upstream default so the role still works standalone.
|
|
|
|
|
monero_restricted_rpc_port: 18089
|
|
|
|
|
|
|
|
|
|
# Peer counts kept modest on purpose: this VM has 2 GB of RAM. See README.
|
|
|
|
|
monero_out_peers: 16
|
|
|
|
|
monero_in_peers: 8
|
|
|
|
|
|
|
|
|
|
# --- RPC credentials --------------------------------------------------------
|
|
|
|
|
# monerod's --rpc-login is HTTP Digest (contrib/epee/src/http_auth.cpp) and the
|
|
|
|
|
# same login guards BOTH the loopback and the restricted server. Feather sends
|
|
|
|
|
# these as the node username/password.
|
|
|
|
|
monero_rpc_user: ""
|
|
|
|
|
monero_rpc_password: ""
|
|
|
|
|
|
|
|
|
|
# monerod.conf holds the RPC login, so --diff is suppressed by default.
|
|
|
|
|
# Pass -e monero_reveal_config=true to see what moved.
|
|
|
|
|
monero_reveal_config: false
|
|
|
|
|
|
|
|
|
|
# --- Health check -----------------------------------------------------------
|
|
|
|
|
# WHERE TO REPORT HEALTH — the one place to plug in monitoring. Empty means
|
|
|
|
|
# check, exit honestly, report nowhere. Any endpoint accepting an HTTP ping
|
|
|
|
|
# works; nothing here is specific to a monitoring product.
|
|
|
|
|
healthcheck_push_url: ""
|
|
|
|
|
# Bearer token for the Gatus external endpoint. Required whenever a push URL
|
|
|
|
|
# is set: Gatus rejects an unauthenticated push with 401.
|
|
|
|
|
healthcheck_push_token: ""
|
monero: check the node once a day, not every five minutes
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>
2026-09-27 11:08:36 +02:00
|
|
|
|
|
|
|
|
# WHEN TO CHECK. Daily, at a fixed hour, rather than an OnUnitActiveSec
|
|
|
|
|
# interval that drifts with every restart.
|
|
|
|
|
#
|
|
|
|
|
# Prefixed because the generic `healthcheck` role owns the unprefixed
|
|
|
|
|
# healthcheck_on_calendar; an unprefixed default here would shadow it for any
|
|
|
|
|
# play that includes both.
|
|
|
|
|
#
|
|
|
|
|
# Paired with the 26h Gatus heartbeat in
|
|
|
|
|
# infra/403_service_probe_registration.yml. Change one and you must change the
|
|
|
|
|
# other: the window has to stay above the interval or the check alerts on its
|
|
|
|
|
# own jitter.
|
|
|
|
|
monero_healthcheck_on_calendar: "*-*-* 05:00:00"
|