# cloud-init: create the unprivileged `ansible` user with ubongo's key + sudo. # (Mirrors the proxmox_vm module's user_account; Hetzner has no structured field.) locals { # Indentation matches the closing EOT (2 spaces) so `<<-` strips to column 0 — # cloud-config requires `#cloud-config` as the first line with no leading space. user_data = <<-EOT #cloud-config users: - name: ansible groups: [sudo] sudo: "ALL=(ALL) NOPASSWD:ALL" shell: /bin/bash ssh_authorized_keys: - ${var.ansible_ssh_pubkey} package_update: true packages: - python3 EOT } resource "hcloud_ssh_key" "ansible" { name = "${var.name}-ansible" public_key = var.ansible_ssh_pubkey } resource "hcloud_firewall" "this" { name = "${var.name}-fw" # SSH from the control node only — and only when admin CIDRs are set. An empty # ssh_admin_cidrs removes the WAN :22 rule entirely (mesh-only SSH; reach the host over # wt0, break-glass = Hetzner console). Mesh-hardening 1/3. dynamic "rule" { for_each = length(var.ssh_admin_cidrs) > 0 ? [1] : [] content { direction = "in" protocol = "tcp" port = "22" source_ips = var.ssh_admin_cidrs } } # Public web (Caddy 80/443) + NetBird STUN/TURN (3478/udp) — only when public_web # (ADR-024, M4). Host nftables stays catalog-driven (ADR-020). dynamic "rule" { for_each = var.public_web ? ["80", "443"] : [] content { direction = "in" protocol = "tcp" port = rule.value source_ips = ["0.0.0.0/0", "::/0"] } } dynamic "rule" { for_each = var.public_web ? ["3478"] : [] content { direction = "in" protocol = "udp" port = rule.value source_ips = ["0.0.0.0/0", "::/0"] } } } resource "hcloud_server" "this" { name = var.name server_type = var.server_type location = var.location image = var.image ssh_keys = [hcloud_ssh_key.ansible.id] user_data = local.user_data firewall_ids = [hcloud_firewall.this.id] labels = var.labels public_net { ipv4_enabled = true ipv6_enabled = true } }