Initial commit: Terraform certificate automation module

- Add Vault AppRole and Ansible integration for certificates
- Configure policies and secret engines
- Add comprehensive documentation
This commit is contained in:
Patrick de Ruiter 2025-11-01 06:18:46 +01:00
commit 47aaaa2143
Signed by: pderuiter
GPG Key ID: 5EBA7F21CF583321
15 changed files with 455 additions and 0 deletions

58
.gitignore vendored Executable file
View File

@ -0,0 +1,58 @@
# Local .terraform directories
**/.terraform/*
# .tfstate files
*.tfstate
*.tfstate.*
# Crash log files
crash.log
crash.*.log
# Exclude all .tfvars files, which are likely to contain sensitive data
*.tfvars
*.tfvars.json
# Ignore override files as they are usually used to override resources locally
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# Include override files you do wish to add to version control using negated pattern
# !example_override.tf
# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
*tfplan*
# Ignore CLI configuration files
.terraformrc
terraform.rc
# Ignore lock files (optional, some prefer to commit these)
.terraform.lock.hcl
# Ansible directories
ansible/
**/ansible/
# SSH keys and sensitive files
*.pem
*.key
**/files/kubernetes_key*
**/files/*_key
# OS files
.DS_Store
Thumbs.db
# IDE files
.idea/
.vscode/
*.swp
*.swo
*~
# Backup files
*.backup
*.bak

146
README.md Executable file
View File

@ -0,0 +1,146 @@
# Terraform Certificate Automation Module
This Terraform module automates TLS certificate deployment by creating Vault AppRole authentication and policies for automated certificate retrieval and renewal.
## Purpose
This module sets up the infrastructure needed to automatically fetch and deploy TLS certificates from HashiCorp Vault to target servers. It creates:
- Vault policies with read-only access to certificate secrets
- AppRole authentication backend configuration
- AppRole credentials for secure authentication
- Ansible inventory entries for automated deployment
## What It Does
1. **Creates Vault Policy**: Generates a read-only policy for accessing certificate secrets at a specific path
2. **Configures AppRole**: Sets up an AppRole with appropriate token TTLs and secret ID expiration
3. **Generates Credentials**: Creates AppRole role_id and secret_id for authentication
4. **Configures Ansible Host**: Registers the target server in Ansible inventory with Vault credentials
## Prerequisites
- HashiCorp Vault server with AppRole auth method enabled
- Terraform >= 0.13
- Vault provider configured with appropriate credentials
- Ansible provider (for inventory management)
## Usage
### Basic Example
```hcl
module "certificate_automation" {
source = "./terraform-certificate-automation/terraform"
environment = "production"
short_hostname = "web01"
vault_address = "https://vault.example.com:8200"
}
```
### Complete Example
```hcl
module "certificate_automation" {
source = "./terraform-certificate-automation/terraform"
environment = "prod"
short_hostname = "api-server"
vault_address = "https://vault.internal.example.com:8200"
}
# Access the generated credentials (sensitive)
output "approle_creds" {
value = module.certificate_automation.approle_credentials
sensitive = true
}
```
## Inputs
| Name | Type | Description | Required | Validation |
|------|------|-------------|----------|------------|
| `environment` | string | Environment name (e.g., dev, staging, prod) | Yes | Alphanumeric, hyphens, and underscores only |
| `short_hostname` | string | Short hostname for the target server | Yes | Alphanumeric and hyphens only |
| `vault_address` | string | Vault server address | Yes | Must be valid HTTP/HTTPS URL |
## Outputs
| Name | Description | Sensitive |
|------|-------------|-----------|
| `approle_credentials` | Object containing role_id and secret_id | Yes |
## Generated Resources
This module creates the following Vault resources:
- **Policy**: `{environment}-{short_hostname}-cert-policy`
- Path: `secret/data/{environment}/{short_hostname}/certificate`
- Capability: read-only
- **AppRole**: `{environment}-{short_hostname}-approle`
- Token TTL: 1 hour
- Token Max TTL: 4 hours
- Secret ID TTL: 24 hours
- **Ansible Host**: Added to `consul_template` group with Vault credentials
## Secret Path Convention
Certificates are expected to be stored in Vault at:
```
secret/data/{environment}/{short_hostname}/certificate
```
Example: `secret/data/production/web01/certificate`
## Token and Secret TTLs
- **Token TTL**: 1 hour (tokens automatically renew)
- **Token Max TTL**: 4 hours (maximum lifetime before re-authentication)
- **Secret ID TTL**: 24 hours (secret_id expires after 24 hours)
## Integration with Ansible
This module automatically creates an Ansible inventory entry with:
- Inventory hostname: `{short_hostname}`
- Group: `consul_template`
- Variables:
- `vault_approle_role_id`
- `vault_approle_secret_id`
- `vault_address`
- `environment`
- `short_hostname`
The generated inventory can be used with the included Ansible playbooks in the `ansible/` directory to deploy consul-template for automated certificate retrieval.
## Deployment Steps
1. Deploy Vault AppRoles and policies with Terraform
2. Generate Ansible Vault credentials (`ansible_vault_output.sh`)
3. Run Ansible playbook to deploy consul-template
4. consul-template automatically fetches and renews certificates from Vault
## Security Considerations
- AppRole credentials are marked as sensitive in outputs
- Policies follow the principle of least privilege (read-only)
- Secret IDs are automatically rotated (24-hour TTL)
- Tokens have limited lifetime (max 4 hours)
- Ensure sensitive files (`vault_credentials.yml`) are always encrypted and handled securely
## Related Components
This module works in conjunction with:
- **Ansible Playbooks** (in `ansible/` directory): Deploy consul-template to target servers
- **Consul-Template**: Automatically fetches and renews certificates from Vault
- **Vault PKI**: Stores certificates that this module provides access to
## Notes
- Ensure the Vault AppRole auth backend is enabled before using this module
- The Ansible directory should be ignored when using this as a Terraform module
- Certificate secrets must be manually populated in Vault at the expected path

21
terraform/backend.tf Normal file
View File

@ -0,0 +1,21 @@
terraform {
backend "s3" {
endpoints = {
s3 = "https://minio.bsdserver.nl:443"
}
bucket = "home-terraform"
key = "home/security/encryption/certificate-automation.tfstate"
# Configure credentials via environment variables:
# export AWS_ACCESS_KEY_ID="your-access-key"
# export AWS_SECRET_ACCESS_KEY="your-secret-key"
region = "main"
skip_credentials_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true
skip_region_validation = true
use_path_style = true
}
}

3
terraform/data.tf Executable file
View File

@ -0,0 +1,3 @@
#data "vault_generic_secret" "dns" {
# path = "secret/dns"
#}

View File

@ -0,0 +1,18 @@
#!/bin/sh
#sudo /usr/bin/growpart /dev/sda 2
#sudo /usr/sbin/pvresize -y -q /dev/sda2
#sudo /usr/sbin/lvresize -y -q -r -L 6G /dev/vg_sys/lv_usr
#sudo /usr/sbin/lvresize -y -q -r -L 45G /dev/vg_sys/lv_var
#sudo /usr/sbin/lvresize -y -q -r -L 2G /dev/vg_sys/lv_var_log
#sudo /usr/sbin/resize2fs -fF /dev/vg_sys/lv_usr
#sudo /usr/sbin/resize2fs -fF /dev/vg_sys/lv_var
#sudo /usr/sbin/resize2fs -fF /dev/vg_sys/lv_var_log
export DEBIAN_FRONTEND=noninteractive
export DEBIAN_PRIORITY=critical
sudo -E apt-get -qy update
sudo -E apt-get -qy -o "Dpkg::Options::=--force-confdef" -o "Dpkg::Options::=--force-confold" upgrade
sudo -E apt-get -qy -o "Dpkg::Options::=--force-confdef" -o "Dpkg::Options::=--force-confold" dist-upgrade
sudo -E apt-get -qy autoclean
sudo mv /etc/netplan/50-cloud-init.yaml /etc/netplan/00-installer-config.yaml
sudo netplan apply
exit 0

View File

@ -0,0 +1,16 @@
#!/bin/sh
sudo /usr/bin/growpart /dev/sda 2
sudo /usr/sbin/pvresize -y -q /dev/sda2
sudo /usr/sbin/lvresize -y -q -r -L 6G /dev/vg_sys/lv_usr
sudo /usr/sbin/lvresize -y -q -r -L 20G /dev/vg_sys/lv_var
sudo /usr/sbin/lvresize -y -q -r -L 2G /dev/vg_sys/lv_var_log
sudo /usr/sbin/resize2fs -fF /dev/vg_sys/lv_usr
sudo /usr/sbin/resize2fs -fF /dev/vg_sys/lv_var
sudo /usr/sbin/resize2fs -fF /dev/vg_sys/lv_var_log
#export DEBIAN_FRONTEND=noninteractive
#export DEBIAN_PRIORITY=critical
#sudo -E apt-get -qy update
#sudo -E apt-get -qy -o "Dpkg::Options::=--force-confdef" -o "Dpkg::Options::=--force-confold" upgrade
#sudo -E apt-get -qy -o "Dpkg::Options::=--force-confdef" -o "Dpkg::Options::=--force-confold" dist-upgrade
#sudo -E apt-get -qy autoclean
exit 0

44
terraform/main.tf Executable file
View File

@ -0,0 +1,44 @@
locals {
secret_path = "secret/data/${var.environment}/${var.short_hostname}/certificate"
policy_name = "${var.environment}-${var.short_hostname}-cert-policy"
approle_name = "${var.environment}-${var.short_hostname}-approle"
}
resource "vault_policy" "cert_access" {
name = local.policy_name
policy = <<EOT
path "${local.secret_path}" {
capabilities = ["read"]
}
EOT
}
resource "vault_approle_auth_backend_role" "cert_role" {
backend = "approle"
role_name = local.approle_name
token_policies = [vault_policy.cert_access.name]
token_ttl = "1h"
token_max_ttl = "4h"
secret_id_ttl = "24h"
}
resource "vault_approle_auth_backend_role_secret_id" "cert_role_secret" {
backend = "approle"
role_name = vault_approle_auth_backend_role.cert_role.role_name
}
resource "ansible_host" "consul_template_node" {
inventory_hostname = var.short_hostname
groups = ["consul_template"]
vars = {
ansible_user = "ansible"
ansible_ssh_private_key_file = "~/.ssh/id_ed25519"
ansible_python_interpreter = "/usr/bin/python3"
vault_approle_role_id = vault_approle_auth_backend_role.cert_role.role_id
vault_approle_secret_id = vault_approle_auth_backend_role_secret_id.cert_role_secret.secret_id
vault_address = var.vault_address
environment = var.environment
short_hostname = var.short_hostname
}
}

7
terraform/outputs.tf Executable file
View File

@ -0,0 +1,7 @@
output "approle_credentials" {
value = {
role_id = vault_approle_auth_backend_role.cert_role.role_id
secret_id = vault_approle_auth_backend_role_secret_id.cert_role_secret.secret_id
}
sensitive = true
}

26
terraform/provider.tf Executable file
View File

@ -0,0 +1,26 @@
terraform {
required_providers {
ansible = {
source = "ansible/ansible"
}
vault = {
source = "hashicorp/vault"
}
}
}
# Configure the Vault provider
provider "vault" {
address = var.vault_address
auth_login {
path = "auth/approle/login"
parameters = {
role_id = var.role_id
secret_id = var.secret_id
}
}
}
# Ansible Provider
provider "ansible" {
}

View File

@ -0,0 +1,21 @@
#!/bin/sh
# Set hostname
echo "${hostname}" > /etc/myname
# Set DNS
echo "nameserver ${dns}" > /etc/resolv.conf
# Configure network interfaces (Assuming the interface is vmx0)
echo "inet ${ip} ${netmask}" > /etc/hostname.vmx0
# Configure default gateway
echo "${gateway}" > /etc/mygate
# Any additional commands go here
# Restart networking service or apply changes
/etc/netstart
# Exit the script
exit 0

View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
#
echo "Setting SSH Key"
#
ssh-add ~/.ssh/id_ed25519
#
echo "Adding vsphere nodes hostnames to known hosts"
%{ for hostname in k8s_master_name ~}
ssh-keyscan -H ${hostname} >> ~/.ssh/known_hosts
%{ endfor ~}

View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
#
echo "Setting SSH Key"
#
ssh-add ~/.ssh/id_ed25519
#
echo "Adding vsphere nodes hostnames to known hosts"
%{ for hostname in vsphere_vm_name ~}
ssh-keyscan -H ${hostname} >> ~/.ssh/known_hosts
%{ endfor ~}

33
terraform/templates/hosts.tpl Executable file
View File

@ -0,0 +1,33 @@
# ## Configure 'ip' variable to bind kubernetes services on a
# ## different ip than the default iface
# ## We should set etcd_member_name for etcd cluster. The node that is not a etcd member do not need to set the value, or can set the empty string value.
[all]
%{ for ip in k8s_master_ip ~}
${ip}.${domain}
%{ endfor ~}
%{ for ip in k8s_worker_name ~}
${ip}.${domain}
%{ endfor ~}
[kube_control_plane]
%{ for ip in k8s_master_ip ~}
${ip}.${domain}
%{ endfor ~}
[etcd]
%{ for ip in k8s_master_ip ~}
${ip}.${domain}
%{ endfor ~}
[kube_node]
%{ for ip in k8s_worker_name ~}
${ip}.${domain}
%{ endfor ~}
[calico_rr]
[k8s_cluster:children]
kube_control_plane
kube_node
calico_rr

View File

@ -0,0 +1,8 @@
# ## Configure 'ip' variable to bind kubernetes services on a
# ## different ip than the default iface
# ## We should set etcd_member_name for etcd cluster. The node that is not a etcd member do not need to set the value, or can set the empty string value.
[all]
%{ for ip in vsphere_vm_ip ~}
${ip}.${domain}
%{ endfor ~}

30
terraform/variables.tf Executable file
View File

@ -0,0 +1,30 @@
variable "environment" {
type = string
description = "Environment name (e.g., dev, staging, prod)"
validation {
condition = can(regex("^[a-zA-Z0-9-_]+$", var.environment))
error_message = "Environment must contain only alphanumeric characters, hyphens, and underscores."
}
}
variable "short_hostname" {
type = string
description = "Short hostname for the target server"
validation {
condition = can(regex("^[a-zA-Z0-9-]+$", var.short_hostname))
error_message = "Hostname must contain only alphanumeric characters and hyphens."
}
}
variable "vault_address" {
type = string
description = "Vault server address (e.g., https://vault.example.com:8200)"
validation {
condition = can(regex("^https?://", var.vault_address))
error_message = "Vault address must be a valid HTTP or HTTPS URL."
}
}