Vault External TLS Configuration

Configure Vault to accept TLS connections from the network, enabling enterprise-grade automation without SSH tunnels.

Prerequisites

  • Vault cluster operational and unsealed

  • Vault PKI engine configured with pki_int (DOMUS-ISSUING-CA)

  • SSH access to vault-01.inside.domusdigitalis.dev

  • Root token access for initial setup

Document Description

Vault SSH CA

SSH certificate authority (requires external TLS for automation)

PKI Cert Issuance

Certificate issuance procedures

Vault Backup

Backup procedures

Architecture Overview

Vault TLS Architecture

Current State Problems

  • Automation scripts cannot reach Vault directly

  • SSH tunnels are manual and fragile

  • Not enterprise-scalable

Target State Benefits

  • Automation scripts work directly from workstation

  • No SSH tunnels required

  • Proper TLS certificate from enterprise PKI

  • Works like production enterprise infrastructure

Phase 1: Issue TLS Certificate for Vault

1.1 SSH to Vault Server

ssh vault-01

1.2 Set Vault Address (Current Localhost)

export VAULT_ADDR='http://127.0.0.1:8200'

1.3 Unseal and Authenticate

Check status:

vault status | awk 'NR <= 5'

If sealed, unseal (threshold 2):

vault operator unseal
vault operator unseal

Set token:

export VAULT_TOKEN='<paste-VAULT_TOKEN-from-dsec>'

1.4 Validate DNS Resolution

Before issuing certificate, verify DNS resolves correctly:

dig +short vault-01.inside.domusdigitalis.dev
Expected Output
10.50.1.60
dig +short -x 10.50.1.60
Expected Output
vault-01.inside.domusdigitalis.dev.

1.5 Verify PKI Role Constraints

Check the role allows localhost and subdomains:

vault read pki_int/roles/domus-server | awk '/allowed_domains|allow_subdomains|allow_bare_domains|allow_localhost/'
Expected Output
allow_bare_domains                    false
allow_localhost                       true
allow_subdomains                      true
allowed_domains                       [inside.domusdigitalis.dev domusdigitalis.dev]
allow_bare_domains: false means short hostnames like vault-01 are not allowed in certificates. This is correct - enterprise best practice uses FQDNs only.

1.6 Issue TLS Certificate from Vault PKI

Issue a server certificate for Vault itself:

vault write -format=json pki_int/issue/domus-server \
  common_name="vault-01.inside.domusdigitalis.dev" \
  alt_names="localhost" \
  ip_sans="10.50.1.60,127.0.0.1" \
  ttl="8760h" > /tmp/vault-tls-cert.json

Best Practice: Do NOT include short hostnames (e.g., vault-01) in certificates.

  • Access via FQDN: vault-01.inside.domusdigitalis.dev

  • Access via IP: 10.50.1.60

  • Local access: localhost or 127.0.0.1

Short hostnames are ambiguous and not enterprise-scalable.

1.7 Verify Certificate Issuance (jq)

Inspect the JSON response structure:

jq '.data | keys' /tmp/vault-tls-cert.json
Expected Output
["ca_chain", "certificate", "expiration", "issuing_ca", "private_key", "private_key_type", "serial_number"]

Verify certificate details directly from JSON + openssl pipeline:

jq -r '.data.certificate' /tmp/vault-tls-cert.json | openssl x509 -noout -subject -dates
Expected Output
subject=CN=vault-01.inside.domusdigitalis.dev
notBefore=Feb 20 21:05:31 2026 GMT
notAfter=Feb 20 21:06:01 2027 GMT

Extract serial and expiration (Unix epoch → human readable):

jq -r '.data | "Serial: \(.serial_number)\nExpires: \(.expiration | todate)"' /tmp/vault-tls-cert.json

1.8 Extract Certificate Components

jq -r '.data.certificate' /tmp/vault-tls-cert.json > /tmp/vault-tls.crt
jq -r '.data.private_key' /tmp/vault-tls-cert.json > /tmp/vault-tls.key
jq -r '.data.ca_chain[]' /tmp/vault-tls-cert.json > /tmp/vault-ca-chain.crt

Verify all files created with content:

for f in /tmp/vault-tls.{crt,key} /tmp/vault-ca-chain.crt; do
  [[ -s "$f" ]] && echo "✓ $f ($(wc -c < "$f") bytes)" || echo "✗ $f MISSING"
done

1.9 Verify Certificate SANs

