Skip to content

Security

This document covers security features and best practices for the Terraformer module.

SSH Key Management

When ssh_key_name is not provided, the module automatically generates and rotates SSH keys.

Key Generation

module "terraformer" {
  source  = "registry.infrahouse.com/infrahouse/terraformer/aws"
  version = "~> 2.0"

  environment = "production"
  zone_id     = "Z1234567890ABC"
  subnet      = "subnet-abc123"

  # Omit ssh_key_name for auto-generation
  ssh_key_rotation_days = 90  # Optional, defaults to 90
}

What happens:

  1. TLS private key generated by Terraform (4096-bit RSA)
  2. Public key uploaded to AWS as Key Pair
  3. Private key stored in Secrets Manager via infrahouse/secret/aws module
  4. Key automatically rotates every 90 days (configurable)

Accessing Auto-Generated Keys

# Get secret ARN from Terraform output
SECRET_ARN=$(terraform output -raw ssh_key_secret_arn)

# Download private key
aws secretsmanager get-secret-value \
  --secret-id "$SECRET_ARN" \
  --query SecretString \
  --output text > ~/.ssh/terraformer.pem

chmod 600 ~/.ssh/terraformer.pem

Controlling Access to Keys

Use ssh_key_readers to specify who can read the private key:

module "terraformer" {
  # ...
  ssh_key_readers = [
    "arn:aws:iam::123456789012:role/DevOpsTeam",
    "arn:aws:iam::123456789012:role/SRE",
    "arn:aws:iam::123456789012:user/admin"
  ]
}

This creates a Secrets Manager resource policy allowing only specified principals to read the key.

User-Provided Keys

If you have existing key management:

module "terraformer" {
  # ...
  ssh_key_name = "my-existing-key"
}

Security considerations:

  • You're responsible for key rotation
  • Private key not stored in Terraform state
  • No automatic rotation

IAM Permissions

Base Permissions

The instance profile includes minimal base permissions:

AssumeRole Capability

{
  "Effect": "Allow",
  "Action": [
    "sts:AssumeRole",
    "iam:GetRole"
  ],
  "Resource": "*"
}

Why: Allows terraformer to assume roles in other accounts for cross-account operations.

CloudWatch Logs

{
  "Effect": "Allow",
  "Action": [
    "logs:CreateLogStream",
    "logs:PutLogEvents",
    "logs:DescribeLogGroups",
    "logs:DescribeLogStreams"
  ],
   "Resource": "arn:aws:logs:*:*:log-group:/aws/ec2/terraformer/${environment}/${dns_name}:*"
}

Why: Write operations logs for audit trail. Scoped to terraformer log group only.

CloudWatch Metrics

{
  "Effect": "Allow",
  "Action": "cloudwatch:PutMetricData",
  "Resource": "*",
  "Condition": {
    "StringEquals": {
      "cloudwatch:namespace": "terraformer"
    }
  }
}

Why: Publish custom metrics. Restricted to terraformer namespace only.

EC2 Tags

{
  "Effect": "Allow",
  "Action": "ec2:DescribeTags",
  "Resource": "*"
}

Why: Required for CloudWatch agent's ec2tagger to enrich metrics with instance tags.

Adding Permissions

For additional operations (e.g., S3 state access):

data "aws_iam_policy_document" "extra_permissions" {
  statement {
    sid = "TerraformStateAccess"
    actions = [
      "s3:GetObject",
      "s3:PutObject",
      "s3:DeleteObject"
    ]
    resources = [
      "arn:aws:s3:::my-terraform-states/*"
    ]
  }

  statement {
    sid = "DynamoDBLocking"
    actions = [
      "dynamodb:GetItem",
      "dynamodb:PutItem",
      "dynamodb:DeleteItem"
    ]
    resources = [
      "arn:aws:dynamodb:*:*:table/terraform-locks"
    ]
  }
}

module "terraformer" {
  # ...
  extra_instance_profile_permissions = data.aws_iam_policy_document.extra_permissions.json
}

Cross-Account Access

Configure trust relationships in target accounts:

# In target account
resource "aws_iam_role" "terraformer_access" {
  name = "terraformer-admin-access"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Principal = {
        AWS = "arn:aws:iam::SOURCE-ACCOUNT:role/terraformer-XXXXXXXXXXXX"
      }
      Action = "sts:AssumeRole"
      Condition = {
        StringEquals = {
          "sts:ExternalId" = "unique-external-id"
        }
      }
    }]
  })

  managed_policy_arns = [
    "arn:aws:iam::aws:policy/ReadOnlyAccess"
  ]
}

Best practices:

  1. Use ExternalId for additional security
  2. Grant least privilege (not AdministratorAccess unless necessary)
  3. Enable CloudTrail in both accounts for audit
  4. Consider session duration limits

Auto-Recovery

Hardware Failure Protection

System Status Check Alarm:

  • Metric: StatusCheckFailed_System
  • Threshold: 2 consecutive failures (2 minutes)
  • Action: ec2:recover

What it does:

  1. Migrates instance to healthy hardware
  2. Preserves instance ID, private IP, Elastic IP, volumes
  3. Minimal downtime (~2-3 minutes)

