Creating A Web Certificate

Technical Background

Web certificates (SSL/TLS) provide secure encrypted connections (HTTPS) and authenticate the identity of websites. They ensure that traffic between client and server is encrypted and that clients can trust they are connecting to the correct service.

This exercise focuses on using Terraform with the ACME provider to automatically request and generate a wildcard certificate for an entire DNS zone. A wildcard certificate secures both the zone apex (e.g., example.domain.com) and all its subdomains (e.g., *.example.domain.com).

Info

Wildcard certificates are issued only via DNS-01 challenges, which verify domain ownership by adding a special TXT record to the DNS zone.

Terraform uses: - The ACME provider to interact with Let's Encrypt. - The rfc2136 DNS provider to automate DNS record creation for the challenge. - The local_file resource to save the generated key and certificate.

Warning

Always use the staging URL https://acme-staging-v02.api.letsencrypt.org/directory while testing.
Production URLs are rate-limited and can block you for hours if too many requests fail.

Solution

Prerequisits

Create the same files and folder structure as done before in exercise 21 Creating A Fixed Number Of Servers.

Register ACME Provider and Account Key

  1. Add the provider ACME to the /KnownHostsByModule/providers.tf:
provider "acme" {
  server_url = "https://acme-staging-v02.api.letsencrypt.org/directory"
}
  1. Define the required ACME version inside required_providers:
terraform {
  required_providers {
    hcloud = {
      source = "hetznercloud/hcloud"
    }
    dns = {
      source  = "hashicorp/dns"
      version = "~> 3.0"
    }

    acme = {
      source = "vancluever/acme"
    }
  }
  required_version = ">= 0.13"
}

Note

The ACME provider version must be at least v2.23.2 due to DNS-related issues.

Do not set a fixed version so Terraform always uses the latest one.

  1. Exchange the TLS resource to the RSA alhorithm inside the /KnownHostsByModule/main.tf:
resource "tls_private_key" "private_key" {
  count     = var.server_count
  algorithm = "RSA"
}

Info

This key uniquely identifies your ACME account and is used to sign all certificate requests.

  1. Register the ACME account with the ACME registration resource inside the /KnownHostsByModule/main.tf:
resource "acme_registration" "reg" {
  count     = var.server_count
  account_key_pem = tls_private_key.host[count.index].private_key_pem
  email_address   = "jr125@hdm-stuttgart.de"
}

Warning

Use a valid email address. Let's Encrypt uses it for expiry and security notifications.

Add Count

  1. Inside server key wrapper:
resource "local_file" "server_private_key" {
  count           = var.server_count
  content         = tls_private_key.host[count.index].private_key_pem
  filename        = "gen/host_private_key_${count.index}.pem"
  file_permission = "600"
}

resource "local_file" "server_public_key" {
  count           = var.server_count
  content         = tls_private_key.host[count.index].public_key_openssh
  filename        = "gen/host_public_key_${count.index}.pub"
  file_permission = "644"
}
  1. Update the loginUser:
resource "hcloud_ssh_key" "loginUser" {
  count      = var.server_count
  name       = "${var.loginUser_name}-${count.index}"
  public_key = tls_private_key.host[count.index].public_key_openssh
}
  1. Cloud-init generator:
resource "local_file" "user_data" {
  count = var.server_count
  content = templatefile("tpl/userData.yml", {
    host_ed25519_private = local_file.server_private_key[count.index].content
    host_ed25519_public  = local_file.server_public_key[count.index].content
    devopsSSHPublicKey   = hcloud_ssh_key.loginUser[count.index].public_key
    volume_name          = hcloud_volume.volume[count.index].name
    volume_id            = hcloud_volume.volume[count.index].id
  })
  filename = "gen/userData_${count.index}.yml"
}
  1. Hcloud server setup:
resource "hcloud_server" "web" {
  count        = var.server_count
  name         = local.servers[count.index]
  image        = "debian-12"
  server_type  = "cx22"
  firewall_ids = [hcloud_firewall.sshFw.id]
  ssh_keys     = [hcloud_ssh_key.loginUser[count.index].id]
  user_data    = local_file.user_data[count.index].content
}
  1. Module setup:
module "createSshKnownHosts" {
  count        = var.server_count
  source              = "../Module/SshKnownHosts"
  loginName           = hcloud_ssh_key.loginUser[count.index].name
  dnsZone             = var.dns_zone
  serverName          = var.server_name 
  server_count         = var.server_count 
  server_base_name      = var.server_base_name 
  host_public_keys   = [for key in tls_private_key.host : key.public_key_openssh]
  servers             = local.servers
}

Configure the Certificate

  1. Request a wildcard certificate with the ACME certificate resource:
resource "acme_certificate" "certificate" {
  count     = var.server_count
  account_key_pem = acme_registration.reg[count.index].account_key_pem
  common_name     = "*.g11.sdi.hdm-stuttgart.cloud"

  dns_challenge {
    provider = "rfc2136"

    config = {
      RFC2136_NAMESERVER     = "ns1.sdi.hdm-stuttgart.cloud"
      RFC2136_TSIG_ALGORITHM = "hmac-sha512"
      RFC2136_TSIG_KEY       = "g11.key."
      RFC2136_TSIG_SECRET    = var.dns_secret
    }
  }
}

Info

  • dnsZone: Base zone (e.g., g03.sdi.hdm-stuttgart.cloud)
  • dns_secret: TSIG secret key for RFC2136 authentication
  • subject_alternative_names: Adds *.zone to secure all subdomains.

Warning

If the DNS record does not propagate in time, Let's Encrypt validation will fail. Re-run terraform apply after fixing DNS issues.

  1. Save the certificate and the private key locally:
resource "local_file" "private_key" {
  count     = var.server_count
  content  = acme_certificate.certificate[count.index].private_key_pem
  filename = "${path.module}/gen/private.pem"
}

resource "local_file" "certificate" {
  count     = var.server_count
  content  = acme_certificate.certificate[count.index].certificate_pem
  filename = "${path.module}/gen/certificate.pem"
}

Deploy and verify

Initialize and apply Terraform inside /KnownHostsByModule:

terraform init
terraform apply

Success

After running terraform apply, you will find: - gen/certificate.pem: The wildcard certificate - gen/private.pem: The corresponding private key - DNS TXT records are automatically created for validation

After a successful run, you have a fully automated TLS certificate generation workflow that can be reused and triggered whenever certificates need renewal.

These can now be installed on your web server.

DNS

Understanding Web Certificates

DNS Provider

rfc2135

Terraform Provider Configuration

ACME Cert

Terraform local_file

acme 2.23.2