Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 6 additions & 94 deletions config/traefik/dynamic/middlewares.yml
Original file line number Diff line number Diff line change
@@ -1,32 +1,9 @@
# =============================================================================
# Traefik — Dynamic Middleware Configuration
# All middlewares defined here are available project-wide via @file provider.
#
# Usage in docker-compose labels:
# traefik.http.routers.<name>.middlewares=authentik@file,security-headers@file
# Traefik Dynamic Configuration — ForwardAuth Middleware
# Protects services that don't natively support OIDC via Authentik
# =============================================================================

http:
middlewares:

# -------------------------------------------------------------------------
# BasicAuth — Traefik Dashboard protection
# Generate hash: echo $(htpasswd -nb admin PASSWORD) | sed -e s/\$/\$\$/g
# Then set TRAEFIK_DASHBOARD_PASSWORD_HASH in .env
# -------------------------------------------------------------------------
traefik-auth:
basicAuth:
usersFile: /dynamic/.htpasswd
removeHeader: true # Strip Authorization header from upstream request

# -------------------------------------------------------------------------
# Authentik ForwardAuth
# Protects any service — redirects unauthenticated requests to SSO login.
# Requires: SSO stack running (stacks/sso/docker-compose.yml)
#
# Usage: add to any router:
# traefik.http.routers.<name>.middlewares=authentik@file
# -------------------------------------------------------------------------
authentik:
forwardAuth:
address: "http://authentik-server:9000/outpost.goauthentik.io/auth/traefik"
Expand All @@ -35,73 +12,8 @@ http:
- X-authentik-username
- X-authentik-groups
- X-authentik-email
- X-authentik-name
- X-authentik-uid
- X-authentik-jwt
- X-authentik-meta-jwks
- X-authentik-meta-outpost
- X-authentik-meta-provider
- X-authentik-meta-app
- X-authentik-meta-version

# -------------------------------------------------------------------------
# Security Headers — applied to all public-facing services
# Reference: https://securityheaders.com
# -------------------------------------------------------------------------
security-headers:
headers:
# HSTS: force HTTPS for 1 year, include subdomains
stsSeconds: 31536000
stsIncludeSubdomains: true
stsPreload: true
# Prevent clickjacking
frameDeny: false # false = allow iframes from same origin (Portainer embeds)
customFrameOptionsValue: "SAMEORIGIN"
# XSS protection
browserXssFilter: true
contentTypeNosniff: true
# Referrer policy
referrerPolicy: "strict-origin-when-cross-origin"
# Remove identifying headers
customResponseHeaders:
X-Powered-By: ""
Server: ""
# Content Security Policy (permissive default — tighten per-service)
contentSecurityPolicy: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:;"

# -------------------------------------------------------------------------
# Rate Limiting — protect against brute force / DDoS
# -------------------------------------------------------------------------
rate-limit:
rateLimit:
average: 100 # requests per second (average)
burst: 50 # burst allowance

# -------------------------------------------------------------------------
# IP Whitelist — restrict access to local network only
# Use for internal-only services (Portainer, Traefik dashboard, etc.)
# -------------------------------------------------------------------------
local-only:
ipAllowList:
sourceRange:
- "127.0.0.1/32"
- "10.0.0.0/8"
- "172.16.0.0/12"
- "192.168.0.0/16"

# -------------------------------------------------------------------------
# Compress responses
# -------------------------------------------------------------------------
compress:
compress:
excludedContentTypes:
- "text/event-stream"

# -------------------------------------------------------------------------
# Redirect www → non-www
# -------------------------------------------------------------------------
www-redirect:
redirectRegex:
regex: "^https://www\\.(.*)"
replacement: "https://${1}"
permanent: true
authentik-strip-prefix:
chain:
middlewares:
- authentik
197 changes: 197 additions & 0 deletions scripts/authentik-setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
#!/usr/bin/env bash
# =============================================================================
# HomeLab Stack — Authentik SSO Setup Script
# Creates OIDC providers for Grafana, Gitea, Nextcloud, Outline, Open WebUI, Portainer
# Requires: curl, jq
# Usage: ./scripts/authentik-setup.sh [--dry-run]
# =============================================================================
set -euo pipefail

SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
ROOT_DIR=$(dirname "$SCRIPT_DIR")

# Load .env
if [ -f "$ROOT_DIR/.env" ]; then
set -a; source "$ROOT_DIR/.env"; set +a
fi

DRY_RUN=false
[[ "${1:-}" == "--dry-run" ]] && DRY_RUN=true

RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
log_info() { echo -e "${GREEN}[OK]${RESET} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; }
log_error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
log_step() { echo; echo -e "${BOLD}${CYAN}==> $*${RESET}"; }

AUTHENTIK_URL="https://${AUTHENTIK_DOMAIN:-auth.${DOMAIN}}"
API_URL="$AUTHENTIK_URL/api/v3"
TOKEN="${AUTHENTIK_BOOTSTRAP_TOKEN:-}"

if [ -z "$TOKEN" ]; then
log_error "AUTHENTIK_BOOTSTRAP_TOKEN is not set in .env"
exit 1
fi

AUTH_HEADER="Authorization: Bearer $TOKEN"

get_default_flow() {
local designation="$1"
curl -sf "$API_URL/flows/instances/?designation=${designation}&ordering=slug" \
-H "$AUTH_HEADER" | jq -r '.results[0].pk'
}

get_signing_key() {
curl -sf "$API_URL/crypto/certificatekeypairs/?has_key=true&ordering=name" \
-H "$AUTH_HEADER" | jq -r '.results[0].pk'
}

create_oidc_provider() {
local name="$1"
local redirect_uri="$2"
local client_id_var="$3"
local client_secret_var="$4"

log_step "Creating OIDC provider: $name"

if $DRY_RUN; then
log_warn "[DRY RUN] Would create provider: $name"
log_info " Redirect URI: $redirect_uri"
return
fi

local flow_pk signing_key
flow_pk=$(get_default_flow authorize)
signing_key=$(get_signing_key)
local slug
slug=$(echo "$name" | tr '[:upper:]' '[:lower:]')

local payload
payload=$(jq -n \
--arg name "${name} Provider" \
--arg flow "$flow_pk" \
--arg uri "$redirect_uri" \
--arg key "$signing_key" \
'{
name: $name,
authorization_flow: $flow,
client_type: "confidential",
redirect_uris: $uri,
sub_mode: "hashed_user_id",
include_claims_in_id_token: true,
signing_key: $key
}')

local response
response=$(curl -sf -X POST "$API_URL/providers/oauth2/" \
-H "$AUTH_HEADER" \
-H "Content-Type: application/json" \
-d "$payload")

local provider_pk client_id client_secret
provider_pk=$(echo "$response" | jq -r '.pk')
client_id=$(echo "$response" | jq -r '.client_id')
client_secret=$(echo "$response" | jq -r '.client_secret')

log_info "Created provider: $name"
log_info " Client ID: $client_id"
log_info " Client Secret: $client_secret"
log_info " Redirect URI: $redirect_uri"

sed -i "s|^${client_id_var}=.*|${client_id_var}=${client_id}|" "$ROOT_DIR/.env"
sed -i "s|^${client_secret_var}=.*|${client_secret_var}=${client_secret}|" "$ROOT_DIR/.env"

local app_payload
app_payload=$(jq -n \
--arg name "$name" \
--arg slug "$slug" \
--argjson pk "$provider_pk" \
'{name: $name, slug: $slug, provider: $pk}')

curl -sf -X POST "$API_URL/core/applications/" \
-H "$AUTH_HEADER" \
-H "Content-Type: application/json" \
-d "$app_payload" > /dev/null

log_info " Application created: $name"
}

create_groups() {
log_step "Creating user groups"
for group in "homelab-admins" "homelab-users" "media-users"; do
if $DRY_RUN; then
log_warn "[DRY RUN] Would create group: $group"
continue
fi
curl -sf -X POST "$API_URL/core/groups/" \
-H "$AUTH_HEADER" \
-H "Content-Type: application/json" \
-d "{\"name\": \"$group\"}" > /dev/null 2>&1 || true
log_info "Group: $group"
done
}

# ------------------------------------------------------------------
# Wait for Authentik to be ready
# ------------------------------------------------------------------
log_step "Waiting for Authentik API..."
for i in $(seq 1 30); do
if curl -sf "$AUTHENTIK_URL/-/health/ready/" -o /dev/null 2>/dev/null; then
log_info "Authentik is ready"
break
fi
if [ "$i" -eq 30 ]; then
log_error "Authentik did not become ready in 150s"
exit 1
fi
echo -n "."
sleep 5
done

# ------------------------------------------------------------------
# Create groups
# ------------------------------------------------------------------
create_groups

# ------------------------------------------------------------------
# Create providers
# ------------------------------------------------------------------
create_oidc_provider \
"Grafana" \
"https://grafana.${DOMAIN}/login/generic_oauth" \
"GRAFANA_OAUTH_CLIENT_ID" \
"GRAFANA_OAUTH_CLIENT_SECRET"

create_oidc_provider \
"Gitea" \
"https://git.${DOMAIN}/user/oauth2/Authentik/callback" \
"GITEA_OAUTH_CLIENT_ID" \
"GITEA_OAUTH_CLIENT_SECRET"

create_oidc_provider \
"Nextcloud" \
"https://cloud.${DOMAIN}/apps/sociallogin/custom_oidc/callback" \
"NEXTCLOUD_OAUTH_CLIENT_ID" \
"NEXTCLOUD_OAUTH_CLIENT_SECRET"

create_oidc_provider \
"Outline" \
"https://outline.${DOMAIN}/auth/oidc.callback" \
"OUTLINE_OAUTH_CLIENT_ID" \
"OUTLINE_OAUTH_CLIENT_SECRET"

create_oidc_provider \
"Open WebUI" \
"https://ai.${DOMAIN}/oauth/oidc/callback" \
"OPENWEBUI_OAUTH_CLIENT_ID" \
"OPENWEBUI_OAUTH_CLIENT_SECRET"

create_oidc_provider \
"Portainer" \
"https://portainer.${DOMAIN}/" \
"PORTAINER_OAUTH_CLIENT_ID" \
"PORTAINER_OAUTH_CLIENT_SECRET"

log_step "All providers created. Credentials written to .env"
log_info "Authentik OIDC issuer: $AUTHENTIK_URL/application/o/<slug>/"
51 changes: 51 additions & 0 deletions scripts/nextcloud-oidc-setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# =============================================================================
# Nextcloud OIDC Setup via Social Login App
# Configures Nextcloud to use Authentik as OIDC provider
# Usage: ./scripts/nextcloud-oidc-setup.sh
# =============================================================================
set -euo pipefail

SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
ROOT_DIR=$(dirname "$SCRIPT_DIR")

if [ -f "$ROOT_DIR/.env" ]; then
set -a; source "$ROOT_DIR/.env"; set +a
fi

RED='\033[0;31m'; GREEN='\033[0;32m'; RESET='\033[0m'
log_info() { echo -e "${GREEN}[OK]${RESET} $*"; }
log_error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }

