Automatisierte Bereitstellung von HashiCorp Vault mit Terraform und AWS KMS
Ein umfassender Leitfaden zur Integration von Vault in AWS Lambda-Funktionen für sichere Authentifizierung
Einführung
In der heutigen digitalen Welt sind Sicherheit und Verwaltung sensibler Daten von entscheidender Bedeutung. HashiCorp Vault ist ein Tool, das entwickelt wurde, um den Zugriff auf Tokens, Passwörter, Zertifikate und andere vertrauliche Daten zu verwalten. In diesem Blogbeitrag zeige ich dir, wie du mittels Terraform einen Vault-Server vollständig automatisiert konfigurierst und eine einfache Website mit Login-Funktion erstellst, die als AWS Lambda-Funktion läuft und Vault zur Authentifizierung nutzt.
Wir werden sowohl die Vault-Konfiguration als auch die Erstellung der Lambda-Funktion und aller Abhängigkeiten mit Terraform umsetzen. Dabei ersetzen wir den manuellen Entsperrungsprozess durch die Auto-Unseal-Funktion von Vault mit AWS KMS, um den Entsperrprozess sicher zu automatisieren.
Voraussetzungen
Bevor wir beginnen, stelle sicher, dass du folgende Voraussetzungen erfüllst:
- AWS-Konto: Mit ausreichenden Berechtigungen zum Erstellen von Ressourcen.
- Terraform installiert: Version >= 0.12 empfohlen.
- AWS CLI installiert: Für die Interaktion mit AWS-Diensten.
- Grundlegende Kenntnisse in Terraform, AWS Lambda und Vault.
- Programmierkenntnisse: In einer Sprache, die von AWS Lambda unterstützt wird (z. B. Python, Node.js).
- HashiCorp Vault Provider für Terraform: Wir werden diesen verwenden, um Vault zu konfigurieren.
- AWS KMS Schlüssel: Für die Auto-Unseal-Funktion benötigt.
Den Code zu diesem Beispiel findet ihr hier https://github.com/AlexanderWiechert/terraform-vault-lambda
Teil 1: Einrichtung von HashiCorp Vault mit Terraform
Schritt 1: Projektverzeichnis erstellen
Erstelle ein neues Verzeichnis für dein Terraform-Projekt:
mkdir terraform-vault
cd terraform-vault
Schritt 2: Provider konfigurieren
Erstelle eine Datei namens main.tf
und füge die folgenden Provider hinzu:
provider "aws" {
region = "eu-central-1"
}
provider "local" {}
provider "null" {}
provider "template" {}
provider "vault" {
address = "http://${aws_instance.vault.public_ip}:8200"
}
Schritt 3: VPC und Subnetz erstellen
Wir erstellen eine VPC und ein öffentliches Subnetz für die EC2-Instanz.
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "VaultVPC"
}
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "eu-central-1a"
map_public_ip_on_launch = true
tags = {
Name = "VaultPublicSubnet"
}
}
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.main.id
tags = {
Name = "VaultInternetGateway"
}
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
tags = {
Name = "VaultPublicRouteTable"
}
}
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
Schritt 4: Sicherheitsgruppe für Vault konfigurieren
Erstelle eine Sicherheitsgruppe, die den Zugriff auf Vault erlaubt.
resource "aws_security_group" "vault_sg" {
name = "vault_sg"
description = "Allow Vault traffic"
vpc_id = aws_vpc.main.id
# Regel für den SSH-Zugriff
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # Dies erlaubt SSH-Zugriff von überall; achten Sie auf die Sicherheit
}
# Regel für den Zugriff auf den Vault-Port (optional, wenn Vault über HTTP erreichbar ist)
ingress {
from_port = 8200
to_port = 8200
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # Auch hier sollten Sie die IPs einschränken, die Zugriff haben
}
# Regel für den ausgehenden Zugriff (optional)
egress {
from_port = 0
to_port = 0
protocol = "-1" # Erlaubt allen ausgehenden Verkehr
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "VaultSecurityGroup"
}
}
Schritt 5: AWS KMS Schlüssel erstellen
Wir erstellen einen AWS KMS-Schlüssel, den Vault für das Auto-Unseal verwendet.
resource "aws_kms_key" "vault_unseal_key" {
description = "KMS key for Vault auto-unseal"
deletion_window_in_days = 10
enable_key_rotation = true
tags = {
Name = "VaultAutoUnsealKey"
}
}
Schritt 6: IAM-Rolle und -Policy für EC2-Instanz erstellen
Vault benötigt Zugriff auf den KMS-Schlüssel. Wir erstellen eine IAM-Rolle und -Policy.
resource "aws_iam_role" "vault_ec2_role" {
name = "VaultEC2Role"
assume_role_policy = jsonencode({
"Version": "2012-10-17",
"Statement": [{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
}
}]
})
}
resource "aws_iam_role_policy" "vault_kms_policy" {
name = "VaultKMSPolicy"
role = aws_iam_role.vault_ec2_role.id
policy = jsonencode({
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:Encrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
],
"Resource": aws_kms_key.vault_unseal_key.arn
}]
})
}
Schritt 7: EC2-Instance Profile erstellen
resource "aws_iam_instance_profile" "vault_instance_profile" {
name = "VaultInstanceProfile"
role = aws_iam_role.vault_ec2_role.name
}
Schritt 8: EC2-Instanz für Vault erstellen
Wir erstellen eine EC2-Instanz, auf der Vault installiert wird.
resource "aws_instance" "vault" {
ami = "ami-0c55b159cbfafe1f0" # Amazon Linux 2 AMI
instance_type = "t2.micro"
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.vault_sg.id]
associate_public_ip_address = true
iam_instance_profile = aws_iam_instance_profile.vault_instance_profile.name
key_name = "your-key-pair-name" # Ersetze durch deinen Schlüssel
user_data = data.template_file.user_data.rendered
tags = {
Name = "VaultServer"
}
}
Schritt 9: Vault installieren und konfigurieren mit Auto-Unseal
Wir verwenden eine template_file
, um das User Data Skript zu generieren.
data "template_file" "user_data" {
template = file("${path.module}/vault-user-data.tpl")
vars = {
kms_key_id = aws_kms_key.vault_unseal_key.key_id
region = var.region
}
}
Erstelle die Datei vault-user-data.tpl
:
#!/bin/bash
sudo yum update -y
sudo yum install -y wget unzip jq
# Vault installieren
wget https://releases.hashicorp.com/vault/1.9.0/vault_1.9.0_linux_amd64.zip
sudo unzip vault_1.9.0_linux_amd64.zip -d /usr/local/bin/
sudo chmod +x /usr/local/bin/vault
# Vault Benutzer und Verzeichnisse einrichten
sudo mkdir /etc/vault
sudo useradd --system --home /etc/vault --shell /bin/false vault
sudo chown -R vault:vault /etc/vault
sudo mkdir -p /var/lib/vault/data
sudo chown -R vault:vault /var/lib/vault/
# Vault-Konfiguration mit Auto-Unseal
cat << EOF > /etc/vault/config.hcl
echo 'ui = true
listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = 1
}
storage "file" {
path = "/var/lib/vault/data"
}
seal "awskms" {
region = "${region}"
kms_key_id = "${kms_key_id}"
}
EOF
# Systemd Service für Vault erstellen
cat <<EOF >/etc/systemd/system/vault.service
[Unit]
Description=Vault service
Requires=network-online.target
After=network-online.target
[Service]
User=vault
Group=vault
ExecStart=/usr/local/bin/vault server -config=/etc/vault/config.hcl
ExecReload=/bin/kill --signal HUP \$MAINPID
KillMode=process
Restart=on-failure
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
EOF
# Vault starten
sudo systemctl daemon-reload
sudo systemctl start vault
sudo systemctl enable vault
# Warten, bis Vault startet
sleep 30
# Vault initialisieren
vault operator init -format=json > /home/ec2-user/vault_init.json
# Root Token sichern
echo "Root Token: $(jq -r '.root_token' /home/ec2-user/vault_init.json)" >> /home/ec2-user/root_token.txt
# SSH Public Key in authorized_keys hinzufügen
echo "${ssh_pub_key}" | sudo tee /home/ec2-user/.ssh/authorized_keys > /dev/null
sudo chmod 600 /home/ec2-user/.ssh/authorized_keys
sudo chown ec2-user:ec2-user /home/ec2-user/.ssh/authorized_keys
Hinweis: Die Variablen ${kms_key_id}
, ${region}
, ${public_ip}
und ${private_ip}
werden durch die Werte aus der Terraform-Konfiguration ersetzt.
Schritt 10: Vault Provider konfigurieren
Wir konfigurieren den Vault Provider, um Vault weiter zu konfigurieren.
provider "vault" {
address = "http://${aws_instance.vault.public_ip}:8200"
}
Schritt 11: Warten, bis Vault verfügbar ist
Wir verwenden einen null_resource
, um sicherzustellen, dass Vault bereit ist, bevor wir fortfahren.
resource "null_resource" "wait_for_vault" {
provisioner "local-exec" {
command = "sleep 30"
}
}
Schritt 14: UserPass Auth-Methode aktivieren und Benutzer erstellen
resource "vault_auth_backend" "userpass" {
type = "userpass"
depends_on = [vault_initialization.vault_init]
}
resource "vault_generic_endpoint" "userpass_user" {
path = "auth/userpass/users/testuser"
data_json = jsonencode({
password = "pass123"
policies = ["default"]
})
depends_on = [vault_auth_backend.userpass]
}
Teil 2: Erstellen der Lambda-Funktion und aller Abhängigkeiten mit Terraform
Schritt 1: Erstellen der Lambda-Funktion
Wir verwenden Python für die Lambda-Funktion.
Erstelle einen Ordner lambda_function
und eine Datei lambda_function.py
:
mkdir lambda_function
cd lambda_function
Datei: lambda_function.py
import json
import os
import hvac
def lambda_handler(event, context):
username = event.get('username')
password = event.get('password')
if not username or not password:
return {
'statusCode': 400,
'body': json.dumps('Username and password are required.')
}
client = hvac.Client(
url=os.environ['VAULT_ADDR']
)
try:
client.auth.userpass.login(
username=username,
password=password
)
return {
'statusCode': 200,
'body': json.dumps('Login erfolgreich!')
}
except hvac.exceptions.InvalidRequest:
return {
'statusCode': 401,
'body': json.dumps('Ungültige Anmeldeinformationen.')
}
Erstelle eine requirements.txt
-Datei:
hvac==0.11.2
requests==2.25.1
Hinweis: Möglicherweise musst du zusätzliche Abhängigkeiten hinzufügen, abhängig von der hvac
-Version.
Schritt 2: Erstellen des Deployment-Pakets
Erstelle das Deployment-Paket für die Lambda-Funktion.
cd lambda_function
pip install -r requirements.txt -t .
zip -r ../lambda_function.zip .
cd ..
Schritt 3: Terraform-Konfiguration für die Lambda-Funktion
Erstelle eine neue Datei lambda.tf
:
# Lokale Dateien einlesen
data "archive_file" "lambda_zip" {
type = "zip"
source_file = "${path.module}/lambda_function.zip"
output_path = "${path.module}/lambda_function_deploy.zip"
}
# IAM-Rolle für Lambda-Funktion
resource "aws_iam_role" "lambda_role" {
name = "vault_lambda_role"
assume_role_policy = jsonencode({
"Version": "2012-10-17",
"Statement": [{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
}
}]
})
}
# Anfügen von Richtlinien an die IAM-Rolle
resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
role = aws_iam_role.lambda_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
# Lambda-Funktion erstellen
resource "aws_lambda_function" "vault_login_function" {
filename = data.archive_file.lambda_zip.output_path
function_name = "vault_login_function"
role = aws_iam_role.lambda_role.arn
handler = "lambda_function.lambda_handler"
runtime = "python3.8"
timeout = 10
source_code_hash = filebase64sha256(data.archive_file.lambda_zip.output_path)
environment {
variables = {
VAULT_ADDR = "http://${aws_instance.vault.public_ip}:8200"
}
}
depends_on = [aws_iam_role_policy_attachment.lambda_basic_execution]
}
Schritt 4: API Gateway einrichten
Wir richten eine API Gateway REST API ein, um die Lambda-Funktion aufzurufen.
Ergänze in lambda.tf
:
resource "aws_api_gateway_rest_api" "vault_api" {
name = "VaultLoginAPI"
description = "API Gateway für Vault Login"
}
resource "aws_api_gateway_resource" "login_resource" {
rest_api_id = aws_api_gateway_rest_api.vault_api.id
parent_id = aws_api_gateway_rest_api.vault_api.root_resource_id
path_part = "login"
}
resource "aws_api_gateway_method" "login_method" {
rest_api_id = aws_api_gateway_rest_api.vault_api.id
resource_id = aws_api_gateway_resource.login_resource.id
http_method = "POST"
authorization = "NONE"
}
resource "aws_api_gateway_integration" "lambda_integration" {
rest_api_id = aws_api_gateway_rest_api.vault_api.id
resource_id = aws_api_gateway_resource.login_resource.id
http_method = aws_api_gateway_method.login_method.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.vault_login_function.invoke_arn
}
resource "aws_lambda_permission" "api_gateway_permission" {
statement_id = "AllowAPIGatewayInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.vault_login_function.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_api_gateway_rest_api.vault_api.execution_arn}/*/*"
}
resource "aws_api_gateway_deployment" "api_deployment" {
depends_on = [aws_api_gateway_integration.lambda_integration]
rest_api_id = aws_api_gateway_rest_api.vault_api.id
stage_name = "prod"
}
output "api_invoke_url" {
value = "${aws_api_gateway_deployment.invoke_url}login"
}
Schritt 5: Aktualisiere die Terraform-Konfiguration
Füge in main.tf
die folgenden Zeilen hinzu, um die Abhängigkeiten sicherzustellen:
depends_on = [
vault_initialization.vault_init
]
Setze dies in Ressourcen ein, die auf Vault angewiesen sind.
Schritt 6: Terraform erneut ausführen
terraform init
terraform apply
Bestätige die Ausführung und warte, bis Terraform abgeschlossen ist.
Notiere dir die Ausgabe api_invoke_url
.
Schritt 7: Testen der Anwendung
Sende eine HTTP-POST-Anfrage an die API-URL:
curl -X POST -H "Content-Type: application/json" -d '{"username": "testuser", "password": "pass123"}' <api_invoke_url>
Erwarte eine erfolgreiche Login-Antwort.
Fazit
In diesem Blogbeitrag haben wir gezeigt, wie man mit Terraform und der Auto-Unseal-Funktion von Vault mit AWS KMS einen HashiCorp Vault-Server auf AWS vollständig automatisiert einrichtet und eine AWS Lambda Login-Funktion erstellt, die Vault zur Authentifizierung nutzt. Durch die Automatisierung aller Schritte mit Terraform und die Verwendung von AWS KMS für den Entsperrprozess kannst du eine sichere und effiziente Infrastruktur aufbauen.
Weiterführende Ressourcen
- HashiCorp Vault Dokumentation
- Vault Auto-Unseal mit AWS KMS
- Terraform AWS Provider
- Terraform Vault Provider
- AWS KMS Dokumentation
- AWS Lambda Dokumentation
- AWS API Gateway Dokumentation