1. Introduction

When it comes to affordable and reliable hosting solutions, Hetzner stands out as a prominent choice. Based in Europe and in USA, Hetzner is renowned for offering cost-effective cloud resources.

Hetzner’s cloud services provide a range of essential features, including instances, networking, firewalls, and load balancers. These resources are not only robust but also easy to manage, especially when leveraging automation tools like Terraform.

In this article, we aim to explore how to create a secure and efficient networking architecture using Hetzner and Terraform. We will provide practical examples and best practices to ensure your setup is both scalable and secure, suitable for various needs and scenarios.


2. Architecture Overwiew

In this article, we’ll outline a basic setup where both the “database” instance and the “service” instance are situated within a private network, devoid of any public IP addresses. This ensures that they remain isolated and protected from direct internet access.

NAT Instance (Network Address Translation)

  • Used to enable private instances to connect to the internet.
  • Allows for operations such as updates and accessing third-party services.
  • Does not permit inbound connections from the internet, ensuring that private instances
  • While it might seem convenient to use this instance for handling web traffic, it’s crucial to avoid this approach. Instead, opt for Hetzner’s load balancer services, which are specifically designed for efficient and secure web request management, ensuring better scalability and reliability for your applications.

Bastion Host

  • Serves as a secure gateway for SSH connections from the internet.
  • Provides SSH access to private instances through secure tunnels.
  • Utilizes private key authentication for connection.
  • Can be further secured with additional measures such as Google Authenticator, YubiKey, and IP whitelisting.

For this tutorial, we will focus on using private key authentication to connect to the bastion host. To further enhance security, you can implement additional measures. The database instance can be restricted to accept connections only from the private network and through specified ports, ensuring tighter security and controlled access.

This setup ensures a secure environment for your applications and data, providing a foundation that can be expanded with more advanced security measures as needed.


3. Setting Up Your VPC

Step 1: Configure Terraform, Variable and SSH Keys

Install Terraform and set up your Hetzner provider configurations. This allows you to manage your infrastructure as code, ensuring consistency and ease of management.

main.tf

provider "hcloud" {
  token = var.hcloud_token
}

variable.tf

terraform {
  required_providers {
    hcloud = {
      source = "hetznercloud/hcloud"
      version = "~> 1.45"
    }
  }
}
variable "hcloud_token" {
  description = "Hetzner Cloud API token"
  type        = string
  sensitive   = true
}

variable "public_ssh_key" {
  description = "SSH public key for accessing instances"
  type        = string
}

variable "public_ssh_key_name" {
  description = "Name for the SSH key"
  type        = string
}


variable "location" {
  description = "Location for the servers"
  type        = string
  default     = "ash"
}

variable "network_zone" {
    description = "Network zone for the network"
    type        = string
    default     = "us-east"
}

variable "image" {
    description = "Image for the servers"
    type        = string
    default     = "ubuntu-24.04"
}


ssh.tf

resource "hcloud_ssh_key" "main" {
  name      = var.public_ssh_key_name
  public_key = var.public_ssh_key
}

Step 2: Define Your Subnets & Firewall

network.tf
Create public and private subnets. Public subnets will host internet-facing resources, while private subnets keep your services hidden.

resource "hcloud_network" "main" {
  name     = "main-network"
  ip_range = "10.0.0.0/16"
}

resource "hcloud_network_subnet" "public" {
  network_id   = hcloud_network.main.id
  type         = "cloud"
  network_zone = var.network_zone
  ip_range     = "10.0.1.0/24"
}

resource "hcloud_network_subnet" "private" {
  network_id   = hcloud_network.main.id
  type         = "cloud"
  network_zone = var.network_zone
  ip_range     = "10.0.2.0/24"
}
resource "hcloud_network_route" "nat_route" {
  network_id  = hcloud_network.main.id
  destination = "0.0.0.0/0"
  gateway     =  one(hcloud_server.nat_instance.network[*].ip)
}

firewall.tf
Firewall rule (that can be improved) to allow ssh from public internet to bastion and from public subnet to private subnet.


resource "hcloud_firewall" "ssh" {
  name = "ssh-firewall"
  rule {
    direction = "in"
    protocol  = "tcp"
    port      = "22"
    source_ips = ["0.0.0.0/0"]
  }
}

resource "hcloud_firewall" "private" {
  name = "private-firewall"

  rule {
    direction = "in"
    protocol  = "tcp"
    port      = "22"
    source_ips = ["10.0.1.0/24"]  # Only allow SSH from public subnet
  }
}

