Skip to content

Terraform Integration

Provision SSH keys during infrastructure deployment and manage their lifecycle using Terraform with the SSH-KLM API.


┌─────────────────┐ ┌─────────────┐ ┌──────────────────┐
│ 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.

providers.tf
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
}
ssh_keys.tf
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]
}

aws_ec2.tf
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-KLM
resource "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”
azure_vm.tf
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
}
}

Implement automatic key rotation using Terraform lifecycle management and time-based resources.

key_rotation.tf
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 rotates
resource "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.tf
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"
}

IssueCauseResolution
Error generating keySSH-KLM API unreachableVerify network connectivity and API URL
InvalidKeyPair.Duplicate (AWS)Key name already existsUse unique naming with environment/timestamp
key_data is invalid (Azure)Malformed public keyEnsure trimspace() is applied to key content
State drift after rotationKey rotated outside TerraformRun terraform refresh or import updated state
403 Forbidden from SSH-KLMInsufficient API key scopeEnsure key has keys:write and hosts:write permissions
Destroy provisioner failsSSH-KLM host already removedAdd on_failure = continue to destroy provisioner
time_rotating not triggeringTerraform not applied on scheduleUse CI/CD pipeline with scheduled terraform apply

  • Store ssh_klm_api_key in Terraform Cloud variables or a secrets manager
  • Use sensitive = true for all credential variables
  • Tag all cloud resources with ManagedBy = "ssh-klm" for discovery
  • Implement create_before_destroy lifecycle for zero-downtime rotation
  • Use remote state with locking to prevent concurrent modifications
  • Run terraform plan in CI to detect drift before applying