NEXTCLOUD_CONTAINER="nextcloud"

if ! docker ps --format '{{.Names}}' | grep -q "^${NEXTCLOUD_CONTAINER}$"; then
log_error "Nextcloud container is not running"
exit 1
fi

log_info "Installing Social Login app..."
docker exec -u www-data "$NEXTCLOUD_CONTAINER" php occ app:install sociallogin 2>/dev/null || true

log_info "Configuring OIDC provider..."
docker exec -u www-data "$NEXTCLOUD_CONTAINER" php occ config:app:set sociallogin custom_providers --value="$(cat <<EOF
{
"custom_oidc": [
{
"name": "Authentik",
"clientId": "${NEXTCLOUD_OAUTH_CLIENT_ID}",
"clientSecret": "${NEXTCLOUD_OAUTH_CLIENT_SECRET}",
"urlAuthorize": "https://${AUTHENTIK_DOMAIN}/application/o/authorize/",
"urlAccessToken": "https://${AUTHENTIK_DOMAIN}/application/o/token/",
"urlResourceOwnerDetails": "https://${AUTHENTIK_DOMAIN}/application/o/userinfo/",
"scope": "openid profile email",
"displayNameClaim": "name",
"groupsClaim": "groups",
"style": "keycloak"
}
]
}
EOF
)"

log_info "OIDC configured for Nextcloud"
8 changes: 7 additions & 1 deletion stacks/sso/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,16 @@ AUTHENTIK_REDIS_PASSWORD=
AUTHENTIK_BOOTSTRAP_EMAIL=admin@yourdomain.com
AUTHENTIK_BOOTSTRAP_PASSWORD=

# OAuth2 client credentials — filled by scripts/setup-authentik.sh
# OAuth2 client credentials — filled by scripts/authentik-setup.sh
GRAFANA_OAUTH_CLIENT_ID=
GRAFANA_OAUTH_CLIENT_SECRET=
GITEA_OAUTH_CLIENT_ID=
GITEA_OAUTH_CLIENT_SECRET=
NEXTCLOUD_OAUTH_CLIENT_ID=
NEXTCLOUD_OAUTH_CLIENT_SECRET=
OUTLINE_OAUTH_CLIENT_ID=
OUTLINE_OAUTH_CLIENT_SECRET=
OPENWEBUI_OAUTH_CLIENT_ID=
OPENWEBUI_OAUTH_CLIENT_SECRET=
PORTAINER_OAUTH_CLIENT_ID=
PORTAINER_OAUTH_CLIENT_SECRET=
Loading