# Homelab Auth + Portfolio: Authentik Forward-Auth + Homarr SSO + Portfolio Site (Terraform) ## Context Three services (Longhorn, Portainer, Prometheus) currently zero-auth. Goal: rebuild with Authentik forward-auth + Homarr dashboard + personal portfolio site. Infrastructure as Terraform (not Helm). Authentik bootstrap (Proxy Providers, groups) via Python helper. Homarr + Portfolio deployed as K8s manifests via Terraform modules. --- ## Track A — Authentik Forward-Auth (Setup via Python, Ingress via TF) Authentik cluster already running (deployed separately). Python helper script registers Proxy Providers + groups: **`k8s/talos-iam/register_proxy_app.py`** (mirrors `register_oauth_app.py`): - Queries embedded outpost, creates `/providers/proxy/` (mode: `forward_single`) - Creates `/core/applications/` entry - Creates/binds Authentik group - Patches outpost `providers` list ```bash python3 k8s/talos-iam/register_proxy_app.py \ --service-name longhorn --namespace longhorn-system \ --external-host longhorn.riotpiao.homelab.com \ --add-group infra-admins # Repeat for portainer, prometheus ``` **Ingress wiring (Terraform):** `modules/ingress/protected-services.tf`: ```hcl resource "kubernetes_ingress_v1" "protected_services" { for_each = var.protected_services metadata { name = each.key namespace = each.value.namespace annotations = { "nginx.ingress.kubernetes.io/auth-url" = "http://authentik-server.iam.svc.cluster.local/outpost.goauthentik.io/auth/nginx" "nginx.ingress.kubernetes.io/auth-signin" = "https://authentik.riotpiao.homelab.com/outpost.goauthentik.io/start?rd=$scheme://$http_host$escaped_request_uri" "nginx.ingress.kubernetes.io/auth-response-headers" = "Set-Cookie,X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid" } } spec { ingress_class_name = "nginx" rule { host = each.value.host http { path { path = "/" path_type = "Prefix" backend { service { name = each.value.service_name port { number = each.value.service_port } } } } } } } } ``` **NetworkPolicy (per-service, Terraform):** `modules/network-policy/backend-isolation.tf`: ```hcl resource "kubernetes_network_policy" "backend_only_from_ingress" { for_each = var.isolated_services metadata { name = "${each.key}-deny-except-ingress" namespace = each.value.namespace } spec { pod_selector { match_labels = each.value.pod_selector } policy_types = ["Ingress"] ingress { from { namespace_selector { match_labels = { name = "ingress-nginx" } } } } } } ``` --- ## Track B — Homarr (Terraform Deployment) **Homarr Helm chart wrapper (Terraform):** `modules/homarr/main.tf`: ```hcl resource "kubernetes_namespace" "homarr" { metadata { name = "dashboard" labels = { "managed_by" = "terraform" "reloader" = "enabled" } } } resource "kubernetes_secret" "homarr_oidc" { metadata { name = "homarr-oidc" namespace = kubernetes_namespace.homarr.metadata[0].name } data = { AUTH_OIDC_CLIENT_SECRET = var.homarr_oidc_client_secret } type = "Opaque" } resource "helm_release" "homarr" { name = "homarr" namespace = kubernetes_namespace.homarr.metadata[0].name chart = "homarr" repository = "https://homarr-labs.github.io/charts/" version = var.homarr_version set { name = "env.AUTH_PROVIDERS" value = "oidc" } set { name = "env.AUTH_OIDC_CLIENT_ID" value = "homarr" } set { name = "env.AUTH_OIDC_ISSUER" value = "https://authentik.riotpiao.homelab.com/application/o/homarr/" } set { name = "env.AUTH_OIDC_URI" value = "https://authentik.riotpiao.homelab.com/application/o/homarr/.well-known/openid-configuration" } set { name = "env.AUTH_OIDC_GROUPS_ATTRIBUTE" value = "groups" } set { name = "envFrom[0].secretRef.name" value = kubernetes_secret.homarr_oidc.metadata[0].name } set { name = "persistence.enabled" value = "true" } set { name = "persistence.storageClass" value = "longhorn" } set { name = "persistence.size" value = "2Gi" } depends_on = [kubernetes_secret.homarr_oidc] } ``` **Homarr Ingress (no auth, Homarr handles login):** `modules/homarr/ingress.tf`: ```hcl resource "kubernetes_ingress_v1" "homarr" { metadata { name = "homarr" namespace = kubernetes_namespace.homarr.metadata[0].name annotations = { "cert-manager.io/cluster-issuer" = "letsencrypt-prod" } } spec { ingress_class_name = "nginx" tls { hosts = ["homarr.riotpiao.homelab.com"] secret_name = "homarr-tls" } rule { host = "homarr.riotpiao.homelab.com" http { path { path = "/" path_type = "Prefix" backend { service { name = helm_release.homarr.name port { number = 3000 } } } } } } } } ``` **Authentik setup for Homarr (Python):** ```bash python3 k8s/talos-iam/register_oauth_app.py \ --service-name homarr \ --namespace dashboard \ --redirect-uri "https://homarr.riotpiao.homelab.com/api/auth/callback/oidc" \ --add-group homarr-infra-admins ``` Then attach `groups` scope mapping (manual Authentik UI or separate Terraform provider if available). **Homarr board setup (manual runbook in `k8s/homarr/README.md`):** - Create groups: Infra (Longhorn/Portainer/Prometheus), Platform (Grafana/Vault), Workflows (Temporal/kmsvc) - Each tile deep-links to existing ingress - Restrict Infra group visibility to `infra-admins` --- ## Track C — Portfolio Site (Terraform Deployment) Portfolio source repo: `~/workplace/riotpiao` (Next.js, standalone output). **Portfolio Helm chart (Terraform wrapper):** `modules/portfolio/main.tf`: ```hcl resource "kubernetes_namespace" "portfolio" { metadata { name = "portfolio" labels = { "managed_by" = "terraform" } } } resource "helm_release" "portfolio" { name = "portfolio" namespace = kubernetes_namespace.portfolio.metadata[0].name chart = "./k8s/portfolio" # Local chart from homelab repo set { name = "image.repository" value = var.portfolio_image_repo } set { name = "image.tag" value = var.portfolio_image_tag } set { name = "replicaCount" value = 2 } set { name = "resources.requests.cpu" value = "100m" } set { name = "resources.requests.memory" value = "128Mi" } set { name = "resources.limits.cpu" value = "500m" } set { name = "resources.limits.memory" value = "512Mi" } } ``` **Portfolio Ingress (public, no auth):** `modules/portfolio/ingress.tf`: ```hcl resource "kubernetes_ingress_v1" "portfolio" { metadata { name = "portfolio" namespace = kubernetes_namespace.portfolio.metadata[0].name annotations = { "cert-manager.io/cluster-issuer" = "letsencrypt-prod" } } spec { ingress_class_name = "nginx" tls { hosts = ["portfolio.riotpiao.homelab.com"] secret_name = "portfolio-tls" } rule { host = "portfolio.riotpiao.homelab.com" http { path { path = "/" path_type = "Prefix" backend { service { name = helm_release.portfolio.name port { number = 3000 } } } } } } } } ``` **Portfolio Helm chart structure** (local, in homelab repo): ``` k8s/portfolio/ ├── Chart.yaml ├── values.yaml ├── templates/ │ ├── deployment.yaml │ ├── service.yaml │ └── _helpers.tpl └── README.md ``` **Build + push script** (portfolio repo): `~/workplace/riotpiao/build.sh`: ```bash #!/bin/bash set -euo pipefail REGISTRY="forgejo.riotpiao.homelab.com" IMAGE="${REGISTRY}/rock/portfolio:latest" docker buildx build --platform linux/amd64 -t "${IMAGE}" . docker push "${IMAGE}" echo "✓ Pushed ${IMAGE}" ``` --- ## Terraform Structure ``` ~/workplace/homelab-terraform/ (or extend existing homelab repo) ├── main.tf # K8s provider config ├── variables.tf # Inputs ├── outputs.tf │ ├── modules/ │ ├── ingress/ │ │ ├── protected-services.tf # Longhorn, Portainer, Prometheus │ │ └── variables.tf │ │ │ ├── homarr/ │ │ ├── main.tf # Namespace, Secret, Helm release │ │ ├── ingress.tf # Public Homarr ingress │ │ └── variables.tf │ │ │ ├── portfolio/ │ │ ├── main.tf # Namespace, Helm release │ │ ├── ingress.tf # Public portfolio ingress │ │ └── variables.tf │ │ │ └── network-policy/ │ └── backend-isolation.tf # Deny ingress except nginx │ ├── environments/ │ └── prod.tfvars # Domain, image repos, versions │ └── .terraform.lock.hcl ``` **Main entry point:** `main.tf`: ```hcl terraform { required_providers { kubernetes = { source = "hashicorp/kubernetes" version = "~> 2.25" } helm = { source = "hashicorp/helm" version = "~> 2.12" } } backend "local" { path = "terraform.tfstate" } } provider "kubernetes" { host = var.cluster_endpoint cluster_ca_certificate = base64decode(var.cluster_ca_cert) token = var.cluster_token } provider "helm" { kubernetes { host = var.cluster_endpoint cluster_ca_certificate = base64decode(var.cluster_ca_cert) token = var.cluster_token } } module "ingress" { source = "./modules/ingress" protected_services = var.protected_services } module "homarr" { source = "./modules/homarr" homarr_version = var.homarr_version homarr_oidc_client_secret = var.homarr_oidc_client_secret } module "portfolio" { source = "./modules/portfolio" portfolio_image_repo = var.portfolio_image_repo portfolio_image_tag = var.portfolio_image_tag } module "network_policy" { source = "./modules/network-policy" isolated_services = var.isolated_services } ``` **`variables.tf`:** ```hcl variable "cluster_endpoint" { type = string description = "K8s API endpoint" } variable "cluster_ca_cert" { type = string sensitive = true } variable "cluster_token" { type = string sensitive = true } variable "protected_services" { type = map(object({ host = string namespace = string service_name = string service_port = number })) default = { "longhorn" = { host = "longhorn.riotpiao.homelab.com" namespace = "longhorn-system" service_name = "longhorn-frontend" service_port = 80 } "portainer" = { host = "portainer.riotpiao.homelab.com" namespace = "dashboard" service_name = "portainer" service_port = 9000 } "prometheus" = { host = "prometheus.riotpiao.homelab.com" namespace = "monitoring" service_name = "prometheus-operated" service_port = 9090 } } } variable "homarr_version" { type = string default = "~1" } variable "homarr_oidc_client_secret" { type = string sensitive = true description = "From Authentik provider" } variable "portfolio_image_repo" { type = string default = "forgejo.riotpiao.homelab.com/rock/portfolio" } variable "portfolio_image_tag" { type = string default = "latest" } variable "isolated_services" { type = map(object({ namespace = string pod_selector = map(string) })) default = { "longhorn" = { namespace = "longhorn-system" pod_selector = { "app.kubernetes.io/name" = "longhorn" } } "portainer" = { namespace = "dashboard" pod_selector = { "app" = "portainer" } } "prometheus" = { namespace = "monitoring" pod_selector = { "app.kubernetes.io/name" = "prometheus" } } } } ``` --- ## Implementation Sequencing 1. **Authentik setup (Python):** - Register Proxy Providers (Longhorn, Portainer, Prometheus) - Create groups (infra-admins, homarr-infra-admins) - Verify `/outpost.goauthentik.io/auth/nginx` endpoint accessible 2. **Terraform deploy (parallel):** - Ingress + NetworkPolicies (protected services) - Homarr (depends on Authentik, has own OIDC) - Portfolio (independent, public) 3. **Portfolio build + push:** - `cd ~/workplace/riotpiao && ./build.sh` - Verify image in Forgejo registry 4. **Terraform apply:** ```bash terraform init terraform plan terraform apply ``` 5. **Verification:** - `curl -I https://longhorn.riotpiao.homelab.com` → 302 to Authentik (unauthenticated) - Login as `infra-admins` member → Longhorn UI loads - `curl -I https://homarr.riotpiao.homelab.com` → OIDC sign-in page - Login → Homarr boards visible - `curl -I https://portfolio.riotpiao.homelab.com` → 200, no redirect --- ## Files to Create **In homelab repo:** - `terraform.tfvars` (git-ignored) - `main.tf`, `variables.tf`, `outputs.tf` - `modules/{ingress,homarr,portfolio,network-policy}/*` - `k8s/homarr/README.md` (board setup runbook) - `k8s/portfolio/Chart.yaml`, `templates/*` **In portfolio repo (`~/workplace/riotpiao`):** - `app/layout.tsx`, `app/page.tsx`, `app/about/page.tsx` - `components/{Header,Footer,ProjectCard}.tsx` - `content/{projects,experience,metadata}.ts` - `styles/globals.css` - `Dockerfile`, `build.sh`, `next.config.js` - `package.json` (Next.js 15 + Tailwind) --- ## Success Criteria - [ ] Protected services (Longhorn/Portainer/Prometheus) redirect unauthenticated to Authentik - [ ] Infra-admins group members see UI; non-members denied - [ ] Homarr OIDC login works; boards render for authorized groups - [ ] Portfolio site accessible without auth; project grid + about page live - [ ] All deployed via `terraform apply` (no manual kubectl) - [ ] Terraform state tracks 100% of resources