Add Hetzner edge node module for custom domain reverse proxy #420
No reviewers
Labels
No labels
domain:backend
domain:devops
domain:frontend
status:approved
status:in-progress
status:needs-fix
status:qa
type:bug
type:devops
type:feature
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
ldraney/pal-e-platform!420
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "hetzner-edge-node"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
hetzner-edgeTerraform module to provision a Hetzner VPS as a reverse proxy for custom domains (palinks.app, landscaping-assistant.app, westsidekingsandqueens.com)Closes #419
Changes
terraform/modules/hetzner-edge/— new module: server, SSH key, firewall, cloud-initterraform/versions.tf— add hetznercloud/hcloud provider ~> 1.49terraform/providers.tf— add hcloud provider blockterraform/variables.tf— add hetzner_api_token, edge_ssh_public_key, edge_tailscale_auth_keyterraform/main.tf— wire hetzner_edge moduleMakefile— add 3 new vars to TF_SECRET_VARSsalt/pillar/secrets_registry.sls— add metadata for 3 new secretsdocs/hetzner-edge.md— architecture doc with mermaid diagramREADME.md— add module to table, add docs sectionTest Plan
tofu fmt -checkpassestofu validatepasses (aftertofu initwith new provider)tofu planshows 3 resources to create (server, SSH key, firewall)Review Checklist
Related Notes
ldraney/pal-e-platform #419— the Forgejo issue this PR implementsproject-palinks— primary consumer (palinks.app custom domain)PR #420 Review
DOMAIN REVIEW
Tech stack: Terraform (hcloud provider), cloud-init YAML, Salt pillar (secrets registry), Makefile, Markdown docs.
Terraform module structure: The new
hetzner-edgemodule follows the established pattern ofmain.tf+variables.tf+outputs.tf. However, it deviates in several places from existing module conventions observed inkeycloak,monitoring,networking, etc.BLOCKERS
1. Tailscale auth key persisted in Hetzner API via cloud-init user_data (Security)
terraform/modules/hetzner-edge/cloud-init.yamlline 11:This auth key is interpolated into
user_data, which Hetzner stores and exposes viahcloud server describeand the Hetzner Cloud Console. Anyone with Hetzner API or console access can retrieve the plaintext auth key after provisioning.Mitigation options (pick one):
tofu planrun would reveal it)hcloud_serverlifecycleignore_changes = [user_data]and rotate the key immediately after first boot2.
domainsvariable declared but never usedterraform/modules/hetzner-edge/variables.tflines 24-32 declare adomainsvariable with a map of domain-to-upstream mappings. This variable is:terraform/main.tfmodule block (onlyssh_public_keyandtailscale_auth_keyare passed)cloud-init.yamlmain.tfDead code in Terraform modules is a BLOCKER because
tofu validatewill warn, and it misleads future readers into thinking domain routing is configured when it is not. Either wire it into the Caddyfile template (via cloud-init) or remove it and defer to the follow-up SaltStack work documented indocs/hetzner-edge.md.3.
secrets.auto.tfvars.examplenot updatedExisting convention: every new sensitive variable gets a
CHANGEMEplaceholder insecrets.auto.tfvars.example. This PR adds 3 new root-level variables (hetzner_api_token,edge_ssh_public_key,edge_tailscale_auth_key) but does not update the example file. This breaks the onboarding workflow -- a fresh clone won't know these variables exist without reading the diff.NITS
4. SSH port 22 open to
0.0.0.0/0and::/0terraform/modules/hetzner-edge/main.tflines 8-12: SSH is open to the entire internet. Since this VPS joins the Tailscale mesh, SSH access could be restricted to the Tailscale CGNAT range (100.64.0.0/10) or to known IPs. Not a blocker since the VPS uses key-based auth, but it widens the attack surface unnecessarily.5. No
versions.tfin the module directoryExisting modules don't all have per-module
versions.tffiles, but the rootversions.tfpinshcloud ~> 1.49. This is fine for now. Nit: consider adding a module-levelrequired_providersblock if the module is ever extracted for reuse.6. Root variable descriptions could be more detailed
Existing conventions include generation instructions and consumer context in descriptions. For example:
hetzner_api_token: could note "Hetzner Cloud Console > project > API Tokens > Read/Write"edge_tailscale_auth_key: could note "Generate ephemeral, reusable key from Tailscale admin > Settings > Keys"The secrets_registry entries have this detail, which is good, but the Terraform variable descriptions are the first thing operators see during
tofu plan.7.
edge_ssh_public_keynaming conventionOther SSH-related variables in the codebase don't exist yet (this is the first), but the
edge_prefix is module-specific while other variables use the service name as prefix (e.g.,harbor_admin_password,forgejo_admin_password). Considerhetzner_edge_ssh_public_keyfor consistency with the module name.8. Cloud-init uses
curl | shpatternLine 10:
curl -fsSL https://tailscale.com/install.sh | sh-- standard for Tailscale but inherently trusting the remote script. Pinning to a specific Tailscale version or using apt repo + version pin would be more reproducible.9.
hetzner_api_tokenrotation_days: 0The secrets_registry entry sets
rotation_days: 0(no rotation). While Hetzner API tokens don't expire, a rotation policy (e.g., 365 days) is good hygiene for tokens with read/write access to infrastructure.SOP COMPLIANCE
.envfiles or credentials in the diffsecrets.auto.tfvars.examplenot updated (convention violation -- listed as BLOCKER #3)PROCESS OBSERVATIONS
tofu planshould show only additions.tofu destroy -target=module.hetzner_edgeremoves everything.docs/hetzner-edge.mdFollow-Up Work section lists 4 items (Salt states, DNS automation, monitoring, failover). These should become Forgejo issues if they aren't already.VERDICT: NOT APPROVED
Three blockers must be resolved:
domainsvariable is dead code -- remove or wire itsecrets.auto.tfvars.examplemust be updated with the 3 new variables