HashiCorp Vault ACME Konfiguration mittels terraform


Bicycle

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.

Szenario

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.

Technology Voraussetzungen

  • 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

Setup Containers

Network and Container Images

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}

Vault Container

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}

Caddy Container

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}

Vault Konfiguration basierend auf Terraform

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

Root CA Konfiguration

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}

Vault Intermediate CA Konfiguration

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}

React on applied configuration

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

Verifiieren des Zertifikats

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.

Fazit

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.

Zurück Unsere Trainings entdecken

Wir sind für Sie da

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