variable "name" { type = string } variable "service_name" { type = string default = null # defaults to name } variable "host" { type = string default = null } variable "namespace" { type = string } variable "external_name" { type = string default = null } variable "port" { default = "80" } variable "tls_secret_name" {} variable "backend_protocol" { default = "HTTP" } variable "protected" { type = bool default = false } variable "ingress_path" { type = list(string) default = ["/"] } variable "max_body_size" { type = string default = "50m" } variable "extra_annotations" { default = {} } variable "ssl_redirect" { default = true type = bool } variable "allow_local_access_only" { default = false type = bool } variable "root_domain" { default = "viktorbarzin.me" type = string } variable "rybbit_site_id" { default = null type = string } variable "custom_content_security_policy" { type = string default = null } variable "exclude_crowdsec" { type = bool default = false } variable "full_host" { type = string default = null } variable "extra_middlewares" { type = list(string) default = [] } variable "skip_default_rate_limit" { type = bool default = false } variable "anti_ai_scraping" { type = bool default = null # null = auto (enabled when not protected, disabled when protected) } variable "homepage_group" { type = string default = null # auto-detect from namespace } variable "homepage_enabled" { type = bool default = true } locals { effective_host = var.full_host != null ? var.full_host : "${var.host != null ? var.host : var.name}.${var.root_domain}" effective_anti_ai = var.anti_ai_scraping != null ? var.anti_ai_scraping : !var.protected ns_to_group = { monitoring = "Infrastructure" prometheus = "Infrastructure" technitium = "Infrastructure" traefik = "Infrastructure" metallb-system = "Infrastructure" kyverno = "Infrastructure" authentik = "Identity & Security" crowdsec = "Identity & Security" woodpecker = "Development & CI" forgejo = "Development & CI" immich = "Media & Entertainment" frigate = "Smart Home" home-assistant = "Smart Home" ollama = "AI & Data" dbaas = "Infrastructure" servarr = "Media & Entertainment" navidrome = "Media & Entertainment" nextcloud = "Productivity" n8n = "Automation" changedetection = "Automation" finance = "Finance & Personal" homepage = "Core Platform" reverse-proxy = "Smart Home" mailserver = "Infrastructure" } homepage_group = coalesce( var.homepage_group, lookup(local.ns_to_group, var.namespace, "Other") ) homepage_defaults = var.homepage_enabled ? { "gethomepage.dev/enabled" = "true" "gethomepage.dev/name" = replace(replace(var.name, "-", " "), "_", " ") "gethomepage.dev/group" = local.homepage_group "gethomepage.dev/href" = "https://${local.effective_host}" "gethomepage.dev/icon" = "${replace(var.name, "-", "")}.png" } : {} } resource "kubernetes_service" "proxied-service" { count = var.external_name == null ? 0 : 1 metadata { name = var.name namespace = var.namespace labels = { "app" = var.name } } spec { type = var.external_name != null ? "ExternalName" : "ClusterIP" external_name = var.name port { name = "${var.name}-web" port = var.port protocol = "TCP" target_port = var.port } } } resource "kubernetes_ingress_v1" "proxied-ingress" { metadata { name = var.name namespace = var.namespace annotations = merge({ "traefik.ingress.kubernetes.io/router.middlewares" = join(",", compact(concat([ "traefik-retry@kubernetescrd", "traefik-error-pages@kubernetescrd", var.skip_default_rate_limit ? null : "traefik-rate-limit@kubernetescrd", var.custom_content_security_policy == null ? "traefik-csp-headers@kubernetescrd" : null, var.exclude_crowdsec ? null : "traefik-crowdsec@kubernetescrd", local.effective_anti_ai ? "traefik-ai-bot-block@kubernetescrd" : null, local.effective_anti_ai ? "traefik-anti-ai-headers@kubernetescrd" : null, local.effective_anti_ai ? "traefik-strip-accept-encoding@kubernetescrd" : null, local.effective_anti_ai ? "traefik-anti-ai-trap-links@kubernetescrd" : null, var.protected ? "traefik-authentik-forward-auth@kubernetescrd" : null, var.allow_local_access_only ? "traefik-local-only@kubernetescrd" : null, var.rybbit_site_id != null ? "traefik-strip-accept-encoding@kubernetescrd" : null, var.rybbit_site_id != null ? "${var.namespace}-rybbit-analytics-${var.name}@kubernetescrd" : null, var.custom_content_security_policy != null ? "${var.namespace}-custom-csp-${var.name}@kubernetescrd" : null, ], var.extra_middlewares))) "traefik.ingress.kubernetes.io/router.entrypoints" = "websecure" }, local.homepage_defaults, var.extra_annotations) } spec { ingress_class_name = "traefik" tls { hosts = [local.effective_host] secret_name = var.tls_secret_name } rule { host = local.effective_host http { dynamic "path" { for_each = var.ingress_path content { path = path.value backend { service { name = var.service_name != null ? var.service_name : var.name port { number = var.port } } } } } } } } } # Rybbit analytics middleware (rewrite-body plugin with content-type filtering) - created per service when rybbit_site_id is set resource "kubernetes_manifest" "rybbit_analytics" { count = var.rybbit_site_id != null ? 1 : 0 manifest = { apiVersion = "traefik.io/v1alpha1" kind = "Middleware" metadata = { name = "rybbit-analytics-${var.name}" namespace = var.namespace } spec = { plugin = { rewrite-body = { rewrites = [{ regex = "" replacement = "" }] monitoring = { types = ["text/html"] } } } } } } # Custom CSP headers middleware - created per service when custom_content_security_policy is set resource "kubernetes_manifest" "custom_csp" { count = var.custom_content_security_policy != null ? 1 : 0 manifest = { apiVersion = "traefik.io/v1alpha1" kind = "Middleware" metadata = { name = "custom-csp-${var.name}" namespace = var.namespace } spec = { headers = { contentSecurityPolicy = var.custom_content_security_policy } } } }