Skip to main content
  1. Posts/

Vault · TLS Enabled, HA with Raft Storage

·1471 words·7 mins
Author
Daniel Lincu
I build, automate, and document Kubernetes infrastructure in my TechCats Homelab.

Overview
#

This post documents the deployment of a TLS-enabled, highly available HashiCorp Vault cluster on Kubernetes.
The setup is managed entirely through GitOps using Argo CD, providing declarative configuration, automated reconciliation, and version-controlled infrastructure.
Vault runs in HA mode with Raft integrated storage, and all communication is encrypted with certificates issued by cert-manager.
It serves as the core of the homelab’s secrets management stack — integrated with Traefik for secure ingress and with the CSI driver for secret injection into workloads.

The architecture below illustrates the complete Vault deployment, from certificate management and ingress to secret delivery via the CSI driver.

Vault TLS Architecture — HA Cluster with Raft and GitOps Deployment
Figure 1: Vault TLS Architecture — HA cluster with Raft and GitOps deployment. cert-manager manages all certificates, Traefik validates backend communication via ServersTransport, and the Vault CSI integration injects secrets directly into Kubernetes workloads.

Vault
#

Vault acts as the central secrets authority within the cluster.
It stores and manages sensitive data such as credentials, tokens, and certificates, providing access to workloads through Kubernetes authentication and the Vault CSI Provider.
The deployment uses Raft integrated storage for high availability and persistent Longhorn volumes for durability.
All configuration, TLS setup, and lifecycle management are handled declaratively via Argo CD.

Vault Raft list peers output showing leader and followers
Figure 2: Vault Raft cluster in HA mode with one leader and two followers, confirming successful peer replication.

Helm Values
#

Vault is deployed using the official HashiCorp Helm chart, configured for high availability with Raft integrated storage.
Persistent data is stored on Longhorn, and all traffic is encrypted using certificates issued by cert-manager.

# Excerpt from values.yaml
# ============================================================
# Vault | TLS Enabled, HA with Raft Storage
# ============================================================

global:
  enabled: true
  tlsDisable: false

server:
  logLevel: info
  logFormat: json

  # ----------------------------------------------------------
  # TLS Certificates (mounted from cert-manager Secret)
  # ----------------------------------------------------------
  extraEnvironmentVars:
    VAULT_CACERT: /vault/userconfig/tls/vault-internal-tls/ca.crt
    VAULT_TLSCERT: /vault/userconfig/tls/vault-internal-tls/tls.crt
    VAULT_TLSKEY: /vault/userconfig/tls/vault-internal-tls/tls.key

  extraVolumes:
    - type: secret
      name: vault-internal-tls
      path: /vault/userconfig/tls

  # ----------------------------------------------------------
  # Persistent Storage
  # ----------------------------------------------------------
  dataStorage:
    enabled: true
    size: 10Gi
    storageClass: longhorn-retain

  # ----------------------------------------------------------
  # High Availability - Raft
  # ----------------------------------------------------------
  ha:
    enabled: true
    replicas: 3
    raft:
      enabled: true
      setNodeId: true
      config: |
        ui = true
        cluster_name = "vault-cluster"

        api_addr     = "https://$(HOSTNAME).vault-internal.vault.svc.cluster.local:8200"
        cluster_addr = "https://$(HOSTNAME).vault-internal.vault.svc.cluster.local:8201"

        listener "tcp" {
          address = "[::]:8200"
          cluster_address = "[::]:8201"
          tls_cert_file = "/vault/userconfig/tls/vault-internal-tls/tls.crt"
          tls_key_file  = "/vault/userconfig/tls/vault-internal-tls/tls.key"
          tls_client_ca_file = "/vault/userconfig/tls/vault-internal-tls/ca.crt"
        }

        storage "raft" {
          path = "/vault/data"

          retry_join {
            leader_api_addr = "https://vault-0.vault-internal.vault.svc.cluster.local:8200"
            leader_ca_cert_file = "/vault/userconfig/tls/vault-internal-tls/ca.crt"
            leader_client_cert_file = "/vault/userconfig/tls/vault-internal-tls/tls.crt"
            leader_client_key_file  = "/vault/userconfig/tls/vault-internal-tls/tls.key"
          }
          retry_join {
            leader_api_addr = "https://vault-1.vault-internal.vault.svc.cluster.local:8200"
            leader_ca_cert_file = "/vault/userconfig/tls/vault-internal-tls/ca.crt"
            leader_client_cert_file = "/vault/userconfig/tls/vault-internal-tls/tls.crt"
            leader_client_key_file  = "/vault/userconfig/tls/vault-internal-tls/tls.key"
          }
          retry_join {
            leader_api_addr = "https://vault-2.vault-internal.vault.svc.cluster.local:8200"
            leader_ca_cert_file = "/vault/userconfig/tls/vault-internal-tls/ca.crt"
            leader_client_cert_file = "/vault/userconfig/tls/vault-internal-tls/tls.crt"
            leader_client_key_file  = "/vault/userconfig/tls/vault-internal-tls/tls.key"
          }
        }

        disable_mlock = true
        service_registration "kubernetes" {}

        telemetry {
          prometheus_retention_time = "30s"
          disable_hostname = true
        }

