HashiCorp Nomad and Vault: Dynamic Secrets
In einer cloud-nativen Umgebung ist das Management von Geheimnissen ein kritischer Aspekt der Sicherheit. HashiCorp Vault ist ein beliebtes Tool zur Verwaltung
Beginnend mit der Version 1.14.0 unterstützt die Vault PKI-Secrets-Engine nun auch die Automatic Certificate Management Environment (ACME) specification zur automatisierten Aussetellung sowie Erneuerung von Server Zertifikaten.
HashiCorp hat hier bereits ein Tutorial zur Vault ACME Konfiguration erstellt, aber dieses basiert rein auf Befehlen über die Kommandozeile. Nachdem wir ja aber HashiCorp terraform zur Verfügung haben, wollen wir das auch nutzen um das per Infrastructure as Code (IaC) darzustellen.
Unser Beispiel ist direkt angelehnt an das bereits existierende Tutorial, aber wir werden den terraform Code in unserem Beispiel zerlegen, um die Erklärung zu erleichtern.
Zuerst schauen wir hier uns den Code an, den wir brauchen um unsere beiden Dienste - Caddy und HashiCorp Vault - zur Verfügung zu haben. Anschließend werden wir die PKI-Secrets-Engines aktivieren um bei diesen den ACME Support zu konfigurieren. Schließlich werden wir diese Funktion dann auch per Caddy automatisiert nutzen können.
Docker
Wir lassen Caddy and Vault als Container laufen
curl
Um zu verifizieren, dass unser Webserver auch auf https läuft
Terraform
Unser Hauptwerkzeug um eben die Konfiguration abzubilden
Wir starten mit den benötigten Containern, die wir per terraform Docker Provider starten. Zu allererst jedoch erstellen wir ein dezidiertes Netzwerk für dieses Tutorial und definieren die beiden Container Images, die wir verwenden werden.
1resource "docker_network" "learn_vault" {
2 name = "learn_vault"
3 driver = "bridge"
4 ipam_config {
5 subnet = "10.1.1.0/24"
6 }
7}
8resource "docker_image" "caddy" {
9 name = "caddy:2.6.4"
10}
11resource "docker_image" "vault" {
12 name = "hashicorp/vault:1.14.2"
13}
Unser Ziel für dieses Tutorial ist es, den Vault Server container im Development Modus laufen zu lassen.
❕ | Der Development Server mode unterstüzt kein TLS für die Loopback Adresse, und wird hier auch ohne TLS verwenden. Vault selbst sollte selbstverständlich im Produktiven Zustand niemals ohne TLS verwendet werden. Um das zu erreichen bräuchten wir ein gültiges Zertifikat und dessen Schlüssel. |
---|
Die Container Definition is äquivalent zu dem Tutorial das als Vorlage dient, aber hier eben als terraform Code.
1
2resource "docker_container" "vault" {
3 name = "learn-vault"
4 image = docker_image.vault.image_id
5 hostname = "learn-vault"
6 rm = true
7 command = ["vault", "server", "-dev", "-dev-root-token-id=root", "-dev-listen-address=0.0.0.0:8200"]
8 networks_advanced {
9 name = docker_network.learn_vault.name
10 ipv4_address = "10.1.1.100"
11 }
12 host {
13 host = "caddy-server.learn.internal"
14 ip = "10.1.1.200"
15 }
16 ports {
17 internal = 8200
18 external = 8200
19 }
20 capabilities {
21 add = ["IPC_LOCK"]
22 }
23}
Auch caddy werden wir als Container laufen lassen, wobei wir hier bereits von Anfang an unsere Konfiguration verwenden, die es uns ermöglich von Vault Zertifikate ausgestellt zu bekommen. Da Caddy nun schon läuft, bevor wir die Konfiguration von Vault implementieren, wird dieser im aktuellen Zustand Fehlermeldungen ausgeben, die wir aktuell aber ignorieren können.
1resource "local_file" "caddyfile" {
2 content = <<EOF
3{
4 acme_ca http://10.1.1.100:8200/v1/pki_int/acme/directory
5}
6caddy-server {
7 root * /usr/share/caddy
8 file_server browse
9}
10EOF
11 filename = "${abspath(path.module)}/Caddyfile"
12}
13
14resource "local_file" "index" {
15 content = "Hello World"
16 filename = "${abspath(path.module)}/index.html"
17}
18
19resource "docker_container" "caddy" {
20
21 name = "caddy-server"
22 image = docker_image.caddy.image_id
23 hostname = "caddy-server"
24 rm = true
25 networks_advanced {
26 name = docker_network.learn_vault.name
27 ipv4_address = "10.1.1.200"
28 }
29 ports {
30 internal = 80
31 external = 80
32 }
33 ports {
34 internal = 443
35 external = 443
36 }
37 volumes {
38 host_path = local_file.caddyfile.filename
39 container_path = "/etc/caddy/Caddyfile"
40 }
41 volumes {
42 host_path = local_file.index.filename
43 container_path = "/usr/share/caddy/index.html"
44 }
45}
Auf Grund der Abhängigkeiten von Container und Konfiguration, und wie dies in Terraform abgehandelt wird, müssen wir nun die Konfiguration in einem separaten Ordner - z.B. einem Unterordner config
- ablegen. Wir können diese aber vom selben Verzeichnis ausführen lassen mittels
1terraform -chdir=config apply
Die Konfiguration der Root CA basiert auf dem Tutorial von HashiCorp Build Your Own Certificate Authority (CA). Wir empfehlen hier, das praktische Tutorial sich auch anzuschauen, wenn man mit der PKI-Secrets-Engine nicht vertraut ist.
1resource "vault_mount" "pki" {
2 path = "pki"
3 type = "pki"
4 max_lease_ttl_seconds = 87600 * 60
5}
6resource "vault_pki_secret_backend_root_cert" "root" {
7
8 backend = vault_mount.pki.path
9 type = "internal"
10 common_name = "learn.internal"
11 issuer_name = "root-2023"
12 ttl = "87600h"
13
14}
15resource "local_file" "root_ca_cert" {
16 content = vault_pki_secret_backend_root_cert.root.certificate
17 filename = "${path.module}/root_2023_ca.crt"
18}
Und jetzt, wenn wir dem Kommandozeilen folgen, stoßen wir auf ein Problem: Eine Ressource der Clusterkonfiguration steht Terraform einfach nicht zur Verfügung. Um dieses Problem zu umgehen, haben wir die vault_generic_endpoint Ressource zur Verfügung. In Kombination mit der HashiCorp Vault API-Dokumentation+ können wir eine Konfiguration für die Clusterkonfiguration erstellen.
1resource "vault_generic_endpoint" "root_config_cluster" {
2 depends_on = [vault_mount.pki]
3 path = "${vault_mount.pki.path}/config/cluster"
4 ignore_absent_fields = true
5 disable_delete = true
6
7 data_json = <<EOT
8{
9 "aia_path": "http://10.1.1.100:8200/v1/${vault_mount.pki.path}",
10 "path": "http://10.1.1.100:8200/v1/${vault_mount.pki.path}"
11}
12EOT
13}
Die weiteren Befehle der Kommandozeile in unserem Hashicorp Tutorial bringen uns zuerst zur vault_pki_secret_backend_config_urls Resource. Allerdings unterstützt diese nicht die enable_templating
Eigenschaft. Das heißt für uns nun, dass wir wieder auf die vault_generic_endpoint Resource zurückgreifen müssen um hier die Konfiguration desPKI engine's URL Endpunkts vorzunehmen.
1resource "vault_generic_endpoint" "root_config_urls" {
2 depends_on = [vault_mount.pki, vault_generic_endpoint.root_config_cluster]
3 path = "${vault_mount.pki.path}/config/urls"
4 ignore_absent_fields = true
5 disable_delete = true
6
7 data_json = <<EOT
8{
9 "enable_templating": true,
10 "issuing_certificates": "{{cluster_aia_path}}/issuer/{{issuer_id}}/der",
11 "crl_distribution_points": "{{cluster_aia_path}}/issuer/{{issuer_id}}/crl/der",
12 "ocsp_servers": "{{cluster_path}}/ocsp"
13}
14EOT
15}
Zu guter letzt erzeugen wir noch eine Rolle um eben diese PKI-Secrets-Engine auch nutzen zu können.
1resource "vault_pki_secret_backend_role" "server2023" {
2 backend = vault_mount.pki.path
3 name = "2023-servers"
4 no_store = false
5 allow_any_name = true
6}
Die Konfiguration der Intermediate CA PKI-Secrets-Engine folgt anfangs der bisher durchgeführten Konfiguration der Root CA.
1resource "vault_mount" "pki_int" {
2 path = "pki_int"
3 type = "pki"
4 max_lease_ttl_seconds = 43800 * 60
5}
6resource "vault_generic_endpoint" "int_config_cluster" {
7 path = "${vault_mount.pki_int.path}/config/cluster"
8 ignore_absent_fields = true
9 disable_delete = true
10
11 data_json = <<EOT
12{
13 "aia_path": "http://10.1.1.100:8200/v1/${vault_mount.pki_int.path}",
14 "path": "http://10.1.1.100:8200/v1/${vault_mount.pki_int.path}"
15}
16EOT
17}
18resource "vault_generic_endpoint" "int_config_urls" {
19 depends_on = [vault_mount.pki_int, vault_generic_endpoint.int_config_cluster]
20 path = "${vault_mount.pki_int.path}/config/urls"
21 ignore_absent_fields = true
22 disable_delete = true
23
24 data_json = <<EOT
25{
26 "enable_templating": true,
27 "issuing_certificates": "{{cluster_aia_path}}/issuer/{{issuer_id}}/der",
28 "crl_distribution_points": "{{cluster_aia_path}}/issuer/{{issuer_id}}/crl/der",
29 "ocsp_servers": "{{cluster_path}}/ocsp"
30}
31EOT
32}
Damit die Intermediate CA verwendet werden kann, erstellen wir hier einen Certicicate Signing Request ( CSR ), der von der eignenen Root CA signiert wird und wir hier eine Zertifikatskette der beiden PKI-Secrets-Engines erzeugen.
1resource "vault_pki_secret_backend_intermediate_cert_request" "int" {
2 backend = vault_mount.pki_int.path
3 type = vault_pki_secret_backend_root_cert.root.type
4 common_name = "learn.internal Intermediate Authority"
5}
6resource "vault_pki_secret_backend_root_sign_intermediate" "int" {
7 backend = vault_mount.pki.path
8 csr = vault_pki_secret_backend_intermediate_cert_request.int.csr
9 common_name = vault_pki_secret_backend_intermediate_cert_request.int.common_name
10 issuer_ref = "root-2023"
11 format = "pem_bundle"
12 ttl = "43800h"
13}
14resource "vault_pki_secret_backend_intermediate_set_signed" "int" {
15 backend = vault_mount.pki_int.path
16 certificate = vault_pki_secret_backend_root_sign_intermediate.int.certificate
17}
Auch hier erstellen wir nun auch noch eine Rolle. damit diese PKI-Secrets-Engine von Anwendern verwendet werden kann.
1data "vault_pki_secret_backend_issuers" "int" {
2 depends_on = [ vault_pki_secret_backend_intermediate_set_signed.int ]
3 backend = vault_mount.pki_int.path
4}
5resource "vault_pki_secret_backend_role" "learn" {
6 backend = vault_mount.pki_int.path
7 issuer_ref = data.vault_pki_secret_backend_issuers.int.keys[0]
8 name = "learn"
9 max_ttl = 720 * 60
10 allow_any_name = true
11 no_store = false
12}
Für die abschließenden Konfigurationsaufgaben, die es unserer Intermediate CA erlauben werden, ACME zu verwenden, sind wir abermals gewzunden die vault_generic_endpoint Resource zu werden. Mit dieser können wir die Konfigurartionsparameter für das Tuning der Secret Engine secrets engine tuning parameter implementieren, sowie auch die ACME Konfiguration in unserer PKI-Secrets-Engine aktivieren.
1resource "vault_generic_endpoint" "pki_int_tune" {
2 path = "sys/mounts/${vault_mount.pki_int.path}/tune"
3 ignore_absent_fields = true
4 disable_delete = true
5 data_json = <<EOT
6{
7 "allowed_response_headers": [
8 "Last-Modified",
9 "Location",
10 "Replay-Nonce",
11 "Link"
12 ],
13 "passthrough_request_headers": [
14 "If-Modified-Since"
15 ]
16}
17EOT
18}
19resource "vault_generic_endpoint" "pki_int_acme" {
20 depends_on = [vault_pki_secret_backend_role.learn]
21 path = "${vault_mount.pki_int.path}/config/acme"
22 ignore_absent_fields = true
23 disable_delete = true
24
25 data_json = <<EOT
26{
27 "enabled": true
28}
29EOT
30}
Da unser Caddy Container schon lief, bevor wir unsere Vault Konfiguration eingebracht haben, ist dieser noch immer in einem Fehlerzustand - auch wenn Caddy in einem gewissen Interval neue Versuche unternehmen wird, sein Zertifikat zu bekommen, wollen wir hier nun den Container neu starten
1docker restart caddy-server
Nun können wir auch in den Logs des Containers beobachten, dass er ein Zertifikat von Vault bekommen kann und damit unsere ACME Konfiguration funktioniert.
1docker logs caddy-server
Um nun noch das HTTPS Zertifikat auch zu verifizieren, werden wir curl
verwenden. Hier müssen wir aber das Root CA Zertifikat angeben, damit curl
die Zertifikatskette validieren kann.
1curl \
2 --cacert config/root_2023_ca.crt \
3 --resolve caddy-server:443:127.0.0.1 \
4 https://caddy-server
Erwartete Ausgabe:
1Hello World
Eine erfolgreiche Antwort zeigt uns nun, dass Caddy automatisch notwendige Zertifikate von Vault mit seiner ACME CA verwenden kann.
Das existierende Tutorial von der HashiCorp Development Dokumentation in Terraform Code umzuwandeln, ist prinzipiell nicht komplex. Um hier mit den Limitierungen der zur Verfügung stehenden Terraform Resourcen umzugehen, muss man sicher aber öfters der vault_generic_endpoint Resource bedienen. Diese kann man dann mit Hilfe der HashiCorp Vault API Dokumenation entsprechend parametrieren und zum Erfolg kommen.
Sie interessieren sich für unsere Trainings oder haben einfach eine Frage, die beantwortet werden muss? Sie können uns jederzeit kontaktieren! Wir werden unser Bestes tun, um alle Ihre Fragen zu beantworten.
Hier kontaktieren