Skip to content

M7-16 OpenTofu modules hetzner, aws and azure

No Date Ruling Reason
D-M7-16-0 2026-10-20 Base int/m1-wave50 at d200587, branch feat/M7-16-tofu-cloud-modules in its own worktree, with the built but not yet integrated feat/M7-15-tofu-generic-vm at add1b93 merged in first (the conflict in this file resolved as a union); not merged locally and not pushed as a pull request by this build, the full verification runs on the build branch and the PR lane lands it, which replaces the brief's landing steps. The brief relies on no core rule or D-W ruling the base lacks (D-W-100 and D-W-125 are here; docs/OPEN-QUESTIONS.md with OQ-09 is missing on the base, a known gap), so nothing is ported first. Names that differ from the brief (D-W-100): the chapter is docs/spec/16-operations.md; the root of the tofu test files is infra/tofu/test/cloud/, the static test tools/ci/tests/test_tofu_cloud_modules.py, the provider mirror tools/bin/tofu_providers.py. Instructions of the launcher; merged names win (D-W-100).
D-M7-16-1 2026-10-20 Static proof (Design note 1): the pinned OpenTofu 1.13.1 supports tofu test with mock_provider, so infra/tofu/test/cloud/{hetzner,aws,azure}.tftest.hcl apply each module against mocked providers with representative inputs and assert the security rules (only 443 from the internet and only with reverse_proxy, SSH only from ssh_allowed_cidrs, the Azure deny rule), the user data of generic-vm within the limit of the target, the outputs, the off-site bucket of aws and every refusal (expect_failures); make tofu-check runs them. A pytest test reads the configuration for what a plan cannot show: the variable and output names against the contract (requirement 13), one resource per module that admits traffic so the assertions see every rule (requirement 14), no secret name, sensitive flag or secret-holding resource, the enforced encryption of the example root (requirement 15) and the pins against the lock files. No HCL parser was added: tofu fmt fixes the layout the regular expressions read. Mocked plans test the real module code with the real provider schemas; no credential, no cost (D-W-125).
D-M7-16-2 2026-10-20 Offline providers (D-M7-15-3): each module pins its provider to one version (= 1.70.0, = 6.69.1, = 5.10.0); the lock files of test/validate and test/cloud (equal, written by tofu providers lock for linux and darwin, amd64 and arm64) hold the zh: hash of every archive. tools/bin/tofu_providers.py mirror asks the OpenTofu registry for the archive of the current platform, accepts it only when the registry's SHA-256 is one of the locked zh: hashes, checks the downloaded bytes again and stores it in the packed mirror layout in .cache/tofu-providers/ (a cached archive is checked on every call). tofu-check and the check tofu of ops-check use that mirror as the only provider source with a temporary plugin cache, and tofu-check inits with -lockfile=readonly. The registry refuses the default User-Agent of urllib (HTTP 403), so the tool sends its own. Renovate moves pins and lock files in one group ("OpenTofu providers"). The gate stays without registry access during init; the only download is the pinned archive, as for tools/bin/tools.lock.json.
D-M7-16-3 2026-10-20 PostgreSQL 18 per provider (requirement 17), retrieved 2026-10-20: Amazon RDS for PostgreSQL offers 18 with the minor versions 18.1 to 18.6 (https://docs.aws.amazon.com/AmazonRDS/latest/PostgreSQLReleaseNotes/postgresql-versions.html); Azure Database for PostgreSQL flexible server supports 18, current minor 18.6, with some extensions not yet available (https://learn.microsoft.com/en-us/azure/postgresql/flexible-server/concepts-supported-versions); Hetzner Cloud has no managed PostgreSQL service (the provider hetznercloud/hcloud 1.70.0 has no resource for one). Written into 16 3 rule 6 and the module READMEs. Rule 4 (Blob Storage without S3 API) is now verified by the list of access paths in https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blobs-introduction. Requirement 17 and 16 3 rule 6.
D-M7-16-4 2026-10-20 The modules offer only the container path and no external S3 service: database_mode = managed and object_storage_mode = s3 fail the validation of the input with the reason. install.sh (16 3.1) has no option for an external database or an external S3 service, the Compose stack names postgres as the database host, and generic-vm refuses both (D-M7-15-1); an RDS instance, a flexible server or a bucket for the files that the module created would be billed and never used. The unused inputs rds_instance_class (aws) and postgres_sku (azure) of the planned contract are removed; the managed path returns with the setup option, and the PostgreSQL 18 facts of D-M7-16-3 are ready for it. Build the minimum that works; no resource that cannot be used. The setup option is a change of 16 3.1 and of install.sh, outside this brief.
D-M7-16-5 2026-10-20 Off-site append-only per provider (requirement 16, 16 8.2 item 3, R-OP-09): aws with offsite_region creates a bucket in that region with versioning, Object Lock in mode Compliance for offsite_retention_days (default 90, the default of backup.objects.retention), the lifecycle rule of backup-restore.md 2 (noncurrent versions expire after the same days, expired delete markers go), public access blocked, BucketOwnerEnforced, SSE-S3, TLS only, and a writer user whose policy has only ListBucket, ListBucketVersions, GetBucketLocation, GetObject, PutObject and DeleteObject; the location goes to install.sh --offsite. It creates no access key, because its secret would be in the state: the owner creates the writer's key, the separate deleting key and sets backup.offsite.region by hand. offsite_region and backup_offsite exclude each other. hetzner: the provider has no resource for Object Storage, so the owner creates the bucket with Object Lock in the Console and passes it in backup_offsite. azure: bothy-backupd writes to S3 only and Blob Storage has no S3 API, so the target is an S3 bucket elsewhere in backup_offsite. Compliance mode is enforced by the provider for every account; what a module cannot create is written down per README.
D-M7-16-6 2026-10-20 Network and host: the host has a public address in a public subnet (egress for Docker, release and images without a NAT gateway); the security rules admit TCP 443 from the internet only with edge_mode = reverse_proxy and TCP 22 only from the new cloud input ssh_allowed_cidrs (a /0 is refused), nothing else; Azure adds a deny rule for every other inbound packet, AWS keeps every inbound rule in one aws_vpc_security_group_ingress_rule and opens egress (trivy AVD-AWS-0104 ignored with the reason). No load balancer and no Application Gateway: the stack publishes on 127.0.0.1 only, so the TLS proxy of the edge runs on the host as with generic-vm (D-M7-15-5), and 16 3 notes this as built. The setup inputs of generic-vm are inputs of every cloud module and are passed on unchanged. Found by the mocked apply: the user data of generic-vm exceeds the 16 KiB of EC2, so aws passes it gzip-compressed (user_data_base64), and the tests hold the limits of Hetzner (32 KiB) and Azure (64 KiB). hetzner and azure require one SSH key (Hetzner otherwise mails a root password, Azure needs one for the administrator). Changes of user data and image do not replace a host (ignore_changes); a new setup goes over SSH. Only the edge port public (rule 3) with the least that works on each target.
D-M7-16-7 2026-10-20 The example root of requirement 15 is the validation root infra/tofu/test/validate: an encryption block with pbkdf2 and aes_gcm for state and plan, both enforced = true, the passphrase an ephemeral and sensitive input of the root (TF_VAR_state_passphrase); it also configures the three providers without any credential. One root that tofu validate checks in the gate shows what an instance root needs.
D-M7-16-8 2026-10-20 Found by the code review: a host survives changes of its setup inputs on every target. Azure ignores admin_ssh_key and source_image_reference besides custom_data (both force a new VM in azurerm, which would delete the OS disk with the stack and its data); hetzner has the input existing_ssh_keys (names or IDs of keys already in the project), with which it uploads no key, because Hetzner Cloud refuses a public key that the project already holds. A pytest test holds the ignore_changes lists of all three hosts. Rotating a key or a second instance in one project must neither replace a host nor fail.
D-M7-16-9 2026-10-22 Port onto main: the four own commits of the ticket on top of current main (M7-15 landed as #233). main translated the stub READMEs and inputs of the three modules and the operations chapter to English, so the modules, inputs and READMEs of the ticket replace the stubs, the spec change moves from 16-betrieb.md to 16-operations.md (references in the ticket files follow), the doc changes in cloud-migration.md, gates.md, toolchain.md and tools/opscheck/README.md are applied to their English text, and the function main of tools/bin/tofu_providers.py gains the docstring the stricter ruff rule D103 of main asks for. The module code is the code the red team checked; only the ported text, the chapter references in comments, one description and READMEs, and one docstring differ.
D-M7-16-10 2026-10-22 Found by the pull request review: the AWS host gets a public address at launch (associate_public_ip_address = true), because the subnet maps none and the Elastic IP is attached only after the instance exists, so the setup at the first boot could start without egress; Hetzner location also accepts names without a digit (ash, hil, sin); the first key of admin_ssh_public_keys on Azure, the key of the administrator, must be ssh-ed25519 or ssh-rsa, the only types Azure accepts for it (https://learn.microsoft.com/en-us/azure/virtual-machines/linux/create-ssh-keys-detailed, section "Supported SSH key formats", retrieved 2026-10-22). A tofu test run covers each. Each input was accepted by the module and failed only in the real account or at the first boot.