diff --git a/docs/architecture.md b/docs/architecture.md index 038c39a..4810044 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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["Cloudflare
DNS + CDN
*.pez.sh"] + CF -->|HTTPS| HEL - ┌─────────────┐ - │copenhagen-c │ - │ (idle) │ - └─────────────┘ + HEL["helsinki-a
Hetzner Cloud VPS

Caddy (reverse proxy)
Authelia (SSO)
Bitwarden
LLDAP"] + + HEL --> TS["Tailscale Mesh
WireGuard-based VPN"] + + TS --> LB["london-b
Storage / Media
Docker services
(46T ZFS)"] + TS --> LA["london-a
Monitoring
Prometheus / Grafana
(FreeBSD)"] + TS --> NA["nuremberg-a
Mail
poste.io
(Alpine)"] + TS --> CA["copenhagen-a
Gaming
Minecraft / WoW/MaNGOS
(Ubuntu)"] + TS --> CC["copenhagen-c
(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["Caddy
forward_auth"] --> Authelia["Authelia
SSO
auth.pez.sh"] + Authelia --> LLDAP["LLDAP
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. diff --git a/terraform/cloudflare_dns.tf b/terraform/cloudflare_dns.tf index 36d541e..44c1da2 100644 --- a/terraform/cloudflare_dns.tf +++ b/terraform/cloudflare_dns.tf @@ -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 } diff --git a/terraform/hetzner_compute.tf b/terraform/hetzner_compute.tf new file mode 100644 index 0000000..781de4d --- /dev/null +++ b/terraform/hetzner_compute.tf @@ -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 + } +} \ No newline at end of file diff --git a/terraform/hetzner_firewall.tf b/terraform/hetzner_firewall.tf new file mode 100644 index 0000000..4b492a8 --- /dev/null +++ b/terraform/hetzner_firewall.tf @@ -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 + ] +} \ No newline at end of file diff --git a/terraform/providers.tf b/terraform/providers.tf index 3d6ac60..a8bbfdf 100644 --- a/terraform/providers.tf +++ b/terraform/providers.tf @@ -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"] +} + diff --git a/terraform/secrets.enc.yaml b/terraform/secrets.enc.yaml index 9b65c39..884c7cc 100644 --- a/terraform/secrets.enc.yaml +++ b/terraform/secrets.enc.yaml @@ -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