Hashicorp Vault Secrets Server selbst hosten (Docker)
Gepostet am 16. Mai 2024 • 15 Minuten • 3184 Wörter • Andere Sprachen: English
Ich habe vor kurzem den Vault-Server von Hashicorp evaluiert und ihn auf mehreren Maschinen in einem einfachem Setup aufgesetzt. Da die Anleitungen im Netz etwas verstreut sind, dokumentiere ich mein Vorgehen in der Hoffnung, dass es anderen helfen mag.
Umgebung
Ich verzichte auf Kubernetes oder Docker Swarm (in beiden kann man mit entsprechendem Placing der Container das ganz schick lösen, wenn man die Herausforderung des Storage meistert). Dass wir getrennte Docker-Instanzen verwenden, hat im Projekt historische Gründe. Der Vorteil ist, dass ich hier die einzelnen Schritte ganz gut dokumentieren kann 😇
Wir haben drei Nodes. Auf jeder läuft Docker und sie sind per VLAN miteinander verbunden:
app01
, IP im VLAN 192.168.100.111app02
, IP im VLAN 192.168.100.112app03
, IP im VLAN 192.168.100.113
Auf den Servern läuft Ubuntu 22.04. Wer kein Docker verwenden mag, kann Vault auch als Dienst installieren - das Vorgehen ist so schwer nicht, man muss die Befehle unten entsprechend anpassen. Die verwendete Vault-Version ist 1.16.
Zertifikate erstellen
Zunächst erstellen wir die Zertifikate für die verschlüsselte Kommunikation mit Vault (und zwischen den Instanzen). Wir erstellen dazu eine Certificate Authority (CA) und Zertifikate für jede Vault-Node. Auf einem beliebigen Server führen wir Folgendes aus:
sudo mkdir /etc/vault
cd /etc/vault
sudo tee extfile.cnf << EOF
[ req ]
distinguished_name = req_distinguished_name
x509_extensions = v3_ca
prompt = no
[ req_distinguished_name ]
C = DE
ST = MyState
L = MyLocation
O = MyPlace
CN = RootCA
[ v3_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical,CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
EOF
sudo tee extfile01.cnf << EOF
[ req ]
distinguished_name = req_distinguished_name
req_extensions = req_ext
prompt = no
[ req_distinguished_name ]
C = DE
ST = MyState
L = MyLocation
O = MyPlace
CN = app01
[req_ext]
subjectAltName = @alt_names
[alt_names]
IP.1 = 192.168.100.111
IP.2 = 127.0.0.1
DNS.1 = app01
DNS.2 = vault
EOF
sudo tee extfile02.cnf << EOF
[ req ]
distinguished_name = req_distinguished_name
req_extensions = req_ext
prompt = no
[ req_distinguished_name ]
C = DE
ST = MyState
L = MyLocation
O = MyPlace
CN = app02
[req_ext]
subjectAltName = @alt_names
[alt_names]
IP.1 = 192.168.100.112
IP.2 = 127.0.0.1
DNS.1 = app02
DNS.2 = vault
EOF
sudo tee extfile03.cnf << EOF
[ req ]
distinguished_name = req_distinguished_name
req_extensions = req_ext
prompt = no
[ req_distinguished_name ]
C = DE
ST = MyState
L = MyLocation
O = MyPlace
CN = app03
[req_ext]
subjectAltName = @alt_names
[alt_names]
IP.1 = 192.168.100.113
IP.2 = 127.0.0.1
DNS.1 = app03
DNS.2 = vault
EOF
Damit wurden die Extension Files für OpenSSL erstellt. Diese enthalten für Vault notwendige Erweiterungen der Zertifikate. Ohne diese Extensions weigert sich Vault, mit den anderen Instanzen zu sprechen. Wichtig ist hier, die möglichen eigenen Namen und IPs der Vault-Instanzen anzugeben. Falls man Vault über weitere IPs oder DNS-Namen aufrufen will, müssen die Listen entsprechend erweitert werden!
Jetzt erstellen wir die eigentlichen Zertifikate für die Server:
cd /etc/vault
# CA
sudo openssl genrsa -out ca.key 4096
sudo openssl req -new -x509 -days 3650 -key ca.key -out ca.crt -config extfile.cnf
# Host 01
sudo openssl genrsa -out vault01.key 4096
sudo openssl req -new -key rsa:4096 -key vault01.key -out vault01.csr -config extfile01.cnf
sudo openssl x509 -req -days 3650 -in vault01.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out vault01.crt -extensions req_ext -extfile extfile01.cnf
# Host 02
sudo openssl genrsa -out vault02.key 4096
sudo openssl req -new -key rsa:4096 -key vault02.key -out vault02.csr -config extfile02.cnf
sudo openssl x509 -req -days 3650 -in vault02.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out vault02.crt -extensions req_ext -extfile extfile02.cnf
# Host 03
sudo openssl genrsa -out vault03.key 4096
sudo openssl req -new -key rsa:4096 -key vault03.key -out vault03.csr -config extfile03.cnf
sudo openssl x509 -req -days 3650 -in vault03.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out vault03.crt -extensions req_ext -extfile extfile03.cnf
Wir erstellen also zunächst das Zertifikat der CA und dann jeweils Schlüssel und Zertifikate für die einzelnen Instanzen. Die Zertifikate sind etwa 10 Jahre gültig. Im Produktivbetrieb ist es sicher sinnvoll, Zertifikate mit kürzerer Laufzeit zu verwenden - oder man setzt gleich eine eigene Stelle für die Ausstellung solcher ein, beispielsweise step-ca .
Zum Abschluss testen wir noch, ob die Zertifikate auch die notwendigen Erweiterungen enthalten:
openssl x509 -noout -text -in vault01.crt | grep -A 1 "Subject Alternative Name"
openssl x509 -noout -text -in vault02.crt | grep -A 1 "Subject Alternative Name"
openssl x509 -noout -text -in vault03.crt | grep -A 1 "Subject Alternative Name"
Bei der Ausgabe sollten alle IPs und DNS-Namen aufgelistet sein, die man benötigt.
Wichtig: Zum Abschluss muss man alle erstellten Dateien auf die anderen Server kopieren, z.B. per rsync oder scp.
Ich gehe davon aus, dass auf app01, app02, app03 am Ende dieses Schritts die Zertifikate auf allen Servern im
Verzeichnis /etc/vault
liegen.
Ebenfalls wichtig: Die Dienste müssen untereinander sprechen können. Dazu müssen die TCP-Ports 8200 und 8201 im VLAN offen sein. Hier beispielhaft die Befehle für UFW:
sudo ufw allow proto tcp from 192.168.100.0/24 to any port 8200
sudo ufw allow proto tcp from 192.168.100.0/24 to any port 8201
Auto-Unsealing mit AWS KMS
Für den Produktivbetrieb ist es unablässig, dass der Vault sich öffnet, wenn er neu gestartet wird. Es gibt hier mehrere Möglichkeiten:
- Eine weitere Vault-Instanz (Transit seal )
- Einen Unsealing-Dienst wie z.B. vault-unseal
- KMS über einen Cloud-Anbieter
Ich habe hier ein wenig experimentiert und bin letztlich beim KMS von AWS gelandet. Dieser ist für 1 USD pro Monat durchaus erschwinglich und ist einfach einzurichten (wenn man weiß, wo man in AWS hinklicken muss 😱). Die anderen Lösungen sind durchaus gangbar, allerdings aufwändiger:
- Eine Transit-Seal-Instanz benötigt eine weitere Node mit entsprechenden Zertifikaten/Zugangsmöglichkeiten. Auch diese muss unsealed sein, damit unser Dienst starten kann. Das birgt eigene Hürden.
- vault-unseal läuft ordentlich und ist einfach einzurichten. Durch das Polling benötigt der Vault-Cluster nach dem Neustart allerdings einige Zeit, um zu Starten (Polling ist standardmäßig auf 30 Sekunden gesetzt). Das machte Kaltstarts aller Nodes etwas holprig und könnte andere Dienste blockieren, die von Vault abhängig sind.
Damit eine kurze Anleitung für AWS:
- Wir brauchen einen AWS-Zugang, der Benutzer sowie KMS-Secretes erstellen und verwalten kann (ja, der Hauptzugang geht natürlich auch).
- In der IAM-Verwaltung legt man eine Gruppe “Vault” mit den folgenden Berechtigungen an:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VaultKMSUnseal",
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:Encrypt",
"kms:DescribeKey"
],
"Resource": "*"
}
]
}
- Danach legt man einen Benutzer mit dieser Rolle an.
- In der Ansicht des Benutzers erstellt man einen Zugriffsschlüssel (Schlüssel-Id und Secret notieren).
- Jetzt erstellt man in der KMS-Verwaltung einen neuen KMS-Schlüssel
.
- Auf die korrekte Zone achten!
- Punkt “Kundenverwaltete Schlüssel”
- Schlüssel ist symmetrisch.
- Schlüsselbenutzer (nicht Admin!) ist der oben angelegte Benutzer.
- Id des KMS-Schlüssel notieren!
Damit besitzen wir folgende Informationen:
- Id des Zugriffsschlüssels des Benutzers =
access_key
- Secret des Zugriffsschlüssels =
secret_key
- Id des KMS-Schlüssels =
kms_key_id
- Region des Schlüssels =
kms_region
Start von Vault auf den einzelnen Servern
app01
Wir beginnen mit app01 - das Vorgehen wiederholt sich unten. Wir benötigen die Schlüsseldaten von oben und führen Folgendes aus:
access_key=123
secret_key=secret
kms_key_id=key
kms_region=eu-central-1
cd /etc/vault
# Config
sudo tee vault.hcl << EOF
cluster_addr = "https://192.168.100.111:8201"
api_addr = "https://0.0.0.0:8200"
storage "raft" {
path = "/vault/data"
retry_join {
leader_api_addr = "https://app02:8200"
leader_ca_cert_file = "/vault/config/ca.crt"
leader_client_cert_file = "/vault/config/vault02.crt"
leader_client_key_file = "/vault/config/vault02.key"
}
retry_join {
leader_api_addr = "https://app03:8200"
leader_ca_cert_file = "/vault/config/ca.crt"
leader_client_cert_file = "/vault/config/vault03.crt"
leader_client_key_file = "/vault/config/vault03.key"
}
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_cert_file = "/vault/config/vault01.crt"
tls_key_file = "/vault/config/vault01.key"
}
seal "awskms" {
region = "${kms_region}"
access_key = "${access_key}"
secret_key = "${secret_key}"
kms_key_id = "${kms_key_id}"
}
EOF
sudo chmod 600 vault.hcl
# Network
docker network create --driver=bridge --subnet=192.168.128.0/24 app
# Volumes
docker volume create vault
docker volume create vault_log
# Access
docker run --rm -v /etc/vault:/data:rw docker.io/hashicorp/vault chown vault:vault /data
docker run --rm -v vault:/data:rw docker.io/hashicorp/vault chown vault:vault /data
docker run --rm -v vault_log:/data:rw docker.io/hashicorp/vault chown vault:vault /data
# Service
docker run -d --restart unless-stopped --network=app --name vault --cap-add IPC_LOCK -p 192.168.100.111:8200:8200 \
-p 192.168.100.111:8201:8201 --add-host app01:192.168.100.111 --add-host app02:192.168.100.112 \
--add-host app03:192.168.100.113 -v /etc/vault:/vault/config:ro -v vault:/vault/data -v vault_log:/vault/logs \
docker.io/hashicorp/vault server
Was passiert hier?
- Wir erstellen eine
vault.hcl
mit den entsprechenden Zugangsdaten (history danach vielleicht löschen).- Storage ist
raft
mit entsprechenden hints auf mögliche andere Nodes - die eigene Node-Id lassen wir frei, damit habe ich bessere Erfahrungen gemacht. - Die Zertifikate der anderen Dienste werden entsprechend vom eigenen Dienst als Client-Zertifikate verwendet. Ich bin damit nicht 100%ig glücklich - im Produktivbetrieb wäre es sicher sinnvoll, Client-Zertifikate einzusetzen. In meinem Test-Setup bin ich aber nicht mehr dazu gekommen, das zu testen. Wer Infos dazu hat, meldet sich gerne!
- Der Dienst horcht an Port 8200 und verwendet die eigenen Zertifikate.
- Storage ist
- Da die Datei sensible Daten enthält, sollte sie abgesichert sein.
- Für vault und die Dienste, die Vault lokal nutzen wollen, ist es sinnvoll, ein eigenes Docker-Netzwerk einzurichten. Ich gebe solchen Netzen auch ganz gerne ein bekanntes Subnetz, was für die späteren Zugangsbeschränkungen in Vault praktisch ist, weil das Netz auf allen Nodes gleich ist.
- Wir erstellen außerdem zwei Volumes für die raft-Daten und das Log.
- Nun müssen wir die Rechte korrekt setzen.
- Im lokalen
/etc/vault
-Verzeichnis muss der Docker-Dienst Leserechte haben. Falls der erste Befehl nicht funktioniert muss man die Rechte entsprechend anpassen (bei Ubuntu war das der Benutzer_apt
und die Gruppe mit der Id 1000). Im Live-Betrieb würde ich die Zertifikate auch von der Konfiguration trennen, das ist sicher sauberer.
- Im lokalen
- Am Schluss starten wir den Docker-Dienst:
- Wir starten den Dienst im Hintergrund und starten ihn bei Bedarf automatisch neu.
- Netzwerk ist das oben erstellte (optional).
- Name ist vault (wichtig: eigener DNS-Name, bzw. Name im Docker-Netzwerk, muss also mit Zertifikat übereinstimmen)
- IPC_LOCK muss gesetzt sein für Vault.
- Wir horchen am lokalen VLAN an den Ports 8200 und 8201 - im eigenen Netz muss das entsprechend angepasst werden. Es ist sinnvoll, hier IPs anzugeben, da in bestimmten Setups Docker dazu neigt, durch Bridging die eigene Firewall zu umgehen. An dieser Stelle also aufpassen!
- Wir geben die anderen Hosts mit ihren Namen an.
- Volumes: Konfigurationsverzeichnis darf nur gelesen werden, die anderen sind beschreibbar.
Damit sollte der Server laufen. Im Log kann man sehen, ob alles in Ordnung ist:
docker logs vault
Das Log wird im Moment noch eine Menge Fehlermeldungen enthalten, das ist in Ordnung und auch normal. Wir richten zunächst die anderen Nodes ein.
app02
Hier nur die Befehle:
access_key=123
secret_key=secret
kms_key_id=key
kms_region=eu-central-1
cd /etc/vault
sudo tee vault.hcl << EOF
cluster_addr = "https://192.168.100.112:8201"
api_addr = "https://0.0.0.0:8200"
storage "raft" {
path = "/vault/data"
retry_join {
leader_api_addr = "https://app01:8200"
leader_ca_cert_file = "/vault/config/ca.crt"
leader_client_cert_file = "/vault/config/vault01.crt"
leader_client_key_file = "/vault/config/vault01.key"
}
retry_join {
leader_api_addr = "https://app03:8200"
leader_ca_cert_file = "/vault/config/ca.crt"
leader_client_cert_file = "/vault/config/vault03.crt"
leader_client_key_file = "/vault/config/vault03.key"
}
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_cert_file = "/vault/config/vault02.crt"
tls_key_file = "/vault/config/vault02.key"
}
seal "awskms" {
region = "${kms_region}"
access_key = "${access_key}"
secret_key = "${secret_key}"
kms_key_id = "${kms_key_id}"
}
EOF
sudo chmod 600 vault.hcl
# Network
docker network create --driver=bridge --subnet=192.168.128.0/24 app
# Volumes
docker volume create vault
docker volume create vault_log
# Access
docker run --rm -v /etc/vault:/data:rw docker.io/hashicorp/vault chown vault:vault /data
docker run --rm -v vault:/data:rw docker.io/hashicorp/vault chown vault:vault /data
docker run --rm -v vault_log:/data:rw docker.io/hashicorp/vault chown vault:vault /data
# Service
docker run -d --restart unless-stopped --network=app --name vault --cap-add IPC_LOCK -p 192.168.100.112:8200:8200 \
-p 192.168.100.112:8201:8201 --add-host app01:192.168.100.111 --add-host app02:192.168.100.112 \
--add-host app03:192.168.100.113 -v /etc/vault:/vault/config:ro -v vault:/vault/data -v vault_log:/vault/logs \
docker.io/hashicorp/vault server
Start app03
Hier nur die Befehle:
access_key=123
secret_key=secret
kms_key_id=key
kms_region=eu-central-1
cd /etc/vault
sudo tee vault.hcl << EOF
cluster_addr = "https://192.168.100.113:8201"
api_addr = "https://0.0.0.0:8200"
storage "raft" {
path = "/vault/data"
retry_join {
leader_api_addr = "https://app01:8200"
leader_ca_cert_file = "/vault/config/ca.crt"
leader_client_cert_file = "/vault/config/vault01.crt"
leader_client_key_file = "/vault/config/vault01.key"
}
retry_join {
leader_api_addr = "https://app02:8200"
leader_ca_cert_file = "/vault/config/ca.crt"
leader_client_cert_file = "/vault/config/vault02.crt"
leader_client_key_file = "/vault/config/vault02.key"
}
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_cert_file = "/vault/config/vault03.crt"
tls_key_file = "/vault/config/vault03.key"
}
seal "awskms" {
region = "${kms_region}"
access_key = "${access_key}"
secret_key = "${secret_key}"
kms_key_id = "${kms_key_id}"
}
EOF
sudo chmod 600 vault.hcl
# Network
docker network create --driver=bridge --subnet=192.168.128.0/24 app
# Volumes
docker volume create vault
docker volume create vault_log
# Access
docker run --rm -v /etc/vault:/data:rw docker.io/hashicorp/vault chown vault:vault /data
docker run --rm -v vault:/data:rw docker.io/hashicorp/vault chown vault:vault /data
docker run --rm -v vault_log:/data:rw docker.io/hashicorp/vault chown vault:vault /data
# Service
docker run -d --restart unless-stopped --network=app --name vault --cap-add IPC_LOCK -p 192.168.100.113:8200:8200 \
-p 192.168.100.113:8201:8201 --add-host app01:192.168.100.111 --add-host app02:192.168.100.112 \
--add-host app03:192.168.100.113 -v /etc/vault:/vault/config:ro -v vault:/vault/data -v vault_log:/vault/logs \
docker.io/hashicorp/vault server
Vault initialisieren
Nachdem Vault nun auf allen Nodes läuft, können wir den Service initialisieren. Dazu führen wir einen temporären Container auf einem beliebigen Server aus - das hat den Vorteil, dass nach der Beendigung der Arbeit nichts in der History des Benutzers oder der Benutzerin übrig bleibt:
docker run --rm -ti --network=app -v /etc/vault:/vault/config:ro -e VAULT_ADDR=https://vault:8200 \
-e VAULT_CACERT=/vault/config/ca.crt -P docker.io/hashicorp/vault ash
Der Container ist also temporär und interaktiv und im selben Netzwerk wie unser Vault-Dienst. Wir benötigen auch die
Konfiguration und setzen gleich zwei Umgebungsvariablen für den vault
-Befehl unten. Das -P
ist wichtig, damit wir
nicht unseren obigen Portbereich überschreiben. Das Image verwendet die ash-Shell.
In der Shell initialisieren wir den Cluster:
vault operator init
Die Schlüssel und das initiale root-Token sollten wir an einem sicheren Ort speichern!
Damit ist vault einsatzbereit und sollte auch nach Neustarts einzelner Container wieder geöffnet werden. Um das zu testen, kann man den Status prüfen (im temporären Container):
vault operator status
Auf dem Host kann man nun Vault neu starten (docker restart vault
) und nach kurzer Zeit sollte der Status wieder auf
unsealed stehen.
Exkurs: Zugang und Go-Schnipsel
Hier noch ein kleiner Exkurs, wie ein Go-Programm einen Secret aufrufen könnte.
Daten in Vault ablegen
Wir legen dazu einige Daten in Vault an. Wieder loggen wir uns auf einer beliebigen Node ein und erstellen wieder einen temporären Container für die Vault-Administration:
docker run --rm -ti --network=app -v /etc/vault:/vault/config:ro -e VAULT_ADDR=https://vault:8200 \
-e VAULT_CACERT=/vault/config/ca.crt -P docker.io/hashicorp/vault ash
Im Container müssen wir das root-Token setzten, das wir in der Initialisation bekommen haben:
export VAULT_TOKEN=MyTOken
Zum Test erstellen wir zunächst einen Secret, den unser Programm holen soll:
vault secrets enable -version=2 -path=app -description="Application secrets" kv
vault kv put -mount=app apiKey key=PaipCijvonEtdysilgEirlOwUbHahachdyazVopejEnerekBiOmukvauWigbimVi
Die Anwendung benötigt einen Zugang, den wir per Approle erstellen:
vault auth enable approle
echo 'path "app/data/apiKey" {
capabilities = ["read"]
}' | vault policy write myapp -
vault write auth/approle/role/myapp token_ttl=1h token_max_ttl=8h secret_id_ttl=0 token_policies="myapp"
# read data
vault read auth/approle/role/myapp/role-id
# create secret id
vault write -force auth/approle/role/myapp/secret-id
Wir bekommen zwei UUIDs zurückgeliefert, nämlich die Rollen-Id und das Secret. Beides benötigen wir in der Applikation. Im Life-Betrieb kann man die Rolle noch einschränken, indem man IP-Bereiche setzt, von den aus die Applikation zugreifen kann - das habe ich hier ausgelassen, um das Testen zu erleichtern. Probieren wir den Login doch gleich mal aus:
vault write auth/approle/login role_id="123" secret_id="456"
export VAULT_TOKEN=AppRoleToken
vault kv get -mount=app apiKey
Wir erstellen zunächst einen Login mit der Rolle und dem Secret. Wir bekommen ein Token zurück, das wir wiederum als Umgebungsvariable setzen können (und damit das root-Token überschreiben). Wir nehmen quasi die Rolle des Dienstes an. In dieser Rolle können wir versuchen, den API-Key zu lesen, was hoffentlich klappen sollte.
Go-Programm
Ich stelle nur einen Code-Schnipsel vor, der gerne verwendet werden kann. Ich habe Teil der Daten vom Token-Renewal-Beispiel verwendet und es in eine eigene kleine Library gepackt.
Für das Programm müssen/können Umgebungsvariablen gesetzt werden:
VAULT_ADDR
: Adresse des Vault-Servers (Voreinstellung:https://vault:8200
)VAULT_CACERT
: Ort des CA-Zertifikats (Voreinstellung:/etc/vault/ca.crt
)APPROLE_ROLE_ID
: Rollen-Id (von oben)APPROLE_SECRET_ID
: Rollen-Secret (von oben)
Die beiden letzten müssen gesetzt werden!
Hier zunächst die Library:
// vault.go
package main
import (
"cmp"
"context"
"fmt"
vault "github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/api/auth/approle"
"github.com/rs/zerolog/log"
"os"
)
// VaultClient is the global vault client
var VaultClient *vault.Client
// InitVault initializes the vault client
func InitVault() {
vaultAddress := cmp.Or(os.Getenv("VAULT_ADDR"), "https://vault:8200")
vaultCAFile := cmp.Or(os.Getenv("VAULT_CACERT"), "/etc/vault/ca.crt")
// define config
config := vault.DefaultConfig() // modify for more granular configuration
config.Address = vaultAddress
if err := config.ConfigureTLS(&vault.TLSConfig{
CAPath: vaultCAFile,
}); err != nil {
log.Fatal().Str("VAULT_ADDR", vaultAddress).Str("VAULT_CACERT", vaultCAFile).Err(err).Msg("Failed to configure Vault TLS")
}
// create client
client, err := vault.NewClient(config)
if err != nil {
log.Fatal().Str("VAULT_ADDR", vaultAddress).Str("VAULT_CACERT", vaultCAFile).Err(err).Msg("Failed to create Vault client")
}
ctx, cancelContextFunc := context.WithCancel(context.Background())
defer cancelContextFunc()
// copy to global variable
VaultClient = client
// initial login
authInfo, err := vaultLogin(ctx)
if err != nil {
log.Fatal().Str("VAULT_ADDR", vaultAddress).Str("VAULT_CACERT", vaultCAFile).Err(err).Msg("Failed to login to Vault")
}
// start the lease-renewal goroutine & wait for it to finish on exit
go vaultStartRenewLeases(authInfo)
// everything ok, log success
log.Info().Str("VAULT_ADDR", vaultAddress).Msg("Vault successfully connected and initial token created.")
}
func vaultLogin(ctx context.Context) (*vault.Secret, error) {
// Get environment variables for Vault
vaultAppRoleId := os.Getenv("APPROLE_ROLE_ID")
if vaultAppRoleId == "" {
log.Fatal().Msg("Error: Vault App Role not set.")
}
// initial login with AppRole
appRoleAuth, err := approle.NewAppRoleAuth(vaultAppRoleId, &approle.SecretID{
FromEnv: "APPROLE_SECRET_ID",
})
// TODO: we might want to create ResponseWrapping somehow
// ref: https://www.vaultproject.io/docs/concepts/response-wrapping
// ref: https://learn.hashicorp.com/tutorials/vault/secure-introduction?in=vault/app-integration#trusted-orchestrator
// ref: https://learn.hashicorp.com/tutorials/vault/approle-best-practices?in=vault/auth-methods#secretid-delivery-best-practices
// and example in: https://github.com/hashicorp/hello-vault-go/blob/main/sample-app/vault.go
if err != nil {
return nil, err
}
return VaultClient.Auth().Login(ctx, appRoleAuth)
}
func vaultStartRenewLeases(authToken *vault.Secret) {
ctx, cancelContextFunc := context.WithCancel(context.Background())
defer cancelContextFunc()
log.Info().Msg("Starting lease renewal service.")
defer log.Info().Msg("Stopping lease renewal service.")
currentAuthToken := authToken
for {
renewed, err := renewLeases(ctx, currentAuthToken)
if err != nil {
log.Fatal().Err(err).Msg("Failed to renew leases")
}
if renewed&exitRequested != 0 {
return
}
if renewed&expiringAuthToken != 0 {
log.Printf("auth token: can no longer be renewed; will log in again")
authToken, err := vaultLogin(ctx)
if err != nil {
log.Fatal().Err(err).Msg("Failed to login to Vault")
}
currentAuthToken = authToken
}
}
}
// renewResult is a bitmask which could contain one or more of the values below
type renewResult uint8
const (
renewError renewResult = 1 << iota
exitRequested
expiringAuthToken // will be revoked soon
)
func renewLeases(ctx context.Context, authToken *vault.Secret) (renewResult, error) {
log.Info().Msg("Starting lease renewal.")
// auth token
authTokenWatcher, err := VaultClient.NewLifetimeWatcher(&vault.LifetimeWatcherInput{
Secret: authToken,
})
if err != nil {
return renewError, fmt.Errorf("unable to initialize auth token lifetime watcher: %w", err)
}
go authTokenWatcher.Start()
defer authTokenWatcher.Stop()
// monitor events from all watchers
for {
select {
case <-ctx.Done():
return exitRequested, nil
// DoneCh will return if renewal fails, or if the remaining lease
// duration is under a built-in threshold and either renewing is not
// extending it or renewing is disabled. In both cases, the caller
// should attempt a re-read of the secret. Clients should check the
// return value of the channel to see if renewal was successful.
case err := <-authTokenWatcher.DoneCh():
// Leases created by a token get revoked when the token is revoked.
return expiringAuthToken, err
// RenewCh is a channel that receives a message when a successful
// renewal takes place and includes metadata about the renewal.
case info := <-authTokenWatcher.RenewCh():
log.Printf("auth token: successfully renewed; remaining duration: %ds", info.Secret.Auth.LeaseDuration)
//case info := <-databaseCredentialsWatcher.RenewCh():
// log.Printf("database credentials: successfully renewed; remaining lease duration: %ds", info.Secret.LeaseDuration)
//}
}
}
}
Die Bibliothek kann in einem lang laufenden Dienst eingesetzt werden und erneuert automatisch das Zugangstoken im Hintergrund (als Go-Routine).
Ein kleines Test-Programm:
// main.go
package main
import (
"context"
"fmt"
)
func main() {
InitVault()
secret, err := VaultClient.KVv2("app").Get(context.Background(), "apiKey")
if err != nil {
panic("Failed to get token key from Vault")
}
fmt.Printf("API-Key: %s\n", secret.Data["key"])
}
In einem Dienst könnte immer auf die globale Variable VaultClient
zugegriffen werden in der Sicherheit, dass die
Session bzw. das aktuelle Token noch gültig ist.
Titelbild: Immeuble du Crédit Lyonnais - Verwendet unter CC BY-SA 3.0 .