Build a production-grade PKI infrastructure using HashiCorp Vault with root and intermediate certificate authorities. Enable automated SSL certificate generation and renewal for your applications with RBAC policies.
Prerequisites
- Root or sudo access
- Basic understanding of PKI concepts
- Familiarity with SSL certificates
- Command line experience
What this solves
HashiCorp Vault provides enterprise-grade PKI capabilities for managing SSL certificates at scale. This tutorial sets up Vault as a certificate authority with automated certificate issuance, renewal, and revocation. You'll configure a root CA, intermediate CA structure, and integrate Vault Agent for automatic certificate lifecycle management across your infrastructure.
Step-by-step installation
Update system packages and install dependencies
Start by updating your system and installing required packages for Vault installation.
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl wget gnupg software-properties-common
Add HashiCorp repository and install Vault
Add the official HashiCorp repository and install Vault using the package manager.
curl -fsSL 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
sudo apt update
sudo apt install -y vault
Create Vault system user and directories
Create a dedicated system user for Vault and set up the required directory structure with proper permissions.
sudo useradd --system --home /etc/vault.d --shell /bin/false vault
sudo mkdir -p /opt/vault/data /etc/vault.d /var/log/vault
sudo chown -R vault:vault /opt/vault /etc/vault.d /var/log/vault
sudo chmod 750 /opt/vault/data /etc/vault.d
sudo chmod 755 /var/log/vault
Configure Vault server
Create the main Vault configuration file with storage backend, listener, and security settings.
storage "file" {
path = "/opt/vault/data"
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = 1
}
api_addr = "http://127.0.0.1:8200"
cluster_addr = "http://127.0.0.1:8201"
ui = true
Enable audit logging
audit {
enabled = true
type = "file"
file_path = "/var/log/vault/audit.log"
}
Disable mlock for development (enable in production)
disable_mlock = true
Set proper file permissions for configuration
Secure the Vault configuration file to prevent unauthorized access to sensitive settings.
sudo chown vault:vault /etc/vault.d/vault.hcl
sudo chmod 640 /etc/vault.d/vault.hcl
Create systemd service file
Configure Vault to run as a systemd service with proper security settings and automatic restart capabilities.
[Unit]
Description=HashiCorp Vault
Documentation=https://vaultproject.io/docs/
Requires=network-online.target
After=network-online.target
[Service]
Type=notify
User=vault
Group=vault
ProtectSystem=full
ProtectHome=read-only
PrivateTmp=yes
PrivateDevices=yes
SecureBits=keep-caps
AmbientCapabilities=CAP_IPC_LOCK
Capabilities=CAP_IPC_LOCK+ep
CapabilityBoundingSet=CAP_SYSLOG CAP_IPC_LOCK
NoNewPrivileges=yes
ExecStart=/usr/bin/vault server -config=/etc/vault.d/vault.hcl
ExecReload=/bin/kill -HUP $MAINPID
KillMode=process
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
StartLimitInterval=60
StartLimitBurst=3
LimitNOFILE=65536
LimitMEMLOCK=infinity
[Install]
WantedBy=multi-user.target
Enable and start Vault service
Start Vault and enable it to automatically start on system boot.
sudo systemctl daemon-reload
sudo systemctl enable --now vault
sudo systemctl status vault
Initialize Vault
Initialize Vault with 5 key shares and a threshold of 3. Store the unseal keys and root token securely.
export VAULT_ADDR='http://127.0.0.1:8200'
vault operator init -key-shares=5 -key-threshold=3
Unseal Vault
Unseal Vault using 3 of the 5 unseal keys from the initialization step.
vault operator unseal [UNSEAL_KEY_1]
vault operator unseal [UNSEAL_KEY_2]
vault operator unseal [UNSEAL_KEY_3]
Authenticate with root token
Log in to Vault using the root token to begin configuration.
vault auth [ROOT_TOKEN]
Configure PKI secrets engine
Enable PKI secrets engine for root CA
Enable the PKI secrets engine at the root CA path with extended TTL for long-lived root certificates.
vault secrets enable -path=pki-root -max-lease-ttl=87600h pki
vault write pki-root/config/urls \
issuing_certificates="http://127.0.0.1:8200/v1/pki-root/ca" \
crl_distribution_points="http://127.0.0.1:8200/v1/pki-root/crl"
Generate root certificate authority
Create the root CA certificate with a 10-year validity period. This will be your trust anchor.
vault write pki-root/root/generate/internal \
common_name="Example.com Root CA" \
country="US" \
locality="San Francisco" \
organization="Example Corporation" \
ou="IT Department" \
ttl=87600h \
key_bits=4096 \
exclude_cn_from_sans=true
Enable intermediate PKI secrets engine
Create a separate PKI mount for the intermediate CA with shorter TTL appropriate for day-to-day certificate signing.
vault secrets enable -path=pki-int -max-lease-ttl=8760h pki
vault write pki-int/config/urls \
issuing_certificates="http://127.0.0.1:8200/v1/pki-int/ca" \
crl_distribution_points="http://127.0.0.1:8200/v1/pki-int/crl"
Generate intermediate certificate signing request
Create a CSR for the intermediate CA that will be signed by the root CA.
vault write -format=json pki-int/intermediate/generate/internal \
common_name="Example.com Intermediate CA" \
country="US" \
locality="San Francisco" \
organization="Example Corporation" \
ou="IT Department" \
key_bits=4096 | jq -r '.data.csr' > pki_intermediate.csr
Sign intermediate certificate with root CA
Use the root CA to sign the intermediate certificate, creating the certificate chain.
vault write -format=json pki-root/root/sign-intermediate \
csr=@pki_intermediate.csr \
format=pem_bundle \
ttl=8760h | jq -r '.data.certificate' > intermediate.cert.pem
Set intermediate certificate
Install the signed intermediate certificate back into the intermediate PKI engine.
vault write pki-int/intermediate/set-signed \
certificate=@intermediate.cert.pem
Create PKI role for certificate issuance
Define a role that specifies which certificates can be issued and their parameters.
vault write pki-int/roles/example-com \
allowed_domains="example.com" \
allow_subdomains=true \
allow_any_name=false \
enforce_hostnames=true \
allow_ip_sans=true \
server_flag=true \
client_flag=true \
max_ttl="720h" \
key_bits=2048
Configure RBAC and policies
Create certificate management policy
Define a policy that allows certificate creation and management operations.
# Allow reading PKI mounts
path "pki-int/cert/*" {
capabilities = ["read"]
}
Allow listing certificates
path "pki-int/certs" {
capabilities = ["list"]
}
Allow certificate generation
path "pki-int/issue/example-com" {
capabilities = ["create", "update"]
}
Allow certificate revocation
path "pki-int/revoke" {
capabilities = ["create", "update"]
}
Allow reading CA certificate
path "pki-int/ca/pem" {
capabilities = ["read"]
}
Allow reading CRL
path "pki-int/crl/pem" {
capabilities = ["read"]
}
vault policy write pki-policy /tmp/pki-policy.hcl
Enable userpass authentication
Enable username/password authentication for user access to the PKI system.
vault auth enable userpass
Create PKI user with limited permissions
Create a user account with the PKI policy for certificate management operations.
vault write auth/userpass/users/pki-user \
password="SecurePKIPassword123!" \
policies="pki-policy"
Set up Vault Agent for certificate automation
Create Vault Agent configuration directory
Set up directories for Vault Agent configuration and certificate storage.
sudo mkdir -p /etc/vault-agent /opt/vault-agent/certs
sudo chown -R vault:vault /etc/vault-agent /opt/vault-agent
sudo chmod 750 /etc/vault-agent /opt/vault-agent
Configure Vault Agent for automatic certificate renewal
Create Vault Agent configuration for automated certificate lifecycle management.
vault {
address = "http://127.0.0.1:8200"
retry {
num_retries = 5
}
}
auto_auth {
method "userpass" {
mount_path = "auth/userpass"
config = {
username = "pki-user"
password = "SecurePKIPassword123!"
}
}
sink "file" {
config = {
path = "/opt/vault-agent/token"
}
}
}
template {
source = "/etc/vault-agent/cert.tpl"
destination = "/opt/vault-agent/certs/server.crt"
perms = 0644
command = "systemctl reload nginx"
}
template {
source = "/etc/vault-agent/key.tpl"
destination = "/opt/vault-agent/certs/server.key"
perms = 0600
command = "systemctl reload nginx"
}
Create certificate template files
Create templates that Vault Agent will use to generate certificate files.
{{- with secret "pki-int/issue/example-com" "common_name=web.example.com" "ttl=720h" }}
{{ .Data.certificate }}
{{ .Data.issuing_ca }}
{{- end }}
{{- with secret "pki-int/issue/example-com" "common_name=web.example.com" "ttl=720h" }}
{{ .Data.private_key }}
{{- end }}
Set proper permissions for Vault Agent files
Secure the Vault Agent configuration and template files.
sudo chown -R vault:vault /etc/vault-agent
sudo chmod 640 /etc/vault-agent/agent.hcl
sudo chmod 644 /etc/vault-agent/*.tpl
Create Vault Agent systemd service
Configure Vault Agent to run as a systemd service for automatic certificate renewal.
[Unit]
Description=Vault Agent
Requires=vault.service
After=vault.service
[Service]
Restart=on-failure
User=vault
Group=vault
ExecStart=/usr/bin/vault agent -config /etc/vault-agent/agent.hcl
ExecReload=/bin/kill -HUP $MAINPID
KillMode=process
[Install]
WantedBy=multi-user.target
Enable and start Vault Agent
Start Vault Agent to begin automatic certificate management.
sudo systemctl daemon-reload
sudo systemctl enable --now vault-agent
sudo systemctl status vault-agent
Verify your setup
Test your PKI infrastructure by generating certificates and verifying the certificate chain.
# Check Vault status
vault status
Generate a test certificate
vault write pki-int/issue/example-com \
common_name="test.example.com" \
ttl="72h"
Verify certificate chain
openssl x509 -in /opt/vault-agent/certs/server.crt -text -noout
Check certificate expiration
openssl x509 -in /opt/vault-agent/certs/server.crt -enddate -noout
Verify Vault Agent is running
sudo systemctl status vault-agent
Check audit logs
sudo tail -f /var/log/vault/audit.log
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Vault won't unseal | Missing or incorrect unseal keys | Use correct unseal keys from initialization |
| Permission denied errors | Incorrect file permissions | sudo chown -R vault:vault /opt/vault |
| PKI role creation fails | Vault not authenticated | Run vault auth [ROOT_TOKEN] |
| Certificate generation fails | Policy doesn't allow operation | Verify policy with vault policy read pki-policy |
| Vault Agent can't authenticate | User password incorrect | Reset password with vault write auth/userpass/users/pki-user password="NewPassword" |
| Template rendering fails | Certificate expired or revoked | Check certificate status and regenerate |
Next steps
- Configure NGINX reverse proxy with SSL certificates
- Set up HAProxy SSL termination with Let's Encrypt
- Monitor your Vault infrastructure with Prometheus and Grafana
- Configure Vault high availability clustering
- Set up Vault auto-unseal with cloud HSM
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# HashiCorp Vault PKI CA Installation Script
# Production-ready installer for Vault with PKI capabilities
# Colors for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m'
# Global variables
VAULT_VERSION="1.15.6"
VAULT_USER="vault"
VAULT_CONFIG_DIR="/etc/vault.d"
VAULT_DATA_DIR="/opt/vault/data"
VAULT_LOG_DIR="/var/log/vault"
# Print colored output
print_status() {
echo -e "${GREEN}[INFO]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Usage message
usage() {
cat << EOF
Usage: $0 [OPTIONS]
Install HashiCorp Vault as PKI Certificate Authority
OPTIONS:
-h, --help Show this help message
-a, --api-addr ADDR Vault API address (default: http://127.0.0.1:8200)
-c, --cluster-addr ADDR Vault cluster address (default: http://127.0.0.1:8201)
Example:
$0
$0 --api-addr http://10.0.1.100:8200
EOF
exit 1
}
# Parse command line arguments
API_ADDR="http://127.0.0.1:8200"
CLUSTER_ADDR="http://127.0.0.1:8201"
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
usage
;;
-a|--api-addr)
API_ADDR="$2"
shift 2
;;
-c|--cluster-addr)
CLUSTER_ADDR="$2"
shift 2
;;
*)
print_error "Unknown option: $1"
usage
;;
esac
done
# Cleanup function
cleanup() {
local exit_code=$?
if [[ $exit_code -ne 0 ]]; then
print_error "Installation failed. Cleaning up..."
systemctl stop vault 2>/dev/null || true
systemctl disable vault 2>/dev/null || true
userdel -r "$VAULT_USER" 2>/dev/null || true
rm -rf "$VAULT_DATA_DIR" "$VAULT_CONFIG_DIR" "$VAULT_LOG_DIR"
rm -f /etc/systemd/system/vault.service
systemctl daemon-reload 2>/dev/null || true
fi
}
trap cleanup ERR
# Check prerequisites
check_prerequisites() {
print_status "[1/12] Checking prerequisites..."
if [[ $EUID -ne 0 ]]; then
print_error "This script must be run as root"
exit 1
fi
if ! command -v systemctl &> /dev/null; then
print_error "systemd is required but not found"
exit 1
fi
}
# Detect distribution
detect_distro() {
print_status "[2/12] Detecting distribution..."
if [[ ! -f /etc/os-release ]]; then
print_error "Cannot detect distribution - /etc/os-release not found"
exit 1
fi
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update && apt upgrade -y"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf update -y"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum update -y"
;;
*)
print_error "Unsupported distribution: $ID"
exit 1
;;
esac
print_status "Detected: $PRETTY_NAME ($PKG_MGR)"
}
# Update system packages
update_system() {
print_status "[3/12] Updating system packages..."
$PKG_UPDATE
case "$PKG_MGR" in
apt)
$PKG_INSTALL curl wget gnupg software-properties-common
;;
dnf|yum)
$PKG_INSTALL curl wget gnupg2 yum-utils
;;
esac
}
# Add HashiCorp repository
add_hashicorp_repo() {
print_status "[4/12] Adding HashiCorp repository..."
case "$PKG_MGR" in
apt)
curl -fsSL https://apt.releases.hashicorp.com/gpg | 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" > /etc/apt/sources.list.d/hashicorp.list
apt update
;;
dnf)
dnf config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
;;
yum)
yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
;;
esac
}
# Install Vault
install_vault() {
print_status "[5/12] Installing Vault..."
$PKG_INSTALL vault
if ! command -v vault &> /dev/null; then
print_error "Vault installation failed"
exit 1
fi
print_status "Vault $(vault version | head -n1) installed successfully"
}
# Create Vault user and directories
create_vault_user() {
print_status "[6/12] Creating Vault system user and directories..."
if ! id "$VAULT_USER" &>/dev/null; then
useradd --system --home "$VAULT_CONFIG_DIR" --shell /bin/false "$VAULT_USER"
fi
mkdir -p "$VAULT_DATA_DIR" "$VAULT_CONFIG_DIR" "$VAULT_LOG_DIR"
chown -R "$VAULT_USER:$VAULT_USER" "$VAULT_DATA_DIR" "$VAULT_CONFIG_DIR" "$VAULT_LOG_DIR"
chmod 750 "$VAULT_DATA_DIR" "$VAULT_CONFIG_DIR"
chmod 755 "$VAULT_LOG_DIR"
}
# Configure Vault
configure_vault() {
print_status "[7/12] Creating Vault configuration..."
cat > "$VAULT_CONFIG_DIR/vault.hcl" << EOF
storage "file" {
path = "$VAULT_DATA_DIR"
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = 1
}
api_addr = "$API_ADDR"
cluster_addr = "$CLUSTER_ADDR"
ui = true
# Enable audit logging
audit {
enabled = true
type = "file"
file_path = "$VAULT_LOG_DIR/audit.log"
}
# Disable mlock for development (enable in production)
disable_mlock = true
EOF
chown "$VAULT_USER:$VAULT_USER" "$VAULT_CONFIG_DIR/vault.hcl"
chmod 640 "$VAULT_CONFIG_DIR/vault.hcl"
}
# Create systemd service
create_systemd_service() {
print_status "[8/12] Creating systemd service..."
cat > /etc/systemd/system/vault.service << 'EOF'
[Unit]
Description=HashiCorp Vault
Documentation=https://vaultproject.io/docs/
Requires=network-online.target
After=network-online.target
[Service]
Type=notify
User=vault
Group=vault
ProtectSystem=full
ProtectHome=read-only
PrivateTmp=yes
PrivateDevices=yes
SecureBits=keep-caps
AmbientCapabilities=CAP_IPC_LOCK
Capabilities=CAP_IPC_LOCK+ep
CapabilityBoundingSet=CAP_SYSLOG CAP_IPC_LOCK
NoNewPrivileges=yes
ExecStart=/usr/bin/vault server -config=/etc/vault.d/vault.hcl
ExecReload=/bin/kill -HUP $MAINPID
KillMode=process
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
StartLimitInterval=60
StartLimitBurst=3
LimitNOFILE=65536
LimitMEMLOCK=infinity
[Install]
WantedBy=multi-user.target
EOF
chmod 644 /etc/systemd/system/vault.service
}
# Configure firewall
configure_firewall() {
print_status "[9/12] Configuring firewall..."
if command -v ufw &> /dev/null && ufw status | grep -q "Status: active"; then
ufw allow 8200/tcp comment "Vault API"
ufw allow 8201/tcp comment "Vault Cluster"
elif command -v firewall-cmd &> /dev/null && systemctl is-active firewalld &> /dev/null; then
firewall-cmd --permanent --add-port=8200/tcp
firewall-cmd --permanent --add-port=8201/tcp
firewall-cmd --reload
else
print_warning "No active firewall detected. Manually configure firewall to allow ports 8200 and 8201"
fi
}
# Start Vault service
start_vault_service() {
print_status "[10/12] Starting Vault service..."
systemctl daemon-reload
systemctl enable vault
systemctl start vault
# Wait for Vault to start
sleep 3
if ! systemctl is-active vault &> /dev/null; then
print_error "Vault service failed to start"
systemctl status vault
exit 1
fi
}
# Set up environment
setup_environment() {
print_status "[11/12] Setting up environment..."
echo "export VAULT_ADDR='$API_ADDR'" > /etc/profile.d/vault.sh
chmod 644 /etc/profile.d/vault.sh
export VAULT_ADDR="$API_ADDR"
# Wait for Vault API to be ready
for i in {1..30}; do
if vault status &> /dev/null; then
break
fi
sleep 1
done
}
# Verify installation
verify_installation() {
print_status "[12/12] Verifying installation..."
# Check service status
if ! systemctl is-active vault &> /dev/null; then
print_error "Vault service is not running"
exit 1
fi
# Check Vault status
if ! vault status &> /dev/null; then
print_error "Cannot connect to Vault API"
exit 1
fi
print_status "Vault installation completed successfully!"
print_warning "Next steps:"
echo "1. Initialize Vault: vault operator init -key-shares=5 -key-threshold=3"
echo "2. Unseal Vault using 3 of the 5 unseal keys"
echo "3. Configure PKI secrets engine"
echo "4. Access Vault UI at: $API_ADDR"
echo "5. Store unseal keys and root token securely!"
}
# Main installation function
main() {
print_status "Starting HashiCorp Vault PKI CA installation..."
check_prerequisites
detect_distro
update_system
add_hashicorp_repo
install_vault
create_vault_user
configure_vault
create_systemd_service
configure_firewall
start_vault_service
setup_environment
verify_installation
print_status "Installation completed successfully!"
}
main "$@"
Review the script before running. Execute with: bash install.sh