From a42f4f7b26667aa9dd3b3bfe1fe4abbbaa35d95e Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Fri, 5 Jun 2026 20:30:07 +0000 Subject: [PATCH] trek: trial-deploy TREK group-trip planner behind Authentik (solo eval) Stand up upstream TREK (mauriceboe/trek:3.0.22, AGPL) as a low-commitment trial to evaluate the self-hosted group-trip use case before building a custom app. Solo, single shared instance, Authentik forward-auth. - stacks/trek: namespace, deployment (pinned, TF-managed, no CI/Keel), service 80->3000, ingress_factory auth=required + proxied DNS at trek.viktorbarzin.me, TLS. Two proxmox-lvm-encrypted PVCs (SQLite data + uploads) -- encrypted per the sensitive-data rule and to avoid the SQLite-over-NFS locking hazard. - Trial secrets posture: ENCRYPTION_KEY auto-generated on the data PVC, bootstrap admin in pod logs -- no Vault/ESO. Graduation TODOs documented in main.tf + service-catalog (Vault key, app-level SQLite backup, OIDC SSO). - kyverno: add mauriceboe/* to require-trusted-registries allowlist (the policy is Enforce since 2026-05-19 -- also fixed the stale "stays in Audit" header comment that said otherwise and misled the deploy). - Runs free on OpenStreetMap (no paid maps key). Rallly availability-poll companion deferred per solo-trial scope. Co-Authored-By: Claude Opus 4.8 --- .claude/reference/service-catalog.md | 1 + .../modules/kyverno/security-policies.tf | 18 +- stacks/trek/main.tf | 248 ++++++++++++++++++ stacks/trek/secrets | 1 + stacks/trek/terragrunt.hcl | 13 + 5 files changed, 273 insertions(+), 8 deletions(-) create mode 100644 stacks/trek/main.tf create mode 120000 stacks/trek/secrets create mode 100644 stacks/trek/terragrunt.hcl diff --git a/.claude/reference/service-catalog.md b/.claude/reference/service-catalog.md index 5a3d53c1..5f5eabdf 100644 --- a/.claude/reference/service-catalog.md +++ b/.claude/reference/service-catalog.md @@ -116,6 +116,7 @@ | status-page | Status page | status-page | | plotting-book | Book plotting/world-building app | plotting-book | | tripit | Self-hosted TripIt-clone travel-itinerary PWA (FastAPI + SvelteKit SPA, same-origin). CNPG (`tripit` db, Vault static role `pg-tripit`) + RWX NFS trip-doc vault (`/srv/nfs/tripit-documents`) + RWO `proxmox-lvm-encrypted` personal-document vault `tripit-personal-documents` (passports/IDs — AES-256-GCM app-layer envelope, master key `DOCUMENT_ENCRYPTION_KEY` in `secret/tripit`). `auth=required` (Authentik forward-auth, reads `X-authentik-email`); second `auth=none` ingress on `/api/calendar` for HMAC-token-gated `.ics` feed. Email-ingest CronJob `tripit-ingest-plans` (`*/15`) is the SOLE inbound path — forward a booking to plans@viktorbarzin.me (catch-all → spam@), polled read-only and routed ONLY to a registered user / verified linked address (no default-owner fallback; strangers ignored), parsed by local LLM (`qwen3vl-4b`), and the sender is emailed the outcome (Added to trip / Couldn't import). Plus `tripit-poll-flights`, `tripit-run-reminders`, `tripit-transport-nudge`, `tripit-weather-brief`. (The old Gmail-scrape `tripit-ingest-mail` CronJob was removed 2026-06-05.) App secrets in Vault `secret/tripit`. | tripit | +| trek | **TRIAL (2026-06-05)** — self-hosted group-trip planner (upstream [TREK](https://github.com/mauriceboe/TREK), `mauriceboe/trek:3.0.22`, AGPL-3.0). Solo evaluation behind Authentik forward-auth (`auth=required`) before deciding build-vs-adopt; covers collaborative trip planning + accommodation records + activities + per-person budget splitting on free OpenStreetMap (no paid maps key). SQLite + uploads on `proxmox-lvm-encrypted` (`trek-data-encrypted` 2Gi, `trek-uploads-encrypted` 5Gi). For the trial only: `ENCRYPTION_KEY` is TREK-auto-generated onto the data PVC and the bootstrap admin (`admin@trek.local`) is printed to pod logs — NO Vault/ESO wiring (graduation TODO: move key to `secret/trek` + ESO, add an app-level SQLite backup CronJob since host file-backup can't read the LUKS PVC, wire TREK↔Authentik OIDC). Pinned image, TF-managed (no CI/Keel). Availability-poll companion (Rallly) deferred. Teardown: `tg destroy` in `stacks/trek`. | trek | ## Cloudflare Domains diff --git a/stacks/kyverno/modules/kyverno/security-policies.tf b/stacks/kyverno/modules/kyverno/security-policies.tf index 4eb2b5c4..2bee1757 100644 --- a/stacks/kyverno/modules/kyverno/security-policies.tf +++ b/stacks/kyverno/modules/kyverno/security-policies.tf @@ -4,10 +4,10 @@ # Kyverno validate policies for pod security standards. # Wave 1 (locked 2026-05-18, beads code-8ywc): deny-privileged-containers, # deny-host-namespaces, restrict-sys-admin flipped from Audit → Enforce with -# a shared 32-namespace exclude list. require-trusted-registries STAYS in -# Audit until the allowlist pattern is tightened beyond `*/*` (separate work -# item — current pattern allows everything with a slash, so Enforce would be -# a no-op for supply-chain protection). +# a shared 32-namespace exclude list. require-trusted-registries followed on +# 2026-05-19 — also Enforce now, with an explicit registry allowlist (the +# `*/*` catch-all was removed so unknown registries fail closed at admission). +# To allow a new image source, add it to policy_require_trusted_registries below. # failurePolicy stays Ignore (chart-level) to prevent admission webhook # failures from cascading. @@ -23,10 +23,10 @@ locals { "xray", "infra-maintenance", "metrics-server", "tigera-operator", "frigate", # Additions discovered during wave 1 enforce flip — these contain workloads # that legitimately need privileged / hostNetwork / SYS_ADMIN: - "kured", # kured DaemonSet is privileged (manages node reboots) - "default", # etcd backup + defrag CronJobs use hostNetwork + "kured", # kured DaemonSet is privileged (manages node reboots) + "default", # etcd backup + defrag CronJobs use hostNetwork "changedetection", # uses SYS_ADMIN for chromium sandbox - "woodpecker", # CI pipeline pods (wp-*) run privileged docker builds + "woodpecker", # CI pipeline pods (wp-*) run privileged docker builds ] } @@ -340,6 +340,7 @@ resource "kubectl_manifest" "policy_require_trusted_registries" { # amruthpillai (resume), athomasson2 (ebook2audiobook), # netboxcommunity (netbox), nousresearch (hermes-agent), # opentripplanner (osm-routing), rhasspy (whisper/piper). + # 2026-06-05: mauriceboe (TREK group-trip planner trial). "actualbudget/*", "afadil/*", "amruthpillai/*", "athomasson2/*", "binwiederhier/*", "bitnami/*", "clickhouse/*", "cloudflare/*", "coturn/*", "crowdsecurity/*", @@ -347,7 +348,8 @@ resource "kubectl_manifest" "policy_require_trusted_registries" { "dpage/*", "dperson/*", "edoburu/*", "esanchezm/*", "freikin/*", "freshrss/*", "hackmdio/*", "hashicorp/*", "headscale/*", "jhonderson/*", "kebe/*", "library/*", - "lissy93/*", "louislam/*", "matrixdotorg/*", "mendhak/*", + "lissy93/*", "louislam/*", "matrixdotorg/*", "mauriceboe/*", + "mendhak/*", "mghee/*", "mindflavor/*", "mpepping/*", "netboxcommunity/*", "netsampler/*", "nousresearch/*", "nvidia/*", "onlyoffice/*", "openresty/*", "opentripplanner/*", "owntracks/*", diff --git a/stacks/trek/main.tf b/stacks/trek/main.tf new file mode 100644 index 00000000..727513c1 --- /dev/null +++ b/stacks/trek/main.tf @@ -0,0 +1,248 @@ +# TREK — self-hosted group-trip planner (https://github.com/mauriceboe/TREK). +# +# TRIAL deployment (2026-06-05): solo evaluation behind Authentik forward-auth +# to decide whether an off-the-shelf tool is good enough before building a +# custom app. Upstream image, pinned tag, Terraform-managed (no Keel/CI). +# +# Secrets posture for the trial: TREK auto-generates its ENCRYPTION_KEY onto the +# persistent data PVC and prints a bootstrap admin password to its logs on first +# boot when no ADMIN_* env is set. We rely on that here — no Vault/ESO wiring — +# because the trial data is disposable. IF TREK GRADUATES to a permanent +# deployment: (1) move ENCRYPTION_KEY into Vault (secret/trek) + an ExternalSecret +# so it survives a PVC loss, (2) add an app-level SQLite backup CronJob — the +# host file-backup can't read the LUKS-encrypted PVC, and (3) wire TREK<->Authentik +# OIDC for single sign-on instead of the local admin account. + +variable "tls_secret_name" { + type = string + sensitive = true +} + +variable "image_tag" { + type = string + default = "3.0.22" +} + +resource "kubernetes_namespace" "trek" { + metadata { + name = "trek" + labels = { + "istio-injection" = "disabled" + tier = local.tiers.aux + } + } + lifecycle { + # KYVERNO_LIFECYCLE_V1: goldilocks-vpa-auto-mode ClusterPolicy stamps this label. + ignore_changes = [metadata[0].labels["goldilocks.fairwinds.com/vpa-update-mode"]] + } +} + +# SQLite DB + the auto-generated ENCRYPTION_KEY live here → sensitive data, so +# proxmox-lvm-encrypted per the storage rule. Local block also sidesteps the +# SQLite-over-NFS file-locking hazard. Autoresizer annotations + ignore_changes +# on requests are required to coexist with pvc-autoresizer. +resource "kubernetes_persistent_volume_claim" "data" { + wait_until_bound = false + metadata { + name = "trek-data-encrypted" + namespace = kubernetes_namespace.trek.metadata[0].name + annotations = { + "resize.topolvm.io/threshold" = "10%" + "resize.topolvm.io/increase" = "100%" + "resize.topolvm.io/storage_limit" = "5Gi" + } + } + spec { + access_modes = ["ReadWriteOnce"] + storage_class_name = "proxmox-lvm-encrypted" + resources { + requests = { storage = "2Gi" } + } + } + lifecycle { + ignore_changes = [spec[0].resources[0].requests] + } +} + +# Trip file attachments (booking confirmations, etc.) → also encrypted local +# block. Separate PVC because TREK mounts uploads at a distinct path. +resource "kubernetes_persistent_volume_claim" "uploads" { + wait_until_bound = false + metadata { + name = "trek-uploads-encrypted" + namespace = kubernetes_namespace.trek.metadata[0].name + annotations = { + "resize.topolvm.io/threshold" = "10%" + "resize.topolvm.io/increase" = "100%" + "resize.topolvm.io/storage_limit" = "20Gi" + } + } + spec { + access_modes = ["ReadWriteOnce"] + storage_class_name = "proxmox-lvm-encrypted" + resources { + requests = { storage = "5Gi" } + } + } + lifecycle { + ignore_changes = [spec[0].resources[0].requests] + } +} + +resource "kubernetes_deployment" "trek" { + metadata { + name = "trek" + namespace = kubernetes_namespace.trek.metadata[0].name + labels = { + app = "trek" + tier = local.tiers.aux + } + } + spec { + replicas = 1 + strategy { + type = "Recreate" # RWO encrypted volumes + } + selector { + match_labels = { + app = "trek" + } + } + template { + metadata { + labels = { + app = "trek" + } + } + spec { + container { + name = "trek" + image = "mauriceboe/trek:${var.image_tag}" + image_pull_policy = "IfNotPresent" + port { + container_port = 3000 + } + # No CPU limit (CFS throttling); memory requests=limits. 512Mi is a + # starting estimate for the Node app — right-size via Goldilocks after + # the trial. + resources { + requests = { + cpu = "100m" + memory = "512Mi" + } + limits = { + memory = "512Mi" + } + } + env { + name = "APP_URL" + value = "https://trek.viktorbarzin.me" + } + env { + name = "ALLOWED_ORIGINS" + value = "https://trek.viktorbarzin.me" + } + env { + name = "FORCE_HTTPS" + value = "true" + } + env { + name = "TRUST_PROXY" + value = "1" + } + volume_mount { + name = "data" + mount_path = "/app/data" + } + volume_mount { + name = "uploads" + mount_path = "/app/uploads" + } + # TCP probes — TREK requires login so an HTTP path would 302; a socket + # check is the robust signal. A generous startup probe protects the + # first-boot SQLite migration from a premature liveness kill. + startup_probe { + tcp_socket { + port = 3000 + } + period_seconds = 5 + failure_threshold = 30 + } + readiness_probe { + tcp_socket { + port = 3000 + } + initial_delay_seconds = 5 + period_seconds = 10 + } + liveness_probe { + tcp_socket { + port = 3000 + } + period_seconds = 30 + } + } + volume { + name = "data" + persistent_volume_claim { + claim_name = kubernetes_persistent_volume_claim.data.metadata[0].name + } + } + volume { + name = "uploads" + persistent_volume_claim { + claim_name = kubernetes_persistent_volume_claim.uploads.metadata[0].name + } + } + } + } + } + lifecycle { + ignore_changes = [ + spec[0].template[0].spec[0].dns_config, # KYVERNO_LIFECYCLE_V1 + ] + } +} + +resource "kubernetes_service" "trek" { + metadata { + name = "trek" + namespace = kubernetes_namespace.trek.metadata[0].name + labels = { + app = "trek" + } + } + spec { + selector = { + app = "trek" + } + port { + port = 80 + target_port = 3000 + } + } +} + +module "tls_secret" { + source = "../../modules/kubernetes/setup_tls_secret" + namespace = kubernetes_namespace.trek.metadata[0].name + tls_secret_name = var.tls_secret_name +} + +# Authentik forward-auth gates the app — for the solo trial it keeps the +# internet out; TREK's own login sits behind it. Proxied via Cloudflare. +module "ingress" { + source = "../../modules/kubernetes/ingress_factory" + auth = "required" + dns_type = "proxied" + namespace = kubernetes_namespace.trek.metadata[0].name + name = "trek" + service_name = kubernetes_service.trek.metadata[0].name + port = 80 + tls_secret_name = var.tls_secret_name + homepage_group = "Productivity" + extra_annotations = { + "gethomepage.dev/description" = "Group trip planner (trial)" + "gethomepage.dev/icon" = "mdi-bag-suitcase" + } +} diff --git a/stacks/trek/secrets b/stacks/trek/secrets new file mode 120000 index 00000000..ca54a7cf --- /dev/null +++ b/stacks/trek/secrets @@ -0,0 +1 @@ +../../secrets \ No newline at end of file diff --git a/stacks/trek/terragrunt.hcl b/stacks/trek/terragrunt.hcl new file mode 100644 index 00000000..f4c920ab --- /dev/null +++ b/stacks/trek/terragrunt.hcl @@ -0,0 +1,13 @@ +include "root" { + path = find_in_parent_folders() +} + +dependency "platform" { + config_path = "../platform" + skip_outputs = true +} + +dependency "vault" { + config_path = "../vault" + skip_outputs = true +}