14 KiB
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
providerslist
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:
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:
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:
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:
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):
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:
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:
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:
#!/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:
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:
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
-
Authentik setup (Python):
- Register Proxy Providers (Longhorn, Portainer, Prometheus)
- Create groups (infra-admins, homarr-infra-admins)
- Verify
/outpost.goauthentik.io/auth/nginxendpoint accessible
-
Terraform deploy (parallel):
- Ingress + NetworkPolicies (protected services)
- Homarr (depends on Authentik, has own OIDC)
- Portfolio (independent, public)
-
Portfolio build + push:
cd ~/workplace/riotpiao && ./build.sh- Verify image in Forgejo registry
-
Terraform apply:
terraform init terraform plan terraform apply -
Verification:
curl -I https://longhorn.riotpiao.homelab.com→ 302 to Authentik (unauthenticated)- Login as
infra-adminsmember → 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.tfmodules/{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.tsxcomponents/{Header,Footer,ProjectCard}.tsxcontent/{projects,experience,metadata}.tsstyles/globals.cssDockerfile,build.sh,next.config.jspackage.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