From 8c4a370a34875673c25c8bc4dfddd51f5ad79125 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Sat, 9 May 2026 09:08:21 +0000 Subject: [PATCH] postiz: add Temporal sidecar; lock both stacks behind Authentik MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Postiz backend was crashlooping on connect ECONNREFUSED ::1:7233 — Postiz needs Temporal for cron/scheduled posts and the Helm chart doesn't bundle it. Added a single-replica temporalio/auto-setup:1.28.1 Deployment in the postiz namespace, backed by the bundled postiz-postgresql (separate `temporal` + `temporal_visibility` databases pre-created via init container), ENABLE_ES=false (Postiz only uses the workflow engine, not visibility search). Skips DYNAMIC_CONFIG_FILE_PATH because that file isn't bundled in auto-setup. Auth audit: - postiz: ingress now `protected = true` (Authentik forward-auth). Postiz also has its own login on top, but registration is no longer exposed to the open internet. - instagram-poster: split into two ingresses on the same host. `/image/*` stays public (Meta + Telegram fetch the 9:16 derivatives). Everything else (/healthz, /queue, /scan, /enqueue, /reject, /post-next) sits behind Authentik. The protected ingress sets dns_type=none — the public one already created the CF DNS record. --- .../modules/instagram-poster/main.tf | 32 +++- stacks/postiz/modules/postiz/main.tf | 151 ++++++++++++++++++ 2 files changed, 176 insertions(+), 7 deletions(-) diff --git a/stacks/instagram-poster/modules/instagram-poster/main.tf b/stacks/instagram-poster/modules/instagram-poster/main.tf index 3f2f235f..d304b96d 100644 --- a/stacks/instagram-poster/modules/instagram-poster/main.tf +++ b/stacks/instagram-poster/modules/instagram-poster/main.tf @@ -268,17 +268,35 @@ resource "kubernetes_service" "instagram_poster" { } } -# Public ingress. No UI — entire host is API-only and Meta needs to fetch -# /image/ unauthenticated to render preview cards. We therefore -# leave `protected = false` so Authentik forward-auth doesn't run on any -# path. Inbound auth is the API's own concern (Postiz webhook signature -# / shared secret as configured by the parallel agent). -module "ingress" { +# Two ingresses on the same host — Traefik picks the longest path prefix. +# +# `/image/*` must be reachable WITHOUT auth so Meta's content fetcher (and +# Telegram's photo preview) can render the 9:16 derivatives we produce. +# Everything else (/queue, /scan, /enqueue, /post-next, /reject, /healthz) +# sits behind Authentik forward-auth — same defense as every other UI on +# the cluster, no random caller can pop items off the approval queue. +module "ingress_image_public" { source = "../../../../modules/kubernetes/ingress_factory" dns_type = "proxied" namespace = kubernetes_namespace.instagram_poster.metadata[0].name - name = "instagram-poster" + name = "instagram-poster-image" + host = "instagram-poster" tls_secret_name = var.tls_secret_name protected = false + ingress_path = ["/image"] port = 80 + service_name = "instagram-poster" +} + +module "ingress_protected" { + source = "../../../../modules/kubernetes/ingress_factory" + dns_type = "none" # DNS record already created by the public ingress above + namespace = kubernetes_namespace.instagram_poster.metadata[0].name + name = "instagram-poster" + host = "instagram-poster" + tls_secret_name = var.tls_secret_name + protected = true + ingress_path = ["/"] + port = 80 + service_name = "instagram-poster" } diff --git a/stacks/postiz/modules/postiz/main.tf b/stacks/postiz/modules/postiz/main.tf index d40bae65..fcf28ff5 100644 --- a/stacks/postiz/modules/postiz/main.tf +++ b/stacks/postiz/modules/postiz/main.tf @@ -132,6 +132,8 @@ resource "helm_release" "postiz" { DISABLE_REGISTRATION = "false" IS_GENERAL = "true" NX_ADD_PLUGINS = "false" + # Postiz uses Temporal for cron/scheduling — bring our own; Helm chart doesn't. + TEMPORAL_ADDRESS = "temporal:7233" } # Postiz reads DATABASE_URL/REDIS_URL from this Secret. The chart does @@ -212,6 +214,7 @@ module "ingress" { host = var.host service_name = "postiz" # chart Service name resolves to fullnameOverride port = 80 + protected = true # Authentik forward-auth — Postiz has its own login on top, but we don't expose registration to the open internet. tls_secret_name = var.tls_secret_name extra_annotations = { "gethomepage.dev/enabled" = "true" @@ -222,3 +225,151 @@ module "ingress" { "gethomepage.dev/pod-selector" = "" } } + +# ────────────────────────────────────────────────────────────────────────────── +# Temporal — cron/workflow engine Postiz requires for scheduled posts. +# +# Lightweight single-replica deployment using temporalio/auto-setup, backed +# by the bundled postiz-postgresql (separate `temporal` database). Visibility +# search via Elasticsearch is disabled (ENABLE_ES=false) — Postiz only uses +# the workflow engine, not visibility, so SQL is enough. +# +# Important: temporalio/auto-setup creates schemas in the `temporal` and +# `temporal_visibility` databases on first boot. We pre-create them with an +# init container running psql against postiz-postgresql. +# ────────────────────────────────────────────────────────────────────────────── + +resource "kubernetes_deployment" "temporal" { + metadata { + name = "temporal" + namespace = kubernetes_namespace.postiz.metadata[0].name + labels = { + app = "temporal" + } + } + spec { + replicas = 1 + strategy { + type = "Recreate" + } + selector { + match_labels = { app = "temporal" } + } + template { + metadata { + labels = { app = "temporal" } + } + spec { + # Pre-create the two databases Temporal expects on the bundled PG. + init_container { + name = "create-temporal-dbs" + image = "docker.io/bitnamilegacy/postgresql:16.4.0-debian-12-r7" + env { + name = "PGPASSWORD" + value = "postiz-password" + } + command = ["/bin/bash", "-c"] + args = [ + <<-EOT + set -e + for db in temporal temporal_visibility; do + psql -h postiz-postgresql -U postiz -d postgres -tc "SELECT 1 FROM pg_database WHERE datname='$db'" | grep -q 1 \ + || psql -h postiz-postgresql -U postiz -d postgres -c "CREATE DATABASE \"$db\"" + done + EOT + ] + } + container { + name = "temporal" + image = "temporalio/auto-setup:1.28.1" + port { + container_port = 7233 + name = "grpc" + } + env { + name = "DB" + value = "postgres12" + } + env { + name = "DB_PORT" + value = "5432" + } + env { + name = "POSTGRES_USER" + value = "postiz" + } + env { + name = "POSTGRES_PWD" + value = "postiz-password" + } + env { + name = "POSTGRES_SEEDS" + value = "postiz-postgresql" + } + env { + name = "DBNAME" + value = "temporal" + } + env { + name = "VISIBILITY_DBNAME" + value = "temporal_visibility" + } + env { + name = "ENABLE_ES" + value = "false" + } + env { + name = "TEMPORAL_NAMESPACE" + value = "default" + } + # NOTE: not setting DYNAMIC_CONFIG_FILE_PATH — that file isn't + # bundled in temporalio/auto-setup. Defaults are fine for our + # use (Postiz only needs the workflow engine, not dynamic config). + resources { + requests = { + cpu = "50m" + memory = "256Mi" + } + limits = { + memory = "1Gi" + } + } + # Auto-setup runs schema migrations on first boot — give it time. + startup_probe { + tcp_socket { + port = 7233 + } + failure_threshold = 30 + period_seconds = 5 + initial_delay_seconds = 10 + } + liveness_probe { + tcp_socket { + port = 7233 + } + period_seconds = 30 + } + } + } + } + } + lifecycle { + ignore_changes = [spec[0].template[0].spec[0].dns_config] # KYVERNO_LIFECYCLE_V1 + } + depends_on = [helm_release.postiz] +} + +resource "kubernetes_service" "temporal" { + metadata { + name = "temporal" + namespace = kubernetes_namespace.postiz.metadata[0].name + } + spec { + selector = { app = "temporal" } + port { + name = "grpc" + port = 7233 + target_port = 7233 + } + } +}