Step 3: Deploy a NAT Instance

Set up a NAT instance to allow private subnets to access the internet securely without exposing them directly.


resource "hcloud_server" "nat_instance" {
  name        = "nat-instance"
  server_type = "cpx11"
  image       = var.image
  location    = var.location
  ssh_keys = [hcloud_ssh_key.main.id]

  network {
    network_id = hcloud_network.main.id
    ip         = "10.0.1.10"
  }

  user_data = file("nat-cloud-init.yml")
}

This configuration enables IP forwarding, allowing traffic to be routed between different networks, and configures a NAT rule that translates traffic from the private 10.0.0.0/16 network, making it appear as though it’s coming from the NAT instance’s public IP address.

#cloud-config
packages:
  - ifupdown
package_update: true
package_upgrade: true
runcmd:
  - |
    cat <<EOL >> /etc/network/interfaces
    auto eth0
    iface eth0 inet dhcp
        post-up echo 1 > /proc/sys/net/ipv4/ip_forward
        post-up iptables -t nat -A POSTROUTING -s '10.0.0.0/16' -o eth0 -j MASQUERADE
    EOL
  - reboot

Step 4: Set Up a Bastion Host

A bastion host provides secure access to your private resources, acting as a gatekeeper for SSH connections.

resource "hcloud_server" "bastion" {
  name        = "bastion"
  server_type = "cpx11"
  image       = var.image
  location    = var.location
  ssh_keys    = [hcloud_ssh_key.main.id]
  firewall_ids = [hcloud_firewall.ssh.id]
  network {
    network_id = hcloud_network.main.id
    ip         = "10.0.1.2"
  }

  user_data = file("bastion-cloud-init.yml")
}

The configuration script below requires you to fill in your own private key. In my case, I utilize Hetzner’s capabilities to store my keys, but for automatic configuration, I push my private key during the app’s initial startup using the script provided below. For this article, I am using a single key for all my instances. In a real-world scenario, you might prefer to use two keys: one for connecting to the bastion host and another for accessing your public or private subnets.

#cloud-config
write_files:
  - path: /root/.ssh/hetzner
    permissions: "0600"
    content: |
      -----BEGIN OPENSSH PRIVATE KEY-----
      YourPrivateKeyHere
      -----END OPENSSH PRIVATE KEY-----
  - path: /root/.ssh/config
    permissions: "0644"
    content: |
      Host *
          IdentityFile /root/.ssh/hetzner
          IdentitiesOnly yes

runcmd:
  - chmod 600 /root/.ssh/hetzner

Step 5: Set Up your Instance for your Database or Service

Then you can setup your instances like that, you can see ipv4 and ipv6 aren’t enabled.


resource "hcloud_server" "service_1" {
  name        = "service-1"
  server_type = "cpx11"
  image       = var.image
  location    = var.location
  ssh_keys = [hcloud_ssh_key.main.id]
  firewall_ids = [hcloud_firewall.private.id]
  user_data   = file("private-cloud-init.yml")
  public_net {
    ipv4_enabled = false
    ipv6_enabled = false
  }
  network {
    network_id = hcloud_network.main.id
    ip         = "10.0.2.100"
  }
}



resource "hcloud_server" "database" {
  name        = "database"
  server_type = "ccx13"
  image       = "ubuntu-22.04"
  location    = var.location
  ssh_keys = [hcloud_ssh_key.main.id]
  firewall_ids = [hcloud_firewall.private.id]
  user_data   = file("private-cloud-init.yml")
  public_net {
    ipv4_enabled = false
    ipv6_enabled = false
  }
  network {
    network_id = hcloud_network.main.id
    ip         = "10.0.2.2"
  }

}

Here is the configuration of these instance, we set DNS and sets the default gateway IP address to 10.0.0.1.

#cloud-config
write_files:
  - path: /etc/systemd/network/10-enp7s0.network
    permissions: "0644"
    content: |
        [Match]
        Name=enp7s0
        
        [Network]
        DHCP=yes
        Gateway=10.0.0.1
runcmd:
  - |
    echo "DNS=8.8.8.8 8.8.4.4" >> /etc/systemd/resolved.conf
  - reboot

6. Conclusion

You can have the full code here: https://github.com/alexisgardin/hetzner-public-private-subnet-terraform-example


7. Further Reading

Catégorisé:

Étiqueté dans :

, , ,