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).
864 lines
23 KiB
HCL
864 lines
23 KiB
HCL
variable "tier" { type = string }
|
|
variable "crowdsec_api_key" {
|
|
type = string
|
|
sensitive = true
|
|
}
|
|
variable "redis_host" { type = string }
|
|
variable "tls_secret_name" {}
|
|
variable "auth_fallback_htpasswd" {
|
|
type = string
|
|
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 {
|
|
name = "traefik"
|
|
labels = {
|
|
"app.kubernetes.io/name" = "traefik"
|
|
"app.kubernetes.io/instance" = "traefik"
|
|
tier = var.tier
|
|
}
|
|
}
|
|
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"]]
|
|
}
|
|
}
|
|
|
|
resource "helm_release" "traefik" {
|
|
namespace = kubernetes_namespace.traefik.metadata[0].name
|
|
create_namespace = false
|
|
name = "traefik"
|
|
repository = "https://traefik.github.io/charts"
|
|
chart = "traefik"
|
|
atomic = true
|
|
timeout = 600
|
|
|
|
values = [yamlencode({
|
|
deployment = {
|
|
replicas = 3
|
|
terminationGracePeriodSeconds = 60
|
|
lifecycle = {
|
|
preStop = {
|
|
exec = {
|
|
command = ["/bin/sh", "-c", "sleep 15"]
|
|
}
|
|
}
|
|
}
|
|
podAnnotations = {
|
|
"diun.enable" = "true"
|
|
"diun.include_tags" = "^v\\d+(?:\\.\\d+)?(?:\\.\\d+)?.*$"
|
|
}
|
|
initContainers = [{
|
|
name = "download-plugins"
|
|
image = "alpine:3"
|
|
command = ["sh", "-c", join("", [
|
|
"set -e; ",
|
|
"STORAGE=/plugins-storage; ",
|
|
"mkdir -p \"$STORAGE/archives/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin\"; ",
|
|
"wget -q -T 30 -O \"$STORAGE/archives/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/v1.4.2.zip\" ",
|
|
"\"https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/archive/refs/tags/v1.4.2.zip\"; ",
|
|
"printf '{\"github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin\":\"v1.4.2\"}' ",
|
|
"> \"$STORAGE/archives/state.json\"; ",
|
|
"echo \"Plugins pre-downloaded successfully\"",
|
|
])]
|
|
volumeMounts = [{
|
|
name = "plugins"
|
|
mountPath = "/plugins-storage"
|
|
}]
|
|
}]
|
|
}
|
|
|
|
updateStrategy = {
|
|
type = "RollingUpdate"
|
|
rollingUpdate = {
|
|
maxUnavailable = 0
|
|
maxSurge = 1
|
|
}
|
|
}
|
|
|
|
ingressClass = {
|
|
enabled = true
|
|
isDefaultClass = true
|
|
}
|
|
|
|
providers = {
|
|
kubernetesIngress = {
|
|
enabled = true
|
|
allowExternalNameServices = true
|
|
publishedService = { enabled = true }
|
|
}
|
|
kubernetesCRD = {
|
|
enabled = true
|
|
allowExternalNameServices = true
|
|
allowCrossNamespace = true
|
|
}
|
|
}
|
|
|
|
# Enable dashboard API (accessible on port 8080 internally)
|
|
api = {
|
|
insecure = false
|
|
}
|
|
|
|
# Entrypoints
|
|
ports = {
|
|
web = {
|
|
port = 8000
|
|
exposedPort = 80
|
|
protocol = "TCP"
|
|
http = {
|
|
redirections = {
|
|
entryPoint = {
|
|
to = "websecure"
|
|
scheme = "https"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
websecure = {
|
|
port = 8443
|
|
exposedPort = 443
|
|
protocol = "TCP"
|
|
http = {
|
|
tls = {
|
|
enabled = true
|
|
}
|
|
middlewares = [
|
|
"traefik-compress@kubernetescrd",
|
|
]
|
|
}
|
|
http3 = {
|
|
enabled = true
|
|
advertisedPort = 443
|
|
}
|
|
}
|
|
whisper-tcp = {
|
|
port = 10300
|
|
exposedPort = 10300
|
|
protocol = "TCP"
|
|
expose = { default = true }
|
|
}
|
|
piper-tcp = {
|
|
port = 10200
|
|
exposedPort = 10200
|
|
protocol = "TCP"
|
|
expose = { default = true }
|
|
}
|
|
}
|
|
|
|
service = {
|
|
type = "LoadBalancer"
|
|
annotations = {
|
|
"metallb.io/loadBalancerIPs" = "10.0.20.200"
|
|
"metallb.io/allow-shared-ip" = "shared"
|
|
}
|
|
spec = {
|
|
externalTrafficPolicy = "Cluster"
|
|
}
|
|
}
|
|
|
|
# Plugins
|
|
experimental = {
|
|
plugins = {
|
|
crowdsec-bouncer = {
|
|
moduleName = "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
|
version = "v1.4.2"
|
|
}
|
|
}
|
|
}
|
|
|
|
# Prometheus metrics
|
|
metrics = {
|
|
prometheus = {
|
|
entryPoint = "metrics"
|
|
addEntryPointsLabels = true
|
|
addServicesLabels = true
|
|
addRoutersLabels = true
|
|
buckets = "0.01,0.05,0.1,0.2,0.5,1.0,2.0,5.0,10.0,30.0"
|
|
}
|
|
}
|
|
|
|
# Access logs
|
|
logs = {
|
|
access = {
|
|
enabled = true
|
|
}
|
|
}
|
|
|
|
additionalArguments = [
|
|
"--global.checknewversion=false",
|
|
"--global.sendanonymoususage=false",
|
|
# Skip TLS verification for self-signed backend certs (proxmox, idrac, etc.)
|
|
"--serversTransport.insecureSkipVerify=true",
|
|
# Increase timeouts for services like Immich
|
|
"--serversTransport.forwardingTimeouts.dialTimeout=60s",
|
|
"--serversTransport.forwardingTimeouts.responseHeaderTimeout=30s",
|
|
"--serversTransport.forwardingTimeouts.idleConnTimeout=90s",
|
|
# Increase backend connection pool (default maxIdleConnsPerHost=2 is too low)
|
|
"--serversTransport.maxIdleConnsPerHost=100",
|
|
# Explicit entrypoint timeouts to bound tail latency from slow clients
|
|
"--entryPoints.websecure.transport.respondingTimeouts.readTimeout=60s",
|
|
"--entryPoints.websecure.transport.respondingTimeouts.writeTimeout=60s",
|
|
"--entryPoints.websecure.transport.respondingTimeouts.idleTimeout=600s",
|
|
# Use forwarded headers from trusted proxies
|
|
"--entryPoints.websecure.forwardedHeaders.insecure=false",
|
|
"--entryPoints.web.forwardedHeaders.insecure=false",
|
|
"--entryPoints.websecure.forwardedHeaders.trustedIPs=173.245.48.0/20,103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,141.101.64.0/18,108.162.192.0/18,190.93.240.0/20,188.114.96.0/20,197.234.240.0/22,198.41.128.0/17,162.158.0.0/15,104.16.0.0/13,104.24.0.0/14,172.64.0.0/13,131.0.72.0/22,10.0.0.0/8,192.168.0.0/16",
|
|
"--entryPoints.web.forwardedHeaders.trustedIPs=173.245.48.0/20,103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,141.101.64.0/18,108.162.192.0/18,190.93.240.0/20,188.114.96.0/20,197.234.240.0/22,198.41.128.0/17,162.158.0.0/15,104.16.0.0/13,104.24.0.0/14,172.64.0.0/13,131.0.72.0/22,10.0.0.0/8,192.168.0.0/16",
|
|
]
|
|
|
|
resources = {
|
|
requests = {
|
|
cpu = "100m"
|
|
memory = "768Mi"
|
|
}
|
|
limits = {
|
|
memory = "768Mi"
|
|
}
|
|
}
|
|
|
|
nodeSelector = {
|
|
"kubernetes.io/os" = "linux"
|
|
}
|
|
|
|
tolerations = []
|
|
|
|
topologySpreadConstraints = [{
|
|
maxSkew = 1
|
|
topologyKey = "kubernetes.io/hostname"
|
|
whenUnsatisfiable = "DoNotSchedule"
|
|
labelSelector = {
|
|
matchLabels = {
|
|
"app.kubernetes.io/name" = "traefik"
|
|
}
|
|
}
|
|
}]
|
|
|
|
podDisruptionBudget = {
|
|
enabled = true
|
|
minAvailable = 2
|
|
}
|
|
})]
|
|
}
|
|
|
|
# Dashboard resources
|
|
module "tls_secret" {
|
|
source = "../../../../modules/kubernetes/setup_tls_secret"
|
|
namespace = kubernetes_namespace.traefik.metadata[0].name
|
|
tls_secret_name = var.tls_secret_name
|
|
}
|
|
|
|
resource "kubernetes_service" "traefik_dashboard" {
|
|
metadata {
|
|
name = "traefik-dashboard"
|
|
namespace = kubernetes_namespace.traefik.metadata[0].name
|
|
labels = {
|
|
"app" = "traefik-dashboard"
|
|
}
|
|
}
|
|
|
|
spec {
|
|
selector = {
|
|
"app.kubernetes.io/name" = "traefik"
|
|
}
|
|
port {
|
|
name = "http"
|
|
port = 8080
|
|
target_port = 8080
|
|
protocol = "TCP"
|
|
}
|
|
}
|
|
}
|
|
|
|
module "ingress" {
|
|
source = "../../../../modules/kubernetes/ingress_factory"
|
|
dns_type = "non-proxied"
|
|
namespace = kubernetes_namespace.traefik.metadata[0].name
|
|
name = "traefik"
|
|
service_name = "traefik-dashboard"
|
|
host = "traefik"
|
|
port = 8080
|
|
tls_secret_name = var.tls_secret_name
|
|
protected = true
|
|
extra_annotations = {
|
|
"gethomepage.dev/enabled" = "true"
|
|
"gethomepage.dev/name" = "Traefik"
|
|
"gethomepage.dev/description" = "Reverse proxy & ingress"
|
|
"gethomepage.dev/icon" = "traefik.png"
|
|
"gethomepage.dev/group" = "Core Platform"
|
|
"gethomepage.dev/pod-selector" = ""
|
|
}
|
|
}
|
|
|
|
# Bot-block resilience proxy: nginx reverse proxy in front of Poison Fountain
|
|
# Returns 200 (allow all traffic) if Poison Fountain is unreachable (fail-open)
|
|
resource "kubernetes_config_map" "bot_block_proxy_config" {
|
|
metadata {
|
|
name = "bot-block-proxy-config"
|
|
namespace = kubernetes_namespace.traefik.metadata[0].name
|
|
}
|
|
|
|
data = {
|
|
"default.conf" = <<-EOT
|
|
upstream poison_fountain {
|
|
server poison-fountain.poison-fountain.svc.cluster.local:8080;
|
|
}
|
|
server {
|
|
listen 8080;
|
|
location /auth {
|
|
access_by_lua_block {
|
|
ngx.req.clear_header("If-Match")
|
|
ngx.req.clear_header("If-None-Match")
|
|
ngx.req.clear_header("If-Modified-Since")
|
|
ngx.req.clear_header("If-Unmodified-Since")
|
|
}
|
|
proxy_pass http://poison_fountain;
|
|
# Tight timeouts: poison-fountain may be scaled to 0 (graveyard
|
|
# endpoints) — failing open in <200ms keeps the 68-ingress chain
|
|
# responsive instead of paying 3s per request. Healthy upstream
|
|
# responds in <50ms anyway.
|
|
proxy_connect_timeout 100ms;
|
|
proxy_read_timeout 200ms;
|
|
proxy_send_timeout 200ms;
|
|
proxy_intercept_errors on;
|
|
error_page 502 503 504 =200 /fallback-allow;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
location = /fallback-allow {
|
|
internal;
|
|
return 200 "allowed";
|
|
}
|
|
location /healthz {
|
|
access_log off;
|
|
return 200 "ok";
|
|
}
|
|
}
|
|
EOT
|
|
}
|
|
}
|
|
|
|
resource "kubernetes_deployment" "bot_block_proxy" {
|
|
metadata {
|
|
name = "bot-block-proxy"
|
|
namespace = kubernetes_namespace.traefik.metadata[0].name
|
|
labels = {
|
|
app = "bot-block-proxy"
|
|
}
|
|
}
|
|
|
|
spec {
|
|
replicas = 2
|
|
strategy {
|
|
type = "RollingUpdate"
|
|
rolling_update {
|
|
max_unavailable = 0
|
|
max_surge = 1
|
|
}
|
|
}
|
|
selector {
|
|
match_labels = {
|
|
app = "bot-block-proxy"
|
|
}
|
|
}
|
|
template {
|
|
metadata {
|
|
labels = {
|
|
app = "bot-block-proxy"
|
|
}
|
|
}
|
|
spec {
|
|
topology_spread_constraint {
|
|
max_skew = 1
|
|
topology_key = "kubernetes.io/hostname"
|
|
when_unsatisfiable = "DoNotSchedule"
|
|
label_selector {
|
|
match_labels = {
|
|
app = "bot-block-proxy"
|
|
}
|
|
}
|
|
}
|
|
container {
|
|
name = "nginx"
|
|
image = "openresty/openresty:alpine"
|
|
|
|
port {
|
|
container_port = 8080
|
|
}
|
|
|
|
volume_mount {
|
|
name = "config"
|
|
mount_path = "/etc/nginx/conf.d"
|
|
read_only = true
|
|
}
|
|
|
|
liveness_probe {
|
|
http_get {
|
|
path = "/healthz"
|
|
port = 8080
|
|
}
|
|
initial_delay_seconds = 3
|
|
period_seconds = 10
|
|
}
|
|
readiness_probe {
|
|
http_get {
|
|
path = "/healthz"
|
|
port = 8080
|
|
}
|
|
initial_delay_seconds = 2
|
|
period_seconds = 5
|
|
}
|
|
|
|
resources {
|
|
requests = {
|
|
cpu = "5m"
|
|
memory = "64Mi"
|
|
}
|
|
limits = {
|
|
memory = "64Mi"
|
|
}
|
|
}
|
|
}
|
|
|
|
volume {
|
|
name = "config"
|
|
config_map {
|
|
name = kubernetes_config_map.bot_block_proxy_config.metadata[0].name
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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" "bot_block_proxy" {
|
|
metadata {
|
|
name = "bot-block-proxy"
|
|
namespace = kubernetes_namespace.traefik.metadata[0].name
|
|
labels = {
|
|
app = "bot-block-proxy"
|
|
}
|
|
}
|
|
|
|
spec {
|
|
selector = {
|
|
app = "bot-block-proxy"
|
|
}
|
|
port {
|
|
name = "http"
|
|
port = 8080
|
|
target_port = 8080
|
|
}
|
|
}
|
|
}
|
|
|
|
# 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" {
|
|
metadata {
|
|
name = "auth-proxy-htpasswd"
|
|
namespace = kubernetes_namespace.traefik.metadata[0].name
|
|
}
|
|
|
|
data = {
|
|
"htpasswd" = var.auth_fallback_htpasswd
|
|
}
|
|
}
|
|
|
|
resource "kubernetes_config_map" "auth_proxy_config" {
|
|
metadata {
|
|
name = "auth-proxy-config"
|
|
namespace = kubernetes_namespace.traefik.metadata[0].name
|
|
}
|
|
|
|
data = {
|
|
"default.conf" = <<-EOT
|
|
upstream authentik {
|
|
server ak-outpost-authentik-embedded-outpost.authentik.svc.cluster.local:9000;
|
|
}
|
|
server {
|
|
listen 9000;
|
|
|
|
location /outpost.goauthentik.io/auth/traefik {
|
|
proxy_pass http://authentik;
|
|
proxy_connect_timeout 3s;
|
|
proxy_read_timeout 5s;
|
|
proxy_send_timeout 5s;
|
|
proxy_intercept_errors on;
|
|
error_page 502 503 504 = @fallback_auth;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
|
|
}
|
|
|
|
location @fallback_auth {
|
|
auth_basic "Emergency Access";
|
|
auth_basic_user_file /etc/nginx/htpasswd;
|
|
# Set ALL X-authentik-* headers to prevent client-supplied header spoofing.
|
|
# Without this, a client could inject fake X-authentik-groups and backends
|
|
# that trust these headers would grant elevated access.
|
|
add_header X-authentik-username $remote_user always;
|
|
add_header X-authentik-uid "" always;
|
|
add_header X-authentik-email "" always;
|
|
add_header X-authentik-name "" always;
|
|
add_header X-authentik-groups "" always;
|
|
add_header X-Auth-Fallback "true" always;
|
|
root /usr/share/nginx/fallback;
|
|
try_files /ok =403;
|
|
}
|
|
|
|
location /outpost.goauthentik.io/ {
|
|
proxy_pass http://authentik;
|
|
proxy_connect_timeout 3s;
|
|
proxy_read_timeout 10s;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
|
|
location /healthz {
|
|
access_log off;
|
|
return 200 "ok";
|
|
}
|
|
}
|
|
EOT
|
|
}
|
|
}
|
|
|
|
resource "kubernetes_config_map" "auth_proxy_fallback" {
|
|
metadata {
|
|
name = "auth-proxy-fallback"
|
|
namespace = kubernetes_namespace.traefik.metadata[0].name
|
|
}
|
|
|
|
data = {
|
|
"ok" = "authenticated"
|
|
}
|
|
}
|
|
|
|
resource "kubernetes_deployment" "auth_proxy" {
|
|
metadata {
|
|
name = "auth-proxy"
|
|
namespace = kubernetes_namespace.traefik.metadata[0].name
|
|
labels = {
|
|
app = "auth-proxy"
|
|
}
|
|
}
|
|
|
|
spec {
|
|
replicas = 2
|
|
strategy {
|
|
type = "RollingUpdate"
|
|
rolling_update {
|
|
max_unavailable = 0
|
|
max_surge = 1
|
|
}
|
|
}
|
|
selector {
|
|
match_labels = {
|
|
app = "auth-proxy"
|
|
}
|
|
}
|
|
template {
|
|
metadata {
|
|
labels = {
|
|
app = "auth-proxy"
|
|
}
|
|
}
|
|
spec {
|
|
topology_spread_constraint {
|
|
max_skew = 1
|
|
topology_key = "kubernetes.io/hostname"
|
|
when_unsatisfiable = "DoNotSchedule"
|
|
label_selector {
|
|
match_labels = {
|
|
app = "auth-proxy"
|
|
}
|
|
}
|
|
}
|
|
container {
|
|
name = "nginx"
|
|
image = "nginx:1-alpine"
|
|
|
|
port {
|
|
container_port = 9000
|
|
}
|
|
|
|
volume_mount {
|
|
name = "config"
|
|
mount_path = "/etc/nginx/conf.d"
|
|
read_only = true
|
|
}
|
|
volume_mount {
|
|
name = "htpasswd"
|
|
mount_path = "/etc/nginx/htpasswd"
|
|
sub_path = "htpasswd"
|
|
read_only = true
|
|
}
|
|
volume_mount {
|
|
name = "fallback"
|
|
mount_path = "/usr/share/nginx/fallback"
|
|
read_only = true
|
|
}
|
|
|
|
liveness_probe {
|
|
http_get {
|
|
path = "/healthz"
|
|
port = 9000
|
|
}
|
|
initial_delay_seconds = 3
|
|
period_seconds = 10
|
|
}
|
|
readiness_probe {
|
|
http_get {
|
|
path = "/healthz"
|
|
port = 9000
|
|
}
|
|
initial_delay_seconds = 2
|
|
period_seconds = 5
|
|
}
|
|
|
|
resources {
|
|
requests = {
|
|
cpu = "5m"
|
|
memory = "64Mi"
|
|
}
|
|
limits = {
|
|
memory = "64Mi"
|
|
}
|
|
}
|
|
}
|
|
|
|
volume {
|
|
name = "config"
|
|
config_map {
|
|
name = kubernetes_config_map.auth_proxy_config.metadata[0].name
|
|
}
|
|
}
|
|
volume {
|
|
name = "htpasswd"
|
|
secret {
|
|
secret_name = kubernetes_secret.auth_proxy_htpasswd.metadata[0].name
|
|
}
|
|
}
|
|
volume {
|
|
name = "fallback"
|
|
config_map {
|
|
name = kubernetes_config_map.auth_proxy_fallback.metadata[0].name
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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" "auth_proxy" {
|
|
metadata {
|
|
name = "auth-proxy"
|
|
namespace = kubernetes_namespace.traefik.metadata[0].name
|
|
labels = {
|
|
app = "auth-proxy"
|
|
}
|
|
}
|
|
|
|
spec {
|
|
selector = {
|
|
app = "auth-proxy"
|
|
}
|
|
port {
|
|
name = "http"
|
|
port = 9000
|
|
target_port = 9000
|
|
}
|
|
}
|
|
}
|