How to Setup HashiCorp Vaulton Ubuntu & Use It for AWS RDS MySQL with Python
A production-grade guide — install Vault with Let's Encrypt TLS, Raft HA storage, AWS KMS auto-unseal, configure dynamic MySQL credentials for RDS, and integrate into a Python FastAPI backend. Zero static passwords anywhere.
Python App + Vault + AWS RDS
Dynamic Secrets for MySQL
What is HashiCorp Vault?
HashiCorp Vault is an identity-based secrets management tool. It provides a unified interface to manage secrets — API keys, passwords, certificates, and database credentials — with tight access control, audit logging, and automatic rotation.
Dynamic Secrets
Generate short-lived credentials on demand. No static passwords in config files.
Lease & Revoke
Every secret has a TTL. Vault auto-revokes expired credentials, limiting blast radius.
Audit Trail
Every request is logged. Know exactly who accessed what secret and when.
Prerequisites
- ▹Ubuntu 22.04/24.04 LTS (EC2 t3.medium or larger)
- ▹Root or sudo access
- ▹Domain name pointing to the server (e.g. vault.yourdomain.com)
- ▹AWS RDS MySQL instance running in a private subnet
- ▹AWS KMS key for auto-unseal
- ▹IAM role on EC2 with KMS permissions
- ▹Vault server in same VPC as RDS (private subnet)
- ▹MySQL admin user on RDS with CREATE USER and GRANT OPTION
- ▹Ports open: 80 (ACME), 8200 (Vault API), 3306 outbound to RDS
Step 1: Install HashiCorp Vault
# Install prerequisites
sudo apt-get update sudo apt-get install -y gpg wget lsb-release
# Add HashiCorp GPG key and repository
wget -O- https://apt.releases.hashicorp.com/gpg | \ sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg echo "deb [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
# Install Vault
sudo apt-get update sudo apt-get install -y vault vault --version
Step 2: Let's Encrypt TLS Certificate
Vault must use TLS in production. We use Certbot standalone mode — no Nginx needed.
# Install Certbot
sudo apt-get install -y certbot
# Request certificate (port 80 must be open)
sudo certbot certonly --standalone \ -d vault.yourdomain.com \ --non-interactive \ --agree-tos \ -m admin@yourdomain.com
# Copy certs to Vault TLS directory
sudo mkdir -p /opt/vault/tls sudo cp /etc/letsencrypt/live/vault.yourdomain.com/fullchain.pem /opt/vault/tls/cert.pem sudo cp /etc/letsencrypt/live/vault.yourdomain.com/privkey.pem /opt/vault/tls/key.pem sudo chown vault:vault /opt/vault/tls/*.pem sudo chmod 600 /opt/vault/tls/key.pem
# Auto-renewal hook (copies new certs + reloads Vault)
sudo tee /etc/letsencrypt/renewal-hooks/deploy/vault-reload.sh << 'EOF' #!/bin/bash cp /etc/letsencrypt/live/vault.yourdomain.com/fullchain.pem /opt/vault/tls/cert.pem cp /etc/letsencrypt/live/vault.yourdomain.com/privkey.pem /opt/vault/tls/key.pem chown vault:vault /opt/vault/tls/*.pem chmod 600 /opt/vault/tls/key.pem systemctl reload vault EOF sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/vault-reload.sh
# Test renewal
sudo certbot renew --dry-run
Auto-renewal: Certbot's systemd timer runs twice daily. When the cert is within 30 days of expiry, it renews and fires the deploy hook — zero downtime.
Step 3: Configure Vault (Production)
Production config with Raft HA storage, Let's Encrypt TLS, and AWS KMS auto-unseal.
# Create data directory
sudo mkdir -p /opt/vault/data sudo chown -R vault:vault /opt/vault
# /etc/vault.d/vault.hcl
storage "raft" {
path = "/opt/vault/data"
node_id = "vault-prod-1"
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_cert_file = "/opt/vault/tls/cert.pem"
tls_key_file = "/opt/vault/tls/key.pem"
tls_min_version = "tls12"
}
seal "awskms" {
region = "us-east-1"
kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/your-key-id"
}
api_addr = "https://vault.yourdomain.com:8200"
cluster_addr = "https://vault.yourdomain.com:8201"
ui = true# IAM policy for EC2 (attach to instance role)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:DescribeKey"
],
"Resource": "arn:aws:kms:us-east-1:123456789012:key/your-key-id"
}
]
}# Start Vault
sudo systemctl enable vault sudo systemctl start vault sudo systemctl status vault
Step 4: Initialize & Unseal Vault
With KMS auto-unseal, Vault generates recovery keys instead of unseal keys. After init, it auto-unseals on every restart.
# Set Vault address
export VAULT_ADDR="https://vault.yourdomain.com:8200"
# Initialize (KMS auto-unseal mode)
vault operator init -recovery-shares=5 -recovery-threshold=3
# Check status (should show Sealed: false)
vault status
# Login with root token
vault login <ROOT_TOKEN>
⚠ Important: Save recovery keys securely. After initial setup, create admin policies and revoke the root token.
Step 5: Enable Database Secrets Engine
vault secrets enable database
Step 6: Configure RDS MySQL Connection
Tell Vault how to connect to your RDS MySQL instance.
vault write database/config/my-rds-mysql \
plugin_name="mysql-database-plugin" \
connection_url="{{username}}:{{password}}@tcp(my-rds.abc123.us-east-1.rds.amazonaws.com:3306)/" \
allowed_roles="my-app-role" \
username="vault_admin" \
password="YourRDSAdminPassword"Note: The vault_admin user needs CREATE USER and GRANT OPTION privileges on RDS.
Step 7: Create a Database Role
Roles define what SQL Vault runs to create/revoke users.
vault write database/roles/my-app-role \
db_name="my-rds-mysql" \
creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; \
GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO '{{name}}'@'%';" \
default_ttl="1h" \
max_ttl="24h"Step 8: Generate Dynamic Credentials
# Request credentials
vault read database/creds/my-app-role
# Output
Key Value --- ----- lease_id database/creds/my-app-role/abc123def456 lease_duration 1h lease_renewable true password A1a-xK9mLp2QrStUvWx username v-token-my-app-r-1234567890
Step 9: Rotate Root Credentials
After setup, rotate the root password so only Vault knows it.
vault write -force database/rotate-root/my-rds-mysql
⚠ Warning: After rotation, only Vault knows the admin password. If you lose Vault access, reset the RDS master password via AWS Console.
Step 10: Create Vault Policy
# my-app-policy.hcl
path "database/creds/my-app-role" {
capabilities = ["read"]
}
path "sys/leases/renew" {
capabilities = ["update"]
}
path "auth/token/lookup-self" {
capabilities = ["read"]
}# Apply the policy
vault policy write my-app-policy my-app-policy.hcl
Step 11: Enable AWS IAM Auth (Production)
In production, your Python app authenticates using its IAM role — no static tokens needed.
vault auth enable aws
# Map IAM role to Vault policy
vault write auth/aws/role/my-python-app \ auth_type="iam" \ bound_iam_principal_arn="arn:aws:iam::123456789012:role/my-python-app-role" \ policies="my-app-policy" \ ttl="1h"
Step 12: Python FastAPI Integration
Full production-ready Python app with Vault-managed MySQL credentials.
Project Structure
my-python-app/ ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI entry point │ ├── vault_client.py # Vault integration │ ├── database.py # SQLAlchemy + Vault creds │ └── routes/ │ └── users.py # API routes ├── requirements.txt └── Dockerfile
requirements.txt
fastapi==0.104.1 uvicorn==0.24.0 hvac==2.1.0 sqlalchemy==2.0.23 pymysql==1.1.0 cryptography==41.0.7 boto3==1.34.0
app/vault_client.py
import hvac
import boto3
import os
import threading
import time
import logging
logger = logging.getLogger(__name__)
class VaultClient:
def __init__(self):
self.client = hvac.Client(
url=os.environ.get("VAULT_ADDR", "https://vault.yourdomain.com:8200"),
)
self._lease_id = None
self._lease_duration = 0
self._credentials = None
self._lock = threading.Lock()
# Authenticate via IAM (no static token)
self._auth_iam()
def _auth_iam(self):
session = boto3.Session()
creds = session.get_credentials().get_frozen_credentials()
self.client.auth.aws.iam_login(
access_key=creds.access_key,
secret_key=creds.secret_key,
session_token=creds.token,
role="my-python-app",
)
logger.info("Authenticated to Vault via IAM")
@property
def db_credentials(self):
with self._lock:
if self._credentials is None:
self._fetch_credentials()
return self._credentials
def _fetch_credentials(self):
response = self.client.secrets.database.generate_credentials(
name=os.environ.get("VAULT_DB_ROLE", "my-app-role"),
)
self._lease_id = response["lease_id"]
self._lease_duration = response["lease_duration"]
self._credentials = {
"username": response["data"]["username"],
"password": response["data"]["password"],
}
logger.info(f"Got DB creds: user={self._credentials['username']}, TTL={self._lease_duration}s")
self._start_renewal()
def _start_renewal(self):
def renew():
while True:
time.sleep(self._lease_duration * 0.66)
try:
self.client.sys.renew_lease(lease_id=self._lease_id)
logger.info("Lease renewed")
except Exception as e:
logger.warning(f"Renewal failed: {e}")
with self._lock:
self._fetch_credentials()
break
threading.Thread(target=renew, daemon=True).start()
def revoke(self):
if self._lease_id:
try:
self.client.sys.revoke_lease(lease_id=self._lease_id)
except Exception:
pass
vault = VaultClient()app/database.py
import os
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
from app.vault_client import vault
RDS_HOST = os.environ.get("RDS_HOST", "my-rds.abc123.us-east-1.rds.amazonaws.com")
RDS_PORT = os.environ.get("RDS_PORT", "3306")
RDS_DB = os.environ.get("RDS_DATABASE", "mydb")
Base = declarative_base()
def get_engine():
creds = vault.db_credentials
url = f"mysql+pymysql://{creds['username']}:{creds['password']}@{RDS_HOST}:{RDS_PORT}/{RDS_DB}"
return create_engine(url, pool_size=5, pool_pre_ping=True, pool_recycle=1800)
engine = get_engine()
SessionLocal = sessionmaker(bind=engine)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()app/main.py
from fastapi import FastAPI
from contextlib import asynccontextmanager
from app.vault_client import vault
from app.routes.users import router as users_router
@asynccontextmanager
async def lifespan(app: FastAPI):
yield
vault.revoke() # Revoke lease on shutdown
app = FastAPI(title="My API", lifespan=lifespan)
app.include_router(users_router)
@app.get("/health")
def health():
return {"status": "ok"}Run the App
export VAULT_ADDR="https://vault.yourdomain.com:8200" export VAULT_DB_ROLE="my-app-role" export RDS_HOST="my-rds.abc123.us-east-1.rds.amazonaws.com" export RDS_DATABASE="mydb" uvicorn app.main:app --host 0.0.0.0 --port 8000
Step 13: Docker Deployment with Vault Agent
For containerized deployments, Vault Agent as a sidecar handles auth and credential injection.
# docker-compose.yml
version: "3.8"
services:
vault-agent:
image: hashicorp/vault:1.15
command: vault agent -config=/etc/vault/agent.hcl
volumes:
- ./vault-agent.hcl:/etc/vault/agent.hcl:ro
- ./templates:/etc/vault/templates:ro
- secrets:/secrets
environment:
VAULT_ADDR: "https://vault.yourdomain.com:8200"
python-app:
build: .
depends_on:
- vault-agent
ports:
- "8000:8000"
volumes:
- secrets:/secrets:ro
environment:
DB_CREDS_FILE: "/secrets/db-creds.json"
RDS_HOST: "my-rds.abc123.us-east-1.rds.amazonaws.com"
RDS_DATABASE: "mydb"
volumes:
secrets:# vault-agent.hcl
vault {
address = "https://vault.yourdomain.com:8200"
}
auto_auth {
method "aws" {
config = {
type = "iam"
role = "my-python-app"
}
}
sink "file" {
config = {
path = "/secrets/.vault-token"
}
}
}
template {
source = "/etc/vault/templates/db-creds.tpl"
destination = "/secrets/db-creds.json"
}# templates/db-creds.tpl
{{ with secret "database/creds/my-app-role" }}
{
"username": "{{ .Data.username }}",
"password": "{{ .Data.password }}"
}
{{ end }}Production Security Checklist
TLS Everywhere
Let's Encrypt cert with auto-renewal. Never disable TLS.
KMS Auto-Unseal
Vault auto-unseals on restart. No human intervention needed.
Short TTLs
1h default, 24h max. Leaked creds expire quickly.
IAM Auth
No static tokens. Apps authenticate via their IAM role.
Revoke Root Token
After setup, revoke root. Generate new one only via recovery keys.
Network Isolation
Vault in private subnet. Only app security group can reach port 8200.
Audit Logging
Enable file or syslog audit: vault audit enable file file_path=/var/log/vault/audit.log
Raft Backups
Daily snapshots: vault operator raft snapshot save → S3.
Troubleshooting
Vault not auto-unsealing
Check EC2 IAM role has kms:Encrypt, kms:Decrypt, kms:DescribeKey on the KMS key ARN.
Connection refused to RDS
Vault's security group needs outbound 3306. RDS security group needs inbound from Vault's SG.
Error 1227: Access denied CREATE USER
The vault_admin RDS user needs CREATE USER and GRANT OPTION. Use the RDS master user.
Certbot fails on port 80
Ensure security group allows inbound 80 from 0.0.0.0/0 and no other service is using it.
Summary
You now have a production Vault server with Let's Encrypt TLS, AWS KMS auto-unseal, and dynamic MySQL credentials for RDS. Your Python app authenticates via IAM, gets short-lived DB credentials, and Vault handles the full lifecycle.
0
Static Passwords
1h
Credential TTL
Auto
TLS Renewal
Comments
No comments yet. Be the first to comment!