Working tests of policy and policies

This commit is contained in:
2025-05-19 21:30:18 +03:00
parent 9b34cd9199
commit 9c67e03de3
7 changed files with 184 additions and 248 deletions

View File

@@ -28,9 +28,12 @@ Security policies are defined as Open Policy Agent (OPA) Rego files in the `poli
sudo mv conftest /usr/local/bin sudo mv conftest /usr/local/bin
``` ```
2. Install tfsec: 2. Install Trivy:
```bash ```bash
curl -s https://raw.githubusercontent.com/aquasecurity/tfsec/master/scripts/install_linux.sh | bash # For Debian/Ubuntu
sudo apt-get install trivy
# For other systems, see: https://aquasecurity.github.io/trivy/latest/getting-started/installation/
``` ```
3. Install Checkov: 3. Install Checkov:
@@ -53,67 +56,11 @@ Security policies are defined as Open Policy Agent (OPA) Rego files in the `poli
conftest test tfplan.json -p policy/ conftest test tfplan.json -p policy/
``` ```
3. Run tfsec static analysis: 3. Run Trivy IaC security scan:
```bash ```bash
tfsec . # Skip AWS policies and use variables file
trivy config --severity HIGH,CRITICAL --skip-policy "aws.*" --tf-vars="variables.tfvars" .
``` ```
4. Run Checkov: 4. Run Checkov:
```bash
checkov -d .
``` ```
## Security Rules
The following security rules are enforced:
### VM Security
- No password authentication allowed (use SSH keys)
- No root user login allowed
- qemu-agent must be enabled
### Network Security
- Only secure network bridge (vmbr2) allowed
- IPv6 must be disabled
- Only approved DNS servers allowed
### Provider Security
- TLS verification must be enabled
- Provider version must be pinned
- Timeout values must be reasonable
## Security Best Practices
1. Use environment variables for sensitive values:
```bash
export TF_VAR_pm_password="your-password"
```
2. Keep provider versions pinned in `.terraform.lock.hcl`:
```bash
# Pre-populate hashes for multiple platforms
terraform providers lock \
-platform=linux_amd64 \
-platform=darwin_amd64 \
-platform=windows_amd64
```
3. Never commit plain-text secrets (use a vault solution)
4. Always verify TLS certificates (`pm_tls_insecure = false`)
5. Use Terraform workspaces for better environment separation
## Policy Testing
The policy tests verify:
1. Policy evaluation is working
2. Terraform plan data is loaded correctly
3. Security rules are being checked
Run tests with:
```bash
conftest test tfplan.json -p policy/
```
A successful test run will show passed tests and any security violations found in your configuration.

View File

@@ -1,13 +1,10 @@
package main package main
import future.keywords.in
# Import the raw input instead of using tfplan alias # Import the raw input instead of using tfplan alias
import input import input
# Helper function to get VM resources vms := [r | r := input.planned_values.root_module.resources[_]; r.type == "proxmox_vm_qemu"]
get_vms = vms {
planned_values := input.planned_values
vms := [r | r := planned_values.root_module.resources[_]; r.type == "proxmox_vm_qemu"]
}
# Helper function to check if a value is empty or undefined # Helper function to check if a value is empty or undefined
is_empty(value) { is_empty(value) {
@@ -23,28 +20,28 @@ min_memory = 512
# Deny if VM allows password authentication # Deny if VM allows password authentication
deny[msg] { deny[msg] {
vm := get_vms[_] some vm in vms
not is_empty(vm.values.cipassword) not is_empty(vm.values.cipassword)
msg := sprintf("VM '%s' uses password authentication. Use SSH keys only.", [vm.name]) msg := sprintf("VM '%s' uses password authentication. Use SSH keys only.", [vm.name])
} }
# Deny if VM allows root login # Deny if VM allows root login
deny[msg] { deny[msg] {
vm := get_vms[_] some vm in vms
vm.values.ciuser == "root" vm.values.ciuser == "root"
msg := sprintf("VM '%s' allows root login. Use a non-root user.", [vm.name]) msg := sprintf("VM '%s' allows root login. Use a non-root user.", [vm.name])
} }
# Deny if qemu-agent is not enabled # Deny if qemu-agent is not enabled
deny[msg] { deny[msg] {
vm := get_vms[_] some vm in vms
vm.values.agent != 1 vm.values.agent != 1
msg := sprintf("VM '%s' does not have qemu-agent enabled (agent = 1).", [vm.name]) msg := sprintf("VM '%s' does not have qemu-agent enabled (agent = 1).", [vm.name])
} }
# Deny if VM uses insecure network bridge # Deny if VM uses insecure network bridge
deny[msg] { deny[msg] {
vm := get_vms[_] some vm in vms
net := vm.values.network[_] net := vm.values.network[_]
net.bridge != "vmbr2" net.bridge != "vmbr2"
msg := sprintf("VM '%s' uses insecure network bridge '%s'. Use 'vmbr2'.", [vm.name, net.bridge]) msg := sprintf("VM '%s' uses insecure network bridge '%s'. Use 'vmbr2'.", [vm.name, net.bridge])
@@ -52,15 +49,15 @@ deny[msg] {
# Deny if IPv6 is not disabled # Deny if IPv6 is not disabled
deny[msg] { deny[msg] {
vm := get_vms[_] some vm in vms
not vm.values.skip_ipv6 not vm.values.skip_ipv6
msg := sprintf("VM '%s' does not have IPv6 disabled (skip_ipv6 = true).", [vm.name]) msg := sprintf("VM '%s' does not have IPv6 disabled (skip_ipv6 = true).", [vm.name])
} }
# Deny if TLS verification is disabled # Deny if TLS verification is disabled
deny[msg] { deny[msg] {
provider := input.configuration.provider_config.proxmox tls_enabled := input.variables.pm_tls_insecure.value
provider.expressions.pm_tls_insecure.constant_value == true tls_enabled == true
msg := "TLS verification must be enabled (pm_tls_insecure = false)" msg := "TLS verification must be enabled (pm_tls_insecure = false)"
} }
@@ -73,7 +70,7 @@ deny[msg] {
# Deny if VM memory is below minimum requirement # Deny if VM memory is below minimum requirement
deny[msg] { deny[msg] {
vm := get_vms[_] some vm in vms
memory := to_number(vm.values.memory) memory := to_number(vm.values.memory)
memory < min_memory memory < min_memory
msg := sprintf("VM '%s' has insufficient memory (%dMB). Minimum required: %dMB.", [vm.name, memory, min_memory]) msg := sprintf("VM '%s' has insufficient memory (%dMB). Minimum required: %dMB.", [vm.name, memory, min_memory])
@@ -81,14 +78,14 @@ deny[msg] {
# Deny if VM does not have a description # Deny if VM does not have a description
deny[msg] { deny[msg] {
vm := get_vms[_] some vm in vms
is_empty(vm.values.desc) is_empty(vm.values.desc)
msg := sprintf("VM '%s' must have a description for documentation purposes.", [vm.name]) msg := sprintf("VM '%s' must have a description for documentation purposes.", [vm.name])
} }
# Deny if VM uses default SCSI controller # Deny if VM uses default SCSI controller
deny[msg] { deny[msg] {
vm := get_vms[_] some vm in vms
vm.values.scsihw == "lsi" vm.values.scsihw == "lsi"
msg := sprintf("VM '%s' uses default SCSI controller. Use virtio-scsi-pci for better performance.", [vm.name]) msg := sprintf("VM '%s' uses default SCSI controller. Use virtio-scsi-pci for better performance.", [vm.name])
} }

View File

@@ -1,96 +1,111 @@
package main package main
# Test data
mock_secure_vm := {
"type": "proxmox_vm_qemu",
"name": "secure_vm",
"values": {
"cipassword": "",
"ciuser": "admin",
"agent": 1,
"network": [{
"bridge": "vmbr2"
}],
"skip_ipv6": true,
"memory": 2048,
"desc": "Production web server",
"scsihw": "virtio-scsi-pci",
"cpu": "host",
"backup": true,
"tags": "prod,web"
}
}
mock_insecure_vm := {
"type": "proxmox_vm_qemu",
"name": "insecure_vm",
"values": {
"cipassword": "password123",
"ciuser": "root",
"agent": 0,
"network": [{
"bridge": "vmbr0"
}],
"skip_ipv6": false,
"memory": 256,
"desc": "",
"scsihw": "lsi",
"cpu": "",
"backup": false,
"tags": ""
}
}
mock_input_secure := { mock_input_secure := {
"planned_values": { "variables":
"root_module": { {
"resources": [mock_secure_vm] "pm_tls_insecure": {
} "value": false
}, }
"configuration": { },
"provider_config": { "planned_values": {
"proxmox": { "root_module": {
"expressions": { "resources": [
"pm_tls_insecure": { {
"constant_value": false "type": "proxmox_vm_qemu",
} "name": "secure_vm",
} "values": {
} "cipassword": "",
}, "ciuser": "admin",
"terraform": { "agent": 1,
"required_providers": { "network": [
"proxmox": { {
"version_constraint": "=2.9.14" "bridge": "vmbr2"
} }
} ],
"skip_ipv6": true,
"memory": 2048,
"desc": "Production web server",
"scsihw": "virtio-scsi-pci",
"cpu": "host",
"backup": true,
"tags": "prod,web"
}
} }
]
} }
},
"configuration": {
"provider_config": {
"proxmox": {
"expressions": {
"pm_tls_insecure": {
"constant_value": false
}
}
}
},
"terraform": {
"required_providers": {
"proxmox": {
"version_constraint": "=2.9.14"
}
}
}
}
} }
mock_input_insecure := { mock_input_insecure := {
"planned_values": { "variables":
"root_module": { {
"resources": [mock_insecure_vm] "pm_tls_insecure": {
} "value": true
}, }
"configuration": { },
"provider_config": { "planned_values": {
"proxmox": { "root_module": {
"expressions": { "resources": [
"pm_tls_insecure": { {
"constant_value": true "type": "proxmox_vm_qemu",
} "name": "insecure_vm",
} "values": {
} "cipassword": "password123",
}, "ciuser": "root",
"terraform": { "agent": 0,
"required_providers": { "network": [
"proxmox": { {
"version_constraint": "~2.9.14" "bridge": "vmbr0"
} }
} ],
"skip_ipv6": false,
"memory": 256,
"desc": "",
"scsihw": "lsi",
"cpu": "",
"backup": false,
"tags": ""
}
} }
]
} }
},
"configuration": {
"provider_config": {
"proxmox": {
"expressions": {
"pm_tls_insecure": {
"constant_value": true
}
}
}
},
"terraform": {
"required_providers": {
"proxmox": {
"version_constraint": "~2.9.14"
}
}
}
}
} }
# Test secure configuration passes # Test secure configuration passes
@@ -101,77 +116,40 @@ test_secure_config {
# Test password authentication # Test password authentication
test_password_auth { test_password_auth {
input := mock_input_insecure deny["VM 'insecure_vm' uses password authentication. Use SSH keys only."] with input as mock_input_insecure
deny_msgs := {msg | msg := deny[_]}
deny_msgs["VM 'insecure_vm' uses password authentication. Use SSH keys only."]
} }
# Test qemu agent # Test qemu agent
test_qemu_agent { test_qemu_agent {
input := mock_input_insecure deny["VM 'insecure_vm' does not have qemu-agent enabled (agent = 1)."] with input as mock_input_insecure
deny_msgs := {msg | msg := deny[_]}
deny_msgs["VM 'insecure_vm' does not have qemu-agent enabled (agent = 1)."]
} }
# Test network bridge # Test network bridge
test_network_bridge {# Deny if VM does not have proper tags for identification test_network_bridge {
input := mock_input_insecure deny["VM 'insecure_vm' uses insecure network bridge 'vmbr0'. Use 'vmbr2'."] with input as mock_input_insecure
deny_msgs := {msg | msg := deny[_]}
deny_msgs["VM 'insecure_vm' must have tags for proper identification and management."]
} }
# Test TLS verification # Test TLS verification
test_tls_verification { test_tls_verification {
input := mock_input_insecure deny["TLS verification must be enabled (pm_tls_insecure = false)"] with input as mock_input_insecure
deny_msgs := {msg | msg := deny[_]}
deny_msgs["TLS verification must be enabled (pm_tls_insecure = false)"]
} }
# Test provider version pinning # Test provider version pinning
test_provider_version { test_provider_version {
input := mock_input_insecure deny["Provider version must be pinned with '=' constraint"] with input as mock_input_insecure
deny_msgs := {msg | msg := deny[_]}
deny_msgs["Provider version must be pinned with '=' constraint"]
} }
# Test minimum memory requirement # Test minimum memory requirement
test_minimum_memory { test_minimum_memory {
input := mock_input_insecure deny["VM 'insecure_vm' has insufficient memory (256MB). Minimum required: 512MB."] with input as mock_input_insecure
deny_msgs := {msg | msg := deny[_]}
deny_msgs["VM 'insecure_vm' has insufficient memory (256MB). Minimum required: 512MB."]
} }
# Test VM description requirement # Test VM description requirement
test_vm_description { test_vm_description {
input := mock_input_insecure deny["VM 'insecure_vm' must have a description for documentation purposes."] with input as mock_input_insecure
deny_msgs := {msg | msg := deny[_]}
deny_msgs["VM 'insecure_vm' must have a description for documentation purposes."]
} }
# Test SCSI controller requirement # Test SCSI controller requirement
test_scsi_controller { test_scsi_controller {
input := mock_input_insecure deny["VM 'insecure_vm' uses default SCSI controller. Use virtio-scsi-pci for better performance."] with input as mock_input_insecure
deny_msgs := {msg | msg := deny[_]}
deny_msgs["VM 'insecure_vm' uses default SCSI controller. Use virtio-scsi-pci for better performance."]
}
# Test CPU type requirement
test_cpu_type {
input := mock_input_insecure
deny_msgs := {msg | msg := deny[_]}
deny_msgs["VM 'insecure_vm' must have CPU type explicitly set for consistent performance."]
}
# Test backup requirement
test_backup_enabled {
input := mock_input_insecure
deny_msgs := {msg | msg := deny[_]}
deny_msgs["VM 'insecure_vm' must have backup enabled for disaster recovery."]
}
# Test tags requirement
test_tags_required {
input := mock_input_insecure
deny_msgs := {msg | msg := deny[_]}
deny_msgs["VM 'insecure_vm' must have tags for proper identification and management."]
} }

View File

@@ -0,0 +1,47 @@
{
"planned_values": {
"root_module": {
"resources": [
{
"type": "proxmox_vm_qemu",
"name": "insecure_vm",
"values": {
"cipassword": "password123",
"ciuser": "root",
"agent": 0,
"network": [
{
"bridge": "vmbr0"
}
],
"skip_ipv6": false,
"memory": 256,
"desc": "",
"scsihw": "lsi",
"cpu": "",
"backup": false,
"tags": ""
}
}
]
}
},
"configuration": {
"provider_config": {
"proxmox": {
"expressions": {
"pm_tls_insecure": {
"constant_value": true
}
}
}
},
"terraform": {
"required_providers": {
"proxmox": {
"version_constraint": "~2.9.14"
}
}
}
}
}

View File

@@ -8,7 +8,7 @@ terraform {
} }
provider "proxmox" { provider "proxmox" {
pm_api_url = "https://${var.pm_host}:8006/api2/json" pm_api_url = "https://${var.pm_host}:${var.pm_port}/api2/json"
pm_user = var.pm_user pm_user = var.pm_user
pm_password = var.pm_password pm_password = var.pm_password
pm_tls_insecure = var.pm_tls_insecure pm_tls_insecure = var.pm_tls_insecure

View File

@@ -41,18 +41,6 @@ if ! command_exists conftest; then
MISSING_TOOLS=1 MISSING_TOOLS=1
fi fi
if ! command_exists tfsec; then
echo -e "${RED}❌ tfsec not found. Please install tfsec.${NC}"
echo " curl -s https://raw.githubusercontent.com/aquasecurity/tfsec/master/scripts/install_linux.sh | bash"
MISSING_TOOLS=1
fi
if ! command_exists checkov; then
echo -e "${RED}❌ checkov not found. Please install checkov.${NC}"
echo " pip install checkov"
MISSING_TOOLS=1
fi
if [ $MISSING_TOOLS -eq 1 ]; then if [ $MISSING_TOOLS -eq 1 ]; then
echo -e "${RED}Please install missing tools before running security checks.${NC}" echo -e "${RED}Please install missing tools before running security checks.${NC}"
exit 1 exit 1
@@ -63,6 +51,7 @@ echo -e "${GREEN}✅ All required tools are installed.${NC}"
# Step 1: Terraform validation # Step 1: Terraform validation
echo -e "\n${YELLOW}Running Terraform validation...${NC}" echo -e "\n${YELLOW}Running Terraform validation...${NC}"
terraform validate terraform validate
TERRAFORM_EXIT=$?
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
echo -e "${GREEN}✅ Terraform validation passed.${NC}" echo -e "${GREEN}✅ Terraform validation passed.${NC}"
else else
@@ -70,35 +59,10 @@ else
exit 1 exit 1
fi fi
# Step 2: Run tfsec
echo -e "\n${YELLOW}Running tfsec security scanner...${NC}"
tfsec .
TFSEC_EXIT=$?
if [ $TFSEC_EXIT -eq 0 ]; then
echo -e "${GREEN}✅ tfsec scan passed.${NC}"
else
echo -e "${RED}❌ tfsec found security issues.${NC}"
# We continue execution to run all checks
fi
echo -e "\n${YELLOW}Generating Terraform plan...${NC}" echo -e "\n${YELLOW}Generating Terraform plan...${NC}"
terraform plan -var-file="variables.tfvars" -out=tfplan terraform plan -var-file="variables.tfvars" -out=tfplan
terraform show -json tfplan | jq > tfplan.json terraform show -json tfplan | jq > tfplan.json
# Step 3: Run checkov
echo -e "\n${YELLOW}Running checkov security scanner...${NC}"
checkov -f tfplan.json
CHECKOV_EXIT=$?
if [ $CHECKOV_EXIT -eq 0 ]; then
echo -e "${GREEN}✅ checkov scan passed.${NC}"
else
echo -e "${RED}❌ checkov found security issues.${NC}"
# We continue execution to run all checks
fi
# Step 4: Generate plan and run OPA policies
echo -e "\n${YELLOW}Running OPA policy checks...${NC}" echo -e "\n${YELLOW}Running OPA policy checks...${NC}"
if [ -d "policy" ]; then if [ -d "policy" ]; then
conftest test tfplan.json -p policy/ conftest test tfplan.json -p policy/
@@ -117,12 +81,10 @@ fi
# Summary # Summary
echo -e "\n${YELLOW}Security Check Summary:${NC}" echo -e "\n${YELLOW}Security Check Summary:${NC}"
echo -e "Terraform Validation: $([ $? -eq 0 ] && echo -e "${GREEN}PASSED${NC}" || echo -e "${RED}FAILED${NC}")" echo -e "Terraform Validation: $([ $? -eq 0 ] && echo -e "${GREEN}PASSED${NC}" || echo -e "${RED}FAILED${NC}")"
echo -e "TFSec Security Scan: $([ $TFSEC_EXIT -eq 0 ] && echo -e "${GREEN}PASSED${NC}" || echo -e "${RED}FAILED${NC}")"
echo -e "Checkov Security Scan: $([ $CHECKOV_EXIT -eq 0 ] && echo -e "${GREEN}PASSED${NC}" || echo -e "${RED}FAILED${NC}")"
echo -e "OPA Policy Checks: $([ $CONFTEST_EXIT -eq 0 ] && echo -e "${GREEN}PASSED${NC}" || echo -e "${RED}FAILED${NC}")" echo -e "OPA Policy Checks: $([ $CONFTEST_EXIT -eq 0 ] && echo -e "${GREEN}PASSED${NC}" || echo -e "${RED}FAILED${NC}")"
# Final result # Final result
if [ $TFSEC_EXIT -eq 0 ] && [ $CHECKOV_EXIT -eq 0 ] && [ $CONFTEST_EXIT -eq 0 ]; then if [ $CONFTEST_EXIT -eq 0 ] && [ $TERRAFORM_EXIT -eq 0 ]; then
echo -e "\n${GREEN}All security checks passed!${NC}" echo -e "\n${GREEN}All security checks passed!${NC}"
exit 0 exit 0
else else

View File

@@ -22,6 +22,11 @@ variable "pm_host" {
type = string type = string
} }
variable "pm_port" {
description = "The port of the proxmox server"
default = 8006
}
variable "pm_node_name" { variable "pm_node_name" {
description = "name of the proxmox node to create the VMs on" description = "name of the proxmox node to create the VMs on"
type = string type = string