diff --git a/apps/auth/services/gateway/k8s/deployment.yaml b/apps/auth/services/gateway/k8s/deployment.yaml index bc74496..dc40ba8 100644 --- a/apps/auth/services/gateway/k8s/deployment.yaml +++ b/apps/auth/services/gateway/k8s/deployment.yaml @@ -36,6 +36,8 @@ spec: value: "http://users" - name: OTEL_EXPORTER_OTLP_ENDPOINT value: "jaeger.monitoring.svc:4317" + - name: DOMAIN + value: "gugagr.xyz" livenessProbe: httpGet: path: /healthz diff --git a/apps/auth/services/gateway/k8s/ingress-home.yaml b/apps/auth/services/gateway/k8s/ingress-home.yaml index 1e35b9d..44951b1 100644 --- a/apps/auth/services/gateway/k8s/ingress-home.yaml +++ b/apps/auth/services/gateway/k8s/ingress-home.yaml @@ -5,10 +5,15 @@ metadata: name: gateway-home annotations: traefik.ingress.kubernetes.io/router.middlewares: auth-forward-auth@kubernetescrd + traefik.ingress.kubernetes.io/router.tls: "true" + traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt spec: ingressClassName: traefik + tls: + - hosts: + - gugagr.xyz rules: - - host: homelab.local + - host: gugagr.xyz http: paths: - path: / diff --git a/apps/auth/services/gateway/k8s/ingress.yaml b/apps/auth/services/gateway/k8s/ingress.yaml index 409ed51..9caf719 100644 --- a/apps/auth/services/gateway/k8s/ingress.yaml +++ b/apps/auth/services/gateway/k8s/ingress.yaml @@ -3,10 +3,16 @@ kind: Ingress metadata: namespace: auth name: gateway + annotations: + traefik.ingress.kubernetes.io/router.tls: "true" + traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt spec: ingressClassName: traefik + tls: + - hosts: + - auth.gugagr.xyz rules: - - host: auth.homelab.local + - host: auth.gugagr.xyz http: paths: - path: /api/ diff --git a/apps/auth/services/gateway/main/handler.go b/apps/auth/services/gateway/main/handler.go index e6214bc..bfbe243 100644 --- a/apps/auth/services/gateway/main/handler.go +++ b/apps/auth/services/gateway/main/handler.go @@ -39,6 +39,20 @@ var dashboardTmpl = parseTmpl("templates/base.html", "templates/dashboard.html") var homeTmpl = parseTmpl("templates/base.html", "templates/home.html") var forbiddenTmpl = parseTmpl("templates/base.html", "templates/forbidden.html") +func domain() string { + if d := os.Getenv("DOMAIN"); d != "" { + return d + } + return "homelab.local" +} + +func scheme() string { + if os.Getenv("DOMAIN") != "" { + return "https" + } + return "http" +} + var tracer = otel.Tracer("gateway") type Handler struct{} @@ -142,12 +156,13 @@ func (h *Handler) Home(w http.ResponseWriter, r *http.Request) { _, span := tracer.Start(r.Context(), "Home", trace.WithAttributes(spanAttrs(r)...)) defer span.End() + s, d := scheme(), domain() services := []ServiceCard{ - {Name: "Auth", Description: "Login and account management", URL: "http://auth.homelab.local/dashboard", Icon: "๐Ÿ”‘", Delay: 0}, - {Name: "Finance", Description: "Track your finances", URL: "http://finance.homelab.local", Icon: "๐Ÿ’ฐ", Delay: 0.2}, - {Name: "Test App", Description: "Example Go service", URL: "http://test.homelab.local", Icon: "๐Ÿงช", Delay: 0.4}, - {Name: "Monitoring", Description: "Use Grafana to monitor services", URL: "http://grafana.homelab.local", Icon: "๐Ÿ“Š", Delay: 0.6}, - {Name: "Jaeger", Description: "Trace service requests", URL: "http://jaeger.homelab.local", Icon: "๐Ÿ”", Delay: 0.8}, + {Name: "Auth", Description: "Login and account management", URL: s + "://auth." + d + "/dashboard", Icon: "๐Ÿ”‘", Delay: 0}, + {Name: "Finance", Description: "Track your finances", URL: s + "://finance." + d, Icon: "๐Ÿ’ฐ", Delay: 0.2}, + {Name: "Test App", Description: "Example Go service", URL: s + "://example-service." + d, Icon: "๐Ÿงช", Delay: 0.4}, + {Name: "Monitoring", Description: "Use Grafana to monitor services", URL: s + "://grafana." + d, Icon: "๐Ÿ“Š", Delay: 0.6}, + {Name: "Jaeger", Description: "Trace service requests", URL: s + "://jaeger." + d, Icon: "๐Ÿ”", Delay: 0.8}, } w.Header().Set("Content-Type", "text/html; charset=utf-8") @@ -300,7 +315,7 @@ func (h *Handler) LoginAPI(w http.ResponseWriter, r *http.Request) { Name: "auth_token", Value: token, Path: "/", - Domain: ".homelab.local", + Domain: "." + domain(), HttpOnly: true, SameSite: http.SameSiteLaxMode, }) @@ -343,7 +358,7 @@ func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { Name: "auth_token", Value: token, Path: "/", - Domain: ".homelab.local", + Domain: "." + domain(), HttpOnly: true, SameSite: http.SameSiteLaxMode, }) @@ -364,12 +379,12 @@ func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) { Name: "auth_token", Value: "", Path: "/", - Domain: ".homelab.local", + Domain: "." + domain(), MaxAge: -1, HttpOnly: true, SameSite: http.SameSiteLaxMode, }) - http.Redirect(w, r, "http://homelab.local/", http.StatusSeeOther) + http.Redirect(w, r, scheme()+"://"+domain()+"/", http.StatusSeeOther) slog.InfoContext(ctx, "user logged out") } @@ -384,7 +399,7 @@ func (h *Handler) Verify(w http.ResponseWriter, r *http.Request) { if redirect == "" { redirect = fmt.Sprintf("http://%s/", host) } - loginURL := fmt.Sprintf("http://auth.homelab.local/login?redirect=%s", url.QueryEscape(redirect)) + loginURL := fmt.Sprintf("%s://auth.%s/login?redirect=%s", scheme(), domain(), url.QueryEscape(redirect)) span.SetAttributes(attribute.String("verify.result", "no_cookie")) slog.DebugContext(ctx, "verify: no cookie, redirecting", "to", loginURL) http.Redirect(w, r, loginURL, http.StatusFound) @@ -395,7 +410,7 @@ func (h *Handler) Verify(w http.ResponseWriter, r *http.Request) { if err != nil { span.SetAttributes(attribute.String("verify.result", "invalid_token")) slog.WarnContext(ctx, "verify: invalid token") - http.Redirect(w, r, "http://auth.homelab.local/login", http.StatusFound) + http.Redirect(w, r, scheme()+"://auth."+domain()+"/login", http.StatusFound) return } span.SetAttributes( @@ -407,7 +422,7 @@ func (h *Handler) Verify(w http.ResponseWriter, r *http.Request) { // Check service-specific permission host := originalHost(r) - if reqPerm, ok := servicePermissions[host]; ok { + if reqPerm, ok := servicePermissions()[host]; ok { span.SetAttributes(attribute.String("verify.target_host", host), attribute.String("verify.required_perm", reqPerm)) if !hasPermission(claims.Permissions, reqPerm) { span.SetAttributes(attribute.String("verify.result", "forbidden")) @@ -541,10 +556,13 @@ func (h *Handler) RegisterProxy(w http.ResponseWriter, r *http.Request) { w.Write(respBody) } -var servicePermissions = map[string]string{ - "grafana.homelab.local": "service:grafana:access", - "jaeger.homelab.local": "service:jaeger:access", - "homelab.local": "service:home:access", +func servicePermissions() map[string]string { + d := domain() + return map[string]string{ + "grafana." + d: "service:grafana:access", + "jaeger." + d: "service:jaeger:access", + d: "service:home:access", + } } func hasPermission(perms []string, target string) bool { diff --git a/apps/finance/services/api/k8s/deployment.yaml b/apps/finance/services/api/k8s/deployment.yaml index 829268e..d2dda1d 100644 --- a/apps/finance/services/api/k8s/deployment.yaml +++ b/apps/finance/services/api/k8s/deployment.yaml @@ -32,7 +32,7 @@ spec: - name: OTEL_EXPORTER_OTLP_ENDPOINT value: "jaeger.monitoring.svc:4317" - name: BASE_URL - value: "https://finance.homelab.local" + value: "https://finance.gugagr.xyz" - name: ADMIN_EMAIL valueFrom: secretKeyRef: diff --git a/apps/finance/services/api/k8s/ingress.yaml b/apps/finance/services/api/k8s/ingress.yaml index 2afac33..10f6be2 100644 --- a/apps/finance/services/api/k8s/ingress.yaml +++ b/apps/finance/services/api/k8s/ingress.yaml @@ -3,13 +3,16 @@ kind: Ingress metadata: name: api namespace: finance + annotations: + traefik.ingress.kubernetes.io/router.tls: "true" + traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt spec: ingressClassName: traefik tls: - hosts: - - finance.homelab.local + - finance.gugagr.xyz rules: - - host: finance.homelab.local + - host: finance.gugagr.xyz http: paths: - path: / diff --git a/apps/test/services/example-service/k8s/ingress.yaml b/apps/test/services/example-service/k8s/ingress.yaml index 1a12d2d..22fb1d5 100644 --- a/apps/test/services/example-service/k8s/ingress.yaml +++ b/apps/test/services/example-service/k8s/ingress.yaml @@ -1,12 +1,17 @@ ---- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: example-service + annotations: + traefik.ingress.kubernetes.io/router.tls: "true" + traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt spec: ingressClassName: traefik + tls: + - hosts: + - example-service.gugagr.xyz rules: - - host: example-service.homelab.local + - host: example-service.gugagr.xyz http: paths: - path: / diff --git a/infrastructure/terraform/gitea.tf b/infrastructure/terraform/gitea.tf index 88a3067..561fd26 100644 --- a/infrastructure/terraform/gitea.tf +++ b/infrastructure/terraform/gitea.tf @@ -13,7 +13,7 @@ resource "kubernetes_secret" "gitea_admin" { data = { username = "admin" password = random_password.gitea_admin[0].result - email = "admin@homelab.local" + email = "admin@${var.domain}" } } @@ -35,8 +35,8 @@ resource "helm_release" "gitea" { config = { APP_NAME = "Homelab Git" server = { - DOMAIN = "git.homelab.local" - ROOT_URL = "http://git.homelab.local" + DOMAIN = "git.${var.domain}" + ROOT_URL = "http://git.${var.domain}" SSH_DOMAIN = "localhost" SSH_PORT = 30001 } @@ -57,7 +57,7 @@ resource "helm_release" "gitea" { enabled = true className = "traefik" hosts = [{ - host = "git.homelab.local" + host = "git.${var.domain}" paths = [{ path = "/", pathType = "Prefix" }] }] } @@ -112,7 +112,7 @@ resource "terraform_data" "gitea_runner_registration" { command = <<-EOT set -e echo "Waiting for Gitea to be ready..." - until curl -sf "http://git.homelab.local/api/v1/version" > /dev/null 2>&1; do + until curl -sf "http://git.${var.domain}/api/v1/version" > /dev/null 2>&1; do sleep 5 done @@ -121,7 +121,7 @@ resource "terraform_data" "gitea_runner_registration" { TOKEN=$(curl -sf \ -u "admin:$PASSWORD" \ - "http://git.homelab.local/api/v1/admin/runners/registration-token" \ + "http://git.${var.domain}/api/v1/admin/runners/registration-token" \ | grep -o '"token":"[^"]*"' | cut -d'"' -f4) kubectl patch secret gitea-runner-token -n gitea \ @@ -147,7 +147,7 @@ resource "kubernetes_secret" "gitea_registry" { data = { ".dockerconfigjson" = jsonencode({ auths = { - "git.homelab.local" = { + "git.${var.domain}" = { auth = base64encode("admin:${random_password.gitea_admin[0].result}") } } diff --git a/infrastructure/terraform/monitoring.tf b/infrastructure/terraform/monitoring.tf index eebf256..d9424e8 100644 --- a/infrastructure/terraform/monitoring.tf +++ b/infrastructure/terraform/monitoring.tf @@ -21,7 +21,7 @@ resource "helm_release" "kube_prometheus_stack" { adminPassword = random_password.grafana[0].result ingress = { enabled = true - hosts = ["grafana.homelab.local"] + hosts = ["grafana.${var.domain}"] ingressClassName = "traefik" annotations = { "traefik.ingress.kubernetes.io/router.middlewares" = "auth-forward-auth@kubernetescrd" @@ -32,7 +32,7 @@ resource "helm_release" "kube_prometheus_stack" { name = "Jaeger" type = "jaeger" uid = "jaeger" - url = "http://jaeger.monitoring.svc:16686" + url = "http://jaeger.monitoring.svc.cluster.local:16686" access = "proxy" isDefault = false }, @@ -92,7 +92,7 @@ resource "helm_release" "jaeger" { jaeger = { ingress = { enabled = true - hosts = ["jaeger.homelab.local"] + hosts = ["jaeger.${var.domain}"] annotations = { "traefik.ingress.kubernetes.io/router.middlewares" = "auth-forward-auth@kubernetescrd" } diff --git a/infrastructure/terraform/traefik.tf b/infrastructure/terraform/traefik.tf new file mode 100644 index 0000000..7daaf4b --- /dev/null +++ b/infrastructure/terraform/traefik.tf @@ -0,0 +1,41 @@ +resource "helm_release" "traefik" { + name = "traefik" + namespace = "kube-system" + repository = "https://traefik.github.io/charts" + chart = "traefik" + version = "~> 33.0" + atomic = true + + values = [yamlencode({ + ports = { + web = { + redirectTo = { port = "websecure" } + } + } + + ingressRoute = { + dashboard = { enabled = false } + } + + additionalArguments = [ + "--certificatesresolvers.letsencrypt.acme.email=goncalo.gr@proton.me", + "--certificatesresolvers.letsencrypt.acme.storage=/data/acme.json", + "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web", + ] + + persistence = { + enabled = true + size = "128Mi" + storageClass = "local-path" + } + + service = { + type = "LoadBalancer" + } + + resources = { + requests = { cpu = "50m", memory = "64Mi" } + limits = { cpu = "200m", memory = "128Mi" } + } + })] +} diff --git a/infrastructure/terraform/variables.tf b/infrastructure/terraform/variables.tf index 6b63255..86cc658 100644 --- a/infrastructure/terraform/variables.tf +++ b/infrastructure/terraform/variables.tf @@ -9,3 +9,9 @@ variable "enable_monitoring" { type = bool default = true } + +variable "domain" { + description = "Base domain for all ingress hostnames (e.g. gugagr.xyz). Subdomains are created per service." + type = string + default = "homelab.local" +}