When it triggers:

  • Host hardware degradation
  • Network path failures
  • Power issues on underlying host

Software Failure Protection

Instance Status Check Alarm:

  • Metric: StatusCheckFailed_Instance
  • Threshold: 3 consecutive failures (3 minutes)
  • Action: ec2:reboot

What it does:

  1. Gracefully reboots the instance
  2. Reinitializes operating system
  3. Re-runs cloud-init and Puppet

When it triggers:

  • Kernel panics
  • Out of memory conditions
  • Network misconfiguration
  • Failed system checks

Disabling Auto-Recovery

Auto-recovery is always enabled (no variable to disable). This is intentional for a critical administrative instance.

If you need to disable during maintenance:

# Temporarily disable alarms
INSTANCE_ID=$(terraform output -raw instance_id)

aws cloudwatch disable-alarm-actions \
  --alarm-names \
    "terraformer-system-auto-recovery-$INSTANCE_ID" \
    "terraformer-instance-status-check-$INSTANCE_ID"

# Perform maintenance

# Re-enable
aws cloudwatch enable-alarm-actions \
  --alarm-names \
    "terraformer-system-auto-recovery-$INSTANCE_ID" \
    "terraformer-instance-status-check-$INSTANCE_ID"

Network Security

Security Group Rules

SSH (port 22):

  • Source: VPC CIDR (always allowed)
  • Additional CIDRs via extra_ssh_cidrs variable

ICMP (all types):

  • Source: VPC CIDR only
  • Allows ping, traceroute from internal networks only

Egress:

  • All traffic allowed (required for AWS API, Puppet, package updates)

Subnet Options

Private Subnet (Recommended for Production):

module "terraformer" {
  # ...
  subnet = data.aws_subnet.private.id  # Must have NAT gateway
}
  • Instance gets private IP only
  • DNS record points to private IP
  • Access via bastion host, VPN, or AWS SSM Session Manager

Public Subnet (For Development/Testing):

module "terraformer" {
  # ...
  subnet          = data.aws_subnet.public.id
  extra_ssh_cidrs = ["203.0.113.0/24"]  # Your office/home IP
}
  • Instance gets public IP (if subnet has map_public_ip_on_launch = true)
  • DNS record automatically uses public IP
  • Direct SSH access from allowed CIDRs
# Add VPC endpoints for AWS services
resource "aws_vpc_endpoint" "s3" {
  vpc_id       = data.aws_vpc.main.id
  service_name = "com.amazonaws.${var.region}.s3"
}

resource "aws_vpc_endpoint" "secretsmanager" {
  vpc_id              = data.aws_vpc.main.id
  service_name        = "com.amazonaws.${var.region}.secretsmanager"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = [data.aws_subnet.private.id]
  security_group_ids  = [aws_security_group.vpc_endpoints.id]
}

Metadata Security

IMDSv2 Enforcement

The instance enforces IMDSv2 (Instance Metadata Service Version 2):

metadata_options {
  http_tokens   = "required"  # Enforces IMDSv2
  http_endpoint = "enabled"
}

Why: Protects against SSRF attacks that could retrieve IAM credentials.

Audit and Compliance

CloudWatch Logs

All operations on the instance are logged:

  • Log Group: /aws/ec2/terraformer/${environment}/${dns_name}
  • Retention: 365 days (ISO 27001 compliant)
  • Streams: Organized by instance ID and log type

What's logged:

  • System logs (syslog, auth.log)
  • Application logs (terraform, aws cli)
  • Puppet runs
  • User sessions

Enabling Session Manager

For additional audit capabilities, enable AWS Systems Manager Session Manager:

data "aws_iam_policy_document" "extra_permissions" {
  statement {
    actions = [
      "ssmmessages:CreateControlChannel",
      "ssmmessages:CreateDataChannel",
      "ssmmessages:OpenControlChannel",
      "ssmmessages:OpenDataChannel",
      "ssm:UpdateInstanceInformation"
    ]
    resources = ["*"]
  }

  statement {
    actions = [
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ]
    resources = [
      "arn:aws:logs:*:*:log-group:/aws/ssm/sessions:*"
    ]
  }
}

module "terraformer" {
  # ...
  extra_instance_profile_permissions = data.aws_iam_policy_document.extra_permissions.json
}

Benefits:

  • All sessions logged to CloudWatch
  • No need for SSH keys
  • Integration with AWS CloudTrail

Security Checklist

  • [ ] Instance in private subnet with NAT gateway (production) or public subnet with restricted extra_ssh_cidrs (development)
  • [ ] SSH access restricted to VPC CIDR and explicitly allowed CIDRs only
  • [ ] Auto-generated SSH keys with rotation enabled
  • [ ] ssh_key_readers configured for key access control
  • [ ] Extra IAM permissions follow least privilege
  • [ ] Cross-account roles use ExternalId
  • [ ] CloudWatch Logs enabled with appropriate retention
  • [ ] Auto-recovery alarms active
  • [ ] VPC endpoints configured for AWS services
  • [ ] CloudTrail enabled in all accounts
  • [ ] Regular security audits of assumed roles