From 32e1042ca8528d5a73fa472f950ae5f6c118ad9e Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Mon, 1 Jun 2026 19:45:03 +0000 Subject: [PATCH] t3code: expose `t3 serve` (DevVM) publicly at t3.viktorbarzin.me (app-tier) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New stacks/t3code mirrors stacks/terminal: K8s Service + Endpoints → 10.0.10.10:3773 plus an ingress_factory route (dns_type=proxied, auth="app"). t3 ships its own owner-pairing + bearer-session auth, so Authentik forward-auth is intentionally omitted — it would break the cross-origin native mobile app and app.t3.codes (bearer-only, no Authentik cookie). CrowdSec + anti-AI (both default-on for app-tier) rate-limit the public surface; t3's pairing is the gate. TLS is auto-synced into the namespace by Kyverno's sync-tls-secret policy. Verified end-to-end: t3.viktorbarzin.me → CF → Traefik → devvm:3773 = 200. Trade-off (public RCE surface behind app-native auth, no Authentik SSO) accepted 2026-06-01 to keep the native app + app.t3.codes working. Co-Authored-By: Claude Opus 4.7 --- .claude/reference/service-catalog.md | 3 +- stacks/t3code/main.tf | 88 ++++++++++++++++++++++++++++ stacks/t3code/terragrunt.hcl | 3 + 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 stacks/t3code/main.tf create mode 100644 stacks/t3code/terragrunt.hcl diff --git a/.claude/reference/service-catalog.md b/.claude/reference/service-catalog.md index 7ef62e1c..507be91d 100644 --- a/.claude/reference/service-catalog.md +++ b/.claude/reference/service-catalog.md @@ -32,6 +32,7 @@ |---------|-------------|-------| | k8s-dashboard | Kubernetes dashboard | k8s-dashboard | | reverse-proxy | Generic reverse proxy | reverse-proxy | +| t3code | Coding-agent GUI (`t3 serve`) on DevVM 10.0.10.10:3773, exposed at t3.viktorbarzin.me via Service+Endpoints (no pod). `auth=app` — t3's own owner-pairing/bearer auth + CrowdSec gate it (no Authentik, to keep the native app & app.t3.codes cross-origin clients working). RCE surface; re-pair via `t3 auth pairing create` on DevVM. | t3code | ## Active Use | Service | Description | Stack | @@ -123,7 +124,7 @@ blog, hackmd, privatebin, url, echo, f1tv, excalidraw, send, audiobookshelf, jsoncrack, ntfy, cyberchef, homepage, linkwarden, changedetection, tandoor, n8n, stirling-pdf, dashy, city-guesser, -travel, netbox, phpipam, tripit +travel, netbox, phpipam, tripit, t3 ``` ### Non-Proxied (Direct DNS) diff --git a/stacks/t3code/main.tf b/stacks/t3code/main.tf new file mode 100644 index 00000000..6db18f72 --- /dev/null +++ b/stacks/t3code/main.tf @@ -0,0 +1,88 @@ +variable "tls_secret_name" { + type = string + sensitive = true +} + +resource "kubernetes_namespace" "t3code" { + metadata { + name = "t3code" + labels = { + "istio-injection" : "disabled" + tier = local.tiers.aux + "keel.sh/enrolled" = "true" + } + } + lifecycle { + # KYVERNO_LIFECYCLE_V1: goldilocks-vpa-auto-mode ClusterPolicy stamps this label on every namespace + ignore_changes = [metadata[0].labels["goldilocks.fairwinds.com/vpa-update-mode"]] + } +} + +# TLS secret `tls-secret` (wildcard *.viktorbarzin.me) is auto-cloned into this +# namespace by Kyverno's `sync-tls-secret` ClusterPolicy — no local module or +# cert material needed; the renewal pipeline updates the source and Kyverno +# propagates within seconds. + +# Service + Endpoints reverse-proxy to t3code (`t3 serve`) on the DevVM at +# 10.0.10.10:3773. The t3 server is a systemd unit (t3-serve.service) bound to +# 0.0.0.0:3773 on the DevVM LAN; this stack only owns the Kubernetes side so +# Traefik can route t3.viktorbarzin.me to it. App code lives on the DevVM, not +# in this monorepo (installed globally via `npm i -g t3`). +resource "kubernetes_service" "t3code" { + metadata { + name = "t3" + namespace = kubernetes_namespace.t3code.metadata[0].name + labels = { + app = "t3" + } + } + + spec { + port { + name = "http" + port = 80 + target_port = 3773 + } + } +} + +resource "kubernetes_endpoints" "t3code" { + metadata { + name = "t3" + namespace = kubernetes_namespace.t3code.metadata[0].name + } + + subset { + address { + ip = "10.0.10.10" + } + port { + name = "http" + port = 3773 + } + } +} + +module "ingress" { + source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" + namespace = kubernetes_namespace.t3code.metadata[0].name + name = "t3" + tls_secret_name = var.tls_secret_name + # auth = "app": t3code (`t3 serve`) ships its own user auth — one-time owner + # pairing tokens exchanged for 30-day bearer sessions, with the WebSocket + # guarded by a short-lived wsToken. Authentik forward-auth is deliberately NOT + # used here: it would block the cross-origin native mobile app and the hosted + # app.t3.codes client (both bearer-only, no Authentik cookie). CrowdSec (on by + # default) + anti-AI scraping rate-limit the public surface; t3's pairing is + # the gate. Trade-off accepted by Viktor 2026-06-01 to keep the native app. + auth = "app" + extra_annotations = { + "gethomepage.dev/enabled" = "true" + "gethomepage.dev/name" = "T3 Code" + "gethomepage.dev/description" = "Coding-agent GUI (t3 serve on DevVM)" + "gethomepage.dev/icon" = "mdi-robot" + "gethomepage.dev/group" = "Infrastructure" + "gethomepage.dev/pod-selector" = "" + } +} diff --git a/stacks/t3code/terragrunt.hcl b/stacks/t3code/terragrunt.hcl new file mode 100644 index 00000000..e147285f --- /dev/null +++ b/stacks/t3code/terragrunt.hcl @@ -0,0 +1,3 @@ +include "root" { + path = find_in_parent_folders() +}