# ============================================================
# Prometheus Telemetry Integration
# ============================================================

serverTelemetry:
  serviceMonitor:
    enabled: true
    interval: 30s
    scrapeTimeout: 10s
    tlsConfig:
      insecureSkipVerify: false
      serverName: vault.vault.svc
      ca:
        secret:
          name: vault-metrics-client
          key: ca.crt
    authorization:
      credentials:
        name: prometheus-token
        key: token
    selectors:
      release: prometheus

TLS
#

TLS encrypts all Vault traffic — both internal (between pods) and external (through Traefik).
Certificates are automated by cert-manager, following a layered trust chain where each level has a specific role in securing communication and establishing mutual authentication.

Certificate Hierarchy
#

CertificatePurposeSigned ByUsed For
vault-root-caRoot of trust (Cluster CA)Self-signedSigns all Vault certificates
vault-ca-issuercert-manager issuer for Vaultvault-root-caIssues internal Vault certs
vault-internal-tlsInternal TLS for Raft and APIvault-ca-issuerSecures pod-to-pod and API traffic
vault-tlsPublic certificate for Vault UILet’s EncryptHTTPS access via Traefik

This hierarchy ensures both internal Vault communication and external access through Traefik remain encrypted and verifiable through the appropriate certificate authority.


Root and Issuer Configuration
#

The following resources define the internal CA chain for Vault.
A self-signed ClusterIssuer establishes the root of trust, a Certificate creates the long-lived root CA, and an Issuer in the Vault namespace delegates signing authority for internal certificates.

# Excerpt from internal-ca/ci-vault-selfsigned-root.yaml
# ============================================================
# ClusterIssuer | Vault Self-Signed Root
# ============================================================

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: vault-selfsigned-root
spec:
  selfSigned: {}

---
# Excerpt from internal-ca/cert-vault-root-ca.yaml
# ============================================================
# Certificate | Vault Root CA (Self-Signed)
# ============================================================

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: vault-root-ca
  namespace: vault
spec:
  isCA: true
  commonName: vault-root-ca
  secretName: vault-root-ca
  duration: 87600h        # 10 years
  renewBefore: 720h
  privateKey:
    algorithm: RSA
    size: 2048
  usages:
    - cert sign
    - crl sign
  issuerRef:
    name: vault-selfsigned-root
    kind: ClusterIssuer

---
# Excerpt from internal-ca/issuer-vault-ca.yaml
# ============================================================
# Issuer | Vault CA (Signs Vault TLS Certificates)
# ============================================================

apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: vault-ca-issuer
  namespace: vault
spec:
  ca:
    secretName: vault-root-ca

Internal TLS Certificate
#

Vault pods mount the vault-internal-tls certificate to secure Raft replication and HTTPS API traffic.
The certificate includes Subject Alternative Names (SANs) for all Vault pod DNS records, ensuring that every instance can authenticate its peers through mutual TLS.

# Excerpt from internal-ca/cert-vault-internal.yaml
# ============================================================
# Certificate | Vault Internal TLS
# ============================================================

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: vault-internal-tls
  namespace: vault
spec:
  secretName: vault-internal-tls
  commonName: vault.vault.svc
  dnsNames:
    - vault.vault.svc
    - vault.vault.svc.cluster.local
    - '*.vault.vault.svc'
    - '*.vault.vault.svc.cluster.local'
    - vault-0.vault-internal.vault.svc
    - vault-1.vault-internal.vault.svc
    - vault-2.vault-internal.vault.svc
  ipAddresses:
    - 127.0.0.1
  duration: 8760h
  renewBefore: 240h
  privateKey:
    algorithm: RSA
    size: 2048
  usages:
    - digital signature
    - key encipherment
    - server auth
  issuerRef:
    name: vault-ca-issuer
    kind: Issuer

