Packer¶
When a fresh VPS runs apt-get install nginx certbot wireguard-tools at boot, three things happen: outbound
package downloads appear in egress logs, installation timestamps are written to the host, and the setup window
is visible to anyone watching the host’s traffic. A Packer-built image moves all of that offline. The snapshot
is built once on the bouncer or a dedicated build host, pushed to the provider as a private image, and every VPS
booted from it arrives with the tools already present and no installation history on disk.
The forensic benefit is secondary but real: a seized host booted from a golden image has no apt history
showing when tools were installed, because they were installed before the image was ever deployed.
Build config¶
Packer uses HCL2 config files. A redirector image for Hetzner:
packer {
required_plugins {
hcloud = {
source = "github.com/hashicorp/hcloud"
version = ">= 1.6.0"
}
}
}
variable "hcloud_token" { sensitive = true }
source "hcloud" "redirector" {
token = var.hcloud_token
image = "debian-12"
location = "nbg1"
server_type = "cx22"
snapshot_name = "redirector-{{timestamp}}"
ssh_username = "root"
}
build {
sources = ["source.hcloud.redirector"]
provisioner "shell" {
inline = [
"export DEBIAN_FRONTEND=noninteractive",
"apt-get update -qq",
"apt-get install -y --no-install-recommends nginx certbot python3-certbot-nginx socat wireguard-tools ufw",
"systemctl disable nginx",
"apt-get clean && rm -rf /var/lib/apt/lists/*",
# Wipe shell history from the build session
"unset HISTFILE && history -c",
]
}
}
systemctl disable nginx matters: the image arrives with nginx installed but not running. The per-host
cloud-init or Ansible playbook writes the config and starts the service, so a host does not
announce itself before the config is in place.
Building¶
From the bouncer, with the token in the environment:
export PKR_VAR_hcloud_token="$HCLOUD_TOKEN"
packer init redirector.pkr.hcl
packer build redirector.pkr.hcl
Packer spins up a temporary build server, provisions it, takes a snapshot, and destroys the server. The snapshot ID is printed at the end; add it to the Terraform config as the image to boot from. The build server itself is gone; its IP appeared briefly in provider logs and nowhere else.
Rotation¶
Build a fresh image per operation. Images accumulate in the provider account as a record of what was installed and when; delete previous snapshots at teardown when keeping that history conflicts with the operation’s opsec. For providers without a Packer plugin, the same pattern works with cloud-init scripting the installation at first boot, at the cost of the visible setup window.
What Packer does not cover¶
The image is clean; the hosts booted from it are not necessarily so. Post-boot configuration (domain, upstream address, WireGuard peers) still arrives via cloud-init or Ansible, and those events are in the host’s systemd journal. Packer eliminates the tool-install window, not the configuration window.