postiz: add Temporal sidecar; lock both stacks behind Authentik

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.
This commit is contained in:
Viktor Barzin 2026-05-09 09:08:21 +00:00
parent 7f7698991e
commit 8c4a370a34
No known key found for this signature in database
GPG key ID: 4056458DBDBF8863
2 changed files with 176 additions and 7 deletions

View file

@ -268,17 +268,35 @@ resource "kubernetes_service" "instagram_poster" {
}
}
# Public ingress. No UI entire host is API-only and Meta needs to fetch
# /image/<asset_id> 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"
}

View file

@ -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
}
}
}