root 74eeabb354
Some checks failed
PR Checks / tofu-checks (pull_request) Failing after 2s
1/1 projects applied successfully.
feat: add tenant VM module for VM-as-a-Service (Step 5.2)
Reusable OpenTofu module for creating isolated tenant VMs with:
- Public IP on vmbr1 (bridged, firewall=true)
- Cloud-init: password auth, fail2ban, UFW hardening
- Per-VM Proxmox firewall (IN: SSH+ICMP, OUT: allow, block SMTP)

Includes test-tenant VM (185.47.204.227) for verification.

Changes:
- modules/tenant-vm/ — reusable module (VM + FW + cloud-init)
- environments/production/tenant-vms.tf — tenant VM definitions
- policies/security.rego — require firewall=true on vmbr1
- atlantis.yaml — trigger on module file changes
- main.tf — updated host prerequisites comment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 20:01:38 +01:00

134 lines
3.4 KiB
HCL

# Tenant VM module — creates an isolated VM with public IP on vmbr1
#
# Resources created:
# 1. Cloud-init snippet (user, password, fail2ban, UFW)
# 2. VM with public IP on vmbr1 (firewall=true)
# 3. Per-VM Proxmox firewall (IN: SSH+ICMP, OUT: allow, block SMTP)
terraform {
required_providers {
proxmox = {
source = "bpg/proxmox"
version = "~> 0.90"
}
}
}
# ─── Cloud-init snippet ──────────────────────────────────────────────────────
resource "proxmox_virtual_environment_file" "cloud_init" {
content_type = "snippets"
datastore_id = "local"
node_name = var.node_name
source_raw {
data = templatefile("${path.module}/cloud-init.yaml.tftpl", {
hostname = var.name
username = var.username
password = var.password
ssh_key = var.ssh_public_key
})
file_name = "ci-${var.name}.yaml"
}
}
# ─── VM ───────────────────────────────────────────────────────────────────────
resource "proxmox_virtual_environment_vm" "tenant" {
depends_on = [proxmox_virtual_environment_file.cloud_init]
name = var.name
node_name = var.node_name
vm_id = var.vm_id
tags = ["tenant", "tofu", "ubuntu"]
stop_on_destroy = true
started = var.started
on_boot = true # tenant VMs auto-start on host reboot
cpu {
cores = var.cpu_cores
type = "x86-64-v2-AES"
}
memory {
dedicated = var.ram_mb
}
disk {
datastore_id = "local"
file_id = "local:iso/ubuntu-24.04-cloudimg-amd64.img"
interface = "virtio0"
size = var.disk_gb
file_format = "qcow2"
discard = "on"
iothread = true
}
network_device {
bridge = "vmbr1"
firewall = true # Per-VM Proxmox firewall on bridged interface
}
initialization {
datastore_id = "local"
user_data_file_id = proxmox_virtual_environment_file.cloud_init.id
ip_config {
ipv4 {
address = "${var.public_ip}/${var.subnet_mask}"
gateway = var.gateway
}
}
dns {
servers = ["188.93.16.19", "188.93.17.19"]
}
}
}
# ─── Proxmox Firewall — per-VM options ───────────────────────────────────────
resource "proxmox_virtual_environment_firewall_options" "tenant" {
depends_on = [proxmox_virtual_environment_vm.tenant]
node_name = var.node_name
vm_id = var.vm_id
enabled = true
input_policy = "DROP"
output_policy = "ACCEPT"
}
# ─── Proxmox Firewall — per-VM rules ─────────────────────────────────────────
resource "proxmox_virtual_environment_firewall_rules" "tenant" {
depends_on = [proxmox_virtual_environment_vm.tenant]
node_name = var.node_name
vm_id = var.vm_id
rule {
type = "in"
action = "ACCEPT"
proto = "tcp"
dport = "22"
comment = "Allow SSH"
}
rule {
type = "in"
action = "ACCEPT"
proto = "icmp"
comment = "Allow ICMP"
}
rule {
type = "out"
action = "DROP"
proto = "tcp"
dport = "25"
comment = "Block SMTP (anti-spam)"
}
}