Production-ready Terraform configuration for deploying hardened servers on Hetzner Cloud with optional Cloudflare DNS integration and Mailcow email server support.
- Automated Infrastructure Provisioning - Deploy complete server infrastructure with a single command
- Security Hardened - Fail2ban, automatic security updates, SSH hardening, password-required sudo
- Private Networking - Isolated private network with configurable CIDR ranges
- Intelligent Firewall - Layered firewall rules with IP allowlisting, Cloudflare integration, and Mailcow support
- DNS Management - Automatic Cloudflare DNS record creation with comprehensive email authentication
- Cloud-init Automation - Fully automated server provisioning with Docker support
- High Availability Ready - Optional placement groups for server anti-affinity
- Production Monitoring - Comprehensive logging and provisionable security tooling
┌─────────────────────────────────────────────────────────────┐
│ Root Module │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Network │ │ Security │ │ Server │ │ Firewall │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ └─────────────┴──────────────┴─────────────┘ │
│ │ │
│ ┌─────▼─────┐ │
│ │Cloudflare │ │
│ │(Optional) │ │
│ └───────────┘ │
└─────────────────────────────────────────────────────────────┘
| Module | Purpose | Key Resources |
|---|---|---|
| network | Private networking | Hetzner private network, subnet |
| security | SSH key management | SSH keys for server access |
| server | Server provisioning | Hetzner server, cloud-init, placement groups, rDNS |
| firewall | Network security | Consolidated firewall rules, allowlisting |
| cloudflare | DNS management | A/AAAA records, Mailcow DNS (MX, SRV, TXT, SPF, DMARC, DKIM, TLSA) |
- Terraform >= 1.0.0
- Hetzner Cloud account with API token
- (Optional) Cloudflare account with API token and Zone ID
- SSH key pair (ed25519, rsa, or ecdsa)
-
Clone the repository
git clone <repository-url> cd hcloud-staerkcloud
-
Copy the example configuration
cp terraform.tfvars.example terraform.tfvars
-
Edit
terraform.tfvarswith your values# Required hcloud_token = "your-hetzner-api-token" security_ssh_key_public_key = "ssh-ed25519 AAAA..." server_domain = "server.example.com" # Optional enable_cloudflare = true cloudflare_api_token = "your-cloudflare-token" cloudflare_zone_id = "your-zone-id"
-
Initialize Terraform
terraform init
-
Review the plan
terraform plan
-
Apply the configuration
terraform apply
-
Get your server details
terraform output
| Variable | Description | Example |
|---|---|---|
hcloud_token |
Hetzner Cloud API token | "abc123..." |
security_ssh_key_public_key |
SSH public key content | "ssh-ed25519 AAAA..." |
server_domain |
Fully qualified domain name | "server.example.com" |
| Flag | Description | Default |
|---|---|---|
enable_cloudflare |
Enable Cloudflare DNS and firewall rules | false |
enable_mailcow |
Enable Mailcow firewall and DNS records | false |
Server Configuration:
server_name- Server hostname in Hetzner Cloudserver_server_type- Server type (e.g.,cx22,cax11)server_location- Datacenter location (fsn1,nbg1,hel1,ash,hil)server_image_name- OS image (e.g.,debian-13,ubuntu-24.04)server_username- Primary user for SSH accessserver_user_password_hash- Hashed password for sudo/console (optional, see below)server_install_docker- Automatically install Docker (default:true)server_enable_backups- Enable automatic backups (default:false)server_ipv4_enabled- Enable public IPv4 (default:true)server_ipv6_enabled- Enable public IPv6 (default:true)
Network Configuration:
network_name- Name of private networknetwork_ip_range- CIDR block for private network (e.g.,10.0.0.0/16)network_subnet_netnumber- Subnet number (0-255)
Firewall Configuration:
firewall_name- Firewall namefirewall_allowlist_ips- List of trusted IPs in CIDR notationfirewall_rules- Custom firewall rules (see below)
Set a password for sudo and console access:
Quick method:
./scripts/generate-password-hash.shManual methods:
# Option 1: OpenSSL (easiest on macOS)
openssl passwd -6
# Option 2: Python3
python3 -c "import crypt; print(crypt.crypt('your_password', crypt.mksalt(crypt.METHOD_SHA512)))"
# Option 3: mkpasswd (requires: brew install whois)
mkpasswd --method=SHA-512 --rounds=4096
# Option 4: Docker
docker run --rm alpine sh -c "apk add mkpasswd && mkpasswd --method=SHA-512"Then add to terraform.tfvars:
server_user_password_hash = "$6$rounds=4096$salt$hash..."Note: Password authentication over SSH is disabled for security. The password is only used for:
sudocommands (when you need root access)- Console access via Hetzner Cloud web interface
- Emergency access situations
Add custom firewall rules in terraform.tfvars:
firewall_rules = [
{
direction = "in"
protocol = "tcp"
port = "8080"
source_ips = ["0.0.0.0/0", "::/0"]
description = "Allow HTTP traffic on port 8080"
}
]When enable_mailcow = true, configure email authentication in cloudflare/mailcow.tf:
locals {
dkim_public_key = "v=DKIM1; k=rsa; p=MIGfMA0GCS..."
dmarc_policy = "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com"
spf_policy = "v=spf1 a mx ~all"
tlsa_certificate = "3 1 1 abcdef123456..."
}This creates:
- MX record pointing to your server
- SPF, DKIM, and DMARC records for email authentication
- SRV records for autodiscovery (Outlook, Thunderbird)
- CalDAV/CardDAV service discovery records
- TLSA record for DANE (SMTP TLS authentication)
The server is automatically configured with:
- ✅ SSH Hardening - Root login disabled, password authentication disabled
- ✅ Sudo Security - Password required for sudo (no NOPASSWD)
- ✅ Fail2ban - Intrusion prevention system
- ✅ Automatic Updates - Unattended security updates (no auto-reboot)
- ✅ System Tuning - Optimized network and file descriptor limits
- ✅ Minimal Attack Surface - Only essential packages installed
Firewall rules are applied in this order:
- ICMP - Allow ping from everywhere (network diagnostics)
- Allowlist - Full access for trusted IPs (all ports, all protocols)
- Custom Rules - Your application-specific rules
- Mailcow Rules - Email server ports (SMTP, IMAP, POP3, etc.)
- Cloudflare Rules - Port 443 from Cloudflare IPs only
- Use IP Allowlisting - Add your trusted IPs to
firewall_allowlist_ips - Enable Backups - Set
server_enable_backups = truefor production - Use Strong SSH Keys - Prefer ed25519 keys over RSA
- Monitor Logs - Check
/var/log/cloud-init-userdata.logon first boot - Configure Email Auth - Set SPF, DKIM, and DMARC for email servers
After deployment, Terraform provides:
| Output | Description |
|---|---|
server_ipv4_address |
Public IPv4 address |
server_ipv6_address |
Public IPv6 address |
server_private_ip |
Private network IP |
ssh_command |
Ready-to-use SSH command |
Example:
$ terraform output ssh_command
ssh myuser@2001:db8::1# Get SSH command
terraform output -raw ssh_command
# Or manually
ssh <username>@<ip-address># Cloud-init provisioning log
ssh <username>@<ip> sudo cat /var/log/cloud-init-userdata.log
# Cloud-init output
ssh <username>@<ip> sudo cat /var/log/cloud-init-output.log
# System logs
ssh <username>@<ip> sudo journalctl -xe# Make changes to terraform.tfvars or *.tf files
terraform plan
terraform apply# Remove all resources
terraform destroy# Update only firewall
terraform apply -target=module.firewall
# Update only DNS records
terraform apply -target=module.cloudflare.
├── README.md # This file
├── CLAUDE.md # Claude Code guidance
├── terraform.tfvars.example # Configuration template
├── terraform.tfvars # Your configuration (gitignored)
├── main.tf # Module orchestration
├── variables.tf # Root variables (6 categories)
├── outputs.tf # Root outputs
├── providers.tf # Provider configuration
├── cloudflare/ # Cloudflare DNS module
│ ├── main.tf # A/AAAA records
│ ├── mailcow.tf # Email DNS records
│ ├── variables.tf
│ ├── outputs.tf
│ └── versions.tf
├── firewall/ # Firewall module
│ ├── main.tf # Firewall resource
│ ├── locals.tf # Consolidated rule logic
│ ├── data.tf # Cloudflare IP ranges
│ ├── variables.tf
│ ├── outputs.tf
│ └── versions.tf
├── network/ # Network module
│ ├── main.tf # Private network
│ ├── variables.tf
│ ├── outputs.tf
│ └── versions.tf
├── security/ # Security module
│ ├── main.tf # SSH keys
│ ├── variables.tf
│ ├── outputs.tf
│ └── versions.tf
└── server/ # Server module
├── main.tf # Server, network attachment, rDNS
├── cloudinit.tftpl # Cloud-init template
├── data.tf # Image lookup
├── variables.tf
├── outputs.tf
└── versions.tf
All variables have comprehensive validation rules:
- CIDR Notation - Network ranges validated with
cidrhost() - Domain Names - Regex validation for FQDNs
- SSH Keys - Format validation (ed25519, rsa, ecdsa)
- Usernames - Linux username format validation
- IP Addresses - IPv4/IPv6 format validation
- Cloudflare Zone ID - 32-character hex validation
- Firewall Protocols - Enum validation (tcp, udp, icmp, esp, gre)
# Check cloud-init status
ssh <user>@<ip> cloud-init status
# View detailed logs
ssh <user>@<ip> sudo cat /var/log/cloud-init-userdata.log# List active firewall rules
ssh <user>@<ip> sudo iptables -L -n -v# Check DNS records
dig @1.1.1.1 server.example.com A
dig @1.1.1.1 server.example.com AAAA
dig @1.1.1.1 example.com MX# Verify Docker is installed
ssh <user>@<ip> docker --version
# Check if user is in docker group
ssh <user>@<ip> groupsContributions are welcome! Please ensure:
- All code is formatted with
terraform fmt -recursive - Configuration validates with
terraform validate - Variables are alphabetically organized
- New features include validation rules
- Documentation is updated
This project is licensed under the MIT License - see the LICENSE file for details.
Copyright (c) 2025 Jeppe Stærk
For issues and questions:
- Check the CLAUDE.md file for architecture details
- Review Terraform plan output before applying
- Check cloud-init logs for provisioning issues
- Open an issue on GitHub for bugs or feature requests
- Hetzner Cloud - Infrastructure provider
- Cloudflare - DNS and CDN
- Mailcow - Email server suite
- Terraform - Infrastructure as Code tool