openssl x509 -in /tmp/vault-tls.crt -noout -text | awk '/Subject Alternative Name/{getline; gsub(/^[[:space:]]+/, ""); print}'
Expected Output
DNS:vault-01.inside.domusdigitalis.dev, DNS:localhost, IP Address:10.50.1.60, IP Address:127.0.0.1
No short hostname (vault-01) - this is correct per best practice.

Phase 2: Install TLS Certificate

2.1 Create Vault TLS Directory

sudo mkdir -p /opt/vault/tls
sudo chown vault:vault /opt/vault/tls
sudo chmod 750 /opt/vault/tls

2.2 Install Certificate and Key

sudo cp /tmp/vault-tls.crt /opt/vault/tls/vault.crt
sudo cp /tmp/vault-tls.key /opt/vault/tls/vault.key
sudo cp /tmp/vault-ca-chain.crt /opt/vault/tls/ca-chain.crt

Set ownership and permissions:

sudo chown vault:vault /opt/vault/tls/*
sudo chmod 640 /opt/vault/tls/vault.crt
sudo chmod 600 /opt/vault/tls/vault.key
sudo chmod 640 /opt/vault/tls/ca-chain.crt

2.3 Verify Installation

Check files exist with correct permissions (awk formatted):

sudo ls -la /opt/vault/tls/ | awk 'NR>1 {printf "%-12s %-6s %-6s %8s  %s\n", $1, $3, $4, $5, $NF}'
sudo required - directory has 750 permissions (vault:vault only).
Expected Output
-rw-r-----   vault  vault     4248  ca-chain.crt
-rw-r-----   vault  vault     1879  vault.crt
-rw-------   vault  vault     1675  vault.key
If old files exist from previous attempts, clean up: sudo rm /opt/vault/tls/{tls.crt,tls.key,vault-tls.key} 2>/dev/null

Verify certificate subject matches hostname:

openssl x509 -in /opt/vault/tls/vault.crt -noout -subject | awk -F= '{print $NF}'
Expected Output
vault-01.inside.domusdigitalis.dev

Verify CA chain contains both certificates:

openssl storeutl -certs /opt/vault/tls/ca-chain.crt 2>/dev/null | awk '/subject=/ {gsub(/.*CN=/, ""); print}'
Expected Output
DOMUS-ISSUING-CA
DOMUS-ROOT-CA

Phase 3: Configure Vault TLS Listener

3.1 Backup Current Configuration

sudo cp /etc/vault.d/vault.hcl /etc/vault.d/vault.hcl.bak.$(date +%Y%m%d)

3.2 View Current Configuration

sudo awk '{print NR": "$0}' /etc/vault.d/vault.hcl

3.3 Update Vault Configuration

Vault 1.20+ Breaking Change: disable_mlock must be explicitly set.

  • disable_mlock = true - Safe when disk is LUKS encrypted (secrets protected at rest)

  • disable_mlock = false - More secure but requires setcap cap_ipc_lock=+ep /usr/bin/vault and more RAM

First, check your current storage backend:

awk '/storage/ {print NR": "$0}' /etc/vault.d/vault.hcl.bak.*

Storage backends are NOT interchangeable!

  • storage "file" - Single-node, file-based storage

  • storage "raft" - Multi-node, integrated HA storage

If your backup shows storage "file", you MUST keep using file storage. Changing to raft will make Vault appear uninitialized (data loss).

For File Storage (single node):

sudo tee /etc/vault.d/vault.hcl << 'EOF'
# Vault Server Configuration - External TLS with File Storage
ui = true
disable_mlock = true

storage "file" {
  path = "/opt/vault/data"
}

# HTTPS listener (bound to specific IP - not all interfaces)
listener "tcp" {
  address         = "{vault-primary-ip}:8200"
  tls_cert_file   = "/opt/vault/tls/vault.crt"
  tls_key_file    = "/opt/vault/tls/vault.key"
  tls_client_ca_file = "/opt/vault/tls/ca-chain.crt"
  tls_min_version = "tls12"
}

api_addr = "https://vault-01.inside.domusdigitalis.dev:8200"
EOF

For Raft Storage (HA cluster - new installations only):

sudo tee /etc/vault.d/vault.hcl << 'EOF'
# Vault Server Configuration - External TLS with Raft Storage
ui = true
disable_mlock = true

storage "raft" {
  path    = "/opt/vault/data"
  node_id = "vault-1"
}

# HTTPS listener (bound to specific IP - not all interfaces)
listener "tcp" {
  address         = "{vault-primary-ip}:8200"
  tls_cert_file   = "/opt/vault/tls/vault.crt"
  tls_key_file    = "/opt/vault/tls/vault.key"
  tls_client_ca_file = "/opt/vault/tls/ca-chain.crt"
  tls_min_version = "tls12"
}

cluster_addr = "https://{vault-primary-ip}:8201"
api_addr     = "https://vault-01.inside.domusdigitalis.dev:8200"
EOF

Security hardening: Binding to 10.50.1.60:8200 instead of 0.0.0.0:8200 restricts Vault to only accept connections on that specific interface - not all network interfaces.

3.4 Validate Configuration (Dry-Run)

Vault doesn’t support -check. Use dry-run in foreground instead:

sudo -u vault vault server -config=/etc/vault.d/vault.hcl
Expected Output
==> Vault server configuration:

             Api Address: https://vault-01.inside.domusdigitalis.dev:8200
         Cluster Address: https://10.50.1.60:8201
              Listener 1: tcp (addr: "10.50.1.60:8200", ... tls: "enabled")
                   Mlock: supported: true, enabled: false
                 Storage: raft (HA available)
                 Version: Vault v1.21.2

==> Vault server started! Log data will stream in below:

Press Ctrl+C to stop the dry-run, then proceed to restart via systemd.

Phase 4: Restart Vault

Restarting Vault will seal it. Have unseal keys ready.

4.1 Restart Service

sudo systemctl restart vault

4.2 Check Service Status

systemctl is-active vault && echo "Vault service running"

4.3 Copy CA Chain for Client Access

The CA chain in /opt/vault/tls/ has 640 permissions (vault:vault only). Clients need a world-readable copy:

sudo cp /opt/vault/tls/ca-chain.crt /etc/ssl/certs/DOMUS-CA-CHAIN.pem
sudo chmod 644 /etc/ssl/certs/DOMUS-CA-CHAIN.pem
The CA chain is public information - it’s meant to be distributed to all clients. Only the private key (vault.key) must remain restricted.

4.4 Test TLS Connection Locally

export VAULT_ADDR='https://10.50.1.60:8200'
export VAULT_CACERT='/etc/ssl/certs/DOMUS-CA-CHAIN.pem'
vault status | awk 'NR <= 5'
Expected Output (Sealed after restart)
Key             Value
---             -----
Seal Type       shamir
Initialized     true
Sealed          true

4.5 Unseal Vault

vault operator unseal
vault operator unseal

4.6 Verify Unsealed with TLS

vault status | awk '/Sealed/ {print $1, $2}'
Expected Output
Sealed false

Phase 5: Configure Clients

5.1 Export CA Chain

Copy CA chain from vault-01 to workstation:

exit  # Return to workstation
scp vault-01:/opt/vault/tls/ca-chain.crt /tmp/vault-ca-chain.crt

5.2 Install CA Chain on Workstation

For system-wide trust:

sudo cp /tmp/vault-ca-chain.crt /etc/ssl/certs/DOMUS-CA-CHAIN.pem

For Vault CLI only:

mkdir -p ~/.config/vault
cp /tmp/vault-ca-chain.crt ~/.config/vault/ca-chain.pem

5.3 Configure Shell Environment

Add to ~/.zshrc or ~/.bashrc:

# HashiCorp Vault - External TLS
export VAULT_ADDR='https://vault-01.inside.domusdigitalis.dev:8200'
export VAULT_CACERT='/etc/ssl/certs/DOMUS-CA-CHAIN.pem'

Source the configuration:

source ~/.zshrc

5.4 Test External Connection

vault status | awk 'NR <= 5'
Expected Output
Key             Value
---             -----
Seal Type       shamir
Initialized     true
Sealed          false

SUCCESS: Vault is now accessible over TLS from the network.

Phase 6: Update dsec

Update the dsec vault configuration with external access:

dsec edit d000 dev/vault

Update/add these values:

VAULT_ADDR: 'https://vault-01.inside.domusdigitalis.dev:8200'
VAULT_CACERT: '/etc/ssl/certs/DOMUS-CA-CHAIN.pem'
VAULT_TOKEN: '<current-root-token>'
Get current root token from vault token lookup on vault-01 while you still have SSH access.

6.1 Verify dsec Configuration

dsource d000 dev/vault
echo "VAULT_ADDR=$VAULT_ADDR"
vault status | awk 'NR <= 5'

Phase 7: Update Automation Scripts

7.1 Update vault-ssh-sign Script

The automation script in Vault SSH CA now works without SSH tunnels:

tee ~/.local/bin/vault-ssh-sign << 'SCRIPT'
#!/bin/bash
set -euo pipefail

KEY="${1:-$HOME/.ssh/id_ed25519_vault}"
CERT="${KEY}-cert.pub"

# Load Vault credentials from dsec
eval "$(dsource d000 dev/vault 2>/dev/null)" || {
    echo "ERROR: Failed to load Vault credentials from dsec" >&2
    exit 1
}

# Verify VAULT_ADDR and VAULT_CACERT are set
[[ -z "${VAULT_ADDR:-}" ]] && { echo "ERROR: VAULT_ADDR not set" >&2; exit 1; }
[[ -z "${VAULT_CACERT:-}" ]] && { echo "ERROR: VAULT_CACERT not set" >&2; exit 1; }

# Sign the key
vault write -field=signed_key ssh/sign/domus-client \
  public_key=@"${KEY}.pub" > "$CERT"

# Display validity
echo "Certificate signed successfully:"
ssh-keygen -Lf "$CERT" | awk '/Valid:/ {print "  "$0}'
SCRIPT
chmod +x ~/.local/bin/vault-ssh-sign

7.2 Test Automation

vault-ssh-sign
Expected Output
Certificate signed successfully:
        Valid: from 2026-02-20T08:00:00 to 2026-02-20T16:00:00

Phase 8: Firewall Configuration

8.1 VyOS (If Vault is Behind Firewall)

If vault-01 is on a different VLAN/network segment, ensure port 8200 is allowed in VyOS firewall:

  • Source: Workstation VLAN (e.g., 10.50.10.0/24)

  • Destination: 10.50.1.60

  • Port: 8200 (TCP)

  • Action: Allow

8.2 Host Firewall (firewalld)

On vault-01:

sudo firewall-cmd --permanent --add-port=8200/tcp
sudo firewall-cmd --reload

Verify:

sudo firewall-cmd --list-ports | grep 8200

Completion Checklist

Phase Task Status

1

TLS certificate issued from Vault PKI

[ ]

2

Certificate installed in /opt/vault/tls/

[ ]

3

Vault configuration updated with TLS listener

[ ]

4

Vault restarted and unsealed

[ ]

5

CA chain installed on workstation

[ ]

6

dsec updated with external VAULT_ADDR

[ ]

7

Automation scripts working without SSH tunnel

[ ]

8

Firewall rules configured (if needed)

[ ]

Rollback Procedure

If TLS configuration fails:

Restore Localhost-Only Configuration

On vault-01:

sudo cp /etc/vault.d/vault.hcl.bak.* /etc/vault.d/vault.hcl
sudo systemctl restart vault

Unseal with localhost:

export VAULT_ADDR='http://127.0.0.1:8200'
vault operator unseal
vault operator unseal

Certificate Renewal

TLS certificate expires annually. Set a calendar reminder to renew 30 days before expiry.

Renewal Process

  1. SSH to vault-01

  2. Issue new certificate (Phase 1.4)

  3. Install certificate (Phase 2.2)

  4. Restart Vault (Phase 4.1)

  5. Unseal Vault (Phase 4.4)

Automation (Future)

For fully automated renewal, configure a systemd timer to:

  1. Check certificate expiry

  2. Issue new certificate via Vault API

  3. Replace files

  4. Reload Vault (if supported) or restart

Troubleshooting

TLS Handshake Failed

Check certificate validity:

openssl s_client -connect vault-01.inside.domusdigitalis.dev:8200 -CAfile /etc/ssl/certs/DOMUS-CA-CHAIN.pem

Look for:

  • Verify return code: 0 (ok) = Success

  • Verify return code: 19 = CA not trusted - install CA chain

  • Verify return code: 10 = Certificate expired - renew

Connection Refused

  1. Check Vault is running:

    ssh vault-01 "systemctl is-active vault"
  2. Check Vault is listening on network:

    ssh vault-01 "ss -tlnp | grep 8200"
    Expected Output (network accessible)
    LISTEN  0  128  0.0.0.0:8200  ...

    If showing 127.0.0.1:8200, TLS configuration not applied.

  3. Check firewall:

    ssh vault-01 "sudo firewall-cmd --list-ports"

Certificate Not Trusted

Ensure VAULT_CACERT points to the CA chain:

echo $VAULT_CACERT
ls -la $VAULT_CACERT

Verify chain contains both CAs:

openssl storeutl -certs $VAULT_CACERT | grep subject
Expected Output
subject=CN=DOMUS-ISSUING-CA
subject=CN=DOMUS-ROOT-CA

See Also