x402: consolidate to a single shared forwardAuth gateway

The per-site `x402_instance` module created one Deployment + Service +
PDB per protected host (9 in total, 9×64Mi). Every pod was running the
exact same logic with the same config — the only thing that varied
was the upstream URL, which we don't even need since the gateway can
return 200 to "allow" and Traefik handles the upstream itself.

Refactor to the same pattern as `ai-bot-block`:
 * single deployment + service in `traefik` namespace, 2 replicas, HA
 * Traefik `Middleware` CRD `x402` (forwardAuth → x402-gateway:8080/auth)
 * each consumer ingress just appends `traefik-x402@kubernetescrd` to
   its middleware chain via `extra_middlewares`

x402-gateway gains a `MODE=forwardauth` env var that returns 200 (allow)
or 402 (with x402 PaymentRequiredResponse body) instead of reverse-
proxying. Image: ghcr ... f4804d62.

Pod count: 9 → 2 (78% memory saved). All 9 sites verified still
serving the Anubis challenge to plain curl with identical TTFB.
DRY_RUN until `var.x402_wallet_address` is set on the traefik stack.

Removes `modules/kubernetes/x402_instance/` (dead code now).
This commit is contained in:
Viktor Barzin 2026-05-10 10:54:38 +00:00
parent ce4a75d79a
commit 753e9bb971
12 changed files with 269 additions and 419 deletions

View file

