add hetzner servers terraform

This commit is contained in:
Rasmus Wejlgaard 2026-03-29 20:57:43 +01:00
parent 353c2ad790
commit 8958d85f20
6 changed files with 315 additions and 106 deletions

View file

@ -8,44 +8,22 @@ The setup is entirely self-hosted (with the exception of Hetzner Cloud VPSs and
## Network Topology
```
┌──────────────┐
│ Cloudflare │
│ DNS + CDN │
│ *.pez.sh │
└──────┬───────┘
│ HTTPS
┌────────────▼────────────┐
│ helsinki-a │
│ Hetzner Cloud VPS │
│ │
│ Caddy (reverse proxy) │
│ Authelia (SSO) │
│ Bitwarden │
│ LLDAP │
└────────────┬────────────┘
┌───────────────┼───────────────┐
│ Tailscale Mesh │
│ (WireGuard-based VPN) │
└───┬───────┬───────┬───────┬───┘
│ │ │ │
┌────────▼──┐ ┌──▼────────┐ ┌────▼───────┐ ┌──▼──────────┐
│ london-b │ │ london-a │ │nuremberg-a │ │copenhagen-a │
│ │ │ │ │ │ │ │
│ Storage │ │ Monitoring│ │ Mail │ │ Gaming │
│ Media │ │ Prometheus│ │ poste.io │ │ Minecraft │
│ Docker │ │ Grafana │ │ │ │ WoW/MaNGOS │
│ services │ │ │ │ │ │ │
│ (46T ZFS) │ │ (FreeBSD) │ │ (Alpine) │ │ (Ubuntu) │
└───────────┘ └───────────┘ └────────────┘ └─────────────┘
```mermaid
graph TD
CF["<b>Cloudflare</b><br/>DNS + CDN<br/>*.pez.sh"]
CF -->|HTTPS| HEL
┌─────────────┐
│copenhagen-c │
│ (idle) │
└─────────────┘
HEL["<b>helsinki-a</b><br/>Hetzner Cloud VPS<br/><br/>Caddy (reverse proxy)<br/>Authelia (SSO)<br/>Bitwarden<br/>LLDAP"]
HEL --> TS["<b>Tailscale Mesh</b><br/>WireGuard-based VPN"]
TS --> LB["<b>london-b</b><br/>Storage / Media<br/>Docker services<br/>(46T ZFS)"]
TS --> LA["<b>london-a</b><br/>Monitoring<br/>Prometheus / Grafana<br/>(FreeBSD)"]
TS --> NA["<b>nuremberg-a</b><br/>Mail<br/>poste.io<br/>(Alpine)"]
TS --> CA["<b>copenhagen-a</b><br/>Gaming<br/>Minecraft / WoW/MaNGOS<br/>(Ubuntu)"]
TS --> CC["<b>copenhagen-c</b><br/>(idle)"]
style CC stroke-dasharray: 5 5
```
## Traffic Flow
@ -62,39 +40,27 @@ User → Cloudflare (DNS + TLS) → helsinki-a (Caddy) → Backend (over Tailsca
4. For protected services, Caddy calls Authelia first (`forward_auth`)
5. If authenticated (or no auth required), traffic is proxied over Tailscale to the backend
```
┌─────────────────────────────────────────────┐
│ helsinki-a (Caddy) │
│ │
radarr.pez.sh ──► │ forward_auth → Authelia ──► london-b:7878 │
│ │
jellyfin.pez.sh ─►│ (no auth) ───────────────► london-b:8096 │
│ │
grafana.pez.sh ──►│ forward_auth → Authelia ──► london-a:3000 │
│ │
auth.pez.sh ─────►│ (local) ────────────────► localhost:9091 │
└─────────────────────────────────────────────┘
```mermaid
graph LR
subgraph "helsinki-a (Caddy)"
A1["forward_auth → Authelia"]
A2["(no auth)"]
A3["forward_auth → Authelia"]
A4["(local)"]
end
R["radarr.pez.sh"] --> A1 --> LB1["london-b:7878"]
J["jellyfin.pez.sh"] --> A2 --> LB2["london-b:8096"]
G["grafana.pez.sh"] --> A3 --> LA["london-a:3000"]
AU["auth.pez.sh"] --> A4 --> LO["localhost:9091"]
```
## Auth Architecture
```
┌──────────┐
│ Caddy │
│ │
│ forward_ │
│ auth │
└────┬─────┘
┌────▼─────┐
│ Authelia │ auth.pez.sh
│ (SSO) │
└────┬─────┘
┌────▼─────┐
│ LLDAP │ User directory
│ │
└──────────┘
```mermaid
graph TD
Caddy["<b>Caddy</b><br/>forward_auth"] --> Authelia["<b>Authelia</b><br/>SSO<br/>auth.pez.sh"]
Authelia --> LLDAP["<b>LLDAP</b><br/>User directory"]
```
Authelia authenticates against LLDAP (both on helsinki-a). One place to manage users — add or remove someone in LDAP and it propagates to all protected services.

View file

@ -13,7 +13,7 @@ resource "cloudflare_dns_record" "alertmanager" {
zone_id = cloudflare_zone.pez-sh.id
name = "alertmanager"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = false
ttl = 1
}
@ -22,7 +22,7 @@ resource "cloudflare_dns_record" "apps" {
zone_id = cloudflare_zone.pez-sh.id
name = "apps"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = false
ttl = 1
}
@ -31,7 +31,7 @@ resource "cloudflare_dns_record" "auth" {
zone_id = cloudflare_zone.pez-sh.id
name = "auth"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = false
ttl = 1
}
@ -40,7 +40,7 @@ resource "cloudflare_dns_record" "bitwarden" {
zone_id = cloudflare_zone.pez-sh.id
name = "bitwarden"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = false
ttl = 1
}
@ -49,7 +49,7 @@ resource "cloudflare_dns_record" "cloud" {
zone_id = cloudflare_zone.pez-sh.id
name = "cloud"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = false
ttl = 1
}
@ -58,7 +58,7 @@ resource "cloudflare_dns_record" "download" {
zone_id = cloudflare_zone.pez-sh.id
name = "download"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = false
ttl = 1
}
@ -67,7 +67,7 @@ resource "cloudflare_dns_record" "git" {
zone_id = cloudflare_zone.pez-sh.id
name = "git"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = false
ttl = 1
}
@ -76,7 +76,7 @@ resource "cloudflare_dns_record" "grafana" {
zone_id = cloudflare_zone.pez-sh.id
name = "grafana"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = false
ttl = 1
}
@ -85,7 +85,7 @@ resource "cloudflare_dns_record" "helsinki-a" {
zone_id = cloudflare_zone.pez-sh.id
name = "helsinki-a"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = false
ttl = 1
}
@ -94,7 +94,7 @@ resource "cloudflare_dns_record" "jellyfin" {
zone_id = cloudflare_zone.pez-sh.id
name = "jellyfin"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = false
ttl = 1
}
@ -103,7 +103,7 @@ resource "cloudflare_dns_record" "jellyfin-requests" {
zone_id = cloudflare_zone.pez-sh.id
name = "jellyfin-requests"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = false
ttl = 1
}
@ -112,7 +112,7 @@ resource "cloudflare_dns_record" "ldap" {
zone_id = cloudflare_zone.pez-sh.id
name = "ldap"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = false
ttl = 1
}
@ -121,7 +121,7 @@ resource "cloudflare_dns_record" "lidarr" {
zone_id = cloudflare_zone.pez-sh.id
name = "lidarr"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = false
ttl = 1
}
@ -148,7 +148,7 @@ resource "cloudflare_dns_record" "music" {
zone_id = cloudflare_zone.pez-sh.id
name = "music"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = false
ttl = 1
}
@ -157,7 +157,7 @@ resource "cloudflare_dns_record" "naveen" {
zone_id = cloudflare_zone.pez-sh.id
name = "naveen"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = false
ttl = 1
}
@ -166,7 +166,7 @@ resource "cloudflare_dns_record" "root" {
zone_id = cloudflare_zone.pez-sh.id
name = "@"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = false
ttl = 1
}
@ -175,7 +175,7 @@ resource "cloudflare_dns_record" "plex" {
zone_id = cloudflare_zone.pez-sh.id
name = "plex"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = false
ttl = 1
}
@ -184,7 +184,7 @@ resource "cloudflare_dns_record" "prometheus" {
zone_id = cloudflare_zone.pez-sh.id
name = "prometheus"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = false
ttl = 1
}
@ -193,7 +193,7 @@ resource "cloudflare_dns_record" "prowlarr" {
zone_id = cloudflare_zone.pez-sh.id
name = "prowlarr"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = false
ttl = 1
}
@ -202,7 +202,7 @@ resource "cloudflare_dns_record" "radarr" {
zone_id = cloudflare_zone.pez-sh.id
name = "radarr"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = false
ttl = 1
}
@ -211,7 +211,7 @@ resource "cloudflare_dns_record" "readarr" {
zone_id = cloudflare_zone.pez-sh.id
name = "readarr"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = false
ttl = 1
}
@ -220,7 +220,7 @@ resource "cloudflare_dns_record" "request" {
zone_id = cloudflare_zone.pez-sh.id
name = "request"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = false
ttl = 1
}
@ -229,7 +229,7 @@ resource "cloudflare_dns_record" "rss" {
zone_id = cloudflare_zone.pez-sh.id
name = "rss"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = true
ttl = 1
}
@ -238,7 +238,7 @@ resource "cloudflare_dns_record" "sonarr" {
zone_id = cloudflare_zone.pez-sh.id
name = "sonarr"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = false
ttl = 1
}
@ -247,7 +247,7 @@ resource "cloudflare_dns_record" "soulseek" {
zone_id = cloudflare_zone.pez-sh.id
name = "soulseek"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = false
ttl = 1
}
@ -256,7 +256,7 @@ resource "cloudflare_dns_record" "status" {
zone_id = cloudflare_zone.pez-sh.id
name = "status"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = true
ttl = 1
}
@ -265,7 +265,7 @@ resource "cloudflare_dns_record" "thiswebsitedoesnotexist" {
zone_id = cloudflare_zone.pez-sh.id
name = "thiswebsitedoesnotexist"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = true
ttl = 1
}
@ -274,7 +274,7 @@ resource "cloudflare_dns_record" "webdav" {
zone_id = cloudflare_zone.pez-sh.id
name = "webdav"
type = "A"
content = "65.108.48.44"
content = hcloud_server.helsinki-a.ipv4_address
proxied = false
ttl = 1
}

View file

@ -0,0 +1,39 @@
resource "hcloud_server" "nuremberg-a" {
name = "nuremberg-a"
image = "debian-13"
server_type = "cx23"
location = "nbg1"
delete_protection = true
rebuild_protection = true
keep_disk = true
labels = {
"role" = "mail"
}
public_net {
ipv4_enabled = true
ipv6_enabled = true
}
}
resource "hcloud_server" "helsinki-a" {
name = "helsinki-a"
image = "debian-13"
server_type = "cax11"
location = "hel1"
delete_protection = true
rebuild_protection = true
keep_disk = true
labels = {
"role" = "ingress"
}
public_net {
ipv4_enabled = true
ipv6_enabled = true
}
}

View file

@ -0,0 +1,192 @@
resource "hcloud_firewall" "nuremberg-a" {
name = "nuremberg-a"
rule {
direction = "in"
protocol = "tcp"
port = "22"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
# poste.io mail server ports
rule {
direction = "in"
protocol = "tcp"
port = "25"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
rule {
direction = "in"
protocol = "tcp"
port = "80"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
rule {
direction = "in"
protocol = "tcp"
port = "110"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
rule {
direction = "in"
protocol = "tcp"
port = "143"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
rule {
direction = "in"
protocol = "tcp"
port = "443"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
rule {
direction = "in"
protocol = "tcp"
port = "465"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
rule {
direction = "in"
protocol = "tcp"
port = "587"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
rule {
direction = "in"
protocol = "tcp"
port = "993"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
rule {
direction = "in"
protocol = "tcp"
port = "995"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
rule {
direction = "out"
protocol = "tcp"
port = "any"
destination_ips = [
"0.0.0.0/0",
"::/0"
]
}
rule {
direction = "out"
protocol = "udp"
port = "any"
destination_ips = [
"0.0.0.0/0",
"::/0"
]
}
}
resource "hcloud_firewall_attachment" "nuremberg-a" {
firewall_id = hcloud_firewall.nuremberg-a.id
server_ids = [
hcloud_server.nuremberg-a.id
]
}
resource "hcloud_firewall" "helsinki-a" {
name = "helsinki-a"
rule {
direction = "in"
protocol = "tcp"
port = "22"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
rule {
direction = "in"
protocol = "tcp"
port = "80"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
rule {
direction = "in"
protocol = "tcp"
port = "443"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
rule {
direction = "out"
protocol = "tcp"
port = "any"
destination_ips = [
"0.0.0.0/0",
"::/0"
]
}
rule {
direction = "out"
protocol = "udp"
port = "any"
destination_ips = [
"0.0.0.0/0",
"::/0"
]
}
}
resource "hcloud_firewall_attachment" "helsinki-a" {
firewall_id = hcloud_firewall.helsinki-a.id
server_ids = [
hcloud_server.helsinki-a.id
]
}

View file

@ -5,8 +5,14 @@ terraform {
cloudflare = {
source = "cloudflare/cloudflare"
}
hcloud = {
source = "hetznercloud/hcloud"
version = "~> 1.45"
}
}
backend "s3" {
bucket = "pez-infra-tfstate"
key = "tfstate/terraform.tfstate"
@ -22,3 +28,8 @@ provider "cloudflare" {
email = local.secrets["cloudflare_email"]
api_token = local.secrets["cloudflare_api_key"]
}
provider "hcloud" {
token = local.secrets["hetzner_token"]
}

View file

@ -1,20 +1,21 @@
cloudflare_email: ENC[AES256_GCM,data:IOxyqjzQbw+9zg==,iv:bvMQ3JncMf2suPpshwsgtRm5h1UlQ6kAEm7cB/ExM3w=,tag:R9ZcED/RaW16wnqG99ym8A==,type:str]
cloudflare_api_key: ENC[AES256_GCM,data:z1NWHsh4jJ+QAGILfJuKgkrBjjGKoEh2mlSER3LL8vnG8gMDbVsm9O3hkuMfsMxPsY+zbXs=,iv:sw1+gfPIf8auqdDZO3VTtSOhoi0XNsSca0EbEFWZJuI=,tag:hT9Wjls99sE2jdNVSNQtkQ==,type:str]
backblaze_keyID: ENC[AES256_GCM,data:YneBYL27E8lmSULI9w/HLtizqMrk5nDu2Q==,iv:/gNeG2yy4Em/SIjh7i2tGV+8+KYk/d4/UHceDBM6II8=,tag:pfN0ghvcUDQxYKZdIrWUfQ==,type:str]
backblaze_keyName: ENC[AES256_GCM,data:9tKnmmQWDTO3FHZ3D01Isvo=,iv:wLdbiPj5rgIn9Yeu5w+tOnJ2PdRtCFQLP4rncZHxN6w=,tag:ADwSi5oz613meQjPa3kshw==,type:str]
backblaze_applicationKey: ENC[AES256_GCM,data:veIMwboFDx414vVp+kKw2uYRraayZ1DUswTKQMjfsg==,iv:dYdDd71uNPURiPGuieastA4/TtskVNq6uwsDM6Dl1JQ=,tag:jMnV0ydgTrq3zl6F6V5PPQ==,type:str]
cloudflare_email: ENC[AES256_GCM,data:kzVXRWRT7/RUBg==,iv:g9r2gP1BxrBoAighKUIKgO1ZVgfATywSe8I5CX/SJ3A=,tag:TmWfgAfIuQVoz7ddc/7ykQ==,type:str]
cloudflare_api_key: ENC[AES256_GCM,data:E5ZjsAQ0toXauqGkkQDR2/OqOKNaObkTlK8tnGS2nXYX4gQZaDrRhi5ufklxxO0yzZD9qHE=,iv:5JwQOIuhx1cK1jns2eIR+N1tkc4m7Ydeiya4DRoYRVg=,tag:9ojmEiG8Dlxe1EuNiv1A2w==,type:str]
backblaze_keyID: ENC[AES256_GCM,data:mwAeG2OuxSZ95jZZ5qhJGjePtNbo5wUa2w==,iv:uRSZQsMA6sUCvaQOnRZxgdQWS/TpyjFC8nBksOH2yQE=,tag:yhjjiivBkJkhb42nfPju1A==,type:str]
backblaze_keyName: ENC[AES256_GCM,data:HIxN7kPJPnJDp/pR/yWdayU=,iv:fk9lrFJmlZTnb1lk4AdERS+YPics1XXDOq3McBMhSGU=,tag:Sa3Z+qFs8yBmGA5FLRC/xA==,type:str]
backblaze_applicationKey: ENC[AES256_GCM,data:0J/NTaQe+uvJXc9FgGLN4xl4EHKOxKeSjXya+wC0pA==,iv:f8w7Ir+pVs/0yD/5FFLTnlYFrw95aq73Q+r1eBZedho=,tag:cz9aMPiHWE8iIKBEA3G6xw==,type:str]
hetzner_token: ENC[AES256_GCM,data:kUi0EJlK8xuILT7dp8ql2VQCT/t2DJCtQoXrnC52sr2y73uH4QlSGbYwrJbE+0ZgAeB2l43i8cSvW6MWUt/lrA==,iv:zrshjeeb1oQV6OHhLdXQwwhW8ssN0yHvjbjPxgYgOJk=,tag:hOy8bJuDjNJkQ0URfVwoQA==,type:str]
sops:
age:
- recipient: age1r8uh2w2qad2z5sgq9q7l73962q2sp8zz9hdnh6sjuvanxl565vmswn8squ
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyOHlsaHZKRzJLUjhTamha
cmtFN0J3eEFNaERDNDFlbUd0dWIxV25tMVRBClJIZU55N1lLTFYxblRXd3dma0pX
UnZzeGoyMHR0UWxkM3RaNmloUTBFUHMKLS0tIHB5TmdIWEY4dWJUQWNZcVUwV1or
ekhtYkVLZ1hBbEZEakhXeUh0UW94QTgKdEY6mwWVQpMtaAYn+tnXFUvBk9QvzFX4
ai91WDaO/iRtHluOSp5HxRVh2BNO4uH4opXQEthUIkQzLGtDTUN1uw==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtWTFiajV2cThSN240YVEr
SlpOZUV1WVZkeXdOUXJJNnRpOXlOVnNCRGg4Cklxam1uaFgwMy9UU01STlBBSFhT
ZXNQSU1jQXJUZW5HWDEvVWdEUnhzS2MKLS0tIHBYMWJFYStyZVpMMXQ5MUowMy80
ZTdhWjkzTzRDZy8rM2J4TzhmRFFnaUkKt50w9Oq2O5qdo2NMlWo9S8V4m3X6MQG6
Jx/Oit+4DOCFHpL7yxggdD83NJw+0c6kMSB968J/M0EmRAzoYHqFBw==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-03-22T21:04:06Z"
mac: ENC[AES256_GCM,data:6nWRb9Ne7YlcgAiJQAPx7zO51Fb2qAIup5qUG72b3s+XHbutTO5KGefWEx4/flmQx+ctbQ8fRWPOxBHECnB2xVkU0OgehGWAxKXpalnDSMp3cSjXE/Zjisd6H3U5gm8ilRysfCQE1SL8RvZCWWsKI3v89acP+ADYcU9NNOHswbc=,iv:qiWX7JFgsNgwjRPTYNNORDRUj96HRaVopN69qTAD+pM=,tag:qHw27PIvY2hhcYTLY4VPnQ==,type:str]
lastmodified: "2026-03-29T18:58:01Z"
mac: ENC[AES256_GCM,data:q9lEwaxcWAquQP+Dzg1J5WqM2cwcync9EUSVHxtc0peGAxJzg4afHlJi35mC5PZbzv/4wOpdxFR89r9jF3isvvZ6icHcRKmWmlNEl2YCI7VAKIZXZHPx56xXZoj1pOQwNNmEZgAwcreskAINjNIkP6+eIzUDCZ2QRMEK3ok9cHE=,iv:LxtYfXnwfrLmH5w7N36GGRvy1+MpgcoEzm8+KA+QjjI=,tag:/2fIIlNmJcBAXJOyZuotug==,type:str]
unencrypted_suffix: _unencrypted
version: 3.12.2