feat: switch to gugagr.xyz with TLS via Let's Encrypt (#39)
Adds Traefik Helm release (kube-system) with ACME HTTP-01 challenge configured for Let's Encrypt, replacing the k3s-disabled bundled Traefik. Migrates all hostnames from *.homelab.local to *.gugagr.xyz and upgrades all ingresses to HTTPS with certresolver=letsencrypt annotations. Adds var.domain (default homelab.local) to Terraform so the domain is a single config point for monitoring and Gitea ingresses. Gateway reads DOMAIN env var at runtime — falls back to homelab.local so local k3d dev continues to work without changes. Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8436295bbc
commit
d4ccff518e
@ -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
|
||||
|
||||
@ -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: /
|
||||
|
||||
@ -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/
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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: /
|
||||
|
||||
@ -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: /
|
||||
|
||||
@ -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}")
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
41
infrastructure/terraform/traefik.tf
Normal file
41
infrastructure/terraform/traefik.tf
Normal file
@ -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" }
|
||||
}
|
||||
})]
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user