diff --git a/.env.example b/.env.example index 3165bff..3979783 100644 --- a/.env.example +++ b/.env.example @@ -22,6 +22,13 @@ KABOOT_BACKEND_PORT=3001 # ============================================================================== AUTHENTIK_ERROR_REPORTING=false +# ============================================================================== +# OPTIONAL - Authentik Bootstrap (for automated setup) +# Generate with: openssl rand -base64 36 | tr -d '\n' +# ============================================================================== +AUTHENTIK_BOOTSTRAP_PASSWORD= +AUTHENTIK_BOOTSTRAP_TOKEN= + # ============================================================================== # OPTIONAL - OIDC (Override if using custom domain) # ============================================================================== diff --git a/.gitignore b/.gitignore index 5ae49c6..29eb863 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,9 @@ dist-ssr .env.*.local .env.test +# Local Caddy config (use Caddyfile.example as template) +Caddyfile + # Authentik volumes (keep structure, ignore data) authentik/media/* !authentik/media/.gitkeep diff --git a/Caddyfile.example b/Caddyfile.example index 58cf634..a0d44bb 100644 --- a/Caddyfile.example +++ b/Caddyfile.example @@ -16,5 +16,11 @@ kaboot.example.com { } auth.example.com { - reverse_proxy authentik-server:9000 + reverse_proxy authentik-server:9000 { + header_up X-Forwarded-Proto {scheme} + header_up X-Forwarded-Host {host} + transport http { + keepalive 30s + } + } } diff --git a/authentik/blueprints/kaboot-setup-production.yaml.example b/authentik/blueprints/kaboot-setup-production.yaml.example new file mode 100644 index 0000000..3568792 --- /dev/null +++ b/authentik/blueprints/kaboot-setup-production.yaml.example @@ -0,0 +1,216 @@ +# Kaboot Authentik Blueprint - PRODUCTION +# Copy this file to kaboot-setup-production.yaml and update the domain. +# +# IMPORTANT: Remove or rename kaboot-setup.yaml when using this file +# to avoid conflicting configurations. +# +# Prerequisites: +# - Set AUTHENTIK_BOOTSTRAP_PASSWORD and AUTHENTIK_BOOTSTRAP_TOKEN in .env +# - Mount this directory to /blueprints/custom in docker-compose.yml +# - Update KABOOT_DOMAIN and AUTH_DOMAIN below with your actual domains +# +# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json +--- +version: 1 +metadata: + name: Kaboot Application Setup (Production) + labels: + blueprints.goauthentik.io/description: "Complete Kaboot OAuth2/OIDC setup for production" + +context: + kaboot_domain: kaboot.example.com + auth_domain: auth.example.com + +entries: + # ═══════════════════════════════════════════════════════════════════════════════ + # GROUPS + # ═══════════════════════════════════════════════════════════════════════════════ + + - id: kaboot-users-group + model: authentik_core.group + identifiers: + name: kaboot-users + attrs: + name: kaboot-users + + # ═══════════════════════════════════════════════════════════════════════════════ + # OAUTH2/OIDC PROVIDER + # ═══════════════════════════════════════════════════════════════════════════════ + + - id: kaboot-oauth2-provider + model: authentik_providers_oauth2.oauth2provider + identifiers: + name: Kaboot OAuth2 + attrs: + name: Kaboot OAuth2 + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + client_type: public + client_id: kaboot-spa + redirect_uris: + - url: !Format ["https://%s/callback", !Context kaboot_domain] + matching_mode: strict + - url: !Format ["https://%s/silent-renew.html", !Context kaboot_domain] + matching_mode: strict + - url: !Format ["https://%s", !Context kaboot_domain] + matching_mode: strict + access_code_validity: minutes=1 + access_token_validity: minutes=30 + refresh_token_validity: days=30 + sub_mode: hashed_user_id + include_claims_in_id_token: true + issuer_mode: per_provider + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, offline_access]] + + # ═══════════════════════════════════════════════════════════════════════════════ + # APPLICATION + # ═══════════════════════════════════════════════════════════════════════════════ + + - id: kaboot-application + model: authentik_core.application + identifiers: + slug: kaboot + attrs: + name: Kaboot + slug: kaboot + provider: !KeyOf kaboot-oauth2-provider + policy_engine_mode: any + meta_launch_url: !Format ["https://%s", !Context kaboot_domain] + + - id: kaboot-group-policy-binding + model: authentik_policies.policybinding + identifiers: + target: !KeyOf kaboot-application + group: !KeyOf kaboot-users-group + attrs: + order: 0 + enabled: true + negate: false + timeout: 30 + + # ═══════════════════════════════════════════════════════════════════════════════ + # PASSWORD POLICY + # ═══════════════════════════════════════════════════════════════════════════════ + + - id: password-complexity-policy + model: authentik_policies_password.passwordpolicy + identifiers: + name: password-complexity + attrs: + name: password-complexity + password_field: password + length_min: 8 + amount_uppercase: 1 + amount_lowercase: 1 + amount_digits: 1 + error_message: "Password must be at least 8 characters with 1 uppercase, 1 lowercase, and 1 digit." + + # ═══════════════════════════════════════════════════════════════════════════════ + # ENROLLMENT STAGES + # ═══════════════════════════════════════════════════════════════════════════════ + + - id: enrollment-prompt-stage + model: authentik_stages_prompt.promptstage + identifiers: + name: enrollment-prompt + attrs: + name: enrollment-prompt + fields: + - !Find [authentik_stages_prompt.prompt, [name, default-source-enrollment-field-username]] + - !Find [authentik_stages_prompt.prompt, [name, default-user-settings-field-email]] + - !Find [authentik_stages_prompt.prompt, [name, default-password-change-field-password]] + - !Find [authentik_stages_prompt.prompt, [name, default-password-change-field-password-repeat]] + validation_policies: + - !KeyOf password-complexity-policy + + - id: enrollment-user-write-stage + model: authentik_stages_user_write.userwritestage + identifiers: + name: enrollment-user-write + attrs: + name: enrollment-user-write + user_creation_mode: always_create + create_users_as_inactive: false + create_users_group: !KeyOf kaboot-users-group + + - id: enrollment-user-login-stage + model: authentik_stages_user_login.userloginstage + identifiers: + name: enrollment-user-login + attrs: + name: enrollment-user-login + session_duration: hours=24 + remember_me_offset: days=30 + network_binding: no_binding + geoip_binding: no_binding + terminate_other_sessions: false + + # ═══════════════════════════════════════════════════════════════════════════════ + # ENROLLMENT FLOW + # ═══════════════════════════════════════════════════════════════════════════════ + + - id: enrollment-flow + model: authentik_flows.flow + identifiers: + slug: enrollment-flow + attrs: + name: Enrollment Flow + title: Sign Up + slug: enrollment-flow + designation: enrollment + authentication: none + + - id: enrollment-flow-prompt-binding + model: authentik_flows.flowstagebinding + identifiers: + target: !KeyOf enrollment-flow + stage: !KeyOf enrollment-prompt-stage + attrs: + order: 10 + evaluate_on_plan: true + re_evaluate_policies: false + invalid_response_action: retry + + - id: enrollment-flow-user-write-binding + model: authentik_flows.flowstagebinding + identifiers: + target: !KeyOf enrollment-flow + stage: !KeyOf enrollment-user-write-stage + attrs: + order: 20 + evaluate_on_plan: true + re_evaluate_policies: false + invalid_response_action: retry + + - id: enrollment-flow-user-login-binding + model: authentik_flows.flowstagebinding + identifiers: + target: !KeyOf enrollment-flow + stage: !KeyOf enrollment-user-login-stage + attrs: + order: 30 + evaluate_on_plan: true + re_evaluate_policies: false + invalid_response_action: retry + + # ═══════════════════════════════════════════════════════════════════════════════ + # LINK ENROLLMENT FLOW TO DEFAULT LOGIN + # ═══════════════════════════════════════════════════════════════════════════════ + + - id: update-identification-stage + model: authentik_stages_identification.identificationstage + identifiers: + name: default-authentication-identification + attrs: + enrollment_flow: !KeyOf enrollment-flow + user_fields: + - email + - username + case_insensitive_matching: true + show_matched_user: true + show_source_labels: false + pretend_user_exists: true diff --git a/authentik/blueprints/kaboot-setup.yaml b/authentik/blueprints/kaboot-setup.yaml new file mode 100644 index 0000000..8aee45f --- /dev/null +++ b/authentik/blueprints/kaboot-setup.yaml @@ -0,0 +1,257 @@ +# Kaboot Authentik Blueprint +# This blueprint automatically configures all Authentik resources needed for Kaboot. +# Place in authentik/blueprints/ and it will be auto-imported on container startup. +# +# Prerequisites: +# - Set AUTHENTIK_BOOTSTRAP_PASSWORD and AUTHENTIK_BOOTSTRAP_TOKEN in .env +# - Mount this directory to /blueprints/custom in docker-compose.yml +# +# +# What this creates: +# - Kaboot OAuth2/OIDC Provider (public client) +# - Kaboot Application +# - kaboot-users Group +# - Enrollment flow with prompt, user write, and login stages +# - Password complexity policy +# - Test user (kaboottest) - for manual browser testing +# - Service account (kaboot-test-service) - for API testing +# +# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json +--- +version: 1 +metadata: + name: Kaboot Application Setup + labels: + blueprints.goauthentik.io/description: "Complete Kaboot OAuth2/OIDC setup with enrollment flow" + +entries: + # ═══════════════════════════════════════════════════════════════════════════════ + # GROUPS + # ═══════════════════════════════════════════════════════════════════════════════ + + - id: kaboot-users-group + model: authentik_core.group + identifiers: + name: kaboot-users + attrs: + name: kaboot-users + + # ═══════════════════════════════════════════════════════════════════════════════ + # OAUTH2/OIDC PROVIDER + # ═══════════════════════════════════════════════════════════════════════════════ + + - id: kaboot-oauth2-provider + model: authentik_providers_oauth2.oauth2provider + identifiers: + name: Kaboot OAuth2 + attrs: + name: Kaboot OAuth2 + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + client_type: public + client_id: kaboot-spa + redirect_uris: + - url: http://localhost:5173/callback + matching_mode: strict + - url: http://localhost:5173/silent-renew.html + matching_mode: strict + - url: http://localhost:5173 + matching_mode: strict + access_code_validity: minutes=1 + access_token_validity: minutes=30 + refresh_token_validity: days=30 + sub_mode: hashed_user_id + include_claims_in_id_token: true + issuer_mode: per_provider + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, offline_access]] + + # ═══════════════════════════════════════════════════════════════════════════════ + # APPLICATION + # ═══════════════════════════════════════════════════════════════════════════════ + + - id: kaboot-application + model: authentik_core.application + identifiers: + slug: kaboot + attrs: + name: Kaboot + slug: kaboot + provider: !KeyOf kaboot-oauth2-provider + policy_engine_mode: any + meta_launch_url: http://localhost:5173 + + - id: kaboot-group-policy-binding + model: authentik_policies.policybinding + identifiers: + target: !KeyOf kaboot-application + group: !KeyOf kaboot-users-group + attrs: + order: 0 + enabled: true + negate: false + timeout: 30 + + # ═══════════════════════════════════════════════════════════════════════════════ + # PASSWORD POLICY + # ═══════════════════════════════════════════════════════════════════════════════ + + - id: password-complexity-policy + model: authentik_policies_password.passwordpolicy + identifiers: + name: password-complexity + attrs: + name: password-complexity + password_field: password + length_min: 8 + amount_uppercase: 1 + amount_lowercase: 1 + amount_digits: 1 + error_message: "Password must be at least 8 characters with 1 uppercase, 1 lowercase, and 1 digit." + + # ═══════════════════════════════════════════════════════════════════════════════ + # ENROLLMENT STAGES + # ═══════════════════════════════════════════════════════════════════════════════ + + - id: enrollment-prompt-stage + model: authentik_stages_prompt.promptstage + identifiers: + name: enrollment-prompt + attrs: + name: enrollment-prompt + fields: + - !Find [authentik_stages_prompt.prompt, [name, default-source-enrollment-field-username]] + - !Find [authentik_stages_prompt.prompt, [name, default-user-settings-field-email]] + - !Find [authentik_stages_prompt.prompt, [name, default-password-change-field-password]] + - !Find [authentik_stages_prompt.prompt, [name, default-password-change-field-password-repeat]] + validation_policies: + - !KeyOf password-complexity-policy + + - id: enrollment-user-write-stage + model: authentik_stages_user_write.userwritestage + identifiers: + name: enrollment-user-write + attrs: + name: enrollment-user-write + user_creation_mode: always_create + create_users_as_inactive: false + create_users_group: !KeyOf kaboot-users-group + + - id: enrollment-user-login-stage + model: authentik_stages_user_login.userloginstage + identifiers: + name: enrollment-user-login + attrs: + name: enrollment-user-login + session_duration: hours=24 + remember_me_offset: days=30 + network_binding: no_binding + geoip_binding: no_binding + terminate_other_sessions: false + + # ═══════════════════════════════════════════════════════════════════════════════ + # ENROLLMENT FLOW + # ═══════════════════════════════════════════════════════════════════════════════ + + - id: enrollment-flow + model: authentik_flows.flow + identifiers: + slug: enrollment-flow + attrs: + name: Enrollment Flow + title: Sign Up + slug: enrollment-flow + designation: enrollment + authentication: none + + - id: enrollment-flow-prompt-binding + model: authentik_flows.flowstagebinding + identifiers: + target: !KeyOf enrollment-flow + stage: !KeyOf enrollment-prompt-stage + attrs: + order: 10 + evaluate_on_plan: true + re_evaluate_policies: false + invalid_response_action: retry + + - id: enrollment-flow-user-write-binding + model: authentik_flows.flowstagebinding + identifiers: + target: !KeyOf enrollment-flow + stage: !KeyOf enrollment-user-write-stage + attrs: + order: 20 + evaluate_on_plan: true + re_evaluate_policies: false + invalid_response_action: retry + + - id: enrollment-flow-user-login-binding + model: authentik_flows.flowstagebinding + identifiers: + target: !KeyOf enrollment-flow + stage: !KeyOf enrollment-user-login-stage + attrs: + order: 30 + evaluate_on_plan: true + re_evaluate_policies: false + invalid_response_action: retry + + # ═══════════════════════════════════════════════════════════════════════════════ + # LINK ENROLLMENT FLOW TO DEFAULT LOGIN + # ═══════════════════════════════════════════════════════════════════════════════ + + - id: update-identification-stage + model: authentik_stages_identification.identificationstage + identifiers: + name: default-authentication-identification + attrs: + enrollment_flow: !KeyOf enrollment-flow + user_fields: + - email + - username + case_insensitive_matching: true + show_matched_user: true + show_source_labels: false + pretend_user_exists: true + + # ═══════════════════════════════════════════════════════════════════════════════ + # TEST USER (for manual browser testing) + # ═══════════════════════════════════════════════════════════════════════════════ + + - id: kaboot-test-user + model: authentik_core.user + identifiers: + username: kaboottest + attrs: + username: kaboottest + name: Kaboot Test + email: kaboottest@test.com + path: users + is_active: true + groups: + - !KeyOf kaboot-users-group + # Note: Password must be set manually via UI or API after blueprint import + # Run: docker compose exec authentik-server ak setpassword kaboottest + + # ═══════════════════════════════════════════════════════════════════════════════ + # SERVICE ACCOUNT (for API/automated testing) + # ═══════════════════════════════════════════════════════════════════════════════ + + - id: kaboot-test-service-account + model: authentik_core.user + identifiers: + username: kaboot-test-service + attrs: + username: kaboot-test-service + name: Kaboot Test Service + path: users + type: service_account + is_active: true + groups: + - !KeyOf kaboot-users-group + # Note: App password must be created via UI or API after blueprint import + # See docs/AUTHENTIK_SETUP.md for instructions diff --git a/docker-compose.caddy.yml b/docker-compose.caddy.yml index 32093f8..b3a90af 100644 --- a/docker-compose.caddy.yml +++ b/docker-compose.caddy.yml @@ -1,15 +1,16 @@ # Caddy Reverse Proxy for Kaboot Production # # This compose file adds Caddy as a reverse proxy with automatic HTTPS. -# Use with the main docker-compose.yml using the -f flag. +# Use with docker-compose.prod.yml using the -f flag. # # Usage: -# docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d +# docker compose -f docker-compose.prod.yml -f docker-compose.caddy.yml up -d # # Prerequisites: -# 1. Create a Caddyfile in the project root (see docs/PRODUCTION.md) +# 1. Copy Caddyfile.example to Caddyfile and update domains # 2. Build the frontend: npm run build # 3. Update your domain DNS to point to your server +# 4. See docs/PRODUCTION.md for full instructions services: caddy: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..26c3c6d --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,111 @@ +services: + postgresql: + image: docker.io/library/postgres:16-alpine + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"] + start_period: 20s + interval: 30s + timeout: 5s + retries: 5 + volumes: + - postgresql-data:/var/lib/postgresql/data + environment: + POSTGRES_PASSWORD: ${PG_PASS:?database password required} + POSTGRES_USER: ${PG_USER:-authentik} + POSTGRES_DB: ${PG_DB:-authentik} + networks: + - kaboot-network + + redis: + image: docker.io/library/redis:alpine + restart: unless-stopped + command: --save 60 1 --loglevel warning + healthcheck: + test: ["CMD-SHELL", "redis-cli ping | grep PONG"] + start_period: 20s + interval: 30s + timeout: 3s + retries: 5 + volumes: + - redis-data:/data + networks: + - kaboot-network + + authentik-server: + image: ghcr.io/goauthentik/server:2025.2 + restart: unless-stopped + command: server + environment: + AUTHENTIK_REDIS__HOST: redis + AUTHENTIK_POSTGRESQL__HOST: postgresql + AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik} + AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik} + AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS} + AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY} + AUTHENTIK_BOOTSTRAP_PASSWORD: ${AUTHENTIK_BOOTSTRAP_PASSWORD:-} + AUTHENTIK_BOOTSTRAP_TOKEN: ${AUTHENTIK_BOOTSTRAP_TOKEN:-} + volumes: + - ./authentik/media:/media + - ./authentik/custom-templates:/templates + - ./authentik/blueprints:/blueprints/custom + depends_on: + postgresql: + condition: service_healthy + redis: + condition: service_healthy + networks: + - kaboot-network + + authentik-worker: + image: ghcr.io/goauthentik/server:2025.2 + restart: unless-stopped + command: worker + environment: + AUTHENTIK_REDIS__HOST: redis + AUTHENTIK_POSTGRESQL__HOST: postgresql + AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik} + AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik} + AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS} + AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY} + user: root + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./authentik/media:/media + - ./authentik/certs:/certs + - ./authentik/custom-templates:/templates + depends_on: + postgresql: + condition: service_healthy + redis: + condition: service_healthy + networks: + - kaboot-network + + kaboot-backend: + build: + context: ./server + dockerfile: Dockerfile + restart: unless-stopped + environment: + NODE_ENV: production + PORT: 3001 + DATABASE_PATH: /data/kaboot.db + OIDC_ISSUER: ${OIDC_ISSUER} + OIDC_JWKS_URI: ${OIDC_JWKS_URI} + OIDC_INTERNAL_JWKS_URI: http://authentik-server:9000/application/o/kaboot/jwks/ + CORS_ORIGIN: ${CORS_ORIGIN} + LOG_REQUESTS: ${LOG_REQUESTS:-true} + volumes: + - kaboot-data:/data + networks: + - kaboot-network + +volumes: + postgresql-data: + redis-data: + kaboot-data: + +networks: + kaboot-network: + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml index 0b0a3f7..1936206 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -51,9 +51,12 @@ services: AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS} AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?authentik secret key required} AUTHENTIK_ERROR_REPORTING__ENABLED: ${AUTHENTIK_ERROR_REPORTING:-false} + AUTHENTIK_BOOTSTRAP_PASSWORD: ${AUTHENTIK_BOOTSTRAP_PASSWORD:-} + AUTHENTIK_BOOTSTRAP_TOKEN: ${AUTHENTIK_BOOTSTRAP_TOKEN:-} volumes: - ./authentik/media:/media - ./authentik/custom-templates:/templates + - ./authentik/blueprints:/blueprints/custom ports: - "${AUTHENTIK_PORT_HTTP:-9000}:9000" - "${AUTHENTIK_PORT_HTTPS:-9443}:9443" diff --git a/docs/AUTHENTIK_SETUP.md b/docs/AUTHENTIK_SETUP.md index b18ea0c..9884bb1 100644 --- a/docs/AUTHENTIK_SETUP.md +++ b/docs/AUTHENTIK_SETUP.md @@ -2,6 +2,45 @@ This guide walks through configuring Authentik as the OAuth2/OIDC identity provider for Kaboot. +## Quick Start (Automated Setup) + +The recommended approach uses Authentik Blueprints for automatic configuration: + +```bash +# 1. Run setup script (generates all secrets including admin password) +./scripts/setup.sh + +# 2. Start the stack +docker compose up -d + +# 3. Wait for Authentik to initialize (~30 seconds) +docker compose logs -f authentik-server | grep -i blueprint + +# 4. Set password for test user +docker compose exec authentik-server ak set_password kaboottest +# Enter: kaboottest (or your preferred password) + +# 5. (Optional) Create app password for service account via UI +# See "Step 7: Create a Service Account" below +``` + +The blueprint automatically creates: +- Kaboot OAuth2/OIDC Provider (public client, client_id: `kaboot-spa`) +- Kaboot Application with proper redirect URIs +- `kaboot-users` Group +- Enrollment flow with sign-up capability +- Password complexity policy +- Test user (`kaboottest`) +- Service account (`kaboot-test-service`) + +Your admin credentials are printed by `setup.sh` - save them! + +--- + +## Manual Setup (Alternative) + +If you prefer manual configuration or need to customize the setup, follow the steps below. + ## Prerequisites - Docker and Docker Compose installed diff --git a/docs/PRODUCTION.md b/docs/PRODUCTION.md index 31d51ad..a0afd4a 100644 --- a/docs/PRODUCTION.md +++ b/docs/PRODUCTION.md @@ -1,21 +1,59 @@ # Production Deployment Guide -This guide provides instructions for deploying Kaboot to a production environment. It covers security, persistence, and configuration for a robust setup. +This guide provides instructions for deploying Kaboot to a production environment with Caddy reverse proxy and automatic HTTPS. + +## Quick Start (Caddy + Automated Authentik Setup) + +```bash +# 1. Generate secrets +./scripts/setup.sh + +# 2. Configure production blueprint +cp authentik/blueprints/kaboot-setup-production.yaml.example \ + authentik/blueprints/kaboot-setup-production.yaml + +# Edit the blueprint - update domains in the 'context' section: +# kaboot_domain: your-app.com +# auth_domain: auth.your-app.com + +# Remove dev blueprint to avoid conflicts +rm authentik/blueprints/kaboot-setup.yaml + +# 3. Configure Caddyfile +cp Caddyfile.example Caddyfile +# Edit Caddyfile - replace example.com with your domains + +# 4. Update .env with production values +# - OIDC_ISSUER=https://auth.your-app.com/application/o/kaboot/ +# - OIDC_JWKS_URI=https://auth.your-app.com/application/o/kaboot/jwks/ +# - CORS_ORIGIN=https://your-app.com + +# 5. Build frontend with production URLs +VITE_API_URL=https://your-app.com/api \ +VITE_OIDC_AUTHORITY=https://auth.your-app.com/application/o/kaboot/ \ +npm run build + +# 6. Start the stack +docker compose -f docker-compose.prod.yml -f docker-compose.caddy.yml up -d + +# 7. Verify +docker compose -f docker-compose.prod.yml -f docker-compose.caddy.yml ps +``` ## Prerequisites - A Linux server with Docker and Docker Compose installed. -- A registered domain name (e.g., `kaboot.example.com`). -- SSL certificates (e.g., from Let's Encrypt). +- Two registered domain names (e.g., `kaboot.example.com` and `auth.example.com`). +- DNS A records pointing both domains to your server. - A Google Gemini API key. ## Architecture Overview In production, the stack consists of: -- **Nginx**: Reverse proxy handling HTTPS and routing. -- **Authentik**: Identity Provider for authentication. +- **Caddy**: Reverse proxy with automatic HTTPS via Let's Encrypt. +- **Authentik**: Identity Provider for OAuth2/OIDC authentication. - **Kaboot Backend**: Express server for quiz logic and SQLite storage. -- **Kaboot Frontend**: Static assets served via Nginx or a dedicated service. +- **Kaboot Frontend**: Static assets served via Caddy. - **PostgreSQL**: Database for Authentik. - **Redis**: Cache and task queue for Authentik. @@ -26,18 +64,15 @@ Create a production `.env` file. Do not commit this file to version control. ### Backend & Authentik Configuration ```env -# Database Passwords (Generate strong secrets) +# Database Passwords (Generated by setup.sh) PG_PASS=your_strong_postgres_password AUTHENTIK_SECRET_KEY=your_strong_authentik_secret -# Infrastructure -AUTHENTIK_PORT_HTTP=9000 -KABOOT_BACKEND_PORT=3001 +# Bootstrap credentials (Generated by setup.sh) +AUTHENTIK_BOOTSTRAP_PASSWORD=your_admin_password +AUTHENTIK_BOOTSTRAP_TOKEN=your_api_token -# AI Configuration -GEMINI_API_KEY=your_gemini_api_key - -# OIDC Production Settings +# OIDC Production Settings (update with your domains) OIDC_ISSUER=https://auth.example.com/application/o/kaboot/ OIDC_JWKS_URI=https://auth.example.com/application/o/kaboot/jwks/ @@ -52,112 +87,14 @@ The frontend requires environment variables at build time: - `VITE_API_URL`: `https://kaboot.example.com/api` - `VITE_OIDC_AUTHORITY`: `https://auth.example.com/application/o/kaboot/` -## Docker Compose Production Example +## Docker Compose Files -Create a `docker-compose.prod.yml` for your production environment: +The project includes pre-configured compose files: -```yaml -services: - postgresql: - image: docker.io/library/postgres:16-alpine - restart: unless-stopped - healthcheck: - test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"] - interval: 30s - timeout: 5s - retries: 5 - volumes: - - postgresql-data:/var/lib/postgresql/data - environment: - POSTGRES_PASSWORD: ${PG_PASS} - POSTGRES_USER: ${PG_USER:-authentik} - POSTGRES_DB: ${PG_DB:-authentik} - networks: - - kaboot-network +- **`docker-compose.prod.yml`** - Production services (Authentik, Backend, PostgreSQL, Redis) +- **`docker-compose.caddy.yml`** - Caddy reverse proxy overlay - redis: - image: docker.io/library/redis:alpine - restart: unless-stopped - command: --save 60 1 --loglevel warning - volumes: - - redis-data:/data - networks: - - kaboot-network - - authentik-server: - image: ghcr.io/goauthentik/server:2025.2 - restart: unless-stopped - command: server - environment: - AUTHENTIK_REDIS__HOST: redis - AUTHENTIK_POSTGRESQL__HOST: postgresql - AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik} - AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik} - AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS} - AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY} - volumes: - - ./authentik/media:/media - - ./authentik/custom-templates:/templates - depends_on: - postgresql: - condition: service_healthy - redis: - condition: service_healthy - networks: - - kaboot-network - - authentik-worker: - image: ghcr.io/goauthentik/server:2025.2 - restart: unless-stopped - command: worker - environment: - AUTHENTIK_REDIS__HOST: redis - AUTHENTIK_POSTGRESQL__HOST: postgresql - AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik} - AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik} - AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS} - AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY} - user: root - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - ./authentik/media:/media - - ./authentik/certs:/certs - - ./authentik/custom-templates:/templates - depends_on: - postgresql: - condition: service_healthy - redis: - condition: service_healthy - networks: - - kaboot-network - - kaboot-backend: - build: - context: ./server - dockerfile: Dockerfile - restart: unless-stopped - environment: - NODE_ENV: production - PORT: 3001 - DATABASE_PATH: /data/kaboot.db - OIDC_ISSUER: ${OIDC_ISSUER} - OIDC_JWKS_URI: ${OIDC_JWKS_URI} - CORS_ORIGIN: ${CORS_ORIGIN} - LOG_REQUESTS: ${LOG_REQUESTS} - volumes: - - kaboot-data:/data - networks: - - kaboot-network - -volumes: - postgresql-data: - redis-data: - kaboot-data: - -networks: - kaboot-network: - driver: bridge -``` +These files are ready to use - no need to create your own. ## HTTPS and Reverse Proxy @@ -195,7 +132,13 @@ kaboot.example.com { } auth.example.com { - reverse_proxy authentik-server:9000 + reverse_proxy authentik-server:9000 { + header_up X-Forwarded-Proto {scheme} + header_up X-Forwarded-Host {host} + transport http { + keepalive 30s + } + } } ``` @@ -212,7 +155,7 @@ This creates the `dist/` directory with production assets. Use both compose files together: ```bash -docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d +docker compose -f docker-compose.prod.yml -f docker-compose.caddy.yml up -d ``` This will: @@ -225,7 +168,7 @@ This will: Check that all services are running: ```bash -docker compose -f docker-compose.yml -f docker-compose.caddy.yml ps +docker compose -f docker-compose.prod.yml -f docker-compose.caddy.yml ps ``` View Caddy logs: @@ -234,10 +177,16 @@ View Caddy logs: docker logs kaboot-caddy ``` +Check Authentik blueprint was applied: + +```bash +docker compose -f docker-compose.prod.yml logs authentik-server | grep -i blueprint +``` + **Stopping the Stack** ```bash -docker compose -f docker-compose.yml -f docker-compose.caddy.yml down +docker compose -f docker-compose.prod.yml -f docker-compose.caddy.yml down ``` ### Option 2: Nginx @@ -287,12 +236,37 @@ server { ## Authentik Configuration for Production +### Option A: Automated Setup with Blueprint (Recommended) + +Use the production blueprint for automated configuration: + +```bash +# 1. Copy and configure the production blueprint +cp authentik/blueprints/kaboot-setup-production.yaml.example \ + authentik/blueprints/kaboot-setup-production.yaml + +# 2. Edit the blueprint and update the domains +# Find the 'context' section and change: +# - kaboot_domain: kaboot.example.com -> your-app.com +# - auth_domain: auth.example.com -> auth.your-app.com + +# 3. Remove the development blueprint (prevents conflicts) +rm authentik/blueprints/kaboot-setup.yaml + +# 4. Start the stack - blueprint applies automatically +docker compose -f docker-compose.prod.yml up -d +``` + +### Option B: Manual Configuration + 1. **Update Redirect URIs**: In the Authentik Admin interface, go to **Applications** > **Providers** > **Kaboot OAuth2**. Update the **Redirect URIs** to use your production domain: - `https://kaboot.example.com/callback` - `https://kaboot.example.com/silent-renew.html` - `https://kaboot.example.com` -2. **Email Configuration**: To enable password recovery, configure SMTP settings in Authentik. In the Admin interface, go to **System** > **Settings** and update the Email section. +### Email Configuration + +To enable password recovery, configure SMTP settings in Authentik. In the Admin interface, go to **System** > **Settings** and update the Email section. - Host: your SMTP server - Port: 587 or 465 - Username/Password: your credentials @@ -317,10 +291,12 @@ docker exec kaboot-postgresql pg_dump -U authentik authentik > authentik_backup_ ## Security Checklist -- [ ] Change all default passwords (`PG_PASS`, `AUTHENTIK_SECRET_KEY`). +- [ ] Run `./scripts/setup.sh` to generate strong secrets (don't use defaults). - [ ] Ensure `NODE_ENV` is set to `production`. -- [ ] Use HTTPS for all connections. -- [ ] Set `CORS_ORIGIN` to your specific frontend domain. +- [ ] Use HTTPS for all connections (Caddy handles this automatically). +- [ ] Set `CORS_ORIGIN` to your specific frontend domain (e.g., `https://kaboot.example.com`). +- [ ] Update `OIDC_ISSUER` and `OIDC_JWKS_URI` to use your auth domain with HTTPS. - [ ] Regularly back up the `kaboot.db` and PostgreSQL data. - [ ] Monitor logs by setting `LOG_REQUESTS=true`. - [ ] Keep Docker images updated to the latest stable versions. +- [ ] Configure firewall to only allow ports 80 and 443 from the internet. diff --git a/scripts/setup.sh b/scripts/setup.sh index 6331971..efc4982 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -26,23 +26,38 @@ echo "Generating secrets..." PG_PASS=$(openssl rand -base64 36 | tr -d '\n') AUTHENTIK_SECRET_KEY=$(openssl rand -base64 60 | tr -d '\n') +AUTHENTIK_BOOTSTRAP_PASSWORD=$(openssl rand -base64 24 | tr -d '\n') +AUTHENTIK_BOOTSTRAP_TOKEN=$(openssl rand -base64 36 | tr -d '\n') cp "$ENV_EXAMPLE" "$ENV_FILE" if [[ "$OSTYPE" == "darwin"* ]]; then sed -i '' "s|^PG_PASS=.*|PG_PASS=${PG_PASS}|" "$ENV_FILE" sed -i '' "s|^AUTHENTIK_SECRET_KEY=.*|AUTHENTIK_SECRET_KEY=${AUTHENTIK_SECRET_KEY}|" "$ENV_FILE" + sed -i '' "s|^AUTHENTIK_BOOTSTRAP_PASSWORD=.*|AUTHENTIK_BOOTSTRAP_PASSWORD=${AUTHENTIK_BOOTSTRAP_PASSWORD}|" "$ENV_FILE" + sed -i '' "s|^AUTHENTIK_BOOTSTRAP_TOKEN=.*|AUTHENTIK_BOOTSTRAP_TOKEN=${AUTHENTIK_BOOTSTRAP_TOKEN}|" "$ENV_FILE" else sed -i "s|^PG_PASS=.*|PG_PASS=${PG_PASS}|" "$ENV_FILE" sed -i "s|^AUTHENTIK_SECRET_KEY=.*|AUTHENTIK_SECRET_KEY=${AUTHENTIK_SECRET_KEY}|" "$ENV_FILE" + sed -i "s|^AUTHENTIK_BOOTSTRAP_PASSWORD=.*|AUTHENTIK_BOOTSTRAP_PASSWORD=${AUTHENTIK_BOOTSTRAP_PASSWORD}|" "$ENV_FILE" + sed -i "s|^AUTHENTIK_BOOTSTRAP_TOKEN=.*|AUTHENTIK_BOOTSTRAP_TOKEN=${AUTHENTIK_BOOTSTRAP_TOKEN}|" "$ENV_FILE" fi echo "" echo "Created .env file with generated secrets." echo "" +echo "Authentik admin credentials (save these):" +echo " Username: akadmin" +echo " Password: ${AUTHENTIK_BOOTSTRAP_PASSWORD}" +echo "" echo "Next steps:" echo " 1. Review .env and adjust settings if needed" echo " 2. Run: docker compose up -d" -echo " 3. Open: http://localhost:9000/if/flow/initial-setup/" -echo " 4. Follow docs/AUTHENTIK_SETUP.md to configure the OAuth2 provider" +echo " 3. Wait for Authentik to start (~30 seconds)" +echo " 4. The Kaboot application is auto-configured via blueprint!" +echo "" +echo "Remaining manual steps:" +echo " - Set password for test user: docker compose exec authentik-server ak set_password kaboottest" +echo " - Create app password for service account via Authentik UI" +echo " - See docs/AUTHENTIK_SETUP.md for details" echo ""