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:
parent
ce4a75d79a
commit
753e9bb971
12 changed files with 269 additions and 419 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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'"
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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" {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue