personal_infra/tofu/nodito/vms.auto.tfvars

96 lines
2.8 KiB
Text
Raw Normal View History

# Non-secret nodito infrastructure config. Auto-loaded by tofu (*.auto.tfvars),
# so no -var-file is needed. Provider credentials live in terraform.tfvars,
# which is gitignored.
proxmox_node = "nodito"
zfs_storage_name = "proxmox-tank-1"
template_name = "debian-13-cloud-init"
cloud_init_user = "counterweight"
# Public key injected into every VM via cloud-init.
ssh_authorized_keys = <<EOKEY
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOVC68uU7dTpZ8mDleakoBX6crnBdKNuvg6qeD73t+6L counterweightoperator@counterstation
EOKEY
# Note that the resources set here are designed for startup.
# Some of these services are resource hungry at first, but
# can be powered down after their bootstrap finishes.
vms = {
"small-backups-box" = {
vmid = 1200
name = "small-backups-box"
cores = 1
memory_mb = 512
disk_size_gb = 100
ipconfig0 = "ip=dhcp"
},
"knots-box" = {
vmid = 2100
name = "knots-box"
cores = 4
memory_mb = 16384
disk_size_gb = 10
ipconfig0 = "ip=dhcp"
},
"fulcrum-box" = {
vmid = 2200
name = "fulcrum-box"
cores = 1
memory_mb = 2048
disk_size_gb = 10
ipconfig0 = "ip=dhcp"
},
"mempool-box" = {
vmid = 2300
name = "mempool-box"
cores = 2
memory_mb = 4096
disk_size_gb = 30
ipconfig0 = "ip=dhcp"
},
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
# disk_size_gb is the ROOT disk only. monerod-box also carries a second
# 500 GB disk (scsi1) for the blockchain, attached in the Proxmox UI and
# therefore invisible here - the vm resource sets ignore_changes = all, so
# tofu neither created it nor would notice if it vanished. It is mounted at
# /var/lib/monero by ansible/roles/monero (tasks/storage.yml), by UUID, and
# monerod's unit refuses to start without it.
"monerod-box" = {
vmid = 2400
name = "monerod-box"
cores = 4
xmrblocks: the Monero explorer, built off-box because 2 GB cannot compile it The Onion Monero Blockchain Explorer on monerod-box, published at exmrplore.contrapeso.xyz. Ten Gatus endpoints green across the node and the explorer. ── It needs a compiled monero source tree, not a running monerod ─────────── xmrblocks is not an RPC client. cmake/FindMonero.cmake imports 21 STATIC libraries out of a monero build tree and includes its headers, because it opens the LMDB directly. The release tarball we install for the daemon ships only executables - no .a files, no headers - so there is nothing on the box to link against, and building it means compiling monero first. Measured on Debian 13, building only the libraries it links: peak RSS, one cc1plus ~3.2 GB peak RSS at -j4 ~5.5 GB wall time at -j4 167 s source + build tree 195 MB toolchain 1.6 GB That rules the box out at ANY parallelism: 768 MiB and 1800 MiB were both OOM-killed at -j1 and -j2, because a single translation unit over 3 GB is not something lower -j works around. My first estimate of "~10 GB of disk" was wrong by two orders of magnitude - that figure describes upstream's full `make`, which also builds monerod, both wallets and the test suite, none of it linked here. Disk was never the constraint. So scripts/build-xmrblocks.sh does the build in a debian:13 container. An artefact only one machine can produce is one nobody can audit; this way any host with Docker and ~6 GB reproduces it. It verifies the monero source against binaryFate's signature exactly as roles/monero does for the daemon, and the role asserts the resulting sha256 ON THE CONTROL NODE before copying a single byte. ── Three upstream problems, none documented anywhere ─────────────────────── Found by building, not by reading: 1. libasio-dev is absent from upstream's dependency list. That list is Ubuntu-based; Crow wants standalone asio.hpp, not boost/asio.hpp, and on Debian 13 the build dies outright. 2. The explorer does not compile against monero v0.18.5.1. v0.18.5.0 changed get_pruned_transaction_hash from returning the hash to returning bool with an out-param, and src/page.h still calls the old form. Upstream's Dockerfile pins v0.18.4.0, which is why nobody has hit it. Patched from an exact-line anchor, so if upstream fixes it the build STOPS rather than quietly doing something else. We patch instead of building against 0.18.4.0 because the signed hashes.txt only covers the current release - using the older source would mean dropping the provenance chain. LMDB is VERSION 5 in both, so either reads the other's database. 3. FindMonero.cmake hunts for miniupnpc inside the monero build tree with NO_DEFAULT_PATH, but monero stopped vendoring miniupnp as a submodule. Fixed with a cache override rather than patching their cmake. USE_DEVICE_TREZOR=OFF as well - an explorer has no use for hardware wallets and it drops the generated protobuf units. Verified that xmrblocks still links. ── Two things in the unit that look wrong and are not ────────────────────── WorkingDirectory is load-bearing: page.h hardcodes TMPL_DIR as "./templates", resolved relative to cwd at REQUEST time. Get it wrong and the service starts, binds 8081, and 500s every page. That is also why the health check fetches a real page and greps it instead of probing the port. ReadWritePaths must include the blockchain directory even though the database is opened read-only: monero sets mdb_flags = MDB_RDONLY WITHOUT MDB_NOLOCK, so LMDB still registers this reader in lock.mdb. With ProtectSystem=strict and nothing writable the open fails. "It only reads, so grant it nothing" is wrong. ── Timeouts are measured, not guessed ────────────────────────────────────── proxmox-tank-1 is a spinning-disk mirror and xmrblocks says so on startup. First start took 98s to open the LMDB before binding (wait_for now allows 300s), and a COLD index page took 68 SECONDS where the next two took 0.87s and 0.012s (the check now allows 90s, its unit 180s). The first pass used 90s and 25s and produced exactly the false failures you would expect. A stat() bug was caught the same way: `stat` computes a sha1 by default, so checking that data.mdb exists was hashing the entire 40 GB blockchain. --check found it before a real run did. ── Exposure ──────────────────────────────────────────────────────────────── Caddy on vipy terminates TLS; the upstream is reached over the tailnet, which needed a headscale ACL entry for tag:monerod-box:8081 - without it the node is invisible, not merely unreachable, and the symptom is a 502 "no such host". No authentication, deliberately: it is a block explorer over public chain data. That includes --enable-pusher, so a visitor can relay a raw transaction through our node. A choice, not an oversight; flip xmrblocks_enable_pusher to close it. ── Also ──────────────────────────────────────────────────────────────────── monerod-box raised 2048 -> 6144 MiB. Not for the explorer: monerod alone had outgrown 2 GB past ~39% of the chain and was OOM-killed twice in a morning at 1.74 GB anon-rss. Recorded in vms.auto.tfvars because ignore_changes = all means tofu will not reconcile it. The box now idles at ~1.2 GB of 5.9 GB with both services up. No backups, deliberately - binary, templates and chain are all reproducible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 17:06:30 +02:00
# Raised from 2048 on 2026-09-20: monerod was OOM-killed hourly once the
# chain passed ~39% (1.74 GB anon-rss against 1973 MB of RAM), and the
# explorer shares this box. Applied in Proxmox; recorded here because the
# vm resource sets ignore_changes = all and will not reconcile it.
memory_mb = 6144
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
disk_size_gb = 10
ipconfig0 = "ip=dhcp"
},
"memos-box" = {
vmid = 8100
name = "memos-box"
cores = 1
memory_mb = 1024
disk_size_gb = 10
ipconfig0 = "ip=dhcp"
},
"forgejo-runner-box" = {
vmid = 3100
name = "forgejo-runner-box"
cores = 4
memory_mb = 4096
disk_size_gb = 50
ipconfig0 = "ip=dhcp"
},
"arbret-staging-box" = {
vmid = 3200
name = "arbret-staging-box"
cores = 2
memory_mb = 2048
disk_size_gb = 20
ipconfig0 = "ip=dhcp"
}
}