@ -122,20 +122,13 @@ module "anubis" {
target_url = "http://${kubernetes_service.blog.metadata[0].name}.${kubernetes_namespace.website.metadata[0].name}.svc.cluster.local"
}
# x402 payment gateway in front of Anubis. DRY_RUN until wallet_address is set.
module "x402" {
source = "../../modules/kubernetes/x402_instance"
name = "blog"
namespace = kubernetes_namespace.website.metadata[0].name
target_url = "http://${module.anubis.service_name}.${kubernetes_namespace.website.metadata[0].name}.svc.cluster.local:${module.anubis.service_port}"
}
module "ingress" {
source = "../../modules/kubernetes/ingress_factory"
namespace = kubernetes_namespace.website.metadata[0].name
name = "blog"
service_name = module.x402.service_name
port = module.x402.service_port
source = "../../modules/kubernetes/ingress_factory"
namespace = kubernetes_namespace.website.metadata[0].name
name = "blog"
service_name = module.anubis.service_name
port = module.anubis.service_port
extra_middlewares = ["traefik-x402@kubernetescrd"]
full_host = "viktorbarzin.me"
dns_type = "proxied"
tls_secret_name = var.tls_secret_name
@ -151,11 +144,12 @@ module "ingress" {
}
module "ingress-www" {
source = "../../modules/kubernetes/ingress_factory"
namespace = kubernetes_namespace.website.metadata[0].name
name = "blog-www"
service_name = module.x402.service_name
port = module.x402.service_port
source = "../../modules/kubernetes/ingress_factory"
namespace = kubernetes_namespace.website.metadata[0].name
name = "blog-www"
service_name = module.anubis.service_name
port = module.anubis.service_port
extra_middlewares = ["traefik-x402@kubernetescrd"]
full_host = "www.viktorbarzin.me"
tls_secret_name = var.tls_secret_name
anti_ai_scraping = false

View file

@ -111,20 +111,14 @@ module "anubis" {
target_url = "http://${kubernetes_service.cyberchef.metadata[0].name}.${kubernetes_namespace.cyberchef.metadata[0].name}.svc.cluster.local"
}
module "x402" {
source = "../../modules/kubernetes/x402_instance"
name = "cc"
namespace = kubernetes_namespace.cyberchef.metadata[0].name
target_url = "http://${module.anubis.service_name}.${kubernetes_namespace.cyberchef.metadata[0].name}.svc.cluster.local:${module.anubis.service_port}"
}
module "ingress" {
source = "../../modules/kubernetes/ingress_factory"
dns_type = "proxied"
namespace = kubernetes_namespace.cyberchef.metadata[0].name
name = "cc"
service_name = module.x402.service_name
port = module.x402.service_port
source = "../../modules/kubernetes/ingress_factory"
dns_type = "proxied"
namespace = kubernetes_namespace.cyberchef.metadata[0].name
name = "cc"
service_name = module.anubis.service_name
port = module.anubis.service_port
extra_middlewares = ["traefik-x402@kubernetescrd"]
tls_secret_name = var.tls_secret_name
anti_ai_scraping = false
extra_annotations = {

View file

@ -268,23 +268,17 @@ module "anubis" {
EOT
}
module "x402" {
source = "../../modules/kubernetes/x402_instance"
name = "f1"
namespace = kubernetes_namespace.f1-stream.metadata[0].name
target_url = "http://${module.anubis.service_name}.${kubernetes_namespace.f1-stream.metadata[0].name}.svc.cluster.local:${module.anubis.service_port}"
}
module "ingress" {
source = "../../modules/kubernetes/ingress_factory"
dns_type = "non-proxied"
namespace = kubernetes_namespace.f1-stream.metadata[0].name
name = "f1"
service_name = module.x402.service_name
port = module.x402.service_port
tls_secret_name = var.tls_secret_name
exclude_crowdsec = true
anti_ai_scraping = false
source = "../../modules/kubernetes/ingress_factory"
dns_type = "non-proxied"
namespace = kubernetes_namespace.f1-stream.metadata[0].name
name = "f1"
service_name = module.anubis.service_name
port = module.anubis.service_port
tls_secret_name = var.tls_secret_name
exclude_crowdsec = true
anti_ai_scraping = false
extra_middlewares = ["traefik-x402@kubernetescrd"]
extra_annotations = {
"gethomepage.dev/enabled" = "true"
"gethomepage.dev/name" = "F1 Stream"

View file

@ -144,21 +144,15 @@ module "anubis" {
target_url = "http://${kubernetes_service.cache_proxy.metadata[0].name}.${kubernetes_namespace.homepage.metadata[0].name}.svc.cluster.local"
}
module "x402" {
source = "../../modules/kubernetes/x402_instance"
name = "homepage"
namespace = kubernetes_namespace.homepage.metadata[0].name
target_url = "http://${module.anubis.service_name}.${kubernetes_namespace.homepage.metadata[0].name}.svc.cluster.local:${module.anubis.service_port}"
}
module "ingress" {
source = "../../modules/kubernetes/ingress_factory"
namespace = kubernetes_namespace.homepage.metadata[0].name
name = "homepage"
host = "home"
dns_type = "proxied"
service_name = module.x402.service_name
port = module.x402.service_port
source = "../../modules/kubernetes/ingress_factory"
namespace = kubernetes_namespace.homepage.metadata[0].name
name = "homepage"
host = "home"
dns_type = "proxied"
service_name = module.anubis.service_name
port = module.anubis.service_port
extra_middlewares = ["traefik-x402@kubernetescrd"]
tls_secret_name = var.tls_secret_name
anti_ai_scraping = false
extra_annotations = {

View file

@ -91,20 +91,14 @@ module "anubis" {
target_url = "http://${kubernetes_service.jsoncrack.metadata[0].name}.${kubernetes_namespace.jsoncrack.metadata[0].name}.svc.cluster.local"
}
module "x402" {
source = "../../modules/kubernetes/x402_instance"
name = "json"
namespace = kubernetes_namespace.jsoncrack.metadata[0].name
target_url = "http://${module.anubis.service_name}.${kubernetes_namespace.jsoncrack.metadata[0].name}.svc.cluster.local:${module.anubis.service_port}"
}
module "ingress" {
source = "../../modules/kubernetes/ingress_factory"
dns_type = "proxied"
namespace = kubernetes_namespace.jsoncrack.metadata[0].name
name = "json"
service_name = module.x402.service_name
port = module.x402.service_port
source = "../../modules/kubernetes/ingress_factory"
dns_type = "proxied"
namespace = kubernetes_namespace.jsoncrack.metadata[0].name
name = "json"
service_name = module.anubis.service_name
port = module.anubis.service_port
extra_middlewares = ["traefik-x402@kubernetescrd"]
tls_secret_name = var.tls_secret_name
anti_ai_scraping = false
extra_annotations = {

View file

@ -110,20 +110,14 @@ module "anubis" {
target_url = "http://${kubernetes_service.kms-web-page.metadata[0].name}.${kubernetes_namespace.kms.metadata[0].name}.svc.cluster.local"
}
module "x402" {
source = "../../modules/kubernetes/x402_instance"
name = "kms"
namespace = kubernetes_namespace.kms.metadata[0].name
target_url = "http://${module.anubis.service_name}.${kubernetes_namespace.kms.metadata[0].name}.svc.cluster.local:${module.anubis.service_port}"
}
module "ingress" {
source = "../../modules/kubernetes/ingress_factory"
dns_type = "non-proxied"
namespace = kubernetes_namespace.kms.metadata[0].name
name = "kms"
service_name = module.x402.service_name
port = module.x402.service_port
source = "../../modules/kubernetes/ingress_factory"
dns_type = "non-proxied"
namespace = kubernetes_namespace.kms.metadata[0].name
name = "kms"
service_name = module.anubis.service_name
port = module.anubis.service_port
extra_middlewares = ["traefik-x402@kubernetescrd"]
tls_secret_name = var.tls_secret_name
anti_ai_scraping = false
extra_annotations = {

View file

@ -138,21 +138,15 @@ module "anubis" {
target_url = "http://${kubernetes_service.privatebin.metadata[0].name}.${kubernetes_namespace.privatebin.metadata[0].name}.svc.cluster.local"
}
module "x402" {
source = "../../modules/kubernetes/x402_instance"
name = "privatebin"
namespace = kubernetes_namespace.privatebin.metadata[0].name
target_url = "http://${module.anubis.service_name}.${kubernetes_namespace.privatebin.metadata[0].name}.svc.cluster.local:${module.anubis.service_port}"
}
module "ingress" {
source = "../../modules/kubernetes/ingress_factory"
namespace = kubernetes_namespace.privatebin.metadata[0].name
name = "privatebin"
host = "pb"
dns_type = "proxied"
service_name = module.x402.service_name
port = module.x402.service_port
service_name = module.anubis.service_name
port = module.anubis.service_port
extra_middlewares = ["traefik-x402@kubernetescrd"]
anti_ai_scraping = false
tls_secret_name = var.tls_secret_name
custom_content_security_policy = "script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'"

View file

@ -339,20 +339,14 @@ module "anubis" {
target_url = "http://realestate-crawler-ui.${kubernetes_namespace.realestate-crawler.metadata[0].name}.svc.cluster.local"
}
module "x402" {
source = "../../modules/kubernetes/x402_instance"
name = "wrongmove"
namespace = kubernetes_namespace.realestate-crawler.metadata[0].name
target_url = "http://${module.anubis.service_name}.${kubernetes_namespace.realestate-crawler.metadata[0].name}.svc.cluster.local:${module.anubis.service_port}"
}
module "ingress" {
source = "../../modules/kubernetes/ingress_factory"
dns_type = "proxied"
namespace = kubernetes_namespace.realestate-crawler.metadata[0].name
name = "wrongmove"
service_name = module.x402.service_name
port = module.x402.service_port
source = "../../modules/kubernetes/ingress_factory"
dns_type = "proxied"
namespace = kubernetes_namespace.realestate-crawler.metadata[0].name
name = "wrongmove"
service_name = module.anubis.service_name
port = module.anubis.service_port
extra_middlewares = ["traefik-x402@kubernetescrd"]
anti_ai_scraping = false
tls_secret_name = var.tls_secret_name
extra_annotations = {

View file

@ -10,6 +10,11 @@ variable "auth_fallback_htpasswd" {
description = "htpasswd-format string for emergency basicAuth fallback when Authentik is down"
sensitive = true
}
variable "x402_wallet_address" {
type = string
default = ""
description = "EVM wallet (Base mainnet, 0x…) that receives USDC from x402 payments. Empty = DRY_RUN, gateway always returns 200 to forwardAuth so traffic is unaffected."
}
resource "kubernetes_namespace" "traefik" {
metadata {
@ -459,6 +464,177 @@ resource "kubernetes_service" "bot_block_proxy" {
}
}
# x402 payment gateway shared forwardAuth target for every ingress that
# wants to issue HTTP 402 to declared AI-bot UAs / accept X-PAYMENT for paid
# access. One deployment serves all hosts; each consumer ingress just adds
# `traefik-x402@kubernetescrd` to its middleware chain.
#
# DRY_RUN until `var.x402_wallet_address` is set. While dry-run, every
# auth call returns 200 (allow) so traffic is unaffected.
resource "kubernetes_deployment" "x402_gateway" {
metadata {
name = "x402-gateway"
namespace = kubernetes_namespace.traefik.metadata[0].name
labels = { app = "x402-gateway" }
}
spec {
replicas = 2 # Stateless; HA across two pods is cheap.
selector {
match_labels = { app = "x402-gateway" }
}
strategy {
type = "RollingUpdate"
rolling_update {
max_surge = 1
max_unavailable = 0
}
}
template {
metadata {
labels = { app = "x402-gateway" }
}
spec {
image_pull_secrets {
name = "registry-credentials"
}
topology_spread_constraint {
max_skew = 1
topology_key = "kubernetes.io/hostname"
when_unsatisfiable = "ScheduleAnyway"
label_selector {
match_labels = { app = "x402-gateway" }
}
}
container {
name = "x402-gateway"
image = "forgejo.viktorbarzin.me/viktor/x402-gateway:f4804d62"
port {
name = "http"
container_port = 8923
}
port {
name = "metrics"
container_port = 9090
}
env {
name = "MODE"
value = "forwardauth"
}
env {
name = "BIND"
value = ":8923"
}
env {
name = "METRICS_BIND"
value = ":9090"
}
env {
name = "WALLET_ADDRESS"
value = var.x402_wallet_address
}
env {
name = "PRICE_LABEL"
value = "$0.01"
}
env {
name = "PRICE_USDC_MICROS"
value = "10000"
}
env {
name = "NETWORK"
value = "base"
}
env {
name = "FACILITATOR_URL"
value = "https://x402.org/facilitator"
}
resources {
requests = {
cpu = "10m"
memory = "64Mi"
}
limits = {
memory = "128Mi"
}
}
liveness_probe {
http_get {
path = "/healthz"
port = "metrics"
}
initial_delay_seconds = 5
period_seconds = 30
}
readiness_probe {
http_get {
path = "/healthz"
port = "metrics"
}
initial_delay_seconds = 1
period_seconds = 5
}
security_context {
run_as_non_root = true
run_as_user = 65532
run_as_group = 65532
allow_privilege_escalation = false
read_only_root_filesystem = true
capabilities {
drop = ["ALL"]
}
}
}
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "x402_gateway" {
metadata {
name = "x402-gateway"
namespace = kubernetes_namespace.traefik.metadata[0].name
labels = { app = "x402-gateway" }
annotations = {
"prometheus.io/scrape" = "true"
"prometheus.io/path" = "/metrics"
"prometheus.io/port" = "9090"
}
}
spec {
selector = { app = "x402-gateway" }
port {
name = "http"
port = 8080
target_port = 8923
}
port {
name = "metrics"
port = 9090
target_port = 9090
}
}
}
resource "kubernetes_pod_disruption_budget_v1" "x402_gateway" {
metadata {
name = "x402-gateway"
namespace = kubernetes_namespace.traefik.metadata[0].name
}
spec {
min_available = "1"
selector {
match_labels = { app = "x402-gateway" }
}
}
}
# Resilience proxy for Authentik ForwardAuth
# Falls back to basicAuth when Authentik is unreachable
resource "kubernetes_secret" "auth_proxy_htpasswd" {

View file

@ -322,6 +322,31 @@ resource "kubernetes_manifest" "middleware_ai_bot_block" {
depends_on = [helm_release.traefik]
}
# x402 payment-required middleware. Traefik calls the shared x402-gateway
# in this namespace; the gateway returns 200 (allow) to browsers and curl,
# 402 with x402 PaymentRequiredResponse to declared AI-bot UAs (or to any
# request whose X-PAYMENT header fails facilitator validation).
# DRY_RUN until WALLET_ADDRESS is set on the gateway, in which case the
# gateway always returns 200.
resource "kubernetes_manifest" "middleware_x402" {
manifest = {
apiVersion = "traefik.io/v1alpha1"
kind = "Middleware"
metadata = {
name = "x402"
namespace = kubernetes_namespace.traefik.metadata[0].name
}
spec = {
forwardAuth = {
address = "http://x402-gateway.traefik.svc.cluster.local:8080/auth"
trustForwardHeader = true
}
}
}
depends_on = [helm_release.traefik, kubernetes_service.x402_gateway]
}
# X-Robots-Tag header to discourage compliant AI crawlers
resource "kubernetes_manifest" "middleware_anti_ai_headers" {
manifest = {

View file

@ -109,20 +109,14 @@ module "anubis" {
target_url = "http://${kubernetes_service.travel-blog.metadata[0].name}.${kubernetes_namespace.travel-blog.metadata[0].name}.svc.cluster.local"
}
module "x402" {
source = "../../modules/kubernetes/x402_instance"
name = "travel"
namespace = kubernetes_namespace.travel-blog.metadata[0].name
target_url = "http://${module.anubis.service_name}.${kubernetes_namespace.travel-blog.metadata[0].name}.svc.cluster.local:${module.anubis.service_port}"
}
module "ingress" {
source = "../../modules/kubernetes/ingress_factory"
namespace = kubernetes_namespace.travel-blog.metadata[0].name
name = "travel"
tls_secret_name = var.tls_secret_name
service_name = module.x402.service_name
port = module.x402.service_port
source = "../../modules/kubernetes/ingress_factory"
namespace = kubernetes_namespace.travel-blog.metadata[0].name
name = "travel"
tls_secret_name = var.tls_secret_name
service_name = module.anubis.service_name
port = module.anubis.service_port
extra_middlewares = ["traefik-x402@kubernetescrd"]
anti_ai_scraping = false
extra_annotations = {
"gethomepage.dev/enabled" = "true"