feat: Add bpg/proxmox provider for bare-metal VM management (Step 4.5)
Some checks failed
PR Checks / tofu-checks (pull_request) Failing after 4s
1/1 projects applied successfully.
Some checks failed
PR Checks / tofu-checks (pull_request) Failing after 4s
1/1 projects applied successfully.
- Enable bpg/proxmox provider (~> 0.90) in production environment - Add data source to verify Proxmox connectivity (read nodes) - SOPS-encrypt Proxmox API token (root@pam!tofu) - Custom Atlantis workflow: decrypt SOPS → inject PROXMOX_VE_API_TOKEN - Update all OPA policies for bpg resource types: - proxmox_vm_qemu → proxmox_virtual_environment_vm - proxmox_lxc → proxmox_virtual_environment_container - Adjust field paths (cpu[0].cores, memory[0].dedicated, etc.) - Firewall check: per-network-device instead of top-level - Password check: via after_sensitive for cloud-init - Tags: list of strings instead of comma-separated
This commit is contained in:
parent
f26e327de7
commit
5155f08584
@ -5,6 +5,7 @@ projects:
|
||||
- name: production
|
||||
dir: environments/production
|
||||
workspace: default
|
||||
workflow: proxmox
|
||||
autoplan:
|
||||
when_modified:
|
||||
- "**/*.tf"
|
||||
@ -12,3 +13,20 @@ projects:
|
||||
enabled: true
|
||||
apply_requirements:
|
||||
- approved
|
||||
|
||||
workflows:
|
||||
proxmox:
|
||||
plan:
|
||||
steps:
|
||||
- env:
|
||||
name: PROXMOX_VE_API_TOKEN
|
||||
command: "sops -d --extract '[\"proxmox_api_token\"]' proxmox.secrets.yaml"
|
||||
- init
|
||||
- plan
|
||||
apply:
|
||||
steps:
|
||||
- env:
|
||||
name: PROXMOX_VE_API_TOKEN
|
||||
command: "sops -d --extract '[\"proxmox_api_token\"]' proxmox.secrets.yaml"
|
||||
- init
|
||||
- apply
|
||||
|
||||
@ -16,18 +16,26 @@ terraform {
|
||||
use_path_style = true
|
||||
}
|
||||
|
||||
# Proxmox provider will be added when bare-metal is connected
|
||||
# required_providers {
|
||||
# proxmox = {
|
||||
# source = "bpg/proxmox"
|
||||
# version = "~> 0.66"
|
||||
# }
|
||||
# }
|
||||
required_providers {
|
||||
proxmox = {
|
||||
source = "bpg/proxmox"
|
||||
version = "~> 0.90"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Proxmox provider configuration (uncomment when ready)
|
||||
# provider "proxmox" {
|
||||
# endpoint = var.proxmox_endpoint
|
||||
# api_token = var.proxmox_api_token
|
||||
# insecure = true
|
||||
# }
|
||||
provider "proxmox" {
|
||||
endpoint = "https://217.168.244.244:8006/"
|
||||
insecure = true # self-signed cert
|
||||
|
||||
# api_token read from PROXMOX_VE_API_TOKEN env var
|
||||
# Decrypted from SOPS by Atlantis custom workflow
|
||||
}
|
||||
|
||||
# Verify Proxmox connectivity — read cluster nodes
|
||||
data "proxmox_virtual_environment_nodes" "nodes" {}
|
||||
|
||||
output "proxmox_nodes" {
|
||||
description = "Proxmox cluster node names"
|
||||
value = data.proxmox_virtual_environment_nodes.nodes.names
|
||||
}
|
||||
|
||||
16
environments/production/proxmox.secrets.yaml
Normal file
16
environments/production/proxmox.secrets.yaml
Normal file
@ -0,0 +1,16 @@
|
||||
proxmox_api_token: ENC[AES256_GCM,data:Dg8+7TWwsaDuQ9JJPyWBI6pc+6n3tVbg3TsjMx8OIS6R00eVTD6o2rAF6CTyIvLN2MI=,iv:cPq5O1Fl2azbVQST0+piq/3yA0Br6OZhcmkl52p2f5Q=,tag:P/CHM/ufI2xm/W4pr91QIQ==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- recipient: age1yttnttdpafzn73mf3g8fw4x04444gymwsfrfm99fv9qkcxqzqs7sld8hln
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUU0MyOEhrWXE1K1V2aUEw
|
||||
VFVkcHMzdnhTSUlhUjQ3b2UxYzhmdHQ5OUhVCkhHRHlFbzlhMkViRmxPTWZCUHJy
|
||||
V3BsYUhmOVRYWEpHWkJrMFFyL1liL3cKLS0tIDB4NWVwN3NhUmoyZWp5Rnk4Yit0
|
||||
VUdrSFVpT0FmTklybFpnOHJYbVdtbDgKzocwM5FdTxgbgL3oi344BH/2Z4oKWDN4
|
||||
mzeExtxt+cg4KGvQXamQIzqwso4j9QrYpOB76EfWhLUL8ijGsdcWlQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
lastmodified: "2026-02-11T07:09:40Z"
|
||||
mac: ENC[AES256_GCM,data:A89cdpQPFOH/x5PBSwdlv1SpupcSi2wp8DiRl6TNMOUDlQfP9d1ThQNE2a1lDG+H1NGDdP7josvERmZ+Y6IIh0QicyQutSizhZXDtPcNIiGBRHaI74g6Ed4TqSSgrbkZ253JGPvZqzcQOHUrfHykKJavYitHYMbQxwEUKTbamKM=,iv:PIg3H0T0IUgwDa6HjZLFghfxjUwF/8Km1x16cDlvnvQ=,tag:Oe8LU8q8lZDMI66xusZw7A==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.11.0
|
||||
@ -1,13 +1,6 @@
|
||||
# Variables for production environment
|
||||
# Secrets are injected via SOPS or environment variables in Atlantis
|
||||
|
||||
# variable "proxmox_endpoint" {
|
||||
# description = "Proxmox API endpoint URL"
|
||||
# type = string
|
||||
# }
|
||||
|
||||
# variable "proxmox_api_token" {
|
||||
# description = "Proxmox API token (user@realm!token=secret)"
|
||||
# type = string
|
||||
# sensitive = true
|
||||
# }
|
||||
#
|
||||
# Proxmox API credentials are injected via environment variables:
|
||||
# PROXMOX_VE_API_TOKEN — decrypted from SOPS by Atlantis workflow
|
||||
#
|
||||
# No explicit variables needed — bpg/proxmox provider reads env vars.
|
||||
|
||||
@ -5,25 +5,25 @@ import rego.v1
|
||||
# VMs must not exceed 16 cores
|
||||
deny contains msg if {
|
||||
some resource in input.resource_changes
|
||||
resource.type == "proxmox_vm_qemu"
|
||||
resource.type == "proxmox_virtual_environment_vm"
|
||||
resource.change.actions[_] in {"create", "update"}
|
||||
to_number(resource.change.after.cores) > 16
|
||||
to_number(resource.change.after.cpu[0].cores) > 16
|
||||
|
||||
msg := sprintf(
|
||||
"COST: VM '%s' requests %v cores. Maximum 16. File ADR for exception.",
|
||||
[resource.address, resource.change.after.cores],
|
||||
[resource.address, resource.change.after.cpu[0].cores],
|
||||
)
|
||||
}
|
||||
|
||||
# VMs must not exceed 32 GB RAM
|
||||
deny contains msg if {
|
||||
some resource in input.resource_changes
|
||||
resource.type == "proxmox_vm_qemu"
|
||||
resource.type == "proxmox_virtual_environment_vm"
|
||||
resource.change.actions[_] in {"create", "update"}
|
||||
to_number(resource.change.after.memory) > 32768
|
||||
to_number(resource.change.after.memory[0].dedicated) > 32768
|
||||
|
||||
msg := sprintf(
|
||||
"COST: VM '%s' requests %v MB RAM. Maximum 32768 (32 GB). File ADR for exception.",
|
||||
[resource.address, resource.change.after.memory],
|
||||
[resource.address, resource.change.after.memory[0].dedicated],
|
||||
)
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ deny contains msg if {
|
||||
some resource in input.resource_changes
|
||||
resource.change.actions[_] == "delete"
|
||||
|
||||
stateful := {"proxmox_vm_qemu", "proxmox_lxc", "docker_volume"}
|
||||
stateful := {"proxmox_virtual_environment_vm", "proxmox_virtual_environment_container", "docker_volume"}
|
||||
resource.type in stateful
|
||||
|
||||
msg := sprintf(
|
||||
@ -22,7 +22,7 @@ deny contains msg if {
|
||||
actions := resource.change.actions
|
||||
"delete" in actions
|
||||
"create" in actions
|
||||
startswith(resource.type, "proxmox_vm")
|
||||
startswith(resource.type, "proxmox_virtual_environment_vm")
|
||||
|
||||
msg := sprintf(
|
||||
"BLOCKED: Resource '%s' will be REPLACED (destroy + recreate). Data loss risk.",
|
||||
|
||||
@ -4,16 +4,16 @@ import rego.v1
|
||||
|
||||
required_tags := {"environment", "managed_by"}
|
||||
|
||||
# Resources being created or updated must have required tags
|
||||
# VMs being created or updated must have required tags
|
||||
warn contains msg if {
|
||||
some resource in input.resource_changes
|
||||
resource.type == "proxmox_virtual_environment_vm"
|
||||
resource.change.actions[_] in {"create", "update"}
|
||||
resource.type == "proxmox_vm_qemu"
|
||||
|
||||
tags := object.get(resource.change.after, "tags", "")
|
||||
tags := object.get(resource.change.after, "tags", [])
|
||||
|
||||
some tag in required_tags
|
||||
not contains(tags, tag)
|
||||
not tag in tags
|
||||
|
||||
msg := sprintf(
|
||||
"TAGS: VM '%s' missing recommended tag '%s'.",
|
||||
|
||||
@ -2,14 +2,16 @@ package main
|
||||
|
||||
import rego.v1
|
||||
|
||||
# VMs must have firewall enabled
|
||||
# All network devices on VMs must have firewall enabled
|
||||
deny contains msg if {
|
||||
some resource in input.resource_changes
|
||||
resource.type == "proxmox_vm_qemu"
|
||||
resource.change.after.firewall == false
|
||||
resource.type == "proxmox_virtual_environment_vm"
|
||||
resource.change.actions[_] in {"create", "update"}
|
||||
some nd in resource.change.after.network_device
|
||||
nd.firewall != true
|
||||
|
||||
msg := sprintf(
|
||||
"SECURITY: VM '%s' has firewall disabled. All VMs must have firewall enabled.",
|
||||
"SECURITY: VM '%s' has a network device with firewall disabled. All NICs must have firewall enabled.",
|
||||
[resource.address],
|
||||
)
|
||||
}
|
||||
@ -17,11 +19,12 @@ deny contains msg if {
|
||||
# No password authentication on VMs — SSH keys only
|
||||
deny contains msg if {
|
||||
some resource in input.resource_changes
|
||||
resource.type == "proxmox_vm_qemu"
|
||||
resource.change.after.cipassword != null
|
||||
resource.type == "proxmox_virtual_environment_vm"
|
||||
resource.change.actions[_] in {"create", "update"}
|
||||
resource.change.after_sensitive.initialization[0].user_account[0].password == true
|
||||
|
||||
msg := sprintf(
|
||||
"SECURITY: VM '%s' uses password auth. Use SSH keys only (sshkeys parameter).",
|
||||
"SECURITY: VM '%s' uses password auth. Use SSH keys only.",
|
||||
[resource.address],
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user