Integrating HashiCorp Vault with Terraform

Introduction

This document provides a step-by-step guide to integrating HashiCorp Vault with an existing Terraform project to securely retrieve the EC2 private key for provisioning.

Prerequisites

  • AWS CLI installed and configured.

  • Terraform installed on your local machine.

  • HashiCorp Vault installed.

  • An AWS account with necessary permissions.

Vault Installation and Setup

Install Vault on the EC2 instance

Install gpg

sudo apt update && sudo apt install gpg

Download the signing key to a new keyring

wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg

Verify the key's fingerprint

gpg --no-default-keyring --keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg --fingerprint

Add the HashiCorp repo to ubuntu

echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list

Finally, Install Vault

sudo apt install vault && sudo apt install vault

Start Vault

vault server -dev -dev-listen-address="0.0.0.0:8200"

  • When we run the vault server -dev we will get the above logs.

  • We need to use this root token and login to hashicorp vault UI so that it will give root access to the hasicorp vault UI

Authenticate with Vault

export VAULT_ADDR='http://<ec2-public-ip>:8200'

Login to Vault Console

Login with EC2 instance public ip

Use the above Root Token after typing the vault server command you will get one Root Token use that token for login into Hashicorp Vault Console.

  • Secret engines means the different type of secrets that we can create in the hashicorp vault.

  • Once Login, Go to Search Engines and select KV ( Key Value Pair )

  • Give path of the secret engine and enable engine.

  • Once secret generated click on create secret.

  • Give the name of the secret

  • Give key of the secret and give value ec2 instance private key

  • Save the secret.

Create and Apply Vault Policy

Create Vault Policy

vault policy write aws-private-key-policy - <<EOF
path "*" {
  capabilities = ["list", "read"]
}

path "kv/data/*" {
  capabilities = ["create", "read", "update", "delete", "list"]
}
EOF

export vault address with your ec2-instance public-ip

Create Vault AppRole

Enable the AppRole authentication method:

vault auth enable approle

Create an AppRole and attach the policy:

vault write auth/approle/role/aws-private-key-role \
    secret_id_ttl=10m \
    token_num_uses=10 \
    token_ttl=20m \
    token_max_ttl=30m \
    secret_id_num_uses=40 \
    token_policies=aws-private-key-policy

Retrieve the Role ID and Secret ID:

vault read auth/approle/role/ec2-role/role-id

vault write -f auth/approle/role/ec2-role/secret-id

Store these values securely as they will be used in Terraform for authentication.

Terraform Configuration

Providers

Update providers.tf to include the Vault provider:

terraform {
    required_providers {
      aws = {
        source = "hashicorp/aws",
        version = "~> 5.0"
      }
    }
}

provider "aws" {
  region = var.aws_region
}

provider "vault" {
  address = "http://<ec2-public-ip>:8200"
  skip_child_token = true
  auth_login {
    path = "auth/approle/login"

    parameters = {
      role_id   = "<role_id>"
      secret_id = "<secret-id>"
    }
  }
}

data "vault_kv_secret_v2" "pem_file" {
  mount = "kv"
  name  = "aws-pem-file"
}
  • Added data block also in the provider file to retrieve secrets from the hashicorp vault.

  • Give the role_id and secret_id which we saved while doing Valut configurations with commands

Update main.tf to read the private key secret from Vault

ec2_private_key = data.vault_kv_secret_v2.pem_file.data["public_key"]
# Define Locals for using dynamic variables with the variable terraform.workspace ( dev/prod )
locals {
  ec2_instance_name         = "${terraform.workspace}-${var.ec2_instance_name}"
  # s3_bucket_name            = "${terraform.workspace}-${var.s3_bucket_name}"
  # aws_dynamodb_table_name   = "${terraform.workspace}-${var.aws_dynamodb_table_name}"
  ec2_security_group_name   = "${terraform.workspace}-${var.ec2_security_group_name}"
}