UI TLS Certificate
#

The Vault UI exposed through Traefik uses a certificate obtained from Let’s Encrypt, but the endpoint itself is not publicly accessible.
All traffic to vault.kub.techcats.org is resolved locally through Pi-hole, ensuring that access remains private within the homelab network.
The certificate is still issued and automatically renewed by cert-manager using a DNS-01 challenge, allowing secure HTTPS communication and full automation even for internal domains.

# Excerpt from resources/cert-vault.yaml
# ============================================================
# Certificate | Vault UI TLS
# ============================================================

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: vault-tls
  namespace: vault
spec:
  secretName: vault-tls-secret
  issuerRef:
    name: letsencrypt-vault-issuer
    kind: ClusterIssuer
  commonName: vault.kub.techcats.org
  dnsNames:
    - vault.kub.techcats.org
  duration: 2160h     # 90 days
  renewBefore: 360h   # Renew 15 days before expiry

Together, these certificates create a secure setup with an internal CA for Vault’s internal traffic and a Let’s Encrypt certificate for the web interface.

Vault dashboard showing active secrets engines and HTTPS access
Figure 3: Vault UI accessible at vault.kub.techcats.org over HTTPS. The interface confirms that the cluster is initialized and TLS-enabled, with active secrets engines mounted.

Traefik Connection
#

Vault runs with internal TLS enabled, which means Traefik must establish a trusted HTTPS connection when proxying requests to the Vault backend service.
The ServersTransport resource below defines how Traefik verifies Vault’s certificate using the internal CA (vault-root-ca), ensuring end-to-end encryption between Traefik and Vault.

# Excerpt from resources/st-vault.yaml
# ============================================================
# ServersTransport | Vault Secure Connection
# ============================================================

apiVersion: traefik.io/v1alpha1
kind: ServersTransport
metadata:
  name: vault-secure
  namespace: vault
spec:
  rootCAsSecrets:
    - vault-root-ca
  serverName: vault.vault.svc
  insecureSkipVerify: false

CSI Provider
#

The Vault CSI provider extends Vault integration into Kubernetes, allowing workloads to mount secrets directly from Vault or sync them as native Kubernetes Secrets.
It operates alongside the main Vault instance but is deployed as a separate Argo CD Application, maintaining independent lifecycle management within the GitOps workflow.

The configuration below enables the CSI driver component inside the Vault Helm chart and configures it to communicate securely with the existing Vault service using the internal CA certificate.

# Excerpt from vault-csi-provider/values.yaml
# ============================================================
# Vault CSI Provider Configuration
# ============================================================

server:
  enabled: false
injector:
  enabled: false

csi:
  enabled: true
  agent:
    enabled: true

  extraArgs:
    - "-vault-addr=https://vault.vault.svc:8200"
    - "-vault-tls-ca-cert=/vault/userconfig/ca/ca.crt"
    - "-log-level=debug"

  volumes:
    - name: vault-root-ca
      secret:
        secretName: vault-root-ca
        items:
          - key: ca.crt
            path: ca.crt

  volumeMounts:
    - name: vault-root-ca
      mountPath: /vault/userconfig/ca
      readOnly: true

CSI Driver
#

The Secrets Store CSI Driver (by kubernetes-sigs) mounts secrets from external managers such as Vault into Kubernetes pods.
It runs as a separate Argo CD application and works with the Vault CSI provider to deliver secrets securely.

# Excerpt from csi-driver/values.yaml
# ============================================================
# Secrets Store CSI Driver Configuration
# ============================================================

syncSecret:
  enabled: true
enableSecretRotation: false
linux:
  enabled: true

Backup and Recovery
#

Cluster-level backups are automated through Velero, ensuring data resilience for all namespaces and persistent volumes.
Velero runs a daily scheduled backup with the following configuration:

  • Schedule: 0 3 * * *
  • Retention: 7 days (ttl: 168h)

The entire backup process and configuration are documented in detail here.


Key Takeaways
#

  • End-to-end TLS with internal CA and Let’s Encrypt automation
  • HA Vault with Raft backend
  • Dynamic secrets via CSI integration
  • Fully declarative deployment through Argo CD

Repository & Implementation
#

All manifests, overlays, and Helm configurations are version-controlled in Git and deployed via Argo CD.
Full implementation available at whitehatcats/k3s-homelab-gitops.


References
#