diff --git a/ansible/playbooks/deploy.yml b/ansible/playbooks/deploy.yml index c2e6c24..e5b95d0 100644 --- a/ansible/playbooks/deploy.yml +++ b/ansible/playbooks/deploy.yml @@ -1,6 +1,6 @@ --- # Deploy applications to client servers -# This playbook deploys Nextcloud and other applications +# This playbook deploys Authentik, Nextcloud, and other applications - name: Deploy applications to client servers hosts: all @@ -26,7 +26,13 @@ client_domain: "{{ client_secrets.client_domain }}" when: client_secrets.client_domain is defined + - name: Set Authentik domain from secrets + set_fact: + authentik_domain: "{{ client_secrets.authentik_domain }}" + when: client_secrets.authentik_domain is defined + roles: + - role: authentik - role: nextcloud post_tasks: @@ -35,4 +41,10 @@ msg: | Deployment complete for client: {{ client_name }} + Authentik SSO: https://{{ authentik_domain }} Nextcloud: https://nextcloud.{{ client_domain }} + + Next steps: + 1. Complete Authentik initial setup at: https://{{ authentik_domain }}/if/flow/initial-setup/ + 2. Create OAuth2/OIDC provider for Nextcloud in Authentik + 3. Configure Nextcloud to use Authentik for SSO diff --git a/ansible/roles/authentik/defaults/main.yml b/ansible/roles/authentik/defaults/main.yml new file mode 100644 index 0000000..883c42a --- /dev/null +++ b/ansible/roles/authentik/defaults/main.yml @@ -0,0 +1,25 @@ +--- +# Defaults for Authentik role + +# Authentik version +authentik_version: "2025.10.3" +authentik_image: "ghcr.io/goauthentik/server" + +# PostgreSQL configuration +authentik_db_user: "authentik" +authentik_db_name: "authentik" + +# Ports (internal to Docker network, exposed via Traefik) +authentik_http_port: 9000 +authentik_https_port: 9443 + +# Docker configuration +authentik_config_dir: "/opt/docker/authentik" +authentik_network: "authentik-internal" +authentik_traefik_network: "traefik" + +# Domain (set per client) +# authentik_domain: "auth.example.com" + +# Bootstrap settings +authentik_bootstrap: true diff --git a/ansible/roles/authentik/handlers/main.yml b/ansible/roles/authentik/handlers/main.yml new file mode 100644 index 0000000..44fbf57 --- /dev/null +++ b/ansible/roles/authentik/handlers/main.yml @@ -0,0 +1,7 @@ +--- +# Handlers for Authentik role + +- name: Restart Authentik + community.docker.docker_compose_v2: + project_src: "{{ authentik_config_dir }}" + state: restarted diff --git a/ansible/roles/authentik/tasks/bootstrap.yml b/ansible/roles/authentik/tasks/bootstrap.yml new file mode 100644 index 0000000..7f4a195 --- /dev/null +++ b/ansible/roles/authentik/tasks/bootstrap.yml @@ -0,0 +1,48 @@ +--- +# Bootstrap tasks for initial Authentik configuration + +- name: Check if bootstrap already completed + stat: + path: "{{ authentik_config_dir }}/.bootstrap_complete" + register: bootstrap_flag + +- name: Bootstrap Authentik instance + when: not bootstrap_flag.stat.exists + block: + - name: Wait for Authentik to be fully ready + uri: + url: "https://{{ authentik_domain }}/" + validate_certs: yes + status_code: [200, 302] + register: authentik_ready + until: authentik_ready.status in [200, 302] + retries: 30 + delay: 10 + + - name: Display bootstrap instructions + debug: + msg: | + ======================================== + Authentik is running! + ======================================== + + URL: https://{{ authentik_domain }} + + Initial Setup: + 1. Visit: https://{{ authentik_domain }}/if/flow/initial-setup/ + 2. Create admin account (username: akadmin recommended) + 3. Configure email settings in Admin UI + 4. Create OAuth2/OIDC provider for Nextcloud integration + + Documentation: https://docs.goauthentik.io + + - name: Mark bootstrap as complete + file: + path: "{{ authentik_config_dir }}/.bootstrap_complete" + state: touch + mode: '0600' + +- name: Bootstrap already completed + debug: + msg: "Authentik bootstrap already completed, skipping initialization" + when: bootstrap_flag.stat.exists diff --git a/ansible/roles/authentik/tasks/docker.yml b/ansible/roles/authentik/tasks/docker.yml new file mode 100644 index 0000000..e996c43 --- /dev/null +++ b/ansible/roles/authentik/tasks/docker.yml @@ -0,0 +1,46 @@ +--- +# Docker Compose setup for Authentik + +- name: Create Authentik configuration directory + file: + path: "{{ authentik_config_dir }}" + state: directory + mode: '0755' + +- name: Create Authentik internal network + community.docker.docker_network: + name: "{{ authentik_network }}" + driver: bridge + internal: yes + +- name: Deploy Authentik Docker Compose configuration + template: + src: docker-compose.authentik.yml.j2 + dest: "{{ authentik_config_dir }}/docker-compose.yml" + mode: '0644' + notify: Restart Authentik + +- name: Start Authentik services + community.docker.docker_compose_v2: + project_src: "{{ authentik_config_dir }}" + state: present + +- name: Wait for Authentik database to be ready + community.docker.docker_container_info: + name: authentik-db + register: db_container + until: db_container.container.State.Health.Status == "healthy" + retries: 30 + delay: 5 + changed_when: false + +- name: Wait for Authentik server to be healthy + uri: + url: "https://{{ authentik_domain }}/" + validate_certs: yes + status_code: [200, 302] + register: authentik_health + until: authentik_health.status in [200, 302] + retries: 30 + delay: 10 + changed_when: false diff --git a/ansible/roles/authentik/tasks/main.yml b/ansible/roles/authentik/tasks/main.yml new file mode 100644 index 0000000..b56e547 --- /dev/null +++ b/ansible/roles/authentik/tasks/main.yml @@ -0,0 +1,9 @@ +--- +# Main tasks file for Authentik role + +- name: Include Docker Compose setup + include_tasks: docker.yml + +- name: Include bootstrap setup + include_tasks: bootstrap.yml + when: authentik_bootstrap | default(true) diff --git a/ansible/roles/authentik/templates/docker-compose.authentik.yml.j2 b/ansible/roles/authentik/templates/docker-compose.authentik.yml.j2 new file mode 100644 index 0000000..cd22a80 --- /dev/null +++ b/ansible/roles/authentik/templates/docker-compose.authentik.yml.j2 @@ -0,0 +1,138 @@ +services: + authentik-db: + image: postgres:16-alpine + container_name: authentik-db + restart: unless-stopped + environment: + POSTGRES_DB: "{{ authentik_db_name }}" + POSTGRES_USER: "{{ authentik_db_user }}" + POSTGRES_PASSWORD: "{{ client_secrets.authentik_db_password }}" + + volumes: + - authentik-db-data:/var/lib/postgresql/data + + networks: + - {{ authentik_network }} + + healthcheck: + test: ["CMD-SHELL", "pg_isready -d {{ authentik_db_name }} -U {{ authentik_db_user }}"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 20s + + deploy: + resources: + limits: + memory: 512M + cpus: "0.5" + + authentik-server: + image: {{ authentik_image }}:{{ authentik_version }} + container_name: authentik-server + restart: unless-stopped + command: server + environment: + # PostgreSQL connection + AUTHENTIK_POSTGRESQL__HOST: authentik-db + AUTHENTIK_POSTGRESQL__NAME: "{{ authentik_db_name }}" + AUTHENTIK_POSTGRESQL__USER: "{{ authentik_db_user }}" + AUTHENTIK_POSTGRESQL__PASSWORD: "{{ client_secrets.authentik_db_password }}" + + # Secret key for encryption + AUTHENTIK_SECRET_KEY: "{{ client_secrets.authentik_secret_key }}" + + # Error reporting (optional) + AUTHENTIK_ERROR_REPORTING__ENABLED: "false" + + # Branding + AUTHENTIK_BRANDING__TITLE: "{{ client_name | title }} SSO" + + # Email configuration (optional, configure later) + # AUTHENTIK_EMAIL__HOST: "smtp.example.com" + # AUTHENTIK_EMAIL__PORT: "587" + # AUTHENTIK_EMAIL__USERNAME: "user@example.com" + # AUTHENTIK_EMAIL__PASSWORD: "password" + # AUTHENTIK_EMAIL__USE_TLS: "true" + # AUTHENTIK_EMAIL__FROM: "authentik@example.com" + + volumes: + - authentik-media:/media + - authentik-templates:/templates + + networks: + - {{ authentik_traefik_network }} + - {{ authentik_network }} + + depends_on: + authentik-db: + condition: service_healthy + + labels: + - "traefik.enable=true" + - "traefik.http.routers.authentik.rule=Host(`{{ authentik_domain }}`)" + - "traefik.http.routers.authentik.tls=true" + - "traefik.http.routers.authentik.tls.certresolver=letsencrypt" + - "traefik.http.routers.authentik.entrypoints=websecure" + - "traefik.http.services.authentik.loadbalancer.server.port={{ authentik_http_port }}" + # Security headers + - "traefik.http.routers.authentik.middlewares=authentik-headers" + - "traefik.http.middlewares.authentik-headers.headers.stsSeconds=31536000" + - "traefik.http.middlewares.authentik-headers.headers.stsIncludeSubdomains=true" + - "traefik.http.middlewares.authentik-headers.headers.stsPreload=true" + + deploy: + resources: + limits: + memory: 1G + cpus: "1.0" + + authentik-worker: + image: {{ authentik_image }}:{{ authentik_version }} + container_name: authentik-worker + restart: unless-stopped + command: worker + environment: + # PostgreSQL connection + AUTHENTIK_POSTGRESQL__HOST: authentik-db + AUTHENTIK_POSTGRESQL__NAME: "{{ authentik_db_name }}" + AUTHENTIK_POSTGRESQL__USER: "{{ authentik_db_user }}" + AUTHENTIK_POSTGRESQL__PASSWORD: "{{ client_secrets.authentik_db_password }}" + + # Secret key for encryption (must match server) + AUTHENTIK_SECRET_KEY: "{{ client_secrets.authentik_secret_key }}" + + # Error reporting (optional) + AUTHENTIK_ERROR_REPORTING__ENABLED: "false" + + volumes: + - authentik-media:/media + - authentik-templates:/templates + + networks: + - {{ authentik_network }} + + depends_on: + authentik-db: + condition: service_healthy + + deploy: + resources: + limits: + memory: 512M + cpus: "0.5" + +volumes: + authentik-db-data: + driver: local + authentik-media: + driver: local + authentik-templates: + driver: local + +networks: + {{ authentik_traefik_network }}: + external: true + {{ authentik_network }}: + driver: bridge + internal: true diff --git a/docs/architecture-decisions.md b/docs/architecture-decisions.md index 61c06d3..619d388 100644 --- a/docs/architecture-decisions.md +++ b/docs/architecture-decisions.md @@ -149,20 +149,58 @@ resource "hetznerdns_record" "client_a" { ## 4. Identity Provider -### Decision: Removed (previously Zitadel) +### Decision: Authentik (replacing Zitadel) -**Status:** Identity provider removed from architecture. +**Choice:** Authentik as the identity provider for SSO across all client installations. -**Reason for Removal:** -- Zitadel v2.63.7 has critical bugs with FirstInstance initialization -- ALL `ZITADEL_FIRSTINSTANCE_*` environment variables cause database migration errors -- Requires manual web UI setup for each instance (not scalable for multi-tenant deployment) +**Why Authentik:** + +| Factor | Authentik | Zitadel | Keycloak | +|--------|-----------|---------|----------| +| License | MIT (permissive) | AGPL 3.0 | Apache 2.0 | +| Setup Complexity | Simple Docker Compose | Complex FirstInstance bugs | Heavy Java setup | +| Database | PostgreSQL only | PostgreSQL only | Multiple options | +| Language | Python | Go | Java | +| Resource Usage | Lightweight | Lightweight | Heavy | +| Maturity | v2025.10 (stable) | v2.x (buggy) | Very mature | +| Architecture | Modern, API-first | Event-sourced | Traditional | + +**Key Advantages:** +- **Truly open source**: MIT license (most permissive OSI license) +- **Simple deployment**: Works out-of-box with Docker Compose, no manual wizard steps +- **Modern architecture**: Python-based, lightweight, API-first design +- **Comprehensive protocols**: SAML, OAuth2/OIDC, LDAP, RADIUS, SCIM +- **No Redis required** (as of 2025.10): All caching moved to PostgreSQL +- **Built-in workflows**: Customizable authentication flows and policies +- **Active development**: Regular releases, strong community + +**Deployment:** +```yaml +services: + authentik-server: + image: ghcr.io/goauthentik/server:2025.10.3 + command: server + environment: + AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY} + AUTHENTIK_POSTGRESQL__HOST: postgresql + depends_on: + - postgresql + + authentik-worker: + image: ghcr.io/goauthentik/server:2025.10.3 + command: worker + environment: + AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY} + AUTHENTIK_POSTGRESQL__HOST: postgresql + depends_on: + - postgresql +``` + +**Previous Choice (Zitadel):** +- Removed due to FirstInstance initialization bugs in v2.63.7 +- Required manual web UI setup (not scalable for multi-tenant) - See: https://github.com/zitadel/zitadel/issues/8791 -**Future Consideration:** -- May revisit with Authentik or other identity providers when needed -- Currently focusing on Nextcloud as standalone solution - --- ## 4. Backup Strategy