Remove Keycloak; standardize on Authelia for SSO

Keycloak never reliably ran (fiddly reverse-proxy/hostname config) and the
repo has standardized on Authelia for SSO + 2FA. This rips Keycloak out
entirely:

- Delete the install block, whiptail menu entry, uninstall plumbing, and
  EXISTING_SERVICES detection from both 24.04 and 26.04 scripts
- Delete docker-compose-keycloak.yml, fix-keycloak-proxy.sh, and
  KEYCLOAK-SETUP-GUIDE.md
- Remove the Keycloak block from caddy-setup-helper.sh
- Update docs (CADDY-FAIL2BAN-SETUP.md, SECURITY-IMPROVEMENTS.md,
  NEW-SCRIPT-STRUCTURE.md, SCRIPT-FLOW-INTRO.txt) to reference Authelia

Also documents the fail2ban/Authelia overlap: Authelia handles failed-login
regulation (per-account lockout); the Caddy fail2ban jail is complementary
firewall-level IP banning. Neither does geo-blocking — noted CrowdSec / Caddy
GeoIP as the path for that.

https://claude.ai/code/session_017eA2qqq9jfF2tNtpUYL8vK
This commit is contained in:
Claude
2026-06-03 03:44:26 +00:00
parent c54cd9ecc6
commit f564b4b6d8
10 changed files with 45 additions and 2473 deletions
+15 -7
View File
@@ -61,17 +61,17 @@ budget.yourdomain.com {
}
```
#### Keycloak (Identity & Access Management)
#### Authelia (SSO + 2FA auth portal)
```caddy
auth.yourdomain.com {
log {
output file /var/log/caddy/keycloak-access.log
output file /var/log/caddy/authelia-access.log
format json
level INFO
}
reverse_proxy localhost:8180
reverse_proxy localhost:9091
# Security headers
header {
@@ -315,15 +315,23 @@ logpath = /var/log/caddy/actualbudget-access.log
maxretry = 3
bantime = 7200
[caddy-keycloak]
[caddy-authelia]
enabled = true
port = http,https
filter = caddy-auth
logpath = /var/log/caddy/keycloak-access.log
logpath = /var/log/caddy/authelia-access.log
maxretry = 5
bantime = 3600
```
> **Note:** Authelia already performs its own failed-login *regulation*
> (per-account lockout after repeated failures). This jail is complementary
> defense-in-depth that bans the offending IP at the firewall level, and also
> covers services that don't sit behind Authelia. Neither Authelia nor
> fail2ban provides **geo-blocking** — for country-level blocking or IP
> reputation feeds, consider [CrowdSec](https://www.crowdsec.net/) (a modern
> fail2ban alternative with a Caddy bouncer) or a Caddy GeoIP module.
## Best Practices
1. **Always backup before changes**
@@ -359,7 +367,7 @@ cp ~/docker/caddy/Caddyfile ~/docker/caddy/Caddyfile.backup
### Service Ports
- **ActualBudget**: 5006
- **Keycloak**: 8180
- **Authelia**: 9091
- **Caddy**: 80 (HTTP), 443 (HTTPS)
## Support
@@ -368,4 +376,4 @@ For issues:
- Caddy documentation: https://caddyserver.com/docs/
- Fail2ban manual: https://www.fail2ban.org/wiki/index.php/MANUAL_0_8
- ActualBudget docs: https://actualbudget.org/docs/
- Keycloak docs: https://www.keycloak.org/documentation
- Authelia docs: https://www.authelia.com/
-678
View File
@@ -1,678 +0,0 @@
# Keycloak Setup Guide
## Complete Manual and Automated Configuration Guide
This guide explains Keycloak concepts and how to configure it both automatically (via the script) and manually (via the web UI).
---
## Table of Contents
1. [What is Keycloak?](#what-is-keycloak)
2. [Key Concepts](#key-concepts)
3. [Automated Setup (via Script)](#automated-setup)
4. [Manual Setup (via Web UI)](#manual-setup)
5. [Configuring External Services](#configuring-external-services)
6. [Reconfiguration & Adding Realms](#reconfiguration)
7. [Common Use Cases](#common-use-cases)
8. [Troubleshooting](#troubleshooting)
---
## What is Keycloak?
Keycloak is an **Identity and Access Management (IAM)** system that provides:
- **Single Sign-On (SSO)**: Log in once, access all your services
- **User Management**: Create, manage, and authenticate users in one place
- **OAuth2/OIDC**: Industry-standard authentication for web apps
- **Social Login**: Allow login via Google, GitHub, etc.
- **Multi-Factor Authentication (MFA)**: Add extra security with 2FA/TOTP
- **LDAP/Active Directory Integration**: Connect to existing user directories
**Think of Keycloak as:** A centralized login system for all your self-hosted services.
---
## Key Concepts
### 1. **Realm**
A **realm** is an isolated container for users, clients, and configuration.
**Analogy:** Think of a realm like a "company" or "organization" in Keycloak.
**Why you need it:**
- The default `master` realm is for Keycloak admin only
- You create a separate realm (e.g., `homelab`) for your actual users and applications
- Realms are completely isolated - users in one realm can't access another
**Example:**
- `master` realm: Only for Keycloak administrators
- `homelab` realm: For your personal services (ActualBudget, Jellyfin, etc.)
- `family` realm: Separate realm for family members (optional)
### 2. **OAuth2/OpenID Connect (OIDC) Client**
A **client** is an application that uses Keycloak for authentication.
**Analogy:** Each service (ActualBudget, Jellyfin, etc.) is a "client" that asks Keycloak "Is this user allowed to log in?"
**Required information:**
- **Client ID**: Name of the application (e.g., `actualbudget`)
- **Client Secret**: Password for the application (auto-generated, 64-char hex)
- **Redirect URIs**: Where Keycloak sends users after login
- Example: `https://budget.yourdomain.com/*`
- Must match EXACTLY or login will fail
**Flow:**
1. User clicks "Login" in ActualBudget
2. ActualBudget redirects to Keycloak: `https://auth.yourdomain.com/login`
3. User logs in with username/password
4. Keycloak redirects back to ActualBudget: `https://budget.yourdomain.com/callback`
5. ActualBudget gets user info and logs them in
### 3. **Users**
A **user** is a person who can log in to your services.
**User attributes:**
- Username (required, unique)
- Email (optional but recommended)
- First name / Last name (optional)
- Password (set via Credentials tab)
- Email verified (set to true to skip verification)
- Enabled (must be true for user to log in)
### 4. **Redirect URIs**
**Critical concept:** The redirect URI is where Keycloak sends the user after successful login.
**Common mistakes:**
-`http://localhost:5006` (won't work for external services)
-`https://budget.example.com` (missing wildcard or path)
-`https://budget.example.com/*` (correct - allows all paths)
**For external services (like Pikapod):**
- Pikapod gives you a URL like: `https://actualbudget-abc123.pikapod.net`
- Your redirect URI: `https://actualbudget-abc123.pikapod.net/*`
- Your Keycloak URL: `https://auth.yourdomain.com` (must be publicly accessible)
---
## Automated Setup (via Script)
The script automates everything for you. Here's what it does:
### Step 1: Install Keycloak
```bash
./ubuntu-post-install.sh
# Select KEYCLOAK in whiptail menu
```
Prompts:
- Admin password (for Keycloak admin console)
- Database password (for PostgreSQL)
### Step 2: Automated Configuration
```
Configure Keycloak with initial realm and clients? (y/n): y
```
This automatically:
1. ✅ Waits for Keycloak to start (health check)
2. ✅ Logs in using admin CLI (`kcadm.sh`)
3. ✅ Creates a realm (e.g., `homelab`)
4. ✅ Creates OAuth client for ActualBudget (if selected)
5. ✅ Creates generic OAuth client template
6. ✅ Saves all credentials to `~/docker/keycloak/*.txt`
7. ✅ Optionally creates initial user
### Step 3: What Gets Created
**Realm:** `homelab` (or your custom name)
**ActualBudget OAuth Client:**
- Client ID: `actualbudget`
- Client Secret: (saved to `actualbudget-oauth.txt`)
- Redirect URIs:
- `http://localhost:5006/*` (local development)
- `http://yourdomain.com:5006/*` (local with domain)
- `https://yourdomain.com/*` (production - any subdomain)
- `https://budget.yourdomain.com/*` (specific subdomain)
**Generic OAuth Client:**
- Client ID: `generic-app`
- Client Secret: (saved to `generic-oauth.txt`)
- Can be cloned for other services
**Initial User:**
- Username, email, password you provide
- Immediately active
- Can log in to all services
### Step 4: Configuration Files
All credentials saved to:
```
~/docker/keycloak/actualbudget-oauth.txt
~/docker/keycloak/generic-oauth.txt
```
These files contain:
- Client ID
- Client Secret
- Authorization URL
- Token URL
- User Info URL
- Instructions for configuring each service
---
## Manual Setup (via Web UI)
If you prefer to configure Keycloak manually, or want to add services later:
### Access Admin Console
```
URL: http://localhost:8180/admin
Username: admin
Password: [your admin password]
```
### Step 1: Create a Realm
1. **Click dropdown** in top-left corner (shows "Master")
2. **Click "Create Realm"**
3. **Realm name:** `homelab` (or your choice)
4. **Click "Create"**
**Settings to configure:**
- **Login tab:**
- ✅ User registration: OFF (you create users manually)
- ✅ Forgot password: ON (allows password resets)
- ✅ Remember me: ON (convenience)
- ✅ Login with email: ON (users can use email instead of username)
- **Email tab:** (optional, for password resets)
- Configure SMTP settings if you want email features
### Step 2: Create an OAuth2 Client (for ActualBudget)
1. **Switch to your realm** (`homelab`) via dropdown
2. **Go to Clients** (left menu)
3. **Click "Create client"**
**General Settings:**
- **Client type:** OpenID Connect
- **Client ID:** `actualbudget`
- **Name:** `ActualBudget`
- **Description:** `Personal Finance Management`
- **Click "Next"**
**Capability config:**
- ✅ Client authentication: ON (creates a secret)
- ✅ Authorization: OFF (not needed)
- ✅ Standard flow: ON (authorization code flow)
- ✅ Direct access grants: ON (allows username/password)
- ❌ Implicit flow: OFF (deprecated)
- ❌ Service accounts: OFF (not needed for web apps)
- **Click "Next"**
**Login settings:**
**Important: Adjust these for your setup!**
**For local ActualBudget:**
```
Root URL: http://localhost:5006
Home URL: http://localhost:5006
Valid redirect URIs:
http://localhost:5006/*
http://localhost:5006/callback
Valid post logout redirect URIs: +
Web origins:
http://localhost:5006
```
**For external ActualBudget (Pikapod, etc.):**
```
Root URL: https://actualbudget-abc123.pikapod.net
Home URL: https://actualbudget-abc123.pikapod.net
Valid redirect URIs:
https://actualbudget-abc123.pikapod.net/*
https://actualbudget-abc123.pikapod.net/callback
Valid post logout redirect URIs: +
Web origins:
https://actualbudget-abc123.pikapod.net
```
**For self-hosted with domain:**
```
Root URL: https://budget.yourdomain.com
Home URL: https://budget.yourdomain.com
Valid redirect URIs:
https://budget.yourdomain.com/*
https://budget.yourdomain.com/callback
Valid post logout redirect URIs: +
Web origins:
https://budget.yourdomain.com
```
4. **Click "Save"**
### Step 3: Get Client Secret
1. **Go to "Credentials" tab**
2. **Copy "Client secret"** (you'll need this for ActualBudget)
3. **Save it somewhere safe!**
### Step 4: Create a User
1. **Go to Users** (left menu)
2. **Click "Create user"**
**User details:**
- **Username:** `john` (required)
- **Email:** `john@example.com` (optional but recommended)
- **Email verified:** ✅ ON (skip email verification)
- **First name:** `John`
- **Last name:** `Doe`
- **Enabled:** ✅ ON (user can log in)
- **Click "Create"**
**Set password:**
1. **Go to "Credentials" tab**
2. **Click "Set password"**
3. **Enter password** (twice)
4. **Temporary:** ❌ OFF (user won't be forced to change it)
5. **Click "Save"**
6. **Confirm** in popup
### Step 5: Test Login
1. **Go to Realm Settings****Endpoints**
2. **Click "OpenID Endpoint Configuration"** (opens JSON)
3. **Find:** `authorization_endpoint`
4. **Copy URL** and open in browser
5. **Add:** `?client_id=actualbudget&response_type=code&redirect_uri=http://localhost:5006/callback`
6. **Log in** with your user
7. **You should see:** Redirect to callback URL (may error if ActualBudget not configured, but login works)
---
## Configuring External Services
### Keycloak MUST be Publicly Accessible
**Critical:** For external services like Pikapod, your Keycloak must be accessible from the internet.
### Requirements:
1.**Domain name** (e.g., `yourdomain.com`)
2.**DNS A record** pointing to your server
3.**Caddy reverse proxy** with HTTPS
4.**Port 80/443 open** in firewall
5.**Keycloak accessible** at `https://auth.yourdomain.com`
### Setup Caddy for Keycloak
**Add to Caddyfile:**
```caddy
auth.yourdomain.com {
log {
output file /var/log/caddy/keycloak-access.log
format json
level INFO
}
reverse_proxy localhost:8180
# Security headers
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
X-XSS-Protection "1; mode=block"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
```
**Reload Caddy:**
```bash
cd ~/docker/caddy
docker exec -w /etc/caddy caddy caddy reload
```
**Test:**
```
https://auth.yourdomain.com/admin
```
### Configure DNS
**Add A record:**
```
auth.yourdomain.com → [Your Server IP]
```
**Or use CNAME:**
```
auth → yourdomain.com
```
### Example: ActualBudget on Pikapod
**Scenario:**
- Keycloak: `https://auth.yourdomain.com` (your server)
- ActualBudget: `https://actualbudget-abc123.pikapod.net` (Pikapod)
**In Keycloak:**
1. **Create client:** `actualbudget-pikapod`
2. **Redirect URIs:**
```
https://actualbudget-abc123.pikapod.net/*
https://actualbudget-abc123.pikapod.net/callback
```
3. **Web origins:**
```
https://actualbudget-abc123.pikapod.net
```
**In ActualBudget (Pikapod):**
Settings → Authentication:
```
Client ID: actualbudget-pikapod
Client Secret: [from Keycloak credentials tab]
Authorization URL: https://auth.yourdomain.com/realms/homelab/protocol/openid-connect/auth
Token URL: https://auth.yourdomain.com/realms/homelab/protocol/openid-connect/token
User Info URL: https://auth.yourdomain.com/realms/homelab/protocol/openid-connect/userinfo
```
**Flow:**
1. User visits `https://actualbudget-abc123.pikapod.net`
2. Clicks "Login"
3. Redirects to `https://auth.yourdomain.com/realms/homelab/...`
4. User logs in
5. Redirects back to `https://actualbudget-abc123.pikapod.net/callback`
6. User is logged in!
---
## Reconfiguration & Adding Realms
You can re-run the script to add more realms or clients!
### Option 1: Re-run the Script
```bash
cd ~/docker/keycloak
docker compose down
cd ~
./ubuntu-post-install.sh
# Select KEYCLOAK again
# Choose "Configure Keycloak..." → Yes
# Enter new realm name: "family"
# Create new users
```
**This creates:**
- New realm with new users
- New OAuth clients for that realm
- Separate from your existing realm
### Option 2: Add Realm Manually
**Via Web UI:**
1. Go to admin console
2. Click realm dropdown
3. "Create Realm"
4. Name: `family`
5. Repeat client/user creation steps
### Option 3: Use Script Helper
The script can be extended to add a helper:
```bash
cd ~/docker/keycloak
# Login to admin CLI
docker exec keycloak /opt/keycloak/bin/kcadm.sh config credentials \
--server http://localhost:8080 \
--realm master \
--user admin \
--password [YOUR_ADMIN_PASSWORD]
# Create new realm
docker exec keycloak /opt/keycloak/bin/kcadm.sh create realms \
-s realm=family \
-s enabled=true
# Create new client
docker exec keycloak /opt/keycloak/bin/kcadm.sh create clients -r family \
-s clientId=my-new-service \
-s enabled=true \
-s clientAuthenticatorType=client-secret \
-s secret=$(openssl rand -hex 32) \
-s 'redirectUris=["https://service.yourdomain.com/*"]'
# Create new user
docker exec keycloak /opt/keycloak/bin/kcadm.sh create users -r family \
-s username=alice \
-s email=alice@example.com \
-s enabled=true
# Set password
docker exec keycloak /opt/keycloak/bin/kcadm.sh set-password -r family \
--username alice \
--new-password 'AlicePassword123!'
```
---
## Common Use Cases
### Use Case 1: All Local Services
**Setup:**
- Keycloak: `http://localhost:8180`
- ActualBudget: `http://localhost:5006`
- Jellyfin: `http://localhost:8096`
**Configuration:**
- No domain needed
- Use `localhost` URLs everywhere
- Redirect URIs: `http://localhost:PORT/*`
### Use Case 2: Self-Hosted with Domain
**Setup:**
- Keycloak: `https://auth.yourdomain.com`
- ActualBudget: `https://budget.yourdomain.com`
- Jellyfin: `https://jellyfin.yourdomain.com`
**Configuration:**
- Requires domain + Caddy
- Use HTTPS URLs
- Redirect URIs: `https://service.yourdomain.com/*`
### Use Case 3: Mixed (Local + External)
**Setup:**
- Keycloak: `https://auth.yourdomain.com` (self-hosted)
- ActualBudget: `https://actualbudget-abc.pikapod.net` (Pikapod)
- Jellyfin: `https://jellyfin.yourdomain.com` (self-hosted)
**Configuration:**
- Keycloak MUST be publicly accessible
- Each service gets its own client
- ActualBudget redirect: `https://actualbudget-abc.pikapod.net/*`
- Jellyfin redirect: `https://jellyfin.yourdomain.com/*`
---
## Troubleshooting
### Issue: "Invalid redirect URI"
**Cause:** Redirect URI in Keycloak doesn't match what the app is using.
**Fix:**
1. Check error message for actual redirect URI
2. Add EXACT URI to Keycloak client settings
3. Include wildcard: `https://domain.com/*`
### Issue: "Client not found"
**Cause:** Client ID doesn't match.
**Fix:**
1. Check client ID in Keycloak
2. Ensure it matches exactly in application
3. Case-sensitive!
### Issue: "Invalid client secret"
**Cause:** Wrong secret or expired.
**Fix:**
1. Go to Keycloak → Clients → Credentials
2. Copy secret again (or regenerate)
3. Update in application
### Issue: External service can't reach Keycloak
**Cause:** Keycloak not publicly accessible.
**Fix:**
1. Ensure Caddy is running: `docker ps | grep caddy`
2. Check DNS: `dig auth.yourdomain.com`
3. Test URL: `curl https://auth.yourdomain.com`
4. Check firewall: `sudo ufw status` (80/443 open?)
### Issue: Login succeeds but redirect fails
**Cause:** CORS or redirect URI mismatch.
**Fix:**
1. Add domain to "Web Origins" in client settings
2. Check redirect URI includes protocol (https://)
3. Check for typos in domain name
### Issue: Can't login to Keycloak admin console
**Cause:** Container not started or wrong password.
**Fix:**
```bash
# Check if running
docker ps | grep keycloak
# Check logs
docker logs keycloak --tail 50
# Restart
cd ~/docker/keycloak
docker compose restart
# Reset admin password (if needed)
docker exec keycloak /opt/keycloak/bin/kcadm.sh config credentials \
--server http://localhost:8080 \
--realm master \
--user admin \
--password NEW_PASSWORD_HERE
```
---
## Quick Reference
### Important URLs
**Local:**
```
Admin Console: http://localhost:8180/admin
Realm Endpoints: http://localhost:8180/realms/{realm-name}/.well-known/openid-configuration
```
**Production:**
```
Admin Console: https://auth.yourdomain.com/admin
Realm Endpoints: https://auth.yourdomain.com/realms/{realm-name}/.well-known/openid-configuration
```
### OAuth URLs (for realm "homelab")
**Local:**
```
Authorization: http://localhost:8180/realms/homelab/protocol/openid-connect/auth
Token: http://localhost:8180/realms/homelab/protocol/openid-connect/token
User Info: http://localhost:8180/realms/homelab/protocol/openid-connect/userinfo
Logout: http://localhost:8180/realms/homelab/protocol/openid-connect/logout
```
**Production:**
```
Authorization: https://auth.yourdomain.com/realms/homelab/protocol/openid-connect/auth
Token: https://auth.yourdomain.com/realms/homelab/protocol/openid-connect/token
User Info: https://auth.yourdomain.com/realms/homelab/protocol/openid-connect/userinfo
Logout: https://auth.yourdomain.com/realms/homelab/protocol/openid-connect/logout
```
### Common Commands
```bash
# Start Keycloak
cd ~/docker/keycloak
docker compose up -d
# Stop Keycloak
docker compose down
# View logs
docker logs keycloak -f
# Access shell
docker exec -it keycloak bash
# Login to admin CLI
docker exec keycloak /opt/keycloak/bin/kcadm.sh config credentials \
--server http://localhost:8080 \
--realm master \
--user admin \
--password YOUR_PASSWORD
# Export realm configuration (backup)
docker exec keycloak /opt/keycloak/bin/kc.sh export \
--dir /opt/keycloak/data/export \
--realm homelab
# Copy export to host
docker cp keycloak:/opt/keycloak/data/export ./backup/
```
---
## Summary
**Keycloak provides:**
- ✅ Single Sign-On for all your services
- ✅ Centralized user management
- ✅ OAuth2/OIDC authentication
- ✅ Works with local and external services
- ✅ Professional-grade security
**Automated setup does:**
- ✅ Creates realm
- ✅ Creates OAuth clients
- ✅ Creates initial user
- ✅ Saves all credentials
- ✅ Ready to use immediately
**Manual setup allows:**
- ✅ Full control over configuration
- ✅ Multiple realms (family, work, etc.)
- ✅ Custom client settings
- ✅ Advanced features (LDAP, MFA, etc.)
**For external services:**
- ✅ Keycloak must be publicly accessible
- ✅ Use Caddy with HTTPS
- ✅ Configure proper redirect URIs
- ✅ Test OAuth flow before production
For questions or issues, check the Keycloak documentation: https://www.keycloak.org/documentation
+2 -2
View File
@@ -51,7 +51,7 @@
│ │
│ === DOCKER SERVICES === │
│ [ ] ACTUALBUDGET Personal finance │
│ [ ] KEYCLOAK Identity management
│ [ ] AUTHELIA SSO + 2FA auth portal
│ [ ] CADDY Reverse proxy │
│ [ ] FAIL2BAN_CADDY Protect Caddy services │
│ [ ] JELLYFIN Media server │
@@ -75,7 +75,7 @@
**Dependency-aware installation order:**
1. Install Caddy first (if selected)
2. Install services that depend on Caddy (Keycloak, etc.)
2. Install services that depend on Caddy (Authelia, etc.)
3. Install fail2ban for Caddy (if selected + Caddy installed)
4. Install independent services in parallel where possible
+1 -1
View File
@@ -31,7 +31,7 @@ This script is divided into TWO main phases:
│ • Skip services you don't want │
│ │
│ Services include: │
│ • Self-hosted apps (ActualBudget, Keycloak, Jellyfin, etc.) │
│ • Self-hosted apps (ActualBudget, Authelia, Jellyfin, etc.) │
│ • Network services (Samba, VPNs, fail2ban) │
│ • Monitoring tools (Uptime Kuma, Portainer, Watchtower) │
│ • And many more... │
+23 -140
View File
@@ -11,7 +11,7 @@ This document describes the comprehensive security and infrastructure improvemen
**Solution:**
- Added `ensure_docker_dir_ownership()` helper function
- Applied to ALL 25+ services (Immich, Keycloak, ActualBudget, Jellyfin, etc.)
- Applied to ALL 25+ services (Immich, ActualBudget, Jellyfin, etc.)
- Fixed disaster recovery path (line 309)
- All Docker directories now properly owned by sudo user
@@ -19,8 +19,8 @@ This document describes the comprehensive security and infrastructure improvemen
---
### 2. Keycloak Security Overhaul
**Problem:** Weak default passwords, special characters causing issues, development mode in production.
### 2. Password & Credential Management
**Problem:** Weak default passwords and credentials hardcoded in compose files.
**Solutions Implemented:**
@@ -30,35 +30,20 @@ This document describes the comprehensive security and infrastructure improvemen
- **Auto-generation:** Press ENTER to generate secure passwords automatically
- **Validation:** Real-time password validation with retry loop
#### Production vs Development Mode
- **Production mode:** Uses `start` command, requires hostname configuration
- **Development mode:** Uses `start-dev` command, relaxed security for testing
- **Hostname support:** Proper `KC_HOSTNAME` configuration for public deployment
#### Generated Secrets
- Services that need cryptographic secrets generate them automatically (e.g. Authelia's JWT, session, and storage secrets via `openssl rand`).
#### Environment Variables
- All credentials moved to `.env` file
- Admin password and database password securely stored
- Passwords and secrets securely stored
- No more hardcoded passwords in docker-compose.yml
**Example Keycloak .env file structure:**
```env
# Keycloak Environment Variables
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=<secure-20-char-password>
POSTGRES_DB=keycloak
POSTGRES_USER=keycloak
POSTGRES_PASSWORD=<secure-32-char-password>
KC_PROXY=edge
KC_HTTP_ENABLED=true
KC_HOSTNAME=auth.yourdomain.com # (if production mode)
```
---
### 3. Environment Variable Management (.env Files)
**Services Now Using .env Files:**
-Keycloak (admin + database passwords)
-Authelia (JWT/session/storage secrets + SMTP password)
- ✅ ActualBudget (timezone and config)
- ✅ Immich (already had .env)
- ✅ FindMyDevice (already had .env)
@@ -120,7 +105,7 @@ labels:
**Services with fail2ban monitoring:**
- ActualBudget
- Keycloak
- Authelia
- All other internet-facing services
**fail2ban Configuration:**
@@ -130,7 +115,10 @@ labels:
- Ban duration: 3600 seconds (1 hour)
- Detection window: 600 seconds
**Detailed Setup:** See `CADDY-FAIL2BAN-SETUP.md` for complete configuration.
**Note:** When Authelia is in use, it provides its own failed-login regulation
(account lockout after repeated failures). The Caddy fail2ban jail is
complementary defense-in-depth at the HTTP layer. See
`CADDY-FAIL2BAN-SETUP.md` for complete configuration.
---
@@ -154,7 +142,7 @@ PASSWORD=$(generate_password 20) # 20-character password
```
### `validate_password(password [min_length])`
Validates passwords for Keycloak compatibility.
Validates passwords for compatibility (alphanumeric only).
**Validation Rules:**
- Minimum length (default: 12 characters)
@@ -170,67 +158,9 @@ fi
---
## Keycloak Setup Guide
### For ActualBudget on Pikapods
1. **Install Keycloak with production mode:**
```bash
sudo bash ubuntu-post-install.sh
# Select Keycloak from menu
# Choose production mode (y)
# Enter hostname: auth.yourdomain.com
# Press ENTER to auto-generate secure passwords
```
2. **Configure Caddy2:**
- Script automatically prompts for Caddy configuration
- Enter your domain (e.g., auth.yourdomain.com)
- Ensure DNS A record points to your server
3. **Configure DNS:**
```
auth.yourdomain.com → Your Server IP
```
4. **Access Keycloak:**
```
https://auth.yourdomain.com
```
5. **Set up ActualBudget OAuth:**
- The script automatically creates an OAuth client for ActualBudget
- Client details saved to: `~/docker/keycloak/actualbudget-oauth.txt`
- Use these credentials in your Pikapod ActualBudget instance
6. **Configure ActualBudget on Pikapods:**
- Go to your ActualBudget settings
- Enable OpenID Connect
- Enter your Keycloak details:
- Issuer: `https://auth.yourdomain.com/realms/homelab`
- Client ID: (from actualbudget-oauth.txt)
- Client Secret: (from actualbudget-oauth.txt)
### For Other Self-Hosted Services
The script can create generic OAuth clients for other services. After Keycloak installation, you can:
1. Access Keycloak admin console
2. Create new OAuth2/OIDC clients
3. Configure redirect URIs for your services
4. Use the client credentials in your service configuration
**Generic Client Template:**
- Client ID: your-service-name
- Client Type: Confidential
- Standard Flow Enabled: Yes
- Valid Redirect URIs: https://your-service.com/*
---
## Password Requirements Reference
### Keycloak Passwords
### Password Rules
- **Minimum:** 12 characters
- **Recommended:** 16+ characters
- **Format:** Alphanumeric only (a-zA-Z0-9)
@@ -238,7 +168,9 @@ The script can create generic OAuth clients for other services. After Keycloak i
- **Generation:** Press ENTER for auto-generated secure passwords
### Why No Special Characters?
Keycloak has issues with special characters in certain authentication flows and database connection strings. Restricting to alphanumeric ensures compatibility.
Some services and database connection strings mishandle special characters in
certain authentication flows. Restricting to alphanumeric ensures broad
compatibility while remaining cryptographically strong.
### Password Strength with Alphanumeric Only
- 12 characters: ~62^12 = 3.2 × 10^21 combinations
@@ -264,21 +196,6 @@ docker ps
# Should work without permission errors
```
### Keycloak
```bash
# Check .env file exists
cat ~/docker/keycloak/.env
# Should contain KEYCLOAK_ADMIN_PASSWORD and POSTGRES_PASSWORD
# Check production mode
cat ~/docker/keycloak/docker-compose.yml | grep command
# Should show "start" for production or "start-dev" for development
# Test access
curl http://localhost:8180/health
# Should return health status
```
### Caddy2
```bash
# Check Caddy is running
@@ -316,20 +233,6 @@ If you have existing services:
4. Verify ownership: `ls -la ~/docker/actualbudget`
5. Restart container: `cd ~/docker/actualbudget && docker compose restart`
### Existing Keycloak
1. **IMPORTANT:** Backup your data first!
```bash
cp -r ~/docker/keycloak ~/docker/keycloak.backup
```
2. Stop existing container:
```bash
cd ~/docker/keycloak && docker compose down
```
3. Run updated script and select Keycloak
4. Choose whether to keep existing data or start fresh
5. If keeping data, manually update .env with your existing passwords
6. Restart: `docker compose up -d`
---
## Troubleshooting
@@ -340,19 +243,6 @@ If you have existing services:
sudo chown -R $USER:$USER ~/docker
```
### Keycloak Won't Start
```bash
# Check logs
docker logs keycloak
# Common issues:
# 1. Missing KC_HOSTNAME in production mode
# 2. Database connection failed (check postgres container)
# 3. Port 8180 already in use
# Fix: Edit .env and docker-compose.yml as needed
```
### Caddy Certificate Errors
```bash
# Check Caddy logs
@@ -375,19 +265,17 @@ sudo ufw status
## Security Best Practices
1. **Change default passwords:** Even with auto-generation, review and update if needed
2. **Use production mode for Keycloak:** Never use development mode for internet-facing deployments
3. **Enable fail2ban:** Monitor and ban malicious IPs
4. **Regular updates:** Keep containers updated (use Watchtower in notify mode)
5. **Backup .env files:** Store securely, separate from compose files
6. **Use HTTPS everywhere:** Configure Caddy2 for all public services
7. **Limit exposed ports:** Only expose necessary ports to the internet
8. **Monitor logs:** Regular review of Caddy and fail2ban logs
2. **Enable fail2ban:** Monitor and ban malicious IPs
3. **Regular updates:** Keep containers updated (use Watchtower in notify mode)
4. **Backup .env files:** Store securely, separate from compose files
5. **Use HTTPS everywhere:** Configure Caddy2 for all public services
6. **Limit exposed ports:** Only expose necessary ports to the internet
7. **Monitor logs:** Regular review of Caddy and fail2ban logs
---
## Additional Resources
- **Keycloak Setup Guide:** `KEYCLOAK-SETUP-GUIDE.md`
- **Caddy + fail2ban Setup:** `CADDY-FAIL2BAN-SETUP.md`
- **Main Script:** `ubuntu-post-install.sh`
- **Caddy Helper:** `caddy-setup-helper.sh`
@@ -402,8 +290,3 @@ If you encounter issues:
2. Verify ownership: `ls -la ~/docker`
3. Review this document for troubleshooting steps
4. Check existing documentation in repository
---
**Last Updated:** 2026-01-13
**Script Version:** Latest (with security improvements)
-36
View File
@@ -394,7 +394,6 @@ print_info "You can now add your services to the Caddyfile"
echo ""
echo "Available services to add:"
echo " - ActualBudget (Personal Finance) - Port 5006"
echo " - Keycloak (Identity & Access Management) - Port 8180"
echo ""
if ask_yn "Would you like to add ActualBudget to Caddyfile?" "n"; then
@@ -432,41 +431,6 @@ $AB_DOMAIN {
fi
fi
if ask_yn "Would you like to add Keycloak to Caddyfile?" "n"; then
read -p "Enter domain for Keycloak (e.g., auth.yourdomain.com): " KC_DOMAIN
if [ -n "$KC_DOMAIN" ]; then
KC_CONFIG="
# Keycloak - Identity & Access Management
$KC_DOMAIN {
log {
output file /var/log/caddy/keycloak-access.log
format json
level INFO
}
reverse_proxy localhost:8180
# Security headers
header {
Strict-Transport-Security \"max-age=31536000; includeSubDomains; preload\"
X-Frame-Options \"SAMEORIGIN\"
X-Content-Type-Options \"nosniff\"
X-XSS-Protection \"1; mode=block\"
Referrer-Policy \"strict-origin-when-cross-origin\"
}
}
"
if echo "$KC_CONFIG" >> "$CADDYFILE_PATH"; then
print_success "Added Keycloak configuration to Caddyfile"
else
print_error "Failed to add Keycloak configuration"
ERROR_MESSAGES+=("Add Keycloak manually - see CADDY-FAIL2BAN-SETUP.md")
fi
fi
fi
# ==============================
# 12. VALIDATE AND RELOAD CADDY
# ==============================
-137
View File
@@ -1,137 +0,0 @@
# Keycloak - Open-source Identity and Access Management
# https://www.keycloak.org/
#
# DEPLOYMENT INSTRUCTIONS:
# 1. Create directory: mkdir -p ~/docker/keycloak
# 2. Copy this file: cp docker-compose-keycloak.yml ~/docker/keycloak/docker-compose.yml
# 3. Create .env file with credentials (see .env template below)
# 4. Start the service: cd ~/docker/keycloak && docker compose up -d
# 5. Access at: http://localhost:8180/admin (admin console)
#
# .ENV FILE TEMPLATE:
# Create a file named .env in ~/docker/keycloak/ with:
# KEYCLOAK_ADMIN=admin
# KEYCLOAK_ADMIN_PASSWORD=<your-secure-password>
# POSTGRES_DB=keycloak
# POSTGRES_USER=keycloak
# POSTGRES_PASSWORD=<your-db-password>
# KC_DB=postgres
# KC_DB_URL=jdbc:postgresql://postgres:5432/keycloak
# KC_DB_USERNAME=keycloak
# KC_DB_PASSWORD=<your-db-password>
# KC_PROXY_HEADERS=xforwarded
# KC_HTTP_ENABLED=true
# KC_HOSTNAME_STRICT=false
# KC_LOG_LEVEL=INFO
# KC_HEALTH_ENABLED=true
# KC_METRICS_ENABLED=true
# # KC_HOSTNAME=auth.yourdomain.com # Uncomment for production
#
# REVERSE PROXY SETUP (with Caddy):
# Add to your Caddyfile:
# auth.yourdomain.com {
# reverse_proxy localhost:8180
# }
#
# PRODUCTION DEPLOYMENT:
# For production, you should:
# 1. Use a PostgreSQL database (see postgres service below)
# 2. Enable HTTPS via reverse proxy
# 3. Set KC_HOSTNAME to your domain
# 4. Use strong admin password
# 5. Configure proper realm and clients
name: keycloak
services:
# PostgreSQL database for Keycloak (recommended for production)
postgres:
image: postgres:16-alpine
container_name: keycloak-db
restart: unless-stopped
env_file:
- .env
volumes:
- ./postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U keycloak"]
interval: 10s
timeout: 5s
retries: 5
keycloak:
image: quay.io/keycloak/keycloak:latest
container_name: keycloak
restart: unless-stopped
command:
- start-dev # Use 'start' for production mode
env_file:
- .env
ports:
- "8180:8080" # HTTP port (use reverse proxy for HTTPS)
# - "8787:8787" # Debug port (uncomment if needed)
volumes:
# Optional: Custom themes
# - ./themes:/opt/keycloak/themes
# Optional: Custom providers/extensions
# - ./providers:/opt/keycloak/providers
- ./data:/opt/keycloak/data
depends_on:
postgres:
condition: service_healthy
labels:
# Fail2ban support
- "io.podman.annotations.label/fail2ban.enable=true"
- "io.podman.annotations.label/fail2ban.filter=caddy-auth"
# NOTES:
# - Admin console: http://localhost:8180/admin
# - Credentials: Stored in .env file
# - Database: PostgreSQL (persistent data in ./postgres-data)
# - Proxy: Uses KC_PROXY_HEADERS=xforwarded (v2 config, no deprecated warnings)
#
# FIRST-TIME SETUP:
# 1. Create .env file with secure passwords (see template above)
# 2. Start containers: docker compose up -d
# 3. Login to admin console at http://localhost:8180/admin
# 4. Create a realm (e.g., "homelab" or "myrealm")
# 5. Create clients for your applications (OAuth2/OIDC)
# 6. Add users or configure identity providers (LDAP, SAML, Social)
#
# COMMON USE CASES:
# - Single Sign-On (SSO) for multiple applications
# - OAuth2/OIDC provider for custom apps (ActualBudget, etc.)
# - SAML 2.0 identity provider
# - User federation with LDAP/Active Directory
# - Multi-factor authentication (MFA/2FA)
# - Social login (Google, GitHub, Facebook, etc.)
#
# PRODUCTION CHECKLIST:
# [ ] Create .env file with secure passwords (12+ chars, alphanumeric only)
# [ ] Set KC_HOSTNAME in .env to your domain (e.g., auth.yourdomain.com)
# [ ] Use 'start' instead of 'start-dev' command in docker-compose.yml
# [ ] Configure HTTPS via reverse proxy (Caddy/nginx)
# [ ] Set KC_HOSTNAME_STRICT=true in .env for production
# [ ] Configure DNS A record for your hostname
# [ ] Set proper file permissions: chmod 600 .env
# [ ] Configure backup strategy for PostgreSQL
# [ ] Set up monitoring (metrics enabled via KC_METRICS_ENABLED=true)
#
# BACKUP:
# docker compose down
# tar -czf keycloak-backup-$(date +%Y%m%d).tar.gz postgres-data data
# docker compose up -d
#
# RESTORE:
# docker compose down
# tar -xzf keycloak-backup-YYYYMMDD.tar.gz
# docker compose up -d
#
# UPDATES:
# docker compose pull
# docker compose up -d
#
# DOCUMENTATION:
# - Official docs: https://www.keycloak.org/documentation
# - Getting started: https://www.keycloak.org/getting-started/getting-started-docker
# - Server admin: https://www.keycloak.org/docs/latest/server_admin/
-184
View File
@@ -1,184 +0,0 @@
#!/bin/bash
#
# Fix Keycloak proxy configuration
# This updates Keycloak to use v2 proxy headers (KC_PROXY_HEADERS)
# instead of deprecated v1 (KC_PROXY)
#
set -e
KC_DIR="$HOME/docker/keycloak"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Keycloak Proxy Configuration Fix"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "This script will:"
echo " 1. Backup your current .env file"
echo " 2. Replace deprecated KC_PROXY with KC_PROXY_HEADERS"
echo " 3. Ensure docker-compose.yml uses env_file"
echo " 4. Restart Keycloak with new configuration"
echo ""
if [ ! -d "$KC_DIR" ]; then
echo "❌ Error: Keycloak directory not found at $KC_DIR"
exit 1
fi
cd "$KC_DIR"
# Backup existing configuration
BACKUP_DIR="$KC_DIR/backups"
mkdir -p "$BACKUP_DIR"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# Check if .env exists
if [ -f ".env" ]; then
echo "✓ Found existing .env file"
cp .env "$BACKUP_DIR/.env.backup.$TIMESTAMP"
echo "✓ Backed up .env to $BACKUP_DIR/.env.backup.$TIMESTAMP"
# Check if it has the old KC_PROXY setting
if grep -q "KC_PROXY=" .env 2>/dev/null; then
echo ""
echo "Updating .env file..."
# Replace KC_PROXY with KC_PROXY_HEADERS
sed -i 's/^KC_PROXY=.*/KC_PROXY_HEADERS=xforwarded/' .env
# Add KC_PROXY_HEADERS if it doesn't exist and KC_PROXY didn't either
if ! grep -q "KC_PROXY_HEADERS=" .env 2>/dev/null; then
echo "" >> .env
echo "# Proxy settings (v2) - Trust X-Forwarded-* headers from Caddy2" >> .env
echo "KC_PROXY_HEADERS=xforwarded" >> .env
fi
echo "✓ Updated KC_PROXY to KC_PROXY_HEADERS=xforwarded"
elif grep -q "KC_PROXY_HEADERS=" .env 2>/dev/null; then
echo "✓ Already using KC_PROXY_HEADERS - no changes needed"
else
echo ""
echo "Adding KC_PROXY_HEADERS to .env..."
echo "" >> .env
echo "# Proxy settings (v2) - Trust X-Forwarded-* headers from Caddy2" >> .env
echo "KC_PROXY_HEADERS=xforwarded" >> .env
echo "✓ Added KC_PROXY_HEADERS=xforwarded"
fi
else
echo "⚠ No .env file found"
echo ""
echo "Please create a .env file with your Keycloak credentials."
echo "See SECURITY-IMPROVEMENTS.md for the template."
exit 1
fi
# Check docker-compose.yml
if [ -f "docker-compose.yml" ]; then
cp docker-compose.yml "$BACKUP_DIR/docker-compose.yml.backup.$TIMESTAMP"
echo "✓ Backed up docker-compose.yml to $BACKUP_DIR/docker-compose.yml.backup.$TIMESTAMP"
# Check if docker-compose.yml has hardcoded KC_PROXY
if grep -q "KC_PROXY=" docker-compose.yml 2>/dev/null; then
echo ""
echo "⚠ Found KC_PROXY in docker-compose.yml"
echo " Removing it (should be in .env file instead)..."
# Remove the KC_PROXY line from docker-compose.yml
sed -i '/KC_PROXY=/d' docker-compose.yml
echo "✓ Removed KC_PROXY from docker-compose.yml"
fi
# Ensure it uses env_file
if ! grep -q "env_file:" docker-compose.yml 2>/dev/null; then
echo "⚠ docker-compose.yml doesn't use env_file"
echo " You may need to update it manually to use 'env_file: - .env'"
else
echo "✓ docker-compose.yml uses env_file"
fi
fi
# Show current configuration
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Current Configuration:"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
if [ -f ".env" ]; then
echo "Proxy Settings:"
grep "KC_PROXY" .env | grep -v "^#" || echo " (none found)"
echo ""
if grep -q "KC_HOSTNAME=" .env | grep -v "^#" 2>/dev/null; then
echo "Hostname:"
grep "KC_HOSTNAME=" .env | grep -v "^#"
echo ""
fi
fi
# Ask to restart
echo ""
read -p "Restart Keycloak with new configuration? (y/n): " RESTART
if [ "$RESTART" = "y" ] || [ "$RESTART" = "Y" ]; then
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Restarting Keycloak..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
docker compose down
echo "✓ Stopped Keycloak"
echo ""
echo "Starting Keycloak (this may take a minute)..."
docker compose up -d
# Wait for Keycloak to be ready
echo ""
echo "Waiting for Keycloak to be ready..."
KC_READY=false
for i in {1..60}; do
if docker exec keycloak curl -sf http://localhost:8080/health/ready > /dev/null 2>&1; then
KC_READY=true
echo ""
echo "✓ Keycloak is ready"
break
fi
echo -n "."
sleep 2
done
echo ""
if [ "$KC_READY" = true ]; then
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ Keycloak successfully updated!"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "Changes applied:"
echo " • Deprecated KC_PROXY removed"
echo " • New KC_PROXY_HEADERS=xforwarded configured"
echo " • Configuration stored in .env file"
echo ""
echo "The 'Hostname v1 options [proxy]' warnings should be gone."
echo ""
echo "Check the logs:"
echo " docker compose logs -f keycloak"
echo ""
else
echo ""
echo "⚠ Keycloak may still be starting. Check logs:"
echo " docker compose logs -f keycloak"
fi
else
echo ""
echo "Skipping restart. To apply changes later, run:"
echo " cd $KC_DIR && docker compose restart"
fi
echo ""
echo "Backup location: $BACKUP_DIR/"
echo " - .env.backup.$TIMESTAMP"
echo " - docker-compose.yml.backup.$TIMESTAMP"
echo ""
+2 -644
View File
@@ -1331,7 +1331,7 @@ generate_password() {
openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c "$length"
}
# Validate password for Keycloak (alphanumeric only, minimum length)
# Validate password (alphanumeric only, minimum length)
# Usage: validate_password "password" [min_length]
# Returns 0 if valid, 1 if invalid
validate_password() {
@@ -1344,7 +1344,7 @@ validate_password() {
return 1
fi
# Check for special characters (not allowed for Keycloak)
# Check for special characters (not allowed)
if echo "$password" | grep -q '[^a-zA-Z0-9]'; then
echo " ⚠ Password must contain only letters and numbers (no special characters)"
return 1
@@ -2586,7 +2586,6 @@ else
[ -d "$DOCKER_DIR/filebrowser" ] && EXISTING_SERVICES[FILEBROWSER]="ON"
[ -d "$DOCKER_DIR/magicmirror" ] && EXISTING_SERVICES[MAGICMIRROR]="ON"
[ -d "$DOCKER_DIR/actualbudget" ] && EXISTING_SERVICES[ACTUALBUDGET]="ON"
[ -d "$DOCKER_DIR/keycloak" ] && EXISTING_SERVICES[KEYCLOAK]="ON"
[ -d "$DOCKER_DIR/authelia" ] && EXISTING_SERVICES[AUTHELIA]="ON"
[ -d "$DOCKER_DIR/caddy" ] && EXISTING_SERVICES[CADDY]="ON"
[ -d "$DOCKER_DIR/lms" ] && EXISTING_SERVICES[LYRION]="ON"
@@ -2627,7 +2626,6 @@ else
"FILEBROWSER" "Web-based file manager" ${EXISTING_SERVICES[FILEBROWSER]:-OFF} \
"MAGICMIRROR" "Smart mirror / dashboard display" ${EXISTING_SERVICES[MAGICMIRROR]:-OFF} \
"ACTUALBUDGET" "Personal finance management with bank sync" ${EXISTING_SERVICES[ACTUALBUDGET]:-OFF} \
"KEYCLOAK" "Identity & Access Management (SSO)" ${EXISTING_SERVICES[KEYCLOAK]:-OFF} \
"AUTHELIA" "SSO + 2FA auth portal for Caddy" ${EXISTING_SERVICES[AUTHELIA]:-OFF} \
"CADDY" "Reverse proxy with automatic HTTPS" ${EXISTING_SERVICES[CADDY]:-OFF} \
"FAIL2BAN" "Intrusion prevention system" ${EXISTING_SERVICES[FAIL2BAN]:-OFF} \
@@ -2659,7 +2657,6 @@ else
[ -n "${EXISTING_SERVICES[FILEBROWSER]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS FILEBROWSER \"Web file manager\" ON"
[ -n "${EXISTING_SERVICES[MAGICMIRROR]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS MAGICMIRROR \"Smart mirror\" ON"
[ -n "${EXISTING_SERVICES[ACTUALBUDGET]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS ACTUALBUDGET \"Personal finance\" ON"
[ -n "${EXISTING_SERVICES[KEYCLOAK]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS KEYCLOAK \"Identity management\" ON"
[ -n "${EXISTING_SERVICES[AUTHELIA]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS AUTHELIA \"SSO + 2FA auth portal\" ON"
[ -n "${EXISTING_SERVICES[CADDY]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS CADDY \"Reverse proxy\" ON"
[ -n "${EXISTING_SERVICES[FAIL2BAN]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS FAIL2BAN \"Intrusion prevention\" ON"
@@ -2710,7 +2707,6 @@ else
: ${INSTALL_FILEBROWSER:="n"}
: ${INSTALL_MAGICMIRROR:="n"}
: ${INSTALL_ACTUALBUDGET:="n"}
: ${INSTALL_KEYCLOAK:="n"}
: ${INSTALL_AUTHELIA:="n"}
: ${INSTALL_CADDY:="n"}
: ${INSTALL_FAIL2BAN:="n"}
@@ -2737,7 +2733,6 @@ else
if echo "$SELECTED_SERVICES" | grep -q "FILEBROWSER"; then INSTALL_FILEBROWSER="y"; fi
if echo "$SELECTED_SERVICES" | grep -q "MAGICMIRROR"; then INSTALL_MAGICMIRROR="y"; fi
if echo "$SELECTED_SERVICES" | grep -q "ACTUALBUDGET"; then INSTALL_ACTUALBUDGET="y"; fi
if echo "$SELECTED_SERVICES" | grep -q "KEYCLOAK"; then INSTALL_KEYCLOAK="y"; fi
if echo "$SELECTED_SERVICES" | grep -q "AUTHELIA"; then INSTALL_AUTHELIA="y"; fi
if echo "$SELECTED_SERVICES" | grep -q "CADDY"; then INSTALL_CADDY="y"; fi
if echo "$SELECTED_SERVICES" | grep -q "FAIL2BAN"; then INSTALL_FAIL2BAN="y"; fi
@@ -2811,7 +2806,6 @@ else
if echo "$SELECTED_SERVICES" | grep -q "FILEBROWSER"; then uninstall_service "FileBrowser" "$DOCKER_DIR/filebrowser" "filebrowser"; fi
if echo "$SELECTED_SERVICES" | grep -q "MAGICMIRROR"; then uninstall_service "MagicMirror" "$DOCKER_DIR/magicmirror" "magicmirror"; fi
if echo "$SELECTED_SERVICES" | grep -q "ACTUALBUDGET"; then uninstall_service "ActualBudget" "$DOCKER_DIR/actualbudget" "actualbudget"; fi
if echo "$SELECTED_SERVICES" | grep -q "KEYCLOAK"; then uninstall_service "Keycloak" "$DOCKER_DIR/keycloak" "keycloak"; fi
if echo "$SELECTED_SERVICES" | grep -q "AUTHELIA"; then uninstall_service "Authelia" "$DOCKER_DIR/authelia" "authelia"; fi
if echo "$SELECTED_SERVICES" | grep -q "CADDY"; then uninstall_service "Caddy" "$DOCKER_DIR/caddy" "caddy"; fi
if echo "$SELECTED_SERVICES" | grep -q "LYRION"; then uninstall_service "Lyrion" "$DOCKER_DIR/lms" "lms"; fi
@@ -4231,625 +4225,6 @@ AB_COMPOSE
fi # End AB_RECONFIGURE check
fi # End INSTALL_ACTUALBUDGET check
# ---- KEYCLOAK ----
if [ "$WHIPTAIL_USED" != true ] && [ -z "$INSTALL_KEYCLOAK" ]; then
echo ""
echo "┌─────────────────────────────────────────────────────────────────┐"
echo "│ KEYCLOAK - Identity and Access Management (IAM) │"
echo "│ SSO, OAuth2, SAML, User Management, MFA │"
echo "│ Port: 8180 (HTTP) - Use reverse proxy for HTTPS │"
echo "└─────────────────────────────────────────────────────────────────┘"
prompt_yn "Install Keycloak? (y/n):" "n" INSTALL_KEYCLOAK
fi
if [ "$INSTALL_KEYCLOAK" = "y" ] || [ "$INSTALL_KEYCLOAK" = "Y" ]; then
KC_DIR="$DOCKER_DIR/keycloak"
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Would create $KC_DIR"
else
echo "Installing Keycloak..."
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "PASSWORD REQUIREMENTS:"
echo " • Minimum 12 characters (16+ recommended)"
echo " • Letters and numbers ONLY (no special characters)"
echo " • Press ENTER for secure auto-generated password"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Prompt for admin password with validation
KC_ADMIN_PASS=""
while true; do
read -s -p "Enter Keycloak admin password [auto-generate]: " KC_ADMIN_PASS
echo ""
# Generate secure password if user pressed Enter
if [ -z "$KC_ADMIN_PASS" ]; then
KC_ADMIN_PASS=$(generate_password 20)
echo " ✓ Generated secure admin password (saved in .env)"
break
fi
# Validate password
if validate_password "$KC_ADMIN_PASS" 12; then
echo " ✓ Admin password accepted"
break
fi
echo " Please try again."
done
# Prompt for database password with validation
KC_DB_PASS=""
while true; do
read -s -p "Enter database password [auto-generate]: " KC_DB_PASS
echo ""
# Generate secure password if user pressed Enter
if [ -z "$KC_DB_PASS" ]; then
KC_DB_PASS=$(generate_password 32)
echo " ✓ Generated secure database password (saved in .env)"
break
fi
# Validate password
if validate_password "$KC_DB_PASS" 12; then
echo " ✓ Database password accepted"
break
fi
echo " Please try again."
done
echo ""
# Ask about production vs development mode
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "DEPLOYMENT MODE:"
echo " • Production: Requires HTTPS via Caddy2 (recommended)"
echo " • Development: HTTP only, relaxed security (testing only)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
prompt_yn "Use production mode? (requires Caddy2 reverse proxy) (y/n):" "y" KC_PRODUCTION
KC_HOSTNAME=""
KC_START_CMD="start-dev"
KC_HOSTNAME_STRICT="false"
if [ "$KC_PRODUCTION" = "y" ] || [ "$KC_PRODUCTION" = "Y" ]; then
KC_START_CMD="start"
KC_HOSTNAME_STRICT="false"
echo ""
echo "Enter your Keycloak hostname (e.g., auth.yourdomain.com)"
echo "This should match your Caddy2 configuration."
read -p "Hostname: " KC_HOSTNAME
if [ -n "$KC_HOSTNAME" ]; then
echo " ✓ Production mode enabled with hostname: $KC_HOSTNAME"
echo " ⚠ Make sure Caddy2 is configured for this domain!"
else
echo " ⚠ No hostname provided - using relaxed mode"
KC_HOSTNAME=""
fi
fi
mkdir -p "$KC_DIR/data" "$KC_DIR/postgres-data"
ensure_docker_dir_ownership "$KC_DIR"
cd "$KC_DIR"
# Create .env file for sensitive credentials
cat > .env << KC_ENV
# Keycloak Environment Variables
# ⚠ KEEP THIS FILE SECURE - Contains sensitive passwords
# Admin Credentials
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=$KC_ADMIN_PASS
# Database Credentials
POSTGRES_DB=keycloak
POSTGRES_USER=keycloak
POSTGRES_PASSWORD=$KC_DB_PASS
KC_DB=postgres
KC_DB_URL=jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAME=keycloak
KC_DB_PASSWORD=$KC_DB_PASS
# Keycloak Configuration
# Proxy settings (v2) - Trust X-Forwarded-* headers from Caddy2
KC_PROXY_HEADERS=xforwarded
KC_HTTP_ENABLED=true
KC_HOSTNAME_STRICT=$KC_HOSTNAME_STRICT
KC_LOG_LEVEL=INFO
KC_HEALTH_ENABLED=true
KC_METRICS_ENABLED=true
KC_ENV
# Add hostname to .env if provided
if [ -n "$KC_HOSTNAME" ]; then
echo "KC_HOSTNAME=$KC_HOSTNAME" >> .env
fi
# Create docker-compose.yml
cat > docker-compose.yml << KC_COMPOSE
name: keycloak
services:
postgres:
image: postgres:16-alpine
container_name: keycloak-db
restart: unless-stopped
env_file:
- .env
volumes:
- ./postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U keycloak"]
interval: 10s
timeout: 5s
retries: 5
keycloak:
image: quay.io/keycloak/keycloak:latest
container_name: keycloak
restart: unless-stopped
command:
- $KC_START_CMD
env_file:
- .env
ports:
- "8180:8080"
volumes:
- ./data:/opt/keycloak/data
depends_on:
postgres:
condition: service_healthy
labels:
- "io.podman.annotations.label/fail2ban.enable=true"
- "io.podman.annotations.label/fail2ban.filter=caddy-auth"
KC_COMPOSE
echo ""
echo " ✓ Keycloak configured at $KC_DIR"
echo " ✓ Credentials saved in .env file"
if [ "$KC_PRODUCTION" = "y" ] || [ "$KC_PRODUCTION" = "Y" ]; then
echo " ✓ Production mode enabled"
else
echo " Development mode (use production mode for internet-facing deployments)"
fi
echo ""
# If Caddy is installed/being installed, offer to configure it for Keycloak
if [ "$INSTALL_CADDY" = "y" ] || [ "$INSTALL_CADDY" = "Y" ] || [ -d "$DOCKER_DIR/caddy" ]; then
echo ""
prompt_yn "Configure Caddy reverse proxy for Keycloak? (y/n):" "y" CONFIGURE_CADDY_KC
if [ "$CONFIGURE_CADDY_KC" = "y" ] || [ "$CONFIGURE_CADDY_KC" = "Y" ]; then
CADDY_DIR="$DOCKER_DIR/caddy"
# Ask for domain
prompt_text " Domain for Keycloak (e.g., auth.yourdomain.com):" "auth.localhost" KC_CADDY_DOMAIN
if [ -f "$CADDY_DIR/Caddyfile" ]; then
# Backup existing Caddyfile
mkdir -p "$CADDY_DIR/backups"
cp "$CADDY_DIR/Caddyfile" "$CADDY_DIR/backups/Caddyfile.backup.$(date +%Y%m%d_%H%M%S)"
echo " ✓ Backed up existing Caddyfile"
# Check if Keycloak config already exists
if ! grep -q "$KC_CADDY_DOMAIN" "$CADDY_DIR/Caddyfile"; then
# Add Keycloak configuration
cat >> "$CADDY_DIR/Caddyfile" << EOF
# Keycloak - Identity and Access Management
$KC_CADDY_DOMAIN {
log {
output file /var/log/caddy/keycloak-access.log
format json
level INFO
}
reverse_proxy localhost:8180
# Security headers
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
X-XSS-Protection "1; mode=block"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
EOF
echo " ✓ Added Keycloak configuration to Caddyfile"
# Reload Caddy if it's running
if docker ps --format '{{.Names}}' | grep -q "caddy"; then
CADDY_CONTAINER=$(docker ps --format '{{.Names}}' | grep "caddy" | head -1)
echo " Reloading Caddy configuration..."
if docker exec -w /etc/caddy "$CADDY_CONTAINER" caddy fmt --overwrite 2>/dev/null; then
echo " ✓ Formatted Caddyfile"
fi
if docker exec -w /etc/caddy "$CADDY_CONTAINER" caddy reload 2>/dev/null; then
echo " ✓ Caddy reloaded successfully"
echo ""
echo " Keycloak will be available at: https://$KC_CADDY_DOMAIN"
else
echo " ⚠ Failed to reload Caddy - check logs"
echo " Manual reload: cd $CADDY_DIR && docker exec -w /etc/caddy caddy caddy reload"
fi
else
echo " ⚠ Caddy container not running - start it to use this configuration"
fi
else
echo " Keycloak configuration already exists in Caddyfile"
fi
else
echo " ⚠ Caddyfile not found at $CADDY_DIR/Caddyfile"
echo " You can configure Caddy manually later"
fi
fi
fi
prompt_yn "Start Keycloak now? (y/n):" "y" START_KC
if [ "$START_KC" = "y" ] || [ "$START_KC" = "Y" ]; then
echo " Starting Keycloak (this may take a minute)..."
docker compose up -d 2>/dev/null && echo " ✓ Keycloak started" || echo " ⚠ Failed to start Keycloak"
# Automated initial configuration
echo ""
prompt_yn "Configure Keycloak with initial realm and clients? (y/n):" "y" CONFIGURE_KC
if [ "$CONFIGURE_KC" = "y" ] || [ "$CONFIGURE_KC" = "Y" ]; then
echo ""
echo " Configuring Keycloak..."
echo " This will create a realm and OAuth2 clients for your services."
echo ""
# Get realm name
prompt_text " Realm name (e.g., homelab, services):" "homelab" KC_REALM
# Get domain configuration for redirect URIs
echo ""
echo " ──────────────────────────────────────────────────────────────"
echo " DOMAIN CONFIGURATION"
echo " ──────────────────────────────────────────────────────────────"
echo ""
echo " Keycloak needs to know where your services are hosted."
echo ""
echo " Options:"
echo " 1. Local only (http://localhost:PORT)"
echo " 2. Public domain (https://yourdomain.com)"
echo " 3. Both local and public"
echo ""
prompt_text " Enter your setup (1/2/3):" "1" KC_SETUP_TYPE
KC_DOMAIN="localhost"
KC_PUBLIC_DOMAIN=""
KC_EXTERNAL_SERVICE=""
if [ "$KC_SETUP_TYPE" = "2" ] || [ "$KC_SETUP_TYPE" = "3" ]; then
echo ""
prompt_text " Your public domain (e.g., example.com):" "" KC_PUBLIC_DOMAIN
echo ""
echo " ⚠ IMPORTANT: For Keycloak to work with external services,"
echo " it MUST be accessible at https://auth.$KC_PUBLIC_DOMAIN"
echo ""
echo " This requires:"
echo " ✓ DNS A record: auth.$KC_PUBLIC_DOMAIN → Your Server IP"
echo " ✓ Caddy reverse proxy configured"
echo " ✓ Ports 80/443 open in firewall"
echo ""
prompt_yn " Is Keycloak accessible at https://auth.$KC_PUBLIC_DOMAIN? (y/n):" "n" KC_DOMAIN_READY
if [ "$KC_DOMAIN_READY" != "y" ] && [ "$KC_DOMAIN_READY" != "Y" ]; then
echo ""
echo " ⚠ WARNING: Keycloak won't work with external services until"
echo " you configure Caddy and DNS. See KEYCLOAK-SETUP-GUIDE.md"
echo ""
echo " You can still proceed and configure Caddy later."
echo ""
fi
# Ask about external services (like Pikapod)
echo ""
prompt_yn " Are you using external hosted services (e.g., Pikapod)? (y/n):" "n" KC_HAS_EXTERNAL
if [ "$KC_HAS_EXTERNAL" = "y" ] || [ "$KC_HAS_EXTERNAL" = "Y" ]; then
echo ""
echo " Enter the URL of your external service (e.g., https://actualbudget-abc.pikapod.net)"
prompt_text " External service URL:" "" KC_EXTERNAL_SERVICE
fi
fi
if [ "$KC_SETUP_TYPE" = "1" ] || [ "$KC_SETUP_TYPE" = "3" ]; then
KC_DOMAIN="localhost"
fi
# Wait for Keycloak to be fully ready (can take 30-60 seconds)
echo ""
echo " Waiting for Keycloak to be ready..."
KC_READY=false
for i in {1..60}; do
if docker exec keycloak curl -sf http://localhost:8080/health/ready > /dev/null 2>&1; then
KC_READY=true
echo " ✓ Keycloak is ready"
break
fi
echo -n "."
sleep 2
done
echo ""
if [ "$KC_READY" = true ]; then
# Login to Keycloak admin CLI
echo " Logging in to Keycloak admin CLI..."
docker exec keycloak /opt/keycloak/bin/kcadm.sh config credentials \
--server http://localhost:8080 \
--realm master \
--user admin \
--password "$KC_ADMIN_PASS" > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo " ✓ Logged in to Keycloak"
# Create realm
echo " Creating realm '$KC_REALM'..."
docker exec keycloak /opt/keycloak/bin/kcadm.sh create realms \
-s realm="$KC_REALM" \
-s enabled=true \
-s displayName="$KC_REALM" \
-s registrationAllowed=false \
-s resetPasswordAllowed=true \
-s rememberMe=true \
-s loginWithEmailAllowed=true \
-s duplicateEmailsAllowed=false \
-s sslRequired=EXTERNAL > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo " ✓ Created realm '$KC_REALM'"
fi
# Create OAuth2 client for ActualBudget
if [ "$INSTALL_ACTUALBUDGET" = "y" ] || [ "$INSTALL_ACTUALBUDGET" = "Y" ]; then
echo " Creating OAuth2 client for ActualBudget..."
AB_CLIENT_SECRET=$(openssl rand -hex 32)
# Build redirect URIs based on configuration
AB_REDIRECT_URIS='["http://localhost:5006/*","http://localhost:5006/callback"'
if [ -n "$KC_PUBLIC_DOMAIN" ]; then
AB_REDIRECT_URIS="$AB_REDIRECT_URIS"',"https://budget.'$KC_PUBLIC_DOMAIN'/*","https://budget.'$KC_PUBLIC_DOMAIN'/callback"'
AB_REDIRECT_URIS="$AB_REDIRECT_URIS"',"https://'$KC_PUBLIC_DOMAIN':5006/*","https://'$KC_PUBLIC_DOMAIN':5006/callback"'
fi
if [ -n "$KC_EXTERNAL_SERVICE" ]; then
AB_REDIRECT_URIS="$AB_REDIRECT_URIS"',"'$KC_EXTERNAL_SERVICE'/*","'$KC_EXTERNAL_SERVICE'/callback"'
fi
AB_REDIRECT_URIS="$AB_REDIRECT_URIS"']'
# Build web origins
AB_WEB_ORIGINS='["http://localhost:5006"'
if [ -n "$KC_PUBLIC_DOMAIN" ]; then
AB_WEB_ORIGINS="$AB_WEB_ORIGINS"',"https://budget.'$KC_PUBLIC_DOMAIN'","https://'$KC_PUBLIC_DOMAIN':5006"'
fi
if [ -n "$KC_EXTERNAL_SERVICE" ]; then
AB_WEB_ORIGINS="$AB_WEB_ORIGINS"',"'$KC_EXTERNAL_SERVICE'"'
fi
AB_WEB_ORIGINS="$AB_WEB_ORIGINS"']'
docker exec keycloak /opt/keycloak/bin/kcadm.sh create clients -r "$KC_REALM" \
-s clientId=actualbudget \
-s name="ActualBudget" \
-s description="Personal Finance Management" \
-s enabled=true \
-s clientAuthenticatorType=client-secret \
-s secret="$AB_CLIENT_SECRET" \
-s publicClient=false \
-s standardFlowEnabled=true \
-s directAccessGrantsEnabled=true \
-s serviceAccountsEnabled=false \
-s "redirectUris=$AB_REDIRECT_URIS" \
-s "webOrigins=$AB_WEB_ORIGINS" \
-s protocol=openid-connect > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo " ✓ Created ActualBudget client"
echo " Client ID: actualbudget"
echo " Client Secret: $AB_CLIENT_SECRET"
echo ""
# Save to file with appropriate URLs
KC_AUTH_URL="http://localhost:8180"
if [ -n "$KC_PUBLIC_DOMAIN" ]; then
KC_AUTH_URL="https://auth.$KC_PUBLIC_DOMAIN"
fi
cat > "$KC_DIR/actualbudget-oauth.txt" << EOF
ActualBudget OAuth2 Configuration
==================================
Client ID: actualbudget
Client Secret: $AB_CLIENT_SECRET
LOCAL DEVELOPMENT:
Authorization URL: http://localhost:8180/realms/$KC_REALM/protocol/openid-connect/auth
Token URL: http://localhost:8180/realms/$KC_REALM/protocol/openid-connect/token
User Info URL: http://localhost:8180/realms/$KC_REALM/protocol/openid-connect/userinfo
EOF
if [ -n "$KC_PUBLIC_DOMAIN" ]; then
cat >> "$KC_DIR/actualbudget-oauth.txt" << EOF
PRODUCTION (with Caddy at https://auth.$KC_PUBLIC_DOMAIN):
Authorization URL: https://auth.$KC_PUBLIC_DOMAIN/realms/$KC_REALM/protocol/openid-connect/auth
Token URL: https://auth.$KC_PUBLIC_DOMAIN/realms/$KC_REALM/protocol/openid-connect/token
User Info URL: https://auth.$KC_PUBLIC_DOMAIN/realms/$KC_REALM/protocol/openid-connect/userinfo
EOF
fi
if [ -n "$KC_EXTERNAL_SERVICE" ]; then
cat >> "$KC_DIR/actualbudget-oauth.txt" << EOF
EXTERNAL SERVICE ($KC_EXTERNAL_SERVICE):
- Use PRODUCTION URLs above
- Keycloak MUST be accessible at: https://auth.$KC_PUBLIC_DOMAIN
- Redirect URI configured: $KC_EXTERNAL_SERVICE/*
EOF
fi
cat >> "$KC_DIR/actualbudget-oauth.txt" << EOF
Redirect URIs configured:
- http://localhost:5006/* (local)
EOF
if [ -n "$KC_PUBLIC_DOMAIN" ]; then
cat >> "$KC_DIR/actualbudget-oauth.txt" << EOF
- https://budget.$KC_PUBLIC_DOMAIN/* (self-hosted)
EOF
fi
if [ -n "$KC_EXTERNAL_SERVICE" ]; then
cat >> "$KC_DIR/actualbudget-oauth.txt" << EOF
- $KC_EXTERNAL_SERVICE/* (external)
EOF
fi
cat >> "$KC_DIR/actualbudget-oauth.txt" << EOF
To configure ActualBudget:
1. Go to ActualBudget settings
2. Enable OpenID/OAuth authentication
3. Enter the Client ID and Secret above
4. Use the URLs above based on your setup
EOF
echo " ✓ Saved OAuth config to $KC_DIR/actualbudget-oauth.txt"
fi
fi
# Create a generic OAuth2 client template for other services
echo " Creating generic OAuth2 client for other services..."
GENERIC_CLIENT_SECRET=$(openssl rand -hex 32)
docker exec keycloak /opt/keycloak/bin/kcadm.sh create clients -r "$KC_REALM" \
-s clientId=generic-app \
-s name="Generic Application" \
-s description="Template client for other services" \
-s enabled=true \
-s clientAuthenticatorType=client-secret \
-s secret="$GENERIC_CLIENT_SECRET" \
-s publicClient=false \
-s standardFlowEnabled=true \
-s directAccessGrantsEnabled=true \
-s 'redirectUris=["http://localhost:*/*","https://'$KC_DOMAIN'/*","https://*.'$KC_DOMAIN'/*"]' \
-s 'webOrigins=["*"]' \
-s protocol=openid-connect > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo " ✓ Created generic OAuth2 client template"
cat > "$KC_DIR/generic-oauth.txt" << EOF
Generic OAuth2 Client Configuration
====================================
Client ID: generic-app
Client Secret: $GENERIC_CLIENT_SECRET
Use this as a template for other services. You can clone this client
in the Keycloak admin console and modify the redirect URIs.
Base URLs:
- Authorization: http://localhost:8180/realms/$KC_REALM/protocol/openid-connect/auth
- Token: http://localhost:8180/realms/$KC_REALM/protocol/openid-connect/token
- User Info: http://localhost:8180/realms/$KC_REALM/protocol/openid-connect/userinfo
For production: Replace localhost:8180 with https://auth.$KC_DOMAIN
EOF
echo " ✓ Saved config to $KC_DIR/generic-oauth.txt"
fi
# Optionally create initial user
echo ""
prompt_yn "Create an initial user in realm '$KC_REALM'? (y/n):" "y" CREATE_USER
if [ "$CREATE_USER" = "y" ] || [ "$CREATE_USER" = "Y" ]; then
prompt_text " Username:" "$ACTUAL_USER" KC_USERNAME
prompt_text " Email:" "${KC_USERNAME}@${KC_DOMAIN}" KC_EMAIL
prompt_text " First name:" "" KC_FIRSTNAME
prompt_text " Last name:" "" KC_LASTNAME
echo " Password for $KC_USERNAME:"
read -s KC_USER_PASS
echo ""
docker exec keycloak /opt/keycloak/bin/kcadm.sh create users -r "$KC_REALM" \
-s username="$KC_USERNAME" \
-s email="$KC_EMAIL" \
-s firstName="$KC_FIRSTNAME" \
-s lastName="$KC_LASTNAME" \
-s enabled=true \
-s emailVerified=true > /dev/null 2>&1
if [ $? -eq 0 ]; then
# Set password
KC_USER_ID=$(docker exec keycloak /opt/keycloak/bin/kcadm.sh get users -r "$KC_REALM" -q username="$KC_USERNAME" 2>/dev/null | grep -o '"id" : "[^"]*"' | cut -d'"' -f4)
docker exec keycloak /opt/keycloak/bin/kcadm.sh set-password -r "$KC_REALM" \
--username "$KC_USERNAME" \
--new-password "$KC_USER_PASS" > /dev/null 2>&1
echo " ✓ Created user: $KC_USERNAME"
echo " ✓ Password set"
echo ""
echo " This user can now log in to ActualBudget and other services!"
fi
fi
echo ""
echo " ✓ Keycloak configuration complete!"
echo ""
echo " Next steps:"
echo " 1. Go to http://localhost:8180/admin"
echo " 2. Login with admin / $KC_ADMIN_PASS"
echo " 3. Switch to realm '$KC_REALM' (top-left dropdown)"
echo " 4. Manage users in Users menu"
echo " 5. OAuth configs saved to $KC_DIR/*.txt"
echo ""
else
echo " ⚠ Failed to login to Keycloak admin CLI"
echo " You can configure Keycloak manually via the web UI"
fi
else
echo " ⚠ Keycloak did not become ready in time"
echo " You can configure it manually after it starts"
fi
fi
fi
echo ""
echo " Admin console: http://localhost:8180/admin"
echo " Username: admin"
echo " Password: $KC_ADMIN_PASS"
echo " Database: PostgreSQL (./postgres-data)"
if [ -n "$KC_REALM" ]; then
echo " Realm: $KC_REALM"
echo " Config files: $KC_DIR/*.txt"
fi
echo ""
echo " ⚠ For production:"
echo " - Use HTTPS via reverse proxy (Caddy)"
echo " - Change command to 'start' instead of 'start-dev'"
echo " - Set KC_HOSTNAME to your domain"
echo ""
fi
fi
# ---- CADDY WEB SERVER ----
if [ "$WHIPTAIL_USED" != true ] && [ -z "$INSTALL_CADDY" ]; then
echo ""
@@ -4966,23 +4341,6 @@ CADDY_COMPOSE
# }
# }
# Keycloak
# auth.yourdomain.com {
# log {
# output file /var/log/caddy/keycloak-access.log
# format json
# level INFO
# }
# reverse_proxy localhost:8180
# header {
# Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# X-Frame-Options "SAMEORIGIN"
# X-Content-Type-Options "nosniff"
# X-XSS-Protection "1; mode=block"
# Referrer-Policy "strict-origin-when-cross-origin"
# }
# }
# Add more services here...
CADDYFILE
echo " ✓ Created example Caddyfile"
+2 -644
View File
@@ -1331,7 +1331,7 @@ generate_password() {
openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c "$length"
}
# Validate password for Keycloak (alphanumeric only, minimum length)
# Validate password (alphanumeric only, minimum length)
# Usage: validate_password "password" [min_length]
# Returns 0 if valid, 1 if invalid
validate_password() {
@@ -1344,7 +1344,7 @@ validate_password() {
return 1
fi
# Check for special characters (not allowed for Keycloak)
# Check for special characters (not allowed)
if echo "$password" | grep -q '[^a-zA-Z0-9]'; then
echo " ⚠ Password must contain only letters and numbers (no special characters)"
return 1
@@ -2586,7 +2586,6 @@ else
[ -d "$DOCKER_DIR/filebrowser" ] && EXISTING_SERVICES[FILEBROWSER]="ON"
[ -d "$DOCKER_DIR/magicmirror" ] && EXISTING_SERVICES[MAGICMIRROR]="ON"
[ -d "$DOCKER_DIR/actualbudget" ] && EXISTING_SERVICES[ACTUALBUDGET]="ON"
[ -d "$DOCKER_DIR/keycloak" ] && EXISTING_SERVICES[KEYCLOAK]="ON"
[ -d "$DOCKER_DIR/authelia" ] && EXISTING_SERVICES[AUTHELIA]="ON"
[ -d "$DOCKER_DIR/caddy" ] && EXISTING_SERVICES[CADDY]="ON"
[ -d "$DOCKER_DIR/lms" ] && EXISTING_SERVICES[LYRION]="ON"
@@ -2627,7 +2626,6 @@ else
"FILEBROWSER" "Web-based file manager" ${EXISTING_SERVICES[FILEBROWSER]:-OFF} \
"MAGICMIRROR" "Smart mirror / dashboard display" ${EXISTING_SERVICES[MAGICMIRROR]:-OFF} \
"ACTUALBUDGET" "Personal finance management with bank sync" ${EXISTING_SERVICES[ACTUALBUDGET]:-OFF} \
"KEYCLOAK" "Identity & Access Management (SSO)" ${EXISTING_SERVICES[KEYCLOAK]:-OFF} \
"AUTHELIA" "SSO + 2FA auth portal for Caddy" ${EXISTING_SERVICES[AUTHELIA]:-OFF} \
"CADDY" "Reverse proxy with automatic HTTPS" ${EXISTING_SERVICES[CADDY]:-OFF} \
"FAIL2BAN" "Intrusion prevention system" ${EXISTING_SERVICES[FAIL2BAN]:-OFF} \
@@ -2659,7 +2657,6 @@ else
[ -n "${EXISTING_SERVICES[FILEBROWSER]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS FILEBROWSER \"Web file manager\" ON"
[ -n "${EXISTING_SERVICES[MAGICMIRROR]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS MAGICMIRROR \"Smart mirror\" ON"
[ -n "${EXISTING_SERVICES[ACTUALBUDGET]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS ACTUALBUDGET \"Personal finance\" ON"
[ -n "${EXISTING_SERVICES[KEYCLOAK]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS KEYCLOAK \"Identity management\" ON"
[ -n "${EXISTING_SERVICES[AUTHELIA]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS AUTHELIA \"SSO + 2FA auth portal\" ON"
[ -n "${EXISTING_SERVICES[CADDY]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS CADDY \"Reverse proxy\" ON"
[ -n "${EXISTING_SERVICES[FAIL2BAN]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS FAIL2BAN \"Intrusion prevention\" ON"
@@ -2710,7 +2707,6 @@ else
: ${INSTALL_FILEBROWSER:="n"}
: ${INSTALL_MAGICMIRROR:="n"}
: ${INSTALL_ACTUALBUDGET:="n"}
: ${INSTALL_KEYCLOAK:="n"}
: ${INSTALL_AUTHELIA:="n"}
: ${INSTALL_CADDY:="n"}
: ${INSTALL_FAIL2BAN:="n"}
@@ -2737,7 +2733,6 @@ else
if echo "$SELECTED_SERVICES" | grep -q "FILEBROWSER"; then INSTALL_FILEBROWSER="y"; fi
if echo "$SELECTED_SERVICES" | grep -q "MAGICMIRROR"; then INSTALL_MAGICMIRROR="y"; fi
if echo "$SELECTED_SERVICES" | grep -q "ACTUALBUDGET"; then INSTALL_ACTUALBUDGET="y"; fi
if echo "$SELECTED_SERVICES" | grep -q "KEYCLOAK"; then INSTALL_KEYCLOAK="y"; fi
if echo "$SELECTED_SERVICES" | grep -q "AUTHELIA"; then INSTALL_AUTHELIA="y"; fi
if echo "$SELECTED_SERVICES" | grep -q "CADDY"; then INSTALL_CADDY="y"; fi
if echo "$SELECTED_SERVICES" | grep -q "FAIL2BAN"; then INSTALL_FAIL2BAN="y"; fi
@@ -2811,7 +2806,6 @@ else
if echo "$SELECTED_SERVICES" | grep -q "FILEBROWSER"; then uninstall_service "FileBrowser" "$DOCKER_DIR/filebrowser" "filebrowser"; fi
if echo "$SELECTED_SERVICES" | grep -q "MAGICMIRROR"; then uninstall_service "MagicMirror" "$DOCKER_DIR/magicmirror" "magicmirror"; fi
if echo "$SELECTED_SERVICES" | grep -q "ACTUALBUDGET"; then uninstall_service "ActualBudget" "$DOCKER_DIR/actualbudget" "actualbudget"; fi
if echo "$SELECTED_SERVICES" | grep -q "KEYCLOAK"; then uninstall_service "Keycloak" "$DOCKER_DIR/keycloak" "keycloak"; fi
if echo "$SELECTED_SERVICES" | grep -q "AUTHELIA"; then uninstall_service "Authelia" "$DOCKER_DIR/authelia" "authelia"; fi
if echo "$SELECTED_SERVICES" | grep -q "CADDY"; then uninstall_service "Caddy" "$DOCKER_DIR/caddy" "caddy"; fi
if echo "$SELECTED_SERVICES" | grep -q "LYRION"; then uninstall_service "Lyrion" "$DOCKER_DIR/lms" "lms"; fi
@@ -4231,625 +4225,6 @@ AB_COMPOSE
fi # End AB_RECONFIGURE check
fi # End INSTALL_ACTUALBUDGET check
# ---- KEYCLOAK ----
if [ "$WHIPTAIL_USED" != true ] && [ -z "$INSTALL_KEYCLOAK" ]; then
echo ""
echo "┌─────────────────────────────────────────────────────────────────┐"
echo "│ KEYCLOAK - Identity and Access Management (IAM) │"
echo "│ SSO, OAuth2, SAML, User Management, MFA │"
echo "│ Port: 8180 (HTTP) - Use reverse proxy for HTTPS │"
echo "└─────────────────────────────────────────────────────────────────┘"
prompt_yn "Install Keycloak? (y/n):" "n" INSTALL_KEYCLOAK
fi
if [ "$INSTALL_KEYCLOAK" = "y" ] || [ "$INSTALL_KEYCLOAK" = "Y" ]; then
KC_DIR="$DOCKER_DIR/keycloak"
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Would create $KC_DIR"
else
echo "Installing Keycloak..."
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "PASSWORD REQUIREMENTS:"
echo " • Minimum 12 characters (16+ recommended)"
echo " • Letters and numbers ONLY (no special characters)"
echo " • Press ENTER for secure auto-generated password"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Prompt for admin password with validation
KC_ADMIN_PASS=""
while true; do
read -s -p "Enter Keycloak admin password [auto-generate]: " KC_ADMIN_PASS
echo ""
# Generate secure password if user pressed Enter
if [ -z "$KC_ADMIN_PASS" ]; then
KC_ADMIN_PASS=$(generate_password 20)
echo " ✓ Generated secure admin password (saved in .env)"
break
fi
# Validate password
if validate_password "$KC_ADMIN_PASS" 12; then
echo " ✓ Admin password accepted"
break
fi
echo " Please try again."
done
# Prompt for database password with validation
KC_DB_PASS=""
while true; do
read -s -p "Enter database password [auto-generate]: " KC_DB_PASS
echo ""
# Generate secure password if user pressed Enter
if [ -z "$KC_DB_PASS" ]; then
KC_DB_PASS=$(generate_password 32)
echo " ✓ Generated secure database password (saved in .env)"
break
fi
# Validate password
if validate_password "$KC_DB_PASS" 12; then
echo " ✓ Database password accepted"
break
fi
echo " Please try again."
done
echo ""
# Ask about production vs development mode
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "DEPLOYMENT MODE:"
echo " • Production: Requires HTTPS via Caddy2 (recommended)"
echo " • Development: HTTP only, relaxed security (testing only)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
prompt_yn "Use production mode? (requires Caddy2 reverse proxy) (y/n):" "y" KC_PRODUCTION
KC_HOSTNAME=""
KC_START_CMD="start-dev"
KC_HOSTNAME_STRICT="false"
if [ "$KC_PRODUCTION" = "y" ] || [ "$KC_PRODUCTION" = "Y" ]; then
KC_START_CMD="start"
KC_HOSTNAME_STRICT="false"
echo ""
echo "Enter your Keycloak hostname (e.g., auth.yourdomain.com)"
echo "This should match your Caddy2 configuration."
read -p "Hostname: " KC_HOSTNAME
if [ -n "$KC_HOSTNAME" ]; then
echo " ✓ Production mode enabled with hostname: $KC_HOSTNAME"
echo " ⚠ Make sure Caddy2 is configured for this domain!"
else
echo " ⚠ No hostname provided - using relaxed mode"
KC_HOSTNAME=""
fi
fi
mkdir -p "$KC_DIR/data" "$KC_DIR/postgres-data"
ensure_docker_dir_ownership "$KC_DIR"
cd "$KC_DIR"
# Create .env file for sensitive credentials
cat > .env << KC_ENV
# Keycloak Environment Variables
# ⚠ KEEP THIS FILE SECURE - Contains sensitive passwords
# Admin Credentials
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=$KC_ADMIN_PASS
# Database Credentials
POSTGRES_DB=keycloak
POSTGRES_USER=keycloak
POSTGRES_PASSWORD=$KC_DB_PASS
KC_DB=postgres
KC_DB_URL=jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAME=keycloak
KC_DB_PASSWORD=$KC_DB_PASS
# Keycloak Configuration
# Proxy settings (v2) - Trust X-Forwarded-* headers from Caddy2
KC_PROXY_HEADERS=xforwarded
KC_HTTP_ENABLED=true
KC_HOSTNAME_STRICT=$KC_HOSTNAME_STRICT
KC_LOG_LEVEL=INFO
KC_HEALTH_ENABLED=true
KC_METRICS_ENABLED=true
KC_ENV
# Add hostname to .env if provided
if [ -n "$KC_HOSTNAME" ]; then
echo "KC_HOSTNAME=$KC_HOSTNAME" >> .env
fi
# Create docker-compose.yml
cat > docker-compose.yml << KC_COMPOSE
name: keycloak
services:
postgres:
image: postgres:16-alpine
container_name: keycloak-db
restart: unless-stopped
env_file:
- .env
volumes:
- ./postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U keycloak"]
interval: 10s
timeout: 5s
retries: 5
keycloak:
image: quay.io/keycloak/keycloak:latest
container_name: keycloak
restart: unless-stopped
command:
- $KC_START_CMD
env_file:
- .env
ports:
- "8180:8080"
volumes:
- ./data:/opt/keycloak/data
depends_on:
postgres:
condition: service_healthy
labels:
- "io.podman.annotations.label/fail2ban.enable=true"
- "io.podman.annotations.label/fail2ban.filter=caddy-auth"
KC_COMPOSE
echo ""
echo " ✓ Keycloak configured at $KC_DIR"
echo " ✓ Credentials saved in .env file"
if [ "$KC_PRODUCTION" = "y" ] || [ "$KC_PRODUCTION" = "Y" ]; then
echo " ✓ Production mode enabled"
else
echo " Development mode (use production mode for internet-facing deployments)"
fi
echo ""
# If Caddy is installed/being installed, offer to configure it for Keycloak
if [ "$INSTALL_CADDY" = "y" ] || [ "$INSTALL_CADDY" = "Y" ] || [ -d "$DOCKER_DIR/caddy" ]; then
echo ""
prompt_yn "Configure Caddy reverse proxy for Keycloak? (y/n):" "y" CONFIGURE_CADDY_KC
if [ "$CONFIGURE_CADDY_KC" = "y" ] || [ "$CONFIGURE_CADDY_KC" = "Y" ]; then
CADDY_DIR="$DOCKER_DIR/caddy"
# Ask for domain
prompt_text " Domain for Keycloak (e.g., auth.yourdomain.com):" "auth.localhost" KC_CADDY_DOMAIN
if [ -f "$CADDY_DIR/Caddyfile" ]; then
# Backup existing Caddyfile
mkdir -p "$CADDY_DIR/backups"
cp "$CADDY_DIR/Caddyfile" "$CADDY_DIR/backups/Caddyfile.backup.$(date +%Y%m%d_%H%M%S)"
echo " ✓ Backed up existing Caddyfile"
# Check if Keycloak config already exists
if ! grep -q "$KC_CADDY_DOMAIN" "$CADDY_DIR/Caddyfile"; then
# Add Keycloak configuration
cat >> "$CADDY_DIR/Caddyfile" << EOF
# Keycloak - Identity and Access Management
$KC_CADDY_DOMAIN {
log {
output file /var/log/caddy/keycloak-access.log
format json
level INFO
}
reverse_proxy localhost:8180
# Security headers
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
X-XSS-Protection "1; mode=block"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
EOF
echo " ✓ Added Keycloak configuration to Caddyfile"
# Reload Caddy if it's running
if docker ps --format '{{.Names}}' | grep -q "caddy"; then
CADDY_CONTAINER=$(docker ps --format '{{.Names}}' | grep "caddy" | head -1)
echo " Reloading Caddy configuration..."
if docker exec -w /etc/caddy "$CADDY_CONTAINER" caddy fmt --overwrite 2>/dev/null; then
echo " ✓ Formatted Caddyfile"
fi
if docker exec -w /etc/caddy "$CADDY_CONTAINER" caddy reload 2>/dev/null; then
echo " ✓ Caddy reloaded successfully"
echo ""
echo " Keycloak will be available at: https://$KC_CADDY_DOMAIN"
else
echo " ⚠ Failed to reload Caddy - check logs"
echo " Manual reload: cd $CADDY_DIR && docker exec -w /etc/caddy caddy caddy reload"
fi
else
echo " ⚠ Caddy container not running - start it to use this configuration"
fi
else
echo " Keycloak configuration already exists in Caddyfile"
fi
else
echo " ⚠ Caddyfile not found at $CADDY_DIR/Caddyfile"
echo " You can configure Caddy manually later"
fi
fi
fi
prompt_yn "Start Keycloak now? (y/n):" "y" START_KC
if [ "$START_KC" = "y" ] || [ "$START_KC" = "Y" ]; then
echo " Starting Keycloak (this may take a minute)..."
docker compose up -d 2>/dev/null && echo " ✓ Keycloak started" || echo " ⚠ Failed to start Keycloak"
# Automated initial configuration
echo ""
prompt_yn "Configure Keycloak with initial realm and clients? (y/n):" "y" CONFIGURE_KC
if [ "$CONFIGURE_KC" = "y" ] || [ "$CONFIGURE_KC" = "Y" ]; then
echo ""
echo " Configuring Keycloak..."
echo " This will create a realm and OAuth2 clients for your services."
echo ""
# Get realm name
prompt_text " Realm name (e.g., homelab, services):" "homelab" KC_REALM
# Get domain configuration for redirect URIs
echo ""
echo " ──────────────────────────────────────────────────────────────"
echo " DOMAIN CONFIGURATION"
echo " ──────────────────────────────────────────────────────────────"
echo ""
echo " Keycloak needs to know where your services are hosted."
echo ""
echo " Options:"
echo " 1. Local only (http://localhost:PORT)"
echo " 2. Public domain (https://yourdomain.com)"
echo " 3. Both local and public"
echo ""
prompt_text " Enter your setup (1/2/3):" "1" KC_SETUP_TYPE
KC_DOMAIN="localhost"
KC_PUBLIC_DOMAIN=""
KC_EXTERNAL_SERVICE=""
if [ "$KC_SETUP_TYPE" = "2" ] || [ "$KC_SETUP_TYPE" = "3" ]; then
echo ""
prompt_text " Your public domain (e.g., example.com):" "" KC_PUBLIC_DOMAIN
echo ""
echo " ⚠ IMPORTANT: For Keycloak to work with external services,"
echo " it MUST be accessible at https://auth.$KC_PUBLIC_DOMAIN"
echo ""
echo " This requires:"
echo " ✓ DNS A record: auth.$KC_PUBLIC_DOMAIN → Your Server IP"
echo " ✓ Caddy reverse proxy configured"
echo " ✓ Ports 80/443 open in firewall"
echo ""
prompt_yn " Is Keycloak accessible at https://auth.$KC_PUBLIC_DOMAIN? (y/n):" "n" KC_DOMAIN_READY
if [ "$KC_DOMAIN_READY" != "y" ] && [ "$KC_DOMAIN_READY" != "Y" ]; then
echo ""
echo " ⚠ WARNING: Keycloak won't work with external services until"
echo " you configure Caddy and DNS. See KEYCLOAK-SETUP-GUIDE.md"
echo ""
echo " You can still proceed and configure Caddy later."
echo ""
fi
# Ask about external services (like Pikapod)
echo ""
prompt_yn " Are you using external hosted services (e.g., Pikapod)? (y/n):" "n" KC_HAS_EXTERNAL
if [ "$KC_HAS_EXTERNAL" = "y" ] || [ "$KC_HAS_EXTERNAL" = "Y" ]; then
echo ""
echo " Enter the URL of your external service (e.g., https://actualbudget-abc.pikapod.net)"
prompt_text " External service URL:" "" KC_EXTERNAL_SERVICE
fi
fi
if [ "$KC_SETUP_TYPE" = "1" ] || [ "$KC_SETUP_TYPE" = "3" ]; then
KC_DOMAIN="localhost"
fi
# Wait for Keycloak to be fully ready (can take 30-60 seconds)
echo ""
echo " Waiting for Keycloak to be ready..."
KC_READY=false
for i in {1..60}; do
if docker exec keycloak curl -sf http://localhost:8080/health/ready > /dev/null 2>&1; then
KC_READY=true
echo " ✓ Keycloak is ready"
break
fi
echo -n "."
sleep 2
done
echo ""
if [ "$KC_READY" = true ]; then
# Login to Keycloak admin CLI
echo " Logging in to Keycloak admin CLI..."
docker exec keycloak /opt/keycloak/bin/kcadm.sh config credentials \
--server http://localhost:8080 \
--realm master \
--user admin \
--password "$KC_ADMIN_PASS" > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo " ✓ Logged in to Keycloak"
# Create realm
echo " Creating realm '$KC_REALM'..."
docker exec keycloak /opt/keycloak/bin/kcadm.sh create realms \
-s realm="$KC_REALM" \
-s enabled=true \
-s displayName="$KC_REALM" \
-s registrationAllowed=false \
-s resetPasswordAllowed=true \
-s rememberMe=true \
-s loginWithEmailAllowed=true \
-s duplicateEmailsAllowed=false \
-s sslRequired=EXTERNAL > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo " ✓ Created realm '$KC_REALM'"
fi
# Create OAuth2 client for ActualBudget
if [ "$INSTALL_ACTUALBUDGET" = "y" ] || [ "$INSTALL_ACTUALBUDGET" = "Y" ]; then
echo " Creating OAuth2 client for ActualBudget..."
AB_CLIENT_SECRET=$(openssl rand -hex 32)
# Build redirect URIs based on configuration
AB_REDIRECT_URIS='["http://localhost:5006/*","http://localhost:5006/callback"'
if [ -n "$KC_PUBLIC_DOMAIN" ]; then
AB_REDIRECT_URIS="$AB_REDIRECT_URIS"',"https://budget.'$KC_PUBLIC_DOMAIN'/*","https://budget.'$KC_PUBLIC_DOMAIN'/callback"'
AB_REDIRECT_URIS="$AB_REDIRECT_URIS"',"https://'$KC_PUBLIC_DOMAIN':5006/*","https://'$KC_PUBLIC_DOMAIN':5006/callback"'
fi
if [ -n "$KC_EXTERNAL_SERVICE" ]; then
AB_REDIRECT_URIS="$AB_REDIRECT_URIS"',"'$KC_EXTERNAL_SERVICE'/*","'$KC_EXTERNAL_SERVICE'/callback"'
fi
AB_REDIRECT_URIS="$AB_REDIRECT_URIS"']'
# Build web origins
AB_WEB_ORIGINS='["http://localhost:5006"'
if [ -n "$KC_PUBLIC_DOMAIN" ]; then
AB_WEB_ORIGINS="$AB_WEB_ORIGINS"',"https://budget.'$KC_PUBLIC_DOMAIN'","https://'$KC_PUBLIC_DOMAIN':5006"'
fi
if [ -n "$KC_EXTERNAL_SERVICE" ]; then
AB_WEB_ORIGINS="$AB_WEB_ORIGINS"',"'$KC_EXTERNAL_SERVICE'"'
fi
AB_WEB_ORIGINS="$AB_WEB_ORIGINS"']'
docker exec keycloak /opt/keycloak/bin/kcadm.sh create clients -r "$KC_REALM" \
-s clientId=actualbudget \
-s name="ActualBudget" \
-s description="Personal Finance Management" \
-s enabled=true \
-s clientAuthenticatorType=client-secret \
-s secret="$AB_CLIENT_SECRET" \
-s publicClient=false \
-s standardFlowEnabled=true \
-s directAccessGrantsEnabled=true \
-s serviceAccountsEnabled=false \
-s "redirectUris=$AB_REDIRECT_URIS" \
-s "webOrigins=$AB_WEB_ORIGINS" \
-s protocol=openid-connect > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo " ✓ Created ActualBudget client"
echo " Client ID: actualbudget"
echo " Client Secret: $AB_CLIENT_SECRET"
echo ""
# Save to file with appropriate URLs
KC_AUTH_URL="http://localhost:8180"
if [ -n "$KC_PUBLIC_DOMAIN" ]; then
KC_AUTH_URL="https://auth.$KC_PUBLIC_DOMAIN"
fi
cat > "$KC_DIR/actualbudget-oauth.txt" << EOF
ActualBudget OAuth2 Configuration
==================================
Client ID: actualbudget
Client Secret: $AB_CLIENT_SECRET
LOCAL DEVELOPMENT:
Authorization URL: http://localhost:8180/realms/$KC_REALM/protocol/openid-connect/auth
Token URL: http://localhost:8180/realms/$KC_REALM/protocol/openid-connect/token
User Info URL: http://localhost:8180/realms/$KC_REALM/protocol/openid-connect/userinfo
EOF
if [ -n "$KC_PUBLIC_DOMAIN" ]; then
cat >> "$KC_DIR/actualbudget-oauth.txt" << EOF
PRODUCTION (with Caddy at https://auth.$KC_PUBLIC_DOMAIN):
Authorization URL: https://auth.$KC_PUBLIC_DOMAIN/realms/$KC_REALM/protocol/openid-connect/auth
Token URL: https://auth.$KC_PUBLIC_DOMAIN/realms/$KC_REALM/protocol/openid-connect/token
User Info URL: https://auth.$KC_PUBLIC_DOMAIN/realms/$KC_REALM/protocol/openid-connect/userinfo
EOF
fi
if [ -n "$KC_EXTERNAL_SERVICE" ]; then
cat >> "$KC_DIR/actualbudget-oauth.txt" << EOF
EXTERNAL SERVICE ($KC_EXTERNAL_SERVICE):
- Use PRODUCTION URLs above
- Keycloak MUST be accessible at: https://auth.$KC_PUBLIC_DOMAIN
- Redirect URI configured: $KC_EXTERNAL_SERVICE/*
EOF
fi
cat >> "$KC_DIR/actualbudget-oauth.txt" << EOF
Redirect URIs configured:
- http://localhost:5006/* (local)
EOF
if [ -n "$KC_PUBLIC_DOMAIN" ]; then
cat >> "$KC_DIR/actualbudget-oauth.txt" << EOF
- https://budget.$KC_PUBLIC_DOMAIN/* (self-hosted)
EOF
fi
if [ -n "$KC_EXTERNAL_SERVICE" ]; then
cat >> "$KC_DIR/actualbudget-oauth.txt" << EOF
- $KC_EXTERNAL_SERVICE/* (external)
EOF
fi
cat >> "$KC_DIR/actualbudget-oauth.txt" << EOF
To configure ActualBudget:
1. Go to ActualBudget settings
2. Enable OpenID/OAuth authentication
3. Enter the Client ID and Secret above
4. Use the URLs above based on your setup
EOF
echo " ✓ Saved OAuth config to $KC_DIR/actualbudget-oauth.txt"
fi
fi
# Create a generic OAuth2 client template for other services
echo " Creating generic OAuth2 client for other services..."
GENERIC_CLIENT_SECRET=$(openssl rand -hex 32)
docker exec keycloak /opt/keycloak/bin/kcadm.sh create clients -r "$KC_REALM" \
-s clientId=generic-app \
-s name="Generic Application" \
-s description="Template client for other services" \
-s enabled=true \
-s clientAuthenticatorType=client-secret \
-s secret="$GENERIC_CLIENT_SECRET" \
-s publicClient=false \
-s standardFlowEnabled=true \
-s directAccessGrantsEnabled=true \
-s 'redirectUris=["http://localhost:*/*","https://'$KC_DOMAIN'/*","https://*.'$KC_DOMAIN'/*"]' \
-s 'webOrigins=["*"]' \
-s protocol=openid-connect > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo " ✓ Created generic OAuth2 client template"
cat > "$KC_DIR/generic-oauth.txt" << EOF
Generic OAuth2 Client Configuration
====================================
Client ID: generic-app
Client Secret: $GENERIC_CLIENT_SECRET
Use this as a template for other services. You can clone this client
in the Keycloak admin console and modify the redirect URIs.
Base URLs:
- Authorization: http://localhost:8180/realms/$KC_REALM/protocol/openid-connect/auth
- Token: http://localhost:8180/realms/$KC_REALM/protocol/openid-connect/token
- User Info: http://localhost:8180/realms/$KC_REALM/protocol/openid-connect/userinfo
For production: Replace localhost:8180 with https://auth.$KC_DOMAIN
EOF
echo " ✓ Saved config to $KC_DIR/generic-oauth.txt"
fi
# Optionally create initial user
echo ""
prompt_yn "Create an initial user in realm '$KC_REALM'? (y/n):" "y" CREATE_USER
if [ "$CREATE_USER" = "y" ] || [ "$CREATE_USER" = "Y" ]; then
prompt_text " Username:" "$ACTUAL_USER" KC_USERNAME
prompt_text " Email:" "${KC_USERNAME}@${KC_DOMAIN}" KC_EMAIL
prompt_text " First name:" "" KC_FIRSTNAME
prompt_text " Last name:" "" KC_LASTNAME
echo " Password for $KC_USERNAME:"
read -s KC_USER_PASS
echo ""
docker exec keycloak /opt/keycloak/bin/kcadm.sh create users -r "$KC_REALM" \
-s username="$KC_USERNAME" \
-s email="$KC_EMAIL" \
-s firstName="$KC_FIRSTNAME" \
-s lastName="$KC_LASTNAME" \
-s enabled=true \
-s emailVerified=true > /dev/null 2>&1
if [ $? -eq 0 ]; then
# Set password
KC_USER_ID=$(docker exec keycloak /opt/keycloak/bin/kcadm.sh get users -r "$KC_REALM" -q username="$KC_USERNAME" 2>/dev/null | grep -o '"id" : "[^"]*"' | cut -d'"' -f4)
docker exec keycloak /opt/keycloak/bin/kcadm.sh set-password -r "$KC_REALM" \
--username "$KC_USERNAME" \
--new-password "$KC_USER_PASS" > /dev/null 2>&1
echo " ✓ Created user: $KC_USERNAME"
echo " ✓ Password set"
echo ""
echo " This user can now log in to ActualBudget and other services!"
fi
fi
echo ""
echo " ✓ Keycloak configuration complete!"
echo ""
echo " Next steps:"
echo " 1. Go to http://localhost:8180/admin"
echo " 2. Login with admin / $KC_ADMIN_PASS"
echo " 3. Switch to realm '$KC_REALM' (top-left dropdown)"
echo " 4. Manage users in Users menu"
echo " 5. OAuth configs saved to $KC_DIR/*.txt"
echo ""
else
echo " ⚠ Failed to login to Keycloak admin CLI"
echo " You can configure Keycloak manually via the web UI"
fi
else
echo " ⚠ Keycloak did not become ready in time"
echo " You can configure it manually after it starts"
fi
fi
fi
echo ""
echo " Admin console: http://localhost:8180/admin"
echo " Username: admin"
echo " Password: $KC_ADMIN_PASS"
echo " Database: PostgreSQL (./postgres-data)"
if [ -n "$KC_REALM" ]; then
echo " Realm: $KC_REALM"
echo " Config files: $KC_DIR/*.txt"
fi
echo ""
echo " ⚠ For production:"
echo " - Use HTTPS via reverse proxy (Caddy)"
echo " - Change command to 'start' instead of 'start-dev'"
echo " - Set KC_HOSTNAME to your domain"
echo ""
fi
fi
# ---- CADDY WEB SERVER ----
if [ "$WHIPTAIL_USED" != true ] && [ -z "$INSTALL_CADDY" ]; then
echo ""
@@ -4946,23 +4321,6 @@ CADDY_COMPOSE
# }
# }
# Keycloak
# auth.yourdomain.com {
# log {
# output file /var/log/caddy/keycloak-access.log
# format json
# level INFO
# }
# reverse_proxy localhost:8180
# header {
# Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# X-Frame-Options "SAMEORIGIN"
# X-Content-Type-Options "nosniff"
# X-XSS-Protection "1; mode=block"
# Referrer-Policy "strict-origin-when-cross-origin"
# }
# }
# Add more services here...
CADDYFILE
echo " ✓ Created example Caddyfile"