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
- 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.
- 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.
- 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
-
Remove the SCP, SSH & Known Hosts Wrapper from
/KnownHostsByModule/main.tf
-
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.
- 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"
}
- 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.