diff --git a/main.tf b/main.tf index 509fb6f9..68c3170d 100644 --- a/main.tf +++ b/main.tf @@ -136,6 +136,9 @@ variable "aiostreams_database_connection_string" { type = string } variable "actualbudget_credentials" { type = map(any) } variable "speedtest_db_password" { type = string } variable "freedify_credentials" { type = map(any) } +variable "mcaptcha_postgresql_password" { type = string } +variable "mcaptcha_cookie_secret" { type = string } +variable "mcaptcha_captcha_salt" { type = string } provider "kubernetes" { config_path = var.prod ? "" : "~/.kube/config" @@ -563,6 +566,10 @@ module "kubernetes_cluster" { speedtest_db_password = var.speedtest_db_password freedify_credentials = var.freedify_credentials + + mcaptcha_postgresql_password = var.mcaptcha_postgresql_password + mcaptcha_cookie_secret = var.mcaptcha_cookie_secret + mcaptcha_captcha_salt = var.mcaptcha_captcha_salt } diff --git a/modules/kubernetes/crowdsec/main.tf b/modules/kubernetes/crowdsec/main.tf index a06fa429..ae859c53 100644 --- a/modules/kubernetes/crowdsec/main.tf +++ b/modules/kubernetes/crowdsec/main.tf @@ -64,6 +64,28 @@ resource "kubernetes_config_map" "crowdsec_custom_scenarios" { } } +# Whitelist for trusted IPs that should never be blocked +resource "kubernetes_config_map" "crowdsec_whitelist" { + metadata { + name = "crowdsec-whitelist" + namespace = kubernetes_namespace.crowdsec.metadata[0].name + labels = { + "app.kubernetes.io/name" = "crowdsec" + } + } + + data = { + "whitelist.yaml" = <<-YAML + name: crowdsecurity/whitelist-trusted-ips + description: "Whitelist for trusted IPs that should never be blocked" + whitelist: + reason: "Trusted IP - never block" + ip: + - "176.12.22.76" + YAML + } +} + resource "helm_release" "crowdsec" { namespace = kubernetes_namespace.crowdsec.metadata[0].name diff --git a/modules/kubernetes/crowdsec/values.yaml b/modules/kubernetes/crowdsec/values.yaml index f644d5f9..034d803a 100644 --- a/modules/kubernetes/crowdsec/values.yaml +++ b/modules/kubernetes/crowdsec/values.yaml @@ -31,10 +31,17 @@ agent: mountPath: /etc/crowdsec/scenarios/http-429-abuse.yaml subPath: "http-429-abuse.yaml" readonly: true + - name: whitelist + mountPath: /etc/crowdsec/parsers/s02-enrich/whitelist.yaml + subPath: "whitelist.yaml" + readonly: true extraVolumes: - name: custom-scenarios configMap: name: crowdsec-custom-scenarios + - name: whitelist + configMap: + name: crowdsec-whitelist lapi: replicas: 3 extraSecrets: @@ -117,6 +124,34 @@ lapi: type: RollingUpdate config: + # Custom profiles: captcha for rate limiting, ban for attacks + profiles.yaml: | + # Captcha for rate limiting and 403 abuse (user can unblock themselves) + name: captcha_remediation + filters: + - Alert.Remediation == true && Alert.GetScope() == "Ip" && Alert.GetScenario() in ["crowdsecurity/http-429-abuse", "crowdsecurity/http-403-abuse", "crowdsecurity/http-crawl-non_statics", "crowdsecurity/http-sensitive-files"] + decisions: + - type: captcha + duration: 4h + on_success: break + --- + # Default: Ban for serious attacks (CVE exploits, scanners, brute force) + name: default_ip_remediation + filters: + - Alert.Remediation == true && Alert.GetScope() == "Ip" + decisions: + - type: ban + duration: 4h + on_success: break + --- + name: default_range_remediation + filters: + - Alert.Remediation == true && Alert.GetScope() == "Range" + decisions: + - type: ban + duration: 4h + on_success: break + config.yaml.local: | db_config: type: mysql diff --git a/modules/kubernetes/dockerhub_secret/main.tf b/modules/kubernetes/dockerhub_secret/main.tf index f3672f3e..40d713e7 100644 --- a/modules/kubernetes/dockerhub_secret/main.tf +++ b/modules/kubernetes/dockerhub_secret/main.tf @@ -1,9 +1,9 @@ -variable namespace {} -variable password {} -variable dockerhub_creds_secret_name { +variable "namespace" {} +variable "password" {} +variable "dockerhub_creds_secret_name" { default = "dockerhub-creds" } -variable username { +variable "username" { default = "viktorbarzin" } diff --git a/modules/kubernetes/immich/main.tf b/modules/kubernetes/immich/main.tf index aa80ea99..a3c2ecf3 100644 --- a/modules/kubernetes/immich/main.tf +++ b/modules/kubernetes/immich/main.tf @@ -374,7 +374,7 @@ resource "kubernetes_deployment" "immich-machine-learning" { } env { name = "MACHINE_LEARNING_MODEL_TTL" - value = 0 + value = "0" } env { name = "TRANSFORMERS_CACHE" @@ -388,10 +388,24 @@ resource "kubernetes_deployment" "immich-machine-learning" { name = "MPLCONFIGDIR" value = "/cache/matplotlib-config" } + # Preload CLIP models (for smart search) env { - name = "MACHINE_LEARNING_PRELOAD__CLIP" + name = "MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL" value = "ViT-B-16-SigLIP2__webli" } + env { + name = "MACHINE_LEARNING_PRELOAD__CLIP__VISUAL" + value = "ViT-B-16-SigLIP2__webli" + } + # Preload facial recognition models + env { + name = "MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION" + value = "buffalo_l" + } + env { + name = "MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION" + value = "buffalo_l" + } volume_mount { name = "cache" diff --git a/modules/kubernetes/main.tf b/modules/kubernetes/main.tf index 121276b4..67f989a3 100644 --- a/modules/kubernetes/main.tf +++ b/modules/kubernetes/main.tf @@ -115,6 +115,9 @@ variable "aiostreams_database_connection_string" { type = string } variable "actualbudget_credentials" { type = map(any) } variable "speedtest_db_password" { type = string } variable "freedify_credentials" { type = map(any) } +variable "mcaptcha_postgresql_password" { type = string } +variable "mcaptcha_cookie_secret" { type = string } +variable "mcaptcha_captcha_salt" { type = string } variable "defcon_level" { @@ -140,7 +143,7 @@ locals { "url", "excalidraw", "travel_blog", "dashy", "send", "ytdlp", "wealthfolio", "rybbit", "stirling-pdf", "networking-toolbox", "navidrome", "freshrss", "forgejo", "tor-proxy", "real-estate-crawler", "n8n", "changedetection", "linkwarden", "matrix", "homepage", "meshcentral", "diun", "cyberchef", "ntfy", "ollama", - "servarr", "jsoncrack", "paperless-ngx", "frigate", "audiobookshelf", "tandoor", "ebook2audiobook", "netbox", "speedtest", "resume", "freedify" + "servarr", "jsoncrack", "paperless-ngx", "frigate", "audiobookshelf", "tandoor", "ebook2audiobook", "netbox", "speedtest", "resume", "freedify", "mcaptcha" ], } active_modules = distinct(flatten([ @@ -332,6 +335,18 @@ module "privatebin" { depends_on = [null_resource.core_services] } +# module "mcaptcha" { +# source = "./mcaptcha" +# for_each = contains(local.active_modules, "mcaptcha") ? { mcaptcha = true } : {} +# tls_secret_name = var.tls_secret_name +# tier = local.tiers.edge +# postgresql_password = var.mcaptcha_postgresql_password +# cookie_secret = var.mcaptcha_cookie_secret +# captcha_salt = var.mcaptcha_captcha_salt + +# depends_on = [null_resource.core_services] +# } + # module "vault" { # source = "./vault" # tier = local.tiers.edge diff --git a/modules/kubernetes/mcaptcha/main.tf b/modules/kubernetes/mcaptcha/main.tf new file mode 100644 index 00000000..4d0e7149 --- /dev/null +++ b/modules/kubernetes/mcaptcha/main.tf @@ -0,0 +1,309 @@ +variable "tls_secret_name" {} +variable "tier" { type = string } +variable "postgresql_password" {} +variable "cookie_secret" {} +variable "captcha_salt" {} + +locals { + domain = "mcaptcha.viktorbarzin.me" + port = 7000 +} + +resource "kubernetes_namespace" "mcaptcha" { + metadata { + name = "mcaptcha" + labels = { + "istio-injection" : "disabled" + } + } +} + +module "tls_secret" { + source = "../setup_tls_secret" + namespace = kubernetes_namespace.mcaptcha.metadata[0].name + tls_secret_name = var.tls_secret_name +} + +# mCaptcha requires a special Redis with the mcaptcha/cache module loaded +resource "kubernetes_deployment" "mcaptcha_redis" { + metadata { + name = "mcaptcha-redis" + namespace = kubernetes_namespace.mcaptcha.metadata[0].name + labels = { + app = "mcaptcha-redis" + tier = var.tier + } + } + + spec { + replicas = 1 + selector { + match_labels = { + app = "mcaptcha-redis" + } + } + + strategy { + type = "Recreate" + } + + template { + metadata { + labels = { + app = "mcaptcha-redis" + } + } + + spec { + container { + image = "mcaptcha/cache:latest" + name = "redis" + + port { + container_port = 6379 + } + + resources { + requests = { + memory = "64Mi" + cpu = "25m" + } + limits = { + memory = "128Mi" + cpu = "200m" + } + } + + liveness_probe { + tcp_socket { + port = 6379 + } + initial_delay_seconds = 10 + period_seconds = 10 + } + + readiness_probe { + tcp_socket { + port = 6379 + } + initial_delay_seconds = 5 + period_seconds = 5 + } + } + } + } + } +} + +resource "kubernetes_service" "mcaptcha_redis" { + metadata { + name = "mcaptcha-redis" + namespace = kubernetes_namespace.mcaptcha.metadata[0].name + labels = { + app = "mcaptcha-redis" + } + } + + spec { + selector = { + app = "mcaptcha-redis" + } + port { + name = "redis" + port = 6379 + target_port = 6379 + } + } +} + +resource "kubernetes_deployment" "mcaptcha" { + metadata { + name = "mcaptcha" + namespace = kubernetes_namespace.mcaptcha.metadata[0].name + labels = { + app = "mcaptcha" + tier = var.tier + } + annotations = { + "reloader.stakater.com/search" = "true" + } + } + + spec { + replicas = 1 + selector { + match_labels = { + app = "mcaptcha" + } + } + + strategy { + type = "Recreate" + } + + template { + metadata { + labels = { + app = "mcaptcha" + } + annotations = { + "diun.enable" = "true" + "diun.include_tags" = "^\\d+(?:\\.\\d+)?(?:\\.\\d+)?$" + } + } + + spec { + container { + image = "mcaptcha/mcaptcha:latest" + name = "mcaptcha" + + port { + container_port = local.port + } + + # Required configuration + env { + name = "MCAPTCHA_server_DOMAIN" + value = local.domain + } + + env { + name = "MCAPTCHA_server_COOKIE_SECRET" + value = var.cookie_secret + } + + env { + name = "MCAPTCHA_captcha_SALT" + value = var.captcha_salt + } + + # Server configuration + env { + name = "PORT" + value = tostring(local.port) + } + + env { + name = "MCAPTCHA_server_IP" + value = "0.0.0.0" + } + + env { + name = "MCAPTCHA_server_PROXY_HAS_TLS" + value = "true" + } + + # Database configuration (PostgreSQL) + env { + name = "DATABASE_URL" + value = "postgres://mcaptcha:${var.postgresql_password}@postgresql.dbaas.svc.cluster.local:5432/mcaptcha" + } + + # Redis configuration (using mcaptcha/cache module) + env { + name = "MCAPTCHA_redis_URL" + value = "redis://mcaptcha-redis.mcaptcha.svc.cluster.local:6379" + } + + # Feature flags + env { + name = "MCAPTCHA_allow_registration" + # value = "true" + value = "false" + } + + env { + name = "MCAPTCHA_allow_demo" + value = "false" + } + + env { + name = "MCAPTCHA_commercial" + value = "false" + } + + env { + name = "MCAPTCHA_captcha_ENABLE_STATS" + value = "true" + } + + env { + name = "MCAPTCHA_captcha_GC" + value = "30" + } + + env { + name = "MCAPTCHA_debug" + value = "false" + } + env { + name = "RUST_BACKTRACE" + value = "1" + } + + resources { + requests = { + memory = "64Mi" + cpu = "50m" + } + limits = { + memory = "256Mi" + cpu = "500m" + } + } + + # Health checks + liveness_probe { + http_get { + path = "/" + port = local.port + } + initial_delay_seconds = 30 + period_seconds = 10 + timeout_seconds = 5 + failure_threshold = 3 + } + + readiness_probe { + http_get { + path = "/" + port = local.port + } + initial_delay_seconds = 10 + period_seconds = 5 + timeout_seconds = 3 + failure_threshold = 3 + } + } + } + } + } +} + +resource "kubernetes_service" "mcaptcha" { + metadata { + name = "mcaptcha" + namespace = kubernetes_namespace.mcaptcha.metadata[0].name + labels = { + "app" = "mcaptcha" + } + } + + spec { + selector = { + app = "mcaptcha" + } + port { + name = "http" + port = 80 + target_port = local.port + } + } +} + +module "ingress" { + source = "../ingress_factory" + namespace = kubernetes_namespace.mcaptcha.metadata[0].name + name = "mcaptcha" + tls_secret_name = var.tls_secret_name +} diff --git a/modules/kubernetes/monitoring/idrac.tf b/modules/kubernetes/monitoring/idrac.tf index b966824f..99ba0b93 100644 --- a/modules/kubernetes/monitoring/idrac.tf +++ b/modules/kubernetes/monitoring/idrac.tf @@ -21,6 +21,14 @@ resource "kubernetes_config_map" "redfish-config" { password: calvin metrics: all: true + # system: true + # sensors: true + # power: true + # sel: false # Disable SEL - often slow + # storage: true # Disable storage - slowest endpoint + # memory: true + # network: false # Disable network adapters + # firmware: false # Don't need this frequently EOF } } @@ -83,11 +91,11 @@ resource "kubernetes_service" "idrac-redfish-exporter" { labels = { "app" = "idrac-redfish-exporter" } - annotations = { - "prometheus.io/scrape" = "true" - "prometheus.io/path" = "/metrics" - "prometheus.io/port" = "9090" - } + # annotations = { + # "prometheus.io/scrape" = "true" + # "prometheus.io/path" = "/metrics" + # "prometheus.io/port" = "9090" + # } } spec { diff --git a/modules/kubernetes/monitoring/prometheus_chart_values.tpl b/modules/kubernetes/monitoring/prometheus_chart_values.tpl index 9f9cd042..33374dc0 100755 --- a/modules/kubernetes/monitoring/prometheus_chart_values.tpl +++ b/modules/kubernetes/monitoring/prometheus_chart_values.tpl @@ -474,6 +474,8 @@ extraScrapeConfigs: | - "crowdsec-service.crowdsec.svc.cluster.local:6060" metrics_path: '/metrics' - job_name: 'snmp-idrac' + scrape_interval: 1m + scrape_timeout: 45s static_configs: - targets: - "idrac.viktorbarzin.lan:161" @@ -492,7 +494,7 @@ extraScrapeConfigs: | regex: '(.*)' replacement: 'r730_idrac_$${1}' - job_name: 'redfish-idrac' - scrape_interval: 1m + scrape_interval: 3m scrape_timeout: 45s metrics_path: /metrics static_configs: diff --git a/modules/kubernetes/monitoring/snmp_exporter.tf b/modules/kubernetes/monitoring/snmp_exporter.tf index 9f97bda8..8ac6b96f 100644 --- a/modules/kubernetes/monitoring/snmp_exporter.tf +++ b/modules/kubernetes/monitoring/snmp_exporter.tf @@ -82,11 +82,11 @@ resource "kubernetes_service" "snmp-exporter" { labels = { "app" = "snmp-exporter" } - annotations = { - "prometheus.io/scrape" = "true" - "prometheus.io/path" = "/snmp?auth=Public0&target=tcp%3A%2F%2F192.%3A161" - "prometheus.io/port" = "9116" - } + # annotations = { + # "prometheus.io/scrape" = "true" + # "prometheus.io/path" = "/snmp?auth=Public0&target=tcp%3A%2F%2F192.%3A161" + # "prometheus.io/port" = "9116" + # } } spec { diff --git a/modules/kubernetes/nginx-ingress/main.tf b/modules/kubernetes/nginx-ingress/main.tf index 8ba56189..ab511f7a 100644 --- a/modules/kubernetes/nginx-ingress/main.tf +++ b/modules/kubernetes/nginx-ingress/main.tf @@ -539,7 +539,7 @@ resource "kubernetes_deployment" "ingress_nginx_controller" { } env { name = "CAPTCHA_PROVIDER" - value = "recaptcha" + value = "hcaptcha" } env { name = "BOUNCING_ON_TYPE" diff --git a/modules/kubernetes/ollama/main.tf b/modules/kubernetes/ollama/main.tf index 7d5bd73a..fac512e5 100644 --- a/modules/kubernetes/ollama/main.tf +++ b/modules/kubernetes/ollama/main.tf @@ -145,7 +145,7 @@ resource "kubernetes_service" "ollama" { } } -# Allow ollama to be connected to from external apps +# Allow ollama to be connected to from external apps (internal LAN only) module "ollama-ingress" { source = "../ingress_factory" namespace = kubernetes_namespace.ollama.metadata[0].name @@ -158,6 +158,20 @@ module "ollama-ingress" { port = 11434 } +# Ollama API ingress for Claude Code access (restricted to LAN/VPN) +module "ollama-api-ingress" { + source = "../ingress_factory" + namespace = kubernetes_namespace.ollama.metadata[0].name + name = "ollama-api" + service_name = "ollama" + root_domain = "viktorbarzin.lan" + tls_secret_name = var.tls_secret_name + allow_local_access_only = true # Restricts to 10.0.0.0/8, 192.168.1.0/24 + ssl_redirect = false + port = 11434 + proxy_timeout = 300 # Longer timeout for model inference +} + # Web UI resource "kubernetes_deployment" "ollama-ui" { metadata { diff --git a/terraform.tfstate b/terraform.tfstate index 4bcceff9..e9486dea 100644 Binary files a/terraform.tfstate and b/terraform.tfstate differ diff --git a/terraform.tfvars b/terraform.tfvars index 573346fd..223edcb8 100644 Binary files a/terraform.tfvars and b/terraform.tfvars differ