·20 min read·

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.

HashiCorp VaultAWS RDSMySQLPythonFastAPILet's Encrypt

Python App + Vault + AWS RDS

Dynamic Secrets for MySQL

PythonFastAPI/Flask
hvac SDK
VaultSecrets Engine
Create User
AWS RDSMySQL

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!