Skip to content

Repository files navigation

Hetzner Cloud Infrastructure with Terraform

License: MIT Terraform Hetzner Cloud Cloudflare

Production-ready Terraform configuration for deploying hardened servers on Hetzner Cloud with optional Cloudflare DNS integration and Mailcow email server support.

Features

  • 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

Architecture

┌─────────────────────────────────────────────────────────────┐
│                         Root Module                          │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐   │
│  │ Network  │  │ Security │  │  Server  │  │ Firewall │   │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘   │
│       │             │              │             │          │
│       └─────────────┴──────────────┴─────────────┘          │
│                          │                                   │
│                    ┌─────▼─────┐                            │
│                    │Cloudflare │                            │
│                    │(Optional) │                            │
│                    └───────────┘                            │
└─────────────────────────────────────────────────────────────┘

Module Overview

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)

Quick Start

Prerequisites

  • 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)

Installation

  1. Clone the repository

    git clone <repository-url>
    cd hcloud-staerkcloud
  2. Copy the example configuration

    cp terraform.tfvars.example terraform.tfvars
  3. Edit terraform.tfvars with 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"
  4. Initialize Terraform

    terraform init
  5. Review the plan

    terraform plan
  6. Apply the configuration

    terraform apply
  7. Get your server details

    terraform output

Configuration

Required Variables

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"

Feature Flags

Flag Description Default
enable_cloudflare Enable Cloudflare DNS and firewall rules false
enable_mailcow Enable Mailcow firewall and DNS records false

Key Configuration Options

Server Configuration:

  • server_name - Server hostname in Hetzner Cloud
  • server_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 access
  • server_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 network
  • network_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 name
  • firewall_allowlist_ips - List of trusted IPs in CIDR notation
  • firewall_rules - Custom firewall rules (see below)

User Password Configuration (Optional)

Set a password for sudo and console access:

Quick method:

./scripts/generate-password-hash.sh

Manual 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:

  • sudo commands (when you need root access)
  • Console access via Hetzner Cloud web interface
  • Emergency access situations

Custom Firewall Rules

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"
  }
]

Email Server Configuration (Mailcow)

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)

Security Features

Cloud-init Security Hardening

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 Layering

Firewall rules are applied in this order:

  1. ICMP - Allow ping from everywhere (network diagnostics)
  2. Allowlist - Full access for trusted IPs (all ports, all protocols)
  3. Custom Rules - Your application-specific rules
  4. Mailcow Rules - Email server ports (SMTP, IMAP, POP3, etc.)
  5. Cloudflare Rules - Port 443 from Cloudflare IPs only

Best Practices

  1. Use IP Allowlisting - Add your trusted IPs to firewall_allowlist_ips
  2. Enable Backups - Set server_enable_backups = true for production
  3. Use Strong SSH Keys - Prefer ed25519 keys over RSA
  4. Monitor Logs - Check /var/log/cloud-init-userdata.log on first boot
  5. Configure Email Auth - Set SPF, DKIM, and DMARC for email servers

Outputs

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

Common Operations

Connect to Server

# Get SSH command
terraform output -raw ssh_command

# Or manually
ssh <username>@<ip-address>

View Logs

# 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

Update Infrastructure

# Make changes to terraform.tfvars or *.tf files
terraform plan
terraform apply

Destroy Infrastructure

# Remove all resources
terraform destroy

Target Specific Modules

# Update only firewall
terraform apply -target=module.firewall

# Update only DNS records
terraform apply -target=module.cloudflare

File Organization

.
├── 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

Validation

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)

Troubleshooting

Cloud-init Issues

# Check cloud-init status
ssh <user>@<ip> cloud-init status

# View detailed logs
ssh <user>@<ip> sudo cat /var/log/cloud-init-userdata.log

Firewall Issues

# List active firewall rules
ssh <user>@<ip> sudo iptables -L -n -v

DNS Propagation

# 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

Docker Installation

# Verify Docker is installed
ssh <user>@<ip> docker --version

# Check if user is in docker group
ssh <user>@<ip> groups

Contributing

Contributions are welcome! Please ensure:

  1. All code is formatted with terraform fmt -recursive
  2. Configuration validates with terraform validate
  3. Variables are alphabetically organized
  4. New features include validation rules
  5. Documentation is updated

License

This project is licensed under the MIT License - see the LICENSE file for details.

Copyright (c) 2025 Jeppe Stærk

Support

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

Acknowledgments

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages