Add no-keycloak and CrowdSec script variants; restore originals
Provide three tiers of the install script for both 24.04 and 26.04: - Originals (ubuntu-post-install-24.04.sh / -26.04.sh): restored to their true original state, with Keycloak intact, as a fallback baseline. (This reverts the in-place Keycloak removal from the previous commit; the cleanup now lives in the -no-keycloak variants instead.) - -no-keycloak.sh: Keycloak fully removed, Authelia as the SSO/2FA option. - -crowdsec.sh: builds on -no-keycloak and replaces fail2ban entirely with CrowdSec (SSH via auth.log/sshd collection, Caddy via caddy collection + log acquisition, firewall bouncer for enforcement, plus geo-blocking and community IP-reputation blocklists). Add SCRIPT-VARIANTS.md documenting the three tiers and how the Authelia / fail2ban / CrowdSec security layers differ. All variants pass 'bash -n'. https://claude.ai/code/session_017eA2qqq9jfF2tNtpUYL8vK
This commit is contained in:
+7
-15
@@ -61,17 +61,17 @@ budget.yourdomain.com {
|
||||
}
|
||||
```
|
||||
|
||||
#### Authelia (SSO + 2FA auth portal)
|
||||
#### Keycloak (Identity & Access Management)
|
||||
|
||||
```caddy
|
||||
auth.yourdomain.com {
|
||||
log {
|
||||
output file /var/log/caddy/authelia-access.log
|
||||
output file /var/log/caddy/keycloak-access.log
|
||||
format json
|
||||
level INFO
|
||||
}
|
||||
|
||||
reverse_proxy localhost:9091
|
||||
reverse_proxy localhost:8180
|
||||
|
||||
# Security headers
|
||||
header {
|
||||
@@ -315,23 +315,15 @@ logpath = /var/log/caddy/actualbudget-access.log
|
||||
maxretry = 3
|
||||
bantime = 7200
|
||||
|
||||
[caddy-authelia]
|
||||
[caddy-keycloak]
|
||||
enabled = true
|
||||
port = http,https
|
||||
filter = caddy-auth
|
||||
logpath = /var/log/caddy/authelia-access.log
|
||||
logpath = /var/log/caddy/keycloak-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**
|
||||
@@ -367,7 +359,7 @@ cp ~/docker/caddy/Caddyfile ~/docker/caddy/Caddyfile.backup
|
||||
### Service Ports
|
||||
|
||||
- **ActualBudget**: 5006
|
||||
- **Authelia**: 9091
|
||||
- **Keycloak**: 8180
|
||||
- **Caddy**: 80 (HTTP), 443 (HTTPS)
|
||||
|
||||
## Support
|
||||
@@ -376,4 +368,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/
|
||||
- Authelia docs: https://www.authelia.com/
|
||||
- Keycloak docs: https://www.keycloak.org/documentation
|
||||
|
||||
@@ -0,0 +1,678 @@
|
||||
# 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
|
||||
@@ -51,7 +51,7 @@
|
||||
│ │
|
||||
│ === DOCKER SERVICES === │
|
||||
│ [ ] ACTUALBUDGET Personal finance │
|
||||
│ [ ] AUTHELIA SSO + 2FA auth portal │
|
||||
│ [ ] KEYCLOAK Identity management │
|
||||
│ [ ] 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 (Authelia, etc.)
|
||||
2. Install services that depend on Caddy (Keycloak, etc.)
|
||||
3. Install fail2ban for Caddy (if selected + Caddy installed)
|
||||
4. Install independent services in parallel where possible
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ This script is divided into TWO main phases:
|
||||
│ • Skip services you don't want │
|
||||
│ │
|
||||
│ Services include: │
|
||||
│ • Self-hosted apps (ActualBudget, Authelia, Jellyfin, etc.) │
|
||||
│ • Self-hosted apps (ActualBudget, Keycloak, Jellyfin, etc.) │
|
||||
│ • Network services (Samba, VPNs, fail2ban) │
|
||||
│ • Monitoring tools (Uptime Kuma, Portainer, Watchtower) │
|
||||
│ • And many more... │
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Install Script Variants
|
||||
|
||||
This repo ships the post-install script in three tiers, for both Ubuntu 24.04
|
||||
and 26.04. Pick **one** and run it — they are mutually exclusive (each is a
|
||||
complete, standalone script).
|
||||
|
||||
| File | Keycloak | SSO | Intrusion prevention |
|
||||
|------|----------|-----|----------------------|
|
||||
| `ubuntu-post-install-<ver>.sh` | ✅ included | Keycloak **or** Authelia | fail2ban (SSH + Caddy) |
|
||||
| `ubuntu-post-install-<ver>-no-keycloak.sh` | ❌ removed | Authelia | fail2ban (SSH + Caddy) |
|
||||
| `ubuntu-post-install-<ver>-crowdsec.sh` | ❌ removed | Authelia | **CrowdSec** (replaces fail2ban) |
|
||||
|
||||
`<ver>` is `24.04` or `26.04`.
|
||||
|
||||
## Which one?
|
||||
|
||||
- **Original (`.sh`)** — unchanged baseline, kept for fallback. Still offers
|
||||
Keycloak in the menu.
|
||||
- **`-no-keycloak`** — same as original but with Keycloak fully removed.
|
||||
Authelia is the SSO + 2FA option. Use this if you never got Keycloak running
|
||||
and have standardized on Authelia.
|
||||
- **`-crowdsec`** — builds on `-no-keycloak` and swaps fail2ban out for
|
||||
[CrowdSec](https://www.crowdsec.net/):
|
||||
- SSH brute-force protection (CrowdSec reads `/var/log/auth.log` via the
|
||||
`crowdsecurity/sshd` collection)
|
||||
- Caddy HTTP auth abuse (the `crowdsecurity/caddy` collection + a log
|
||||
acquisition at `/etc/crowdsec/acquis.d/caddy.yaml`)
|
||||
- Enforcement via `crowdsec-firewall-bouncer-iptables`
|
||||
- **Geo-blocking + community IP-reputation blocklists** — the capability
|
||||
that fail2ban and Authelia both lack
|
||||
|
||||
## Notes on the security layers
|
||||
|
||||
- **Authelia** handles per-account failed-login *regulation* (lockout). It does
|
||||
**not** do geo-blocking.
|
||||
- **fail2ban** bans IPs at the firewall based on Caddy log patterns
|
||||
(401/403/429). No geo-blocking, not credential-aware.
|
||||
- **CrowdSec** covers SSH + Caddy from a single agent, adds geo/ASN enrichment
|
||||
and crowd-sourced reputation, and is the modern successor to fail2ban.
|
||||
|
||||
Useful CrowdSec commands after install:
|
||||
|
||||
```bash
|
||||
sudo cscli metrics # overview / parsing health
|
||||
sudo cscli decisions list # current bans
|
||||
sudo cscli alerts list # detections
|
||||
sudo cscli decisions delete --ip <IP> # unban
|
||||
```
|
||||
+140
-23
@@ -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, ActualBudget, Jellyfin, etc.)
|
||||
- Applied to ALL 25+ services (Immich, Keycloak, 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. Password & Credential Management
|
||||
**Problem:** Weak default passwords and credentials hardcoded in compose files.
|
||||
### 2. Keycloak Security Overhaul
|
||||
**Problem:** Weak default passwords, special characters causing issues, development mode in production.
|
||||
|
||||
**Solutions Implemented:**
|
||||
|
||||
@@ -30,20 +30,35 @@ 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
|
||||
|
||||
#### Generated Secrets
|
||||
- Services that need cryptographic secrets generate them automatically (e.g. Authelia's JWT, session, and storage secrets via `openssl rand`).
|
||||
#### 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
|
||||
|
||||
#### Environment Variables
|
||||
- All credentials moved to `.env` file
|
||||
- Passwords and secrets securely stored
|
||||
- Admin password and database password 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:**
|
||||
- ✅ Authelia (JWT/session/storage secrets + SMTP password)
|
||||
- ✅ Keycloak (admin + database passwords)
|
||||
- ✅ ActualBudget (timezone and config)
|
||||
- ✅ Immich (already had .env)
|
||||
- ✅ FindMyDevice (already had .env)
|
||||
@@ -105,7 +120,7 @@ labels:
|
||||
|
||||
**Services with fail2ban monitoring:**
|
||||
- ActualBudget
|
||||
- Authelia
|
||||
- Keycloak
|
||||
- All other internet-facing services
|
||||
|
||||
**fail2ban Configuration:**
|
||||
@@ -115,10 +130,7 @@ labels:
|
||||
- Ban duration: 3600 seconds (1 hour)
|
||||
- Detection window: 600 seconds
|
||||
|
||||
**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.
|
||||
**Detailed Setup:** See `CADDY-FAIL2BAN-SETUP.md` for complete configuration.
|
||||
|
||||
---
|
||||
|
||||
@@ -142,7 +154,7 @@ PASSWORD=$(generate_password 20) # 20-character password
|
||||
```
|
||||
|
||||
### `validate_password(password [min_length])`
|
||||
Validates passwords for compatibility (alphanumeric only).
|
||||
Validates passwords for Keycloak compatibility.
|
||||
|
||||
**Validation Rules:**
|
||||
- Minimum length (default: 12 characters)
|
||||
@@ -158,9 +170,67 @@ 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
|
||||
|
||||
### Password Rules
|
||||
### Keycloak Passwords
|
||||
- **Minimum:** 12 characters
|
||||
- **Recommended:** 16+ characters
|
||||
- **Format:** Alphanumeric only (a-zA-Z0-9)
|
||||
@@ -168,9 +238,7 @@ fi
|
||||
- **Generation:** Press ENTER for auto-generated secure passwords
|
||||
|
||||
### Why No Special Characters?
|
||||
Some services and database connection strings mishandle special characters in
|
||||
certain authentication flows. Restricting to alphanumeric ensures broad
|
||||
compatibility while remaining cryptographically strong.
|
||||
Keycloak has issues with special characters in certain authentication flows and database connection strings. Restricting to alphanumeric ensures compatibility.
|
||||
|
||||
### Password Strength with Alphanumeric Only
|
||||
- 12 characters: ~62^12 = 3.2 × 10^21 combinations
|
||||
@@ -196,6 +264,21 @@ 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
|
||||
@@ -233,6 +316,20 @@ 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
|
||||
@@ -243,6 +340,19 @@ 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
|
||||
@@ -265,17 +375,19 @@ sudo ufw status
|
||||
## Security Best Practices
|
||||
|
||||
1. **Change default passwords:** Even with auto-generation, review and update if needed
|
||||
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
|
||||
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
|
||||
|
||||
---
|
||||
|
||||
## 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`
|
||||
@@ -290,3 +402,8 @@ 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)
|
||||
|
||||
@@ -394,6 +394,7 @@ 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
|
||||
@@ -431,6 +432,41 @@ $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
|
||||
# ==============================
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
# 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/
|
||||
Executable
+184
@@ -0,0 +1,184 @@
|
||||
#!/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 ""
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1331,7 +1331,7 @@ generate_password() {
|
||||
openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c "$length"
|
||||
}
|
||||
|
||||
# Validate password (alphanumeric only, minimum length)
|
||||
# Validate password for Keycloak (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)
|
||||
# Check for special characters (not allowed for Keycloak)
|
||||
if echo "$password" | grep -q '[^a-zA-Z0-9]'; then
|
||||
echo " ⚠ Password must contain only letters and numbers (no special characters)"
|
||||
return 1
|
||||
@@ -2586,6 +2586,7 @@ 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"
|
||||
@@ -2626,6 +2627,7 @@ 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} \
|
||||
@@ -2657,6 +2659,7 @@ 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"
|
||||
@@ -2707,6 +2710,7 @@ else
|
||||
: ${INSTALL_FILEBROWSER:="n"}
|
||||
: ${INSTALL_MAGICMIRROR:="n"}
|
||||
: ${INSTALL_ACTUALBUDGET:="n"}
|
||||
: ${INSTALL_KEYCLOAK:="n"}
|
||||
: ${INSTALL_AUTHELIA:="n"}
|
||||
: ${INSTALL_CADDY:="n"}
|
||||
: ${INSTALL_FAIL2BAN:="n"}
|
||||
@@ -2733,6 +2737,7 @@ 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
|
||||
@@ -2806,6 +2811,7 @@ 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
|
||||
@@ -4225,6 +4231,625 @@ 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 ""
|
||||
@@ -4341,6 +4966,23 @@ 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"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1331,7 +1331,7 @@ generate_password() {
|
||||
openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c "$length"
|
||||
}
|
||||
|
||||
# Validate password (alphanumeric only, minimum length)
|
||||
# Validate password for Keycloak (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)
|
||||
# Check for special characters (not allowed for Keycloak)
|
||||
if echo "$password" | grep -q '[^a-zA-Z0-9]'; then
|
||||
echo " ⚠ Password must contain only letters and numbers (no special characters)"
|
||||
return 1
|
||||
@@ -2586,6 +2586,7 @@ 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"
|
||||
@@ -2626,6 +2627,7 @@ 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} \
|
||||
@@ -2657,6 +2659,7 @@ 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"
|
||||
@@ -2707,6 +2710,7 @@ else
|
||||
: ${INSTALL_FILEBROWSER:="n"}
|
||||
: ${INSTALL_MAGICMIRROR:="n"}
|
||||
: ${INSTALL_ACTUALBUDGET:="n"}
|
||||
: ${INSTALL_KEYCLOAK:="n"}
|
||||
: ${INSTALL_AUTHELIA:="n"}
|
||||
: ${INSTALL_CADDY:="n"}
|
||||
: ${INSTALL_FAIL2BAN:="n"}
|
||||
@@ -2733,6 +2737,7 @@ 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
|
||||
@@ -2806,6 +2811,7 @@ 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
|
||||
@@ -4225,6 +4231,625 @@ 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 ""
|
||||
@@ -4321,6 +4946,23 @@ 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"
|
||||
|
||||
Reference in New Issue
Block a user