# Defining module for EC2 module
module "ec2_instance_module" {
  source = "./modules/ec2-instance"
  ec2_ami_id = var.ec2_ami_id
  ec2_instance_name = local.ec2_instance_name
  ec2_instance_type = var.ec2_instance_type
  ec2_private_key = data.vault_kv_secret_v2.pem_file.data["public_key"]
  vpc_id = var.vpc_id
  ec2_security_group_name = local.ec2_security_group_name
}


# Defining module for S3 Bucket
module "s3_bucket_module" {
  source = "./modules/s3-bucket"
  s3_bucket_name = var.s3_bucket_name
}


# Deniging module for DynamoDB
module "dynamodb_table_terraform_lock_module" {
  source = "./modules/dynamo-db"
  aws_dynamodb_table_name = var.aws_dynamodb_table_name
}

EC2 Module

Update the EC2 module to use the private key retrieved from Vault

resource "aws_security_group" "web-app-security-group" {
  name = var.ec2_security_group_name
  vpc_id = var.vpc_id
}

resource "aws_vpc_security_group_ingress_rule" "allow_tls_ipv4_22" {
  security_group_id = aws_security_group.web-app-security-group.id
  cidr_ipv4         = "0.0.0.0/0"
  from_port         = 22
  ip_protocol       = "tcp"
  to_port           = 22
}

resource "aws_vpc_security_group_ingress_rule" "allow_tls_ipv4_80" {
  security_group_id = aws_security_group.web-app-security-group.id
  cidr_ipv4         = "0.0.0.0/0"
  from_port         = 80
  ip_protocol       = "tcp"
  to_port           = 80
}

resource "aws_vpc_security_group_ingress_rule" "allow_tls_ipv4_all" {
  security_group_id = aws_security_group.web-app-security-group.id
  cidr_ipv4         = "0.0.0.0/0"
  ip_protocol       = "-1"
}

resource "aws_vpc_security_group_egress_rule" "allow_all_traffic_ipv4" {
  security_group_id = aws_security_group.web-app-security-group.id
  cidr_ipv4         = "0.0.0.0/0"
  ip_protocol       = "-1" # semantically equivalent to all ports
}


resource "aws_instance" "aws_ec2_instance" {
  ami = var.ec2_ami_id
  instance_type = var.ec2_instance_type
  key_name = "first-instance-key-pair"
  vpc_security_group_ids = [ aws_security_group.web-app-security-group.id ]

  tags = {
    Name = var.ec2_instance_name
  }

  provisioner "remote-exec" {
    inline = [ 
        "sudo yum update -y",
        "sudo yum install -y httpd",
        "sudo systemctl start httpd",
        "sudo systemctl enable httpd",
        "echo '<h1>Hello, World!</h1>' | sudo tee /var/www/html/index.html"
     ]

    connection {
        user = "ec2-user"
        host = self.public_ip
        private_key = var.ec2_private_key
    }
  }
}

Deploying the Project

Step 1: Initialize Terraform

terraform init

Step 2: Select or Create a Workspace

terraform workspace new dev

terraform workspace select dev

Step 4: Plan the Configuration

terraform plan -var-file="./workspaces/dev.tfvars"

Step 4: Apply the Configuration

terraform apply -var-file="./workspaces/dev.tfvars"

When prompted to confirm the action, type yes and press Enter.

Complete Terraform Documentation

Project Structure

Here is the overview of the project structure

terraform-project/
├── modules/
│   ├── ec2_instance/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   ├── s3_bucket/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   └── dynamodb_table/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
├── main.tf
├── providers.tf
├── backend.tf
├── variables.tf
├── outputs.tf
└── workspaces/
    ├── dev.tfvars
    └── prod.tfvars

Complete Project Documentation Link :https://hashnode.com/edit/clx7cisya000409mm8h8j28nr

Accessing the Application

Step 1: Get the Public IP of the EC2 Instance

After the deployment is complete, you can get the public IP address of the EC2 instance.

Step 2: Open the Web Application

Open your web browser and navigate to http://<public_ip>, replacing <public_ip> with the actual public IP address obtained in the previous step. You should see the "Hello, World!" message.

Conclusion

This document provided steps to integrate HashiCorp Vault with an Terraform project to securely retrieve the EC2 private key and use it for provisioning. Adjust the configurations as needed for your specific use case.