LINUXOR.SK ... open source notes ...

Vault 09 - Vault server configuration

category: solutionz · date: 2024-12-31 · updated: 2026-10-02 · author: LALA

Vault Solution · Previous: Load balancer · Next: Initialization and Transit auto-unseal

A Vault node is one binary, one configuration file and one data directory. This article installs the binary and reads the configuration file block by block. The file itself is a Config document: vault.hcl.

Installing

Vault comes from the vendor's package repository. The Vault nodes have no route to the Internet, so the repository is reached through the platform's forward proxy.

bash
$ yum install -y yum-utils
$ yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
$ hostnamectl set-hostname prod-vault-node1
$ yum -y install vault
$ vault --version
output 1 line
Vault v1.14.1

The package brings the vault user and group, the systemd unit, /etc/vault.d and /opt/vault. The certificates from TLS, DNS and certificates are already in /opt/vault/tls, and the data disk from Virtual machines and OS build is mounted at /data.

bash
$ vi /etc/vault.d/vault.hcl
$ chown vault:vault /etc/vault.d/vault.hcl
$ chmod 0644 /etc/vault.d/vault.hcl
$ restorecon -RvF /etc/vault.d
$ systemctl enable vault
$ systemctl start vault
$ systemctl status vault

The file, block by block

mermaid
flowchart TB
  subgraph hcl["vault.hcl"]
    g["Top level<br/>names, addresses, lease limits, switches"]
    l1["listener tcp 127.0.0.1:8200<br/>no TLS, administration on the node"]
    l2["listener tcp node address:8200<br/>TLS 1.3, clients and peers<br/>cluster_address :8201"]
    s["storage raft<br/>path /data, node_id, retry_join x5"]
    u["seal transit<br/>COMMON address, key name, token"]
  end
  l2 --> api["API clients through HAProxy"]
  l2 --> peers["Other nodes of the cluster"]
  s --> disk[("/data")]
  u --> common["common-vault"]

Names and addresses

hcl
cluster_name = "prod-vault"
cluster_addr = "https://prod-vault-node1.example.net:8201"
api_addr     = "https://prod-vault.example.net:8200"
SettingSaysValue here
cluster_nameWhat the cluster calls itselfThe same on every node of a cluster
cluster_addrWhere other nodes reach this node for Raft and forwarded requestsThe node's own name, port 8201
api_addrWhere a client should be sent when this node redirects itThe load balancer's name, not the node's

api_addr is the one that is easy to get wrong. A standby that receives a request it cannot forward answers with a redirect to the active node's api_addr. If that were the node's own name, clients behind the firewall rules of Network design and firewall flows would be redirected to an address they cannot reach. Pointing it at the load balancer sends them back to the one door that is open.

The file gives port 8200 for it, while the load balancer listens on 443. Clients were never redirected in practice, because HAProxy only ever hands them the active node, so the mismatch stayed invisible.

Switches

SettingValueWhy it was set
ui"true"The web interface, for administrators
disable_mlock"true"Integrated Storage maps its database file into memory, and mlock on a memory-mapped file works against it
disable_cache"true"The file's comment says what it does, not why; no reason is recorded
default_lease_ttl, max_lease_ttl"10h"Tokens and leases expire after ten hours unless a mount or a token is given its own limit
raw_storage_endpoint"true"No reason is recorded
disable_sealwrap"true"An Enterprise setting; without effect here
disable_printable_check"true"Undocumented in the file itself; the comment beside it is a question mark

Two of these deserve the honesty of the as-built record. The design document says ui = "false"; the nodes ran with "true". And disable_cache and raw_storage_endpoint are settings I would not carry into a new build: the first costs performance for no security gain, since the cache lives inside the same process that holds the keys, and the second opens an endpoint that reads and writes storage underneath every policy.

The ten-hour lease limit has one consequence that reaches into the next article: a token that must live longer needs a mount tuned for it, or an explicit lifetime of its own.

Listeners

hcl
listener "tcp" {
  address     = "127.0.0.1:8200"
  tls_disable = "true"
}

listener "tcp" {
  address                  = "10.10.1.34:8200"
  cluster_address          = "10.10.1.34:8201"
  tls_cert_file            = "/opt/vault/tls/prod-vault-node1.example.net-cert.pem"
  tls_key_file             = "/opt/vault/tls/prod-vault-node1.example.net-key.pem"
  tls_client_ca_file       = "/opt/vault/tls/prod-vault-node1.example.net-ca.pem"
  tls_disable_client_certs = "true"
  tls_min_version          = "tls13"
  tls_max_version          = "tls13"
}
ListenerBound toTLSUsed by
Loopback127.0.0.1:8200OffAn administrator on the node itself, and the snapshot agent
ServiceThe node's address, ports 8200 and 8201TLS 1.3 onlyHAProxy, the other nodes, everything else

The loopback listener is what makes export VAULT_ADDR='http://127.0.0.1:8200' work in every procedure of the following articles. It is convenient and it is a trade: anything that runs on the node can talk to Vault without TLS, though still not without a token.

The service listener binds an address, not 0.0.0.0, and allows TLS 1.3 only. Client certificates are switched off; clients authenticate with AppRole, not with TLS.

Storage

hcl
storage "raft" {
  path    = "/data"
  node_id = "prod-vault-node1.example.net"

  retry_join {
    leader_tls_servername = "prod-vault-node1.example.net"
    leader_api_addr       = "https://prod-vault-node1.example.net:8200"
  }
}

The real block has one retry_join per node of the cluster, the node's own included. A node that starts with an empty data directory tries each address in turn until one of them is an initialized, unsealed member, and joins it. The same file therefore works for the first node, which finds nobody and waits to be initialized, and for every later one.

leader_tls_servername is the name the joining node expects in the peer's certificate. No CA file is named; the internal authority is in the system trust store.

node_id is the node's full name. It is what vault operator raft list-peers shows, and it must never be reused for a different machine with a different disk.

Seal

hcl
seal "transit" {
  address         = "https://common-vault.example.net:443"
  token           = "<TRANSIT_UNSEAL_TOKEN>"
  disable_renewal = "false"
  key_name        = "autounseal-prod-vault"
  mount_path      = "transit/"
  tls_skip_verify = "false"
}

This block is in the files of PROD and NONPROD and absent from COMMON's. It is the subject of Initialization and Transit auto-unseal.

What differs per node and per environment

On the five nodes of a cluster the file differs in four places: cluster_addr, the service listener's address, the three certificate paths and node_id. Between environments it differs in the cluster name, the names and addresses, the number of retry_join blocks, and the seal. Both tables are in vault.hcl.

Checking a node

A node that has started and not yet been initialized says so. On a COMMON node, which has no seal block, the answer looks like this.

bash
$ export VAULT_ADDR='http://127.0.0.1:8200'
$ vault status
output 7 lines
Key                Value
---                -----
Seal Type          shamir
Initialized        false
Sealed             true
Storage Type       raft
HA Enabled         true

The health endpoint says the same in one number, and is what HAProxy will ask.

bash
$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8200/v1/sys/health
output 1 line
501

The server's own messages go to the journal.

bash
$ journalctl -u vault -f

Reading it today

Every setting in the file still loads in Vault 2.1.1, and four lines should go. The full table, setting by setting, is at the end of vault.hcl. The points that change behaviour:

← solutionz