2026-04-06 11:57:55 +03:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Variables — Required
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
2025-10-12 18:54:22 +00:00
|
|
|
variable "vm_name" { type = string }
|
|
|
|
|
variable "vmid" {
|
|
|
|
|
type = number
|
|
|
|
|
default = 0
|
|
|
|
|
}
|
2026-04-06 11:57:55 +03:00
|
|
|
variable "cisnippet_name" {
|
|
|
|
|
type = string
|
|
|
|
|
default = ""
|
|
|
|
|
}
|
|
|
|
|
variable "bridge" { type = string }
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Variables — VM sizing
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
2021-02-07 23:45:55 +00:00
|
|
|
variable "vm_cpus" {
|
|
|
|
|
type = number
|
|
|
|
|
default = 4
|
|
|
|
|
}
|
2026-04-06 11:57:55 +03:00
|
|
|
variable "cpu_sockets" {
|
|
|
|
|
type = number
|
|
|
|
|
default = 1
|
|
|
|
|
}
|
2025-10-11 17:12:02 +00:00
|
|
|
variable "vm_mem_mb" {
|
2021-02-07 23:45:55 +00:00
|
|
|
type = number
|
2025-10-11 17:12:02 +00:00
|
|
|
default = 8192
|
2021-02-07 23:45:55 +00:00
|
|
|
}
|
|
|
|
|
variable "vm_disk_size" {
|
|
|
|
|
type = string
|
2025-10-11 17:12:02 +00:00
|
|
|
default = "64G"
|
2021-02-07 23:45:55 +00:00
|
|
|
}
|
2026-04-06 11:57:55 +03:00
|
|
|
variable "balloon" {
|
|
|
|
|
type = number
|
|
|
|
|
default = 0 # 0 = disabled (recommended for k8s nodes)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Variables — VM identity & networking
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
2021-02-07 23:45:55 +00:00
|
|
|
variable "vm_mac_address" {
|
|
|
|
|
type = string
|
2025-10-11 17:12:02 +00:00
|
|
|
default = null
|
2021-02-07 23:45:55 +00:00
|
|
|
}
|
2025-10-11 20:40:34 +00:00
|
|
|
variable "vlan_tag" {
|
|
|
|
|
type = string
|
|
|
|
|
default = null
|
|
|
|
|
}
|
2026-02-14 13:09:03 +00:00
|
|
|
variable "ipconfig0" {
|
|
|
|
|
type = string
|
|
|
|
|
default = "ip=dhcp,ip6=dhcp"
|
|
|
|
|
}
|
2026-04-06 11:57:55 +03:00
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Variables — Boot & hardware
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
variable "template_name" {
|
|
|
|
|
type = string
|
|
|
|
|
default = "" # empty = no clone (for importing existing VMs)
|
|
|
|
|
}
|
|
|
|
|
variable "scsihw" {
|
|
|
|
|
type = string
|
|
|
|
|
default = "virtio-scsi-pci"
|
|
|
|
|
}
|
|
|
|
|
variable "boot" {
|
|
|
|
|
type = string
|
|
|
|
|
default = "order=scsi0"
|
|
|
|
|
}
|
|
|
|
|
variable "boot_disk" {
|
|
|
|
|
type = string
|
|
|
|
|
default = "" # e.g., "scsi0" — only set if boot = "c" (legacy)
|
|
|
|
|
}
|
|
|
|
|
variable "disk_slot" {
|
|
|
|
|
type = string
|
|
|
|
|
default = "scsi0" # which SCSI slot the OS disk is on
|
|
|
|
|
}
|
2026-02-22 21:45:53 +00:00
|
|
|
variable "agent" {
|
|
|
|
|
type = number
|
2026-03-01 01:58:46 +00:00
|
|
|
default = 1
|
2026-02-22 21:45:53 +00:00
|
|
|
}
|
2026-04-06 11:57:55 +03:00
|
|
|
variable "qemu_os" {
|
|
|
|
|
type = string
|
|
|
|
|
default = "l26"
|
|
|
|
|
}
|
|
|
|
|
variable "numa" {
|
|
|
|
|
type = bool
|
|
|
|
|
default = false
|
|
|
|
|
}
|
|
|
|
|
variable "machine" {
|
|
|
|
|
type = string
|
|
|
|
|
default = "" # empty = provider default. Use "q35" for GPU passthrough
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Variables — Startup/shutdown ordering
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
variable "startup_order" {
|
|
|
|
|
type = number
|
|
|
|
|
default = -1
|
|
|
|
|
}
|
|
|
|
|
variable "startup_delay" {
|
|
|
|
|
type = number
|
|
|
|
|
default = -1
|
|
|
|
|
}
|
|
|
|
|
variable "shutdown_timeout" {
|
|
|
|
|
type = number
|
|
|
|
|
default = -1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Variables — Cloud-Init (optional — disable for non-cloud-init VMs)
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
variable "use_cloud_init" {
|
|
|
|
|
type = bool
|
|
|
|
|
default = true
|
|
|
|
|
}
|
|
|
|
|
variable "ssh_keys" {
|
|
|
|
|
type = string
|
|
|
|
|
default = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDHLhYDfyx237eJgOGVoJRECpUS95+7rEBS9vacsIxtx devvm"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Variables — GPU / PCI passthrough
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
variable "hostpci0" {
|
|
|
|
|
type = string
|
|
|
|
|
default = "" # e.g., "0000:06:00.0" for Tesla T4 passthrough
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Resource
|
|
|
|
|
# ---------------------------------------------------------------------------
|
2021-02-07 23:45:55 +00:00
|
|
|
|
2025-10-11 17:12:02 +00:00
|
|
|
resource "proxmox_vm_qemu" "cloudinit-vm" {
|
2025-10-11 20:40:34 +00:00
|
|
|
vmid = var.vmid
|
2021-02-07 23:45:55 +00:00
|
|
|
name = var.vm_name
|
2025-10-11 17:12:02 +00:00
|
|
|
target_node = "pve"
|
2026-02-22 21:45:53 +00:00
|
|
|
agent = var.agent
|
2025-10-11 17:12:02 +00:00
|
|
|
memory = var.vm_mem_mb
|
2026-04-06 11:57:55 +03:00
|
|
|
balloon = var.balloon
|
|
|
|
|
boot = var.boot
|
|
|
|
|
bootdisk = var.boot_disk != "" ? var.boot_disk : null
|
|
|
|
|
clone = var.template_name != "" ? var.template_name : null
|
|
|
|
|
full_clone = var.template_name != "" ? true : false
|
|
|
|
|
scsihw = var.scsihw
|
2025-10-11 17:12:02 +00:00
|
|
|
vm_state = "running"
|
2026-04-06 11:57:55 +03:00
|
|
|
automatic_reboot = false # never let Terraform reboot VMs — use /reboot-server skill instead
|
|
|
|
|
os_type = var.use_cloud_init ? "cloud-init" : null
|
|
|
|
|
machine = var.machine != "" ? var.machine : null
|
|
|
|
|
|
|
|
|
|
# Cloud-Init configuration (only when use_cloud_init = true)
|
|
|
|
|
cicustom = var.use_cloud_init && var.cisnippet_name != "" ? "vendor=local:snippets/${var.cisnippet_name}" : null
|
|
|
|
|
ciupgrade = var.use_cloud_init ? true : null
|
|
|
|
|
nameserver = var.use_cloud_init ? "1.1.1.1 8.8.8.8" : null
|
|
|
|
|
ipconfig0 = var.use_cloud_init ? var.ipconfig0 : null
|
|
|
|
|
skip_ipv6 = var.use_cloud_init ? true : null
|
|
|
|
|
ciuser = var.use_cloud_init ? "root" : null
|
|
|
|
|
cipassword = var.use_cloud_init ? "root" : null
|
|
|
|
|
sshkeys = var.use_cloud_init ? var.ssh_keys : null
|
|
|
|
|
searchdomain = var.use_cloud_init ? "viktorbarzin.lan" : null
|
|
|
|
|
|
|
|
|
|
start_at_node_boot = true
|
|
|
|
|
qemu_os = var.qemu_os
|
2025-10-11 17:12:02 +00:00
|
|
|
|
|
|
|
|
cpu {
|
2026-04-06 11:57:55 +03:00
|
|
|
cores = var.vm_cpus
|
|
|
|
|
sockets = var.cpu_sockets
|
|
|
|
|
type = "host"
|
2021-02-07 23:45:55 +00:00
|
|
|
}
|
2026-04-06 11:57:55 +03:00
|
|
|
|
2025-12-29 10:19:22 +00:00
|
|
|
startup_shutdown {
|
2026-04-06 11:57:55 +03:00
|
|
|
order = var.startup_order
|
|
|
|
|
shutdown_timeout = var.shutdown_timeout
|
|
|
|
|
startup_delay = var.startup_delay
|
2025-12-29 10:19:22 +00:00
|
|
|
}
|
2021-02-07 23:45:55 +00:00
|
|
|
|
2025-10-11 17:12:02 +00:00
|
|
|
serial {
|
|
|
|
|
id = 0
|
2021-02-07 23:45:55 +00:00
|
|
|
}
|
2026-04-06 11:57:55 +03:00
|
|
|
|
2025-10-11 17:12:02 +00:00
|
|
|
disks {
|
|
|
|
|
scsi {
|
2026-04-06 11:57:55 +03:00
|
|
|
dynamic "scsi0" {
|
|
|
|
|
for_each = var.disk_slot == "scsi0" ? [1] : []
|
|
|
|
|
content {
|
|
|
|
|
disk {
|
ingress_factory: replace `protected` bool with `auth` enum + audit pass across 100 stacks
Phase 3+4 of default-deny ingress plan. Replaces the `protected = bool` (default
false → unprotected) variable in `modules/kubernetes/ingress_factory` with
`auth = string` enum (default "required" → fail-closed). Touches every
ingress_factory caller so the audit decision is recorded explicitly in code.
ingress_factory (Phase 3):
- `auth = "required"`: standard Authentik forward-auth (the legacy
`protected = true` semantic).
- `auth = "public"`: forward-auth via the new `authentik-forward-auth-public`
middleware → dedicated public outpost → guest auto-bind. Logged-in users
keep their real identity.
- `auth = "none"`: no Authentik middleware. For Anubis-fronted content, native
client APIs (Git, /v2/, WebDAV), webhook receivers, the Authentik outpost
itself.
- `effective_anti_ai` default flips ON only when `auth = "none"` (auth-gated
ingresses don't need anti-AI noise; the auth flow already discourages bots).
Audit pass (Phase 4) across 96 ingress_factory call sites:
- 49 explicit `protected = true` → `auth = "required"`
- 8 explicit `protected = false` → `auth = "none"` (5) or `auth = "public"` (3)
- 64 previously-default (no protected line) → `auth = "required"` ADDED, then
reviewed individually:
* 9 Anubis-fronted (blog, www, kms, travel, f1, cyberchef, jsoncrack,
homepage, wrongmove UI, privatebin) → `auth = "none"`
* 22 native-client / programmatic surfaces (Forgejo Git+/v2/, webhook
handler, claude-memory MCP, Nextcloud WebDAV, Matrix, Vault CLI/OIDC,
xray VPN, ntfy, woodpecker webhooks, n8n triggers, ntfy push, dawarich
location ingestion, immich frame kiosk, headscale CP, send anonymous
drops, rybbit beacon, vaultwarden API, Authentik UI itself + outposts) →
`auth = "none"`
* Remaining ~33 → `auth = "required"` confirmed (admin tools, internal
UIs, services without app-level auth)
- Smoke-test promotions to `auth = "public"`: fire-planner public UI,
k8s-portal API, insta2spotify callback.
Three call sites in wrapper modules (`stacks/freedify/factory/`,
`stacks/reverse-proxy/modules/reverse_proxy/`) keep their internal `protected`
bool — they translate to `auth` internally, out of scope for this rename.
Behavior change: previously-default ingresses now fail closed (require
Authentik login) unless explicitly flipped to `auth = "none"` or
`auth = "public"`. This is the audit goal — no more accidentally-unprotected
surfaces. Sites that were intentionally public (Anubis content, native APIs,
webhooks) are now explicitly recorded as `auth = "none"`.
Drive-by: `modules/create-vm/main.tf` picked up cosmetic alignment via
`terraform fmt -recursive` during the audit. Behavior-neutral.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 18:53:49 +00:00
|
|
|
storage = "local-lvm"
|
|
|
|
|
size = var.vm_disk_size
|
|
|
|
|
discard = true # Enable TRIM passthrough to LVM thin pool — reduces CoW overhead
|
2026-04-06 11:57:55 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
dynamic "scsi1" {
|
|
|
|
|
for_each = var.disk_slot == "scsi1" ? [1] : []
|
|
|
|
|
content {
|
|
|
|
|
disk {
|
ingress_factory: replace `protected` bool with `auth` enum + audit pass across 100 stacks
Phase 3+4 of default-deny ingress plan. Replaces the `protected = bool` (default
false → unprotected) variable in `modules/kubernetes/ingress_factory` with
`auth = string` enum (default "required" → fail-closed). Touches every
ingress_factory caller so the audit decision is recorded explicitly in code.
ingress_factory (Phase 3):
- `auth = "required"`: standard Authentik forward-auth (the legacy
`protected = true` semantic).
- `auth = "public"`: forward-auth via the new `authentik-forward-auth-public`
middleware → dedicated public outpost → guest auto-bind. Logged-in users
keep their real identity.
- `auth = "none"`: no Authentik middleware. For Anubis-fronted content, native
client APIs (Git, /v2/, WebDAV), webhook receivers, the Authentik outpost
itself.
- `effective_anti_ai` default flips ON only when `auth = "none"` (auth-gated
ingresses don't need anti-AI noise; the auth flow already discourages bots).
Audit pass (Phase 4) across 96 ingress_factory call sites:
- 49 explicit `protected = true` → `auth = "required"`
- 8 explicit `protected = false` → `auth = "none"` (5) or `auth = "public"` (3)
- 64 previously-default (no protected line) → `auth = "required"` ADDED, then
reviewed individually:
* 9 Anubis-fronted (blog, www, kms, travel, f1, cyberchef, jsoncrack,
homepage, wrongmove UI, privatebin) → `auth = "none"`
* 22 native-client / programmatic surfaces (Forgejo Git+/v2/, webhook
handler, claude-memory MCP, Nextcloud WebDAV, Matrix, Vault CLI/OIDC,
xray VPN, ntfy, woodpecker webhooks, n8n triggers, ntfy push, dawarich
location ingestion, immich frame kiosk, headscale CP, send anonymous
drops, rybbit beacon, vaultwarden API, Authentik UI itself + outposts) →
`auth = "none"`
* Remaining ~33 → `auth = "required"` confirmed (admin tools, internal
UIs, services without app-level auth)
- Smoke-test promotions to `auth = "public"`: fire-planner public UI,
k8s-portal API, insta2spotify callback.
Three call sites in wrapper modules (`stacks/freedify/factory/`,
`stacks/reverse-proxy/modules/reverse_proxy/`) keep their internal `protected`
bool — they translate to `auth` internally, out of scope for this rename.
Behavior change: previously-default ingresses now fail closed (require
Authentik login) unless explicitly flipped to `auth = "none"` or
`auth = "public"`. This is the audit goal — no more accidentally-unprotected
surfaces. Sites that were intentionally public (Anubis content, native APIs,
webhooks) are now explicitly recorded as `auth = "none"`.
Drive-by: `modules/create-vm/main.tf` picked up cosmetic alignment via
`terraform fmt -recursive` during the audit. Behavior-neutral.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 18:53:49 +00:00
|
|
|
storage = "local-lvm"
|
|
|
|
|
size = var.vm_disk_size
|
|
|
|
|
discard = true
|
2026-04-06 11:57:55 +03:00
|
|
|
}
|
2025-10-11 17:12:02 +00:00
|
|
|
}
|
|
|
|
|
}
|
2021-02-07 23:45:55 +00:00
|
|
|
}
|
2026-04-06 11:57:55 +03:00
|
|
|
dynamic "ide" {
|
|
|
|
|
for_each = var.use_cloud_init ? [1] : []
|
|
|
|
|
content {
|
|
|
|
|
ide1 {
|
|
|
|
|
cloudinit {
|
|
|
|
|
storage = "local-lvm"
|
|
|
|
|
}
|
2025-10-11 17:12:02 +00:00
|
|
|
}
|
|
|
|
|
}
|
2021-02-07 23:45:55 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-06 11:57:55 +03:00
|
|
|
|
2025-10-11 17:12:02 +00:00
|
|
|
network {
|
|
|
|
|
id = 0
|
2025-10-11 17:24:26 +00:00
|
|
|
bridge = var.bridge
|
2025-12-14 09:46:39 +00:00
|
|
|
model = "virtio"
|
2025-10-11 17:12:02 +00:00
|
|
|
macaddr = var.vm_mac_address
|
2025-10-11 20:40:34 +00:00
|
|
|
tag = var.vlan_tag
|
2021-02-07 23:45:55 +00:00
|
|
|
}
|
2026-04-06 11:57:55 +03:00
|
|
|
|
|
|
|
|
# Safety: ignore dynamically-attached iSCSI PVC disks (managed by democratic-csi)
|
|
|
|
|
# and cloud-init changes that drift after initial provisioning
|
|
|
|
|
lifecycle {
|
|
|
|
|
prevent_destroy = true
|
|
|
|
|
ignore_changes = [
|
|
|
|
|
# democratic-csi dynamically attaches/detaches iSCSI disks
|
|
|
|
|
disks[0].scsi[0].scsi1,
|
|
|
|
|
disks[0].scsi[0].scsi2,
|
|
|
|
|
disks[0].scsi[0].scsi3,
|
|
|
|
|
disks[0].scsi[0].scsi4,
|
|
|
|
|
disks[0].scsi[0].scsi5,
|
|
|
|
|
# cloud-init config may drift after first boot
|
|
|
|
|
cicustom,
|
|
|
|
|
ciupgrade,
|
|
|
|
|
ciuser,
|
|
|
|
|
cipassword,
|
|
|
|
|
sshkeys,
|
|
|
|
|
# SMBIOS UUID and vmgenid are auto-generated
|
|
|
|
|
smbios,
|
|
|
|
|
# Tags and description may be edited in Proxmox UI
|
|
|
|
|
tags,
|
|
|
|
|
desc,
|
|
|
|
|
# Provider defaults that differ from imported state
|
|
|
|
|
define_connection_info,
|
|
|
|
|
full_clone,
|
|
|
|
|
]
|
|
|
|
|
}
|
2021-02-07 23:45:55 +00:00
|
|
|
}
|