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
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue