152 lines
7.8 KiB
Markdown
152 lines
7.8 KiB
Markdown
|
|
# 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 <fingerprint>` 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.
|
||
|
|
- **Everything after mkfs goes by UUID.** `/dev/sdX` ordering 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_device` is used *only*
|
||
|
|
to create the filesystem.
|
||
|
|
- **`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.
|