Skip to content

M7-15 OpenTofu module generic-vm

No Date Ruling Reason
D-M7-15-0 2026-10-21 The ticket was built on an integration base and ported onto main (after M7-29, #229) with only its own commits; the English docs of main win and text this ticket adds is English. The brief relies on no core rule and no D-W ruling that main lacks (16 3.1, 18 2.1 and 6 came with M7-29; D-W-100, D-W-125 and D-W-127 restate cost and trial rules the brief repeats). Names that differ from the brief (D-W-100): the chapters are 16-operations.md and 18-release.md; the VM runner is tools/ci/tofu_vm_test.py (next to the trial it reuses, so ruff, mypy, bandit and coverage cover it) and infra/tofu/test/generic-vm/ holds the root and its README; the root that calls all four modules is infra/tofu/test/validate/. One PR per ticket on main; merged names win (D-W-100).
D-M7-15-1 2026-10-20 Tool choice (Design note 1, "Open" of the module README): one POSIX sh script files/bothy-setup.sh plus four rendered files (setup.env, install-options, firewall.nft, release.pub). New hosts get them as #cloud-config user data (write_files, runcmd, and users for ssh_user with admin_ssh_public_keys), rendered with yamlencode (output user_data); an existing host gets the same files from terraform_data.ssh_setup (provision_over_ssh = true), whose remote-exec writes a payload to a mktemp file of ssh_user and runs it with sudo; the key comes from the SSH agent, never from an input. New inputs: ssh_port, sshd_port, provision_over_ssh, release_repository, release_base_url, release_public_key, owner_handle, owner_name, virus_scan, stack_dir. bothy_version is the product release (its bundle pins the server version, 18 2.1). Inputs install.sh cannot honour stop the plan with a precondition: database_mode = managed, object_storage_mode = s3, a vendor exporter in telemetry_exporters, backup_offsite.type = sftp; environment reaches only the header of setup.env, because install.sh has no option for it. One tested path for both deliveries; the module never writes .env or secrets/ (requirement 15), and no input can carry a secret into the state (requirement 13).
D-M7-15-2 2026-10-20 Verification on the host (requirement 14): cosign 3.1.3 from its GitHub release, checked against the SHA-256 of tools/bin/tools.lock.json (a test holds the module's copy equal); then cosign verify-blob of SHA256SUMS, the SHA-256 of bundle and install.sh from it (each listed exactly once), their own .sigstore.json signatures, and BOTHY_REGISTRY of the bundle against image_registry, all before install.sh --bundle unpacks anything. Signing mode: release_public_key set means the key of the private phase (--key, --insecure-ignore-tlog, ADR 0062); null means keyless against ^https://github\.com/<release_repository>/\.github/workflows/release\.yml@refs/tags/v and the GitHub OIDC issuer, as the job product-signing verifies. Docker Engine and the Compose plugin come from download.docker.com, the key checked by its fingerprint; the package versions are not pinned (the repository signature and the minimum versions of 16 3 rule 7 are checked), because one version string per distribution and release would need a Renovate manager of its own. Integrity from signatures, not from the transport, so the download address may be plain HTTP in the VM test.
D-M7-15-3 2026-10-20 Offline tofu-check (Design note 3): the module uses only built-in features of OpenTofu (templatefile, yamlencode, terraform_data), so it has no provider to pin or mirror. make tofu-check runs tofu fmt -check, then init -backend=false with an empty local provider mirror as the only provider source (as ops-check) and validate of infra/tofu/test/validate and infra/tofu/test/generic-vm, in make check and therefore in CI. A target module with a provider (M7-16) fails here until it brings a pinned offline path. No network in the gate; the same rule ops-check already enforces per module.
D-M7-15-4 2026-10-20 The enrollment code of the first owner: install.sh prints it once on standard output, and cloud-init and the SSH delivery keep that output in a log (/var/log/cloud-init-output.log, the console of tofu apply). The setup runs install.sh with setsid -w and standard input closed, so it has no controlling terminal even under the pseudo terminal of remote-exec (with a terminal it would print the code to /dev/tty, past any filter), and replaces the line by the command that issues a new code (bothy setup issue-enrollment-code --user <owner_handle> in the API service of the edge, api or api-tailscale as in compose.yaml); the VM test checks that the code is not in the cloud-init log and that detached removes the terminal of an SSH PTY. With edge_mode = tailscale the one-time auth key is a root file /etc/bothy/tailscale-auth-key placed by the operator; the setup passes it to install.sh in the environment and removes the file after the installation. A secret must not land in a log or the user data (requirement 13).
D-M7-15-5 2026-10-20 Edge and firewall (requirement 16): reverse_proxy maps to --edge direct with port 443 public; tailscale to --edge tailscale, private to --edge direct, both without a public port. The nftables table inet bothy_edge (input policy drop) admits established traffic, loopback, the Docker bridges, ICMP, DHCP replies, the sshd port (sshd_port, default ssh_port, so a host with sshd on another port keeps its access) and the edge port, loaded at boot by bothy-firewall.service before the network (DefaultDependencies=no, Before=network-pre.target shutdown.target, as nftables.service does; with the default dependencies the unit forms an ordering cycle through cloud-init.service on Ubuntu, found by the red team); the Compose stack publishes on 127.0.0.1 only. The module installs no reverse proxy: the proxy belongs to the host or the target (a load balancer of M7-16). The brief maps edge_mode to the options of 16 3.1; SSH stays for the maintenance access of the contract.
D-M7-15-6 2026-10-20 VM runner and image pin (Design note 2): make tofu-vm-test runs tools/ci/tofu_vm_test.py: the trial of release_trial.py (registry, images, keys), the product build, SBOMs and private-phase signatures of bundle, install.sh and SHA256SUMS with the trial key; Ubuntu 24.04 ubuntu-24.04-server-cloudimg-<arch>.img of release-20261017, pinned by SHA-256 from its SHA256SUMS, cached in .cache/tofu-vm/; QEMU with user networking, SSH forwarded on 127.0.0.1 with a random port, the NoCloud seed by SMBIOS from the static server, KVM on Linux, HVF on macOS, TCG otherwise. A test-only cloud-config part forwards 127.0.0.1:<port> of the registry and the static server to the host (192.0.2.2; the user network of QEMU is moved to the documentation range 192.0.2.0/24, so no private address appears in the repository) with nftables DNAT, so bundle and Docker use the trial addresses unchanged; Docker accepts a loopback registry over HTTP, so no insecure-registries entry is needed. The smoke test is the probe of M1-73 (bothy-smoke run) through an SSH forward with a code from issue-enrollment-code; precheck and scan do not apply to an installed stack. Idempotence: a snapshot before and after the SSH delivery (file hashes, the listing of secrets/ without contents, packages, firewall, container IDs and start times) must be equal, and the next plan empty. Two broken copies (a changed bundle; the changed bundle with a matching SHA256SUMS under the old signature) must stop the setup in the step verify before the stack directory exists. A reboot at the end must load the firewall without an ordering cycle in the journal and bring the stack back through bothy.service (discovery 200). QEMU is installed with Homebrew on the build host; no CI job (D-W-127). The brief's Design note 2 with the trial of M7-29 as the only release path; no cloud account (D-W-125).