Modules For SSH Host Key Handling

Technical Background

Terraform modules allow you to encapsulate and reuse infrastructure logic. In this exercise, the goal is to centralize SSH host key handling using a reusable module. Instead of copying known_hosts generation and SSH wrapper scripts into every project, a single SshKnownHosts module is created. This module: - Generates a deterministic known_hosts file from the server’s public key. - Creates wrapper scripts for ssh and scp that automatically use this known_hosts file. - Makes your SSH connections reproducible and safe from host key changed warnings after server recreation.

The module is consumed by a parent project called KnownHostsByModule, which contains all files to create the Hetzner infrastructure configuration and an integration with the SshKnownHosts module.

These folders together define a clean separation: - Static infrastructure logic (main.tf, network.tf, tpl/userData.yml). - Generated helper tools (bin/ssh, bin/scp, gen/known_hosts) created at runtime by the module.

Note

This approach decouples infrastructure provisioning (server creation) and local helper file generation (SSH tools). This improves reusability and makes the code cleaner.

Solution

Prerequisits

Create a main.tf, outputs.tf, /tpl/* variables.tf, network.tf,providers.tf and secrets.auto.tfvars like in 16 Mount Point Naming.

Create a folder structure with the files like this:

.
├── KnownHostsByModule
│   ├── (bin)
│   │   ├── (scp)
│   │   └── (ssh)
│   ├── (gen)
│   │   └── (known_hosts)
│   ├── main.tf
│   ├── network.tf
│   ├── outputs.tf
│   ├── providers.tf
│   ├── tpl
│   │   └── userData.yml
│   └── variables.tf
└── Modules
    └── SshKnownHosts
        ├── main.tf
        ├── tpl
        │   ├── scp.sh
        │   └── ssh.sh
        └── variables.tf

Important

Put main.tf from the previous exercise in KnownHostsByModule and create a fresh main.tf inside /Modules/SshKnownHosts.

Put variables.tf from the previous exercise in KnownHostsByModule and create a fresh variables.tf inside /Modules/SshKnownHosts.

Put ssh.sh and scp.sh from the previous exercise inside /Modules/SshKnownHosts/tpl.

Note

The /bin and /gen folders will be generated. You don't have to create them manually.

Setting up SshKnownHosts Module

  1. Create variables for wrapper creation to /Modules/SshKnownHost/variables.tf:
variable "ip4Address" {
  type        = string
  description = "IP of the server"
}

variable "public_key" {
  type        = string
  description = "Public key of the Server"
}

variable "loginName" {
  type        = string
  description = "User Name"
}

Note

The parent server module will pass data through these variables to the child module later.

  1. Add the local_file ressources to create each wrapper to the /Modules/SshKnownHost/main.tf file:
resource "local_file" "known_hosts" {
  content         = "${var.ip4Address} ${var.public_key}"
  filename        = "gen/known_hosts"
  file_permission = "644"
}

resource "local_file" "ssh_script" {
  content = templatefile("${path.module}/tpl/ssh.sh", {
    ip = "${var.ip4Address}"
    loginName = "${var.loginName}"
  })
  filename = "bin/ssh"
  file_permission = "700"
  depends_on      = [local_file.known_hosts]
}

resource "local_file" "scp_wrapper" {
  content = templatefile("${path.module}/tpl/scp.sh", {
    ip = "${var.ip4Address}"
    loginName = "${var.loginName}"
  })
  filename = "bin/scp"
  file_permission = "0755"
}

Warning

Make sure to use ${path.module}/ inside the destination path for the generated paths so they get added to the parent folder and not the child module.

  1. Update variable name inside /Modules/SshKnownHosts/tpl/ssh.sh:
#!/usr/bin/env bash

GEN_DIR=$(dirname "$0")/../gen

ssh \
  -o UserKnownHostsFile="$GEN_DIR/known_hosts" \
  -o IdentitiesOnly=yes \
  -i ~/.ssh/id_ed25519 \
  ${loginName}@${ip}

Setting up KnownHostsByModule Module

  1. Remove the SCP, SSH & Known Hosts Wrapper from /KnownHostsByModule/main.tf

  2. Split up the host SSH Key for the server into a seperate private and public key inside local_file ressources from /KnownHostsByModule/main.tf:

resource "local_file" "server_private_key" {
  content         = tls_private_key.host.private_key_pem
  filename        = "gen/host_private_key.pem"
  file_permission = "600"
}

resource "local_file" "server_public_key" {
  content         = tls_private_key.host.public_key_openssh
  filename        = "gen/host_public_key.pub"
  file_permission = "644"
}

Note

This is required because you can't pass the ressource "tls_private_key" "host" directly to the child module. This module needs only the public key of the server anyway. So only passing this minimizes the risk of the privete key getting leakt.

  1. Update user_data ressource /KnownHostsByModule/main.tf:
resource "local_file" "user_data" {
  content = templatefile("tpl/userData.yml", {
    host_ed25519_private = local_file.server_private_key.content
    host_ed25519_public  = local_file.server_public_key.content
    devopsSSHPublicKey   = hcloud_ssh_key.loginUser.public_key
    volume_name          = hcloud_volume.volume01.name
    volume_id            = hcloud_volume.volume01.id
  })
  filename = "gen/userData.yml"
}
  1. Create module inside /KnownHostsByModule/main.tf:
module "createSshKnownHosts" {
  source              = "../Module/SshKnownHosts"
  loginName           = hcloud_ssh_key.loginUser.name
  ip4Address          = hcloud_server.web.ipv4_address
  public_key          = local_file.server_public_key.content
}

Note

These informations are needed by the file generations in the child module.

Warning

Use the right path to the child module. It's easy to forget that it's inside the /Module folder.

Running Terraform

Run inside /KnownHostsByModule/:

terraform init
terraform apply

Warning

Running the terraform scripts inside the parent folder is crucial to trigger the right events.

Note

It might take a while to finish the creation process.

Success

Output should show:

volume_device_id = "" volume_device_name = "" web_datacenter = "" web_ip_addr = ""

Check if /KnownHostsByModule/gen/* and /KnownHostsByModule/bin/* has been successfully generated.

Warning

Make sure to include the generated /KnownHostsByModule/gen/* folder in your .gitignore because it contains sensetive data.

Terraform Modules docs

Using Terraform Modules