Terraform Integration
Terraform Integration
Section titled “Terraform Integration”Provision SSH keys during infrastructure deployment and manage their lifecycle using Terraform with the SSH-KLM API.
Architecture
Section titled “Architecture”┌─────────────────┐ ┌─────────────┐ ┌──────────────────┐│ SSH-KLM API │◄─────►│ Terraform │──────►│ Cloud Provider ││ (Key Store) │ │ (IaC) │ │ (AWS/Azure/GCP) │└─────────────────┘ └─────────────┘ └──────────────────┘ │ │ │ │ • Generate keys │ • Plan/Apply │ • EC2 Instances │ • Store metadata │ • Lifecycle mgmt │ • Azure VMs │ • Enforce policy │ • State management │ • GCP Compute ▼ ▼ ▼┌─────────────────┐ ┌─────────────┐ ┌──────────────────┐│ Key Inventory │ │ Terraform │ │ Running ││ & Audit Trail │ │ State │ │ Instances │└─────────────────┘ └─────────────┘ └──────────────────┘Step 1: Generate Key Pairs via SSH-KLM API
Section titled “Step 1: Generate Key Pairs via SSH-KLM API”Use the SSH-KLM API to generate key pairs during Terraform provisioning. This ensures all keys are tracked in the central inventory.
Configure the HTTP Provider
Section titled “Configure the HTTP Provider”terraform { required_providers { http = { source = "hashicorp/http" version = "~> 3.0" } aws = { source = "hashicorp/aws" version = "~> 5.0" } }}
variable "ssh_klm_api_url" { description = "SSH-KLM API endpoint" type = string}
variable "ssh_klm_api_key" { description = "SSH-KLM API key" type = string sensitive = true}Generate a Key Pair
Section titled “Generate a Key Pair”resource "terraform_data" "ssh_key" { provisioner "local-exec" { command = <<-EOT curl -s -X POST "${var.ssh_klm_api_url}/keys/generate" \ -H "Authorization: Bearer ${var.ssh_klm_api_key}" \ -H "Content-Type: application/json" \ -d '{ "algorithm": "ed25519", "label": "terraform-${var.environment}-${var.project_name}", "metadata": { "provisioner": "terraform", "environment": "${var.environment}", "project": "${var.project_name}" } }' | jq -r '.public_key' > ${path.module}/generated_key.pub EOT }}
data "local_file" "ssh_public_key" { filename = "${path.module}/generated_key.pub" depends_on = [terraform_data.ssh_key]}Step 2: AWS EC2 with SSH-KLM Managed Keys
Section titled “Step 2: AWS EC2 with SSH-KLM Managed Keys”resource "aws_key_pair" "managed" { key_name = "ssh-klm-${var.environment}-${var.project_name}" public_key = trimspace(data.local_file.ssh_public_key.content)
tags = { ManagedBy = "ssh-klm" Environment = var.environment Project = var.project_name }}
resource "aws_instance" "app_server" { ami = var.ami_id instance_type = var.instance_type key_name = aws_key_pair.managed.key_name subnet_id = var.subnet_id
vpc_security_group_ids = [var.security_group_id]
user_data = <<-EOF #!/bin/bash # Install SSH-KLM agent for ongoing key management curl -fsSL https://get.ssh-klm.example.com/agent | bash -s -- \ --api-url ${var.ssh_klm_api_url} \ --api-key ${var.ssh_klm_api_key} \ --hostname $(hostname) EOF
tags = { Name = "${var.project_name}-${var.environment}" ManagedBy = "ssh-klm" Environment = var.environment }
lifecycle { create_before_destroy = true }}
# Register the instance with SSH-KLMresource "terraform_data" "register_host" { depends_on = [aws_instance.app_server]
provisioner "local-exec" { command = <<-EOT curl -s -X POST "${var.ssh_klm_api_url}/hosts" \ -H "Authorization: Bearer ${var.ssh_klm_api_key}" \ -H "Content-Type: application/json" \ -d '{ "hostname": "${aws_instance.app_server.tags.Name}", "ip_address": "${aws_instance.app_server.private_ip}", "os_type": "linux", "cloud_provider": "aws", "instance_id": "${aws_instance.app_server.id}", "key_name": "${aws_key_pair.managed.key_name}" }' EOT }
provisioner "local-exec" { when = destroy command = <<-EOT curl -s -X DELETE "${var.ssh_klm_api_url}/hosts/${self.id}" \ -H "Authorization: Bearer ${var.ssh_klm_api_key}" EOT }}Step 3: Azure VM with SSH-KLM Managed Keys
Section titled “Step 3: Azure VM with SSH-KLM Managed Keys”resource "azurerm_ssh_public_key" "managed" { name = "ssh-klm-${var.environment}-${var.project_name}" resource_group_name = var.resource_group_name location = var.location public_key = trimspace(data.local_file.ssh_public_key.content)
tags = { ManagedBy = "ssh-klm" Environment = var.environment }}
resource "azurerm_linux_virtual_machine" "app_server" { name = "${var.project_name}-${var.environment}-vm" resource_group_name = var.resource_group_name location = var.location size = var.vm_size admin_username = var.admin_username
network_interface_ids = [azurerm_network_interface.main.id]
admin_ssh_key { username = var.admin_username public_key = azurerm_ssh_public_key.managed.public_key }
os_disk { caching = "ReadWrite" storage_account_type = "Standard_LRS" }
source_image_reference { publisher = "Canonical" offer = "0001-com-ubuntu-server-jammy" sku = "22_04-lts" version = "latest" }
custom_data = base64encode(<<-EOF #!/bin/bash # Install SSH-KLM agent curl -fsSL https://get.ssh-klm.example.com/agent | bash -s -- \ --api-url ${var.ssh_klm_api_url} \ --api-key ${var.ssh_klm_api_key} \ --hostname $(hostname) EOF )
tags = { ManagedBy = "ssh-klm" Environment = var.environment }}Step 4: Key Rotation via Lifecycle Rules
Section titled “Step 4: Key Rotation via Lifecycle Rules”Implement automatic key rotation using Terraform lifecycle management and time-based resources.
resource "time_rotating" "ssh_key_rotation" { rotation_days = 90}
resource "terraform_data" "rotated_key" { triggers_replace = [time_rotating.ssh_key_rotation.rotation_rfc3339]
provisioner "local-exec" { command = <<-EOT curl -s -X POST "${var.ssh_klm_api_url}/keys/rotate" \ -H "Authorization: Bearer ${var.ssh_klm_api_key}" \ -H "Content-Type: application/json" \ -d '{ "label": "terraform-${var.environment}-${var.project_name}", "algorithm": "ed25519", "reason": "scheduled-rotation" }' | jq -r '.public_key' > ${path.module}/generated_key.pub EOT }}
# The key pair resource will be recreated when the key rotatesresource "aws_key_pair" "rotated" { key_name = "ssh-klm-${var.environment}-${var.project_name}-${time_rotating.ssh_key_rotation.unix}" public_key = trimspace(data.local_file.ssh_public_key.content)
depends_on = [terraform_data.rotated_key]
tags = { ManagedBy = "ssh-klm" RotatedAt = time_rotating.ssh_key_rotation.rotation_rfc3339 Environment = var.environment }
lifecycle { create_before_destroy = true }}Variables File
Section titled “Variables File”variable "environment" { description = "Deployment environment" type = string default = "production"}
variable "project_name" { description = "Project identifier" type = string}
variable "ami_id" { description = "AMI ID for EC2 instances" type = string}
variable "instance_type" { description = "EC2 instance type" type = string default = "t3.medium"}
variable "subnet_id" { description = "Subnet ID for instance placement" type = string}
variable "security_group_id" { description = "Security group ID" type = string}
variable "resource_group_name" { description = "Azure resource group" type = string default = ""}
variable "location" { description = "Azure region" type = string default = "eastus"}
variable "vm_size" { description = "Azure VM size" type = string default = "Standard_B2s"}
variable "admin_username" { description = "Admin username for Azure VM" type = string default = "azureuser"}Troubleshooting
Section titled “Troubleshooting”| Issue | Cause | Resolution |
|---|---|---|
Error generating key | SSH-KLM API unreachable | Verify network connectivity and API URL |
InvalidKeyPair.Duplicate (AWS) | Key name already exists | Use unique naming with environment/timestamp |
key_data is invalid (Azure) | Malformed public key | Ensure trimspace() is applied to key content |
| State drift after rotation | Key rotated outside Terraform | Run terraform refresh or import updated state |
403 Forbidden from SSH-KLM | Insufficient API key scope | Ensure key has keys:write and hosts:write permissions |
| Destroy provisioner fails | SSH-KLM host already removed | Add on_failure = continue to destroy provisioner |
time_rotating not triggering | Terraform not applied on schedule | Use CI/CD pipeline with scheduled terraform apply |
Best Practices
Section titled “Best Practices”- Store
ssh_klm_api_keyin Terraform Cloud variables or a secrets manager - Use
sensitive = truefor all credential variables - Tag all cloud resources with
ManagedBy = "ssh-klm"for discovery - Implement
create_before_destroylifecycle for zero-downtime rotation - Use remote state with locking to prevent concurrent modifications
- Run
terraform planin CI to detect drift before applying