chore: remove old pre-modular files and internal planning docs
Keep only the two base install scripts (24.04, 26.04), the modular system (setup.sh, lib/, services/, extras/, bootstrap.sh), and LICENSE/README/VERSION. Everything else was superseded. https://claude.ai/code/session_017WJtGcE5jjerAQCUBWUE3H
This commit is contained in:
@@ -1,371 +0,0 @@
|
||||
# Caddy with Fail2ban Setup Guide
|
||||
|
||||
This guide helps you integrate new services with an existing Caddy reverse proxy and set up fail2ban protection.
|
||||
|
||||
## Quick Start
|
||||
|
||||
For servers with Caddy already installed:
|
||||
|
||||
```bash
|
||||
# Run the automated helper script
|
||||
./caddy-setup-helper.sh
|
||||
```
|
||||
|
||||
This script will:
|
||||
- ✅ Detect your Caddy installation
|
||||
- ✅ Locate and backup your Caddyfile
|
||||
- ✅ Check for fail2ban configuration
|
||||
- ✅ Provide examples for adding new services
|
||||
|
||||
## Manual Setup
|
||||
|
||||
### 1. Backup Your Caddyfile
|
||||
|
||||
**IMPORTANT:** Always backup before making changes!
|
||||
|
||||
```bash
|
||||
# Find your Caddyfile location
|
||||
CADDYFILE=~/docker/caddy/Caddyfile # Adjust path as needed
|
||||
|
||||
# Create backup directory
|
||||
mkdir -p $(dirname "$CADDYFILE")/backups
|
||||
|
||||
# Backup with timestamp
|
||||
cp "$CADDYFILE" "$(dirname "$CADDYFILE")/backups/Caddyfile.backup.$(date +%Y%m%d_%H%M%S)"
|
||||
```
|
||||
|
||||
### 2. Add New Services to Caddy
|
||||
|
||||
Add these blocks to your Caddyfile:
|
||||
|
||||
#### ActualBudget (Personal Finance)
|
||||
|
||||
```caddy
|
||||
budget.yourdomain.com {
|
||||
log {
|
||||
output file /var/log/caddy/actualbudget-access.log
|
||||
format json
|
||||
level INFO
|
||||
}
|
||||
|
||||
reverse_proxy localhost:5006
|
||||
|
||||
# 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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Keycloak (Identity & Access Management)
|
||||
|
||||
```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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Reload Caddy Configuration
|
||||
|
||||
After editing the Caddyfile:
|
||||
|
||||
```bash
|
||||
# Format the Caddyfile (optional but recommended)
|
||||
docker exec -w /etc/caddy caddy caddy fmt --overwrite
|
||||
|
||||
# Reload Caddy configuration
|
||||
docker exec -w /etc/caddy caddy caddy reload
|
||||
```
|
||||
|
||||
If you get errors, check Caddy logs:
|
||||
```bash
|
||||
docker logs caddy
|
||||
```
|
||||
|
||||
### 4. Restore from Backup (if needed)
|
||||
|
||||
If something goes wrong:
|
||||
|
||||
```bash
|
||||
# Find your backup
|
||||
ls -lah ~/docker/caddy/backups/
|
||||
|
||||
# Restore the backup
|
||||
cp ~/docker/caddy/backups/Caddyfile.backup.YYYYMMDD_HHMMSS ~/docker/caddy/Caddyfile
|
||||
|
||||
# Reload Caddy
|
||||
docker exec -w /etc/caddy caddy caddy reload
|
||||
docker exec -w /etc/caddy caddy caddy fmt --overwrite
|
||||
```
|
||||
|
||||
## Fail2ban Configuration
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **Enable JSON logging in Caddy** (shown in examples above)
|
||||
2. **Install fail2ban** on the host:
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install fail2ban -y
|
||||
```
|
||||
|
||||
### Installation Steps
|
||||
|
||||
#### Step 1: Install Fail2ban Filter
|
||||
|
||||
```bash
|
||||
# Copy the filter configuration
|
||||
sudo cp fail2ban-caddy-filter.conf /etc/fail2ban/filter.d/caddy-auth.conf
|
||||
```
|
||||
|
||||
Or create it manually:
|
||||
|
||||
```bash
|
||||
sudo tee /etc/fail2ban/filter.d/caddy-auth.conf > /dev/null <<'EOF'
|
||||
[Definition]
|
||||
failregex = ^.*"remote_ip":"<HOST>".*"status":(?:401|403|429).*$
|
||||
^.*"remote_addr":"<HOST>.*"status":(?:401|403|429).*$
|
||||
ignoreregex = ^.*"remote_ip":"(?:127\.0\.0\.1|::1)".*$
|
||||
datepattern = "ts":%%s
|
||||
EOF
|
||||
```
|
||||
|
||||
#### Step 2: Install Fail2ban Jail
|
||||
|
||||
```bash
|
||||
# Copy the jail configuration
|
||||
sudo cp fail2ban-caddy-jail.conf /etc/fail2ban/jail.d/caddy.conf
|
||||
```
|
||||
|
||||
Or create it manually:
|
||||
|
||||
```bash
|
||||
sudo tee /etc/fail2ban/jail.d/caddy.conf > /dev/null <<'EOF'
|
||||
[caddy-auth]
|
||||
enabled = true
|
||||
port = http,https
|
||||
filter = caddy-auth
|
||||
logpath = /var/log/caddy/access.log
|
||||
/var/log/caddy/*-access.log
|
||||
maxretry = 5
|
||||
findtime = 600
|
||||
bantime = 3600
|
||||
action = iptables-multiport[name=CaddyAuth, port="http,https", protocol=tcp]
|
||||
backend = auto
|
||||
EOF
|
||||
```
|
||||
|
||||
#### Step 3: Create Log Directory
|
||||
|
||||
```bash
|
||||
# Create log directory if using Docker Caddy
|
||||
sudo mkdir -p /var/log/caddy
|
||||
sudo chmod 755 /var/log/caddy
|
||||
|
||||
# If Caddy runs as specific user:
|
||||
# sudo chown caddy:caddy /var/log/caddy
|
||||
```
|
||||
|
||||
#### Step 4: Update Caddy Docker Compose
|
||||
|
||||
Add log volume to your Caddy docker-compose.yml:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
caddy:
|
||||
image: caddy:latest
|
||||
container_name: caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||
- ./data:/data
|
||||
- ./config:/config
|
||||
- /var/log/caddy:/var/log/caddy # Add this line
|
||||
```
|
||||
|
||||
Then restart Caddy:
|
||||
```bash
|
||||
cd ~/docker/caddy
|
||||
docker compose down
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
#### Step 5: Restart Fail2ban
|
||||
|
||||
```bash
|
||||
sudo systemctl restart fail2ban
|
||||
sudo systemctl status fail2ban
|
||||
```
|
||||
|
||||
### Testing Fail2ban
|
||||
|
||||
```bash
|
||||
# Check if jail is running
|
||||
sudo fail2ban-client status caddy-auth
|
||||
|
||||
# Test the filter against your logs
|
||||
sudo fail2ban-regex /var/log/caddy/access.log /etc/fail2ban/filter.d/caddy-auth.conf
|
||||
|
||||
# View banned IPs
|
||||
sudo fail2ban-client get caddy-auth banip
|
||||
|
||||
# Manually ban/unban an IP (for testing)
|
||||
sudo fail2ban-client set caddy-auth banip 1.2.3.4
|
||||
sudo fail2ban-client set caddy-auth unbanip 1.2.3.4
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
#### Fail2ban not detecting attacks
|
||||
|
||||
1. **Check log format:**
|
||||
```bash
|
||||
tail -f /var/log/caddy/access.log
|
||||
```
|
||||
Ensure it's JSON format with `remote_ip` or `remote_addr` field.
|
||||
|
||||
2. **Test filter manually:**
|
||||
```bash
|
||||
sudo fail2ban-regex /var/log/caddy/access.log /etc/fail2ban/filter.d/caddy-auth.conf --print-all-matched
|
||||
```
|
||||
|
||||
3. **Check fail2ban logs:**
|
||||
```bash
|
||||
sudo tail -f /var/log/fail2ban.log
|
||||
```
|
||||
|
||||
#### Caddy configuration errors
|
||||
|
||||
1. **Validate Caddyfile:**
|
||||
```bash
|
||||
docker exec caddy caddy validate --config /etc/caddy/Caddyfile
|
||||
```
|
||||
|
||||
2. **Check Caddy logs:**
|
||||
```bash
|
||||
docker logs caddy --tail 50
|
||||
```
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Aggressive Fail2ban Settings
|
||||
|
||||
For tighter security:
|
||||
|
||||
```ini
|
||||
[caddy-auth]
|
||||
maxretry = 3 # Ban after 3 attempts (instead of 5)
|
||||
findtime = 300 # Within 5 minutes (instead of 10)
|
||||
bantime = 86400 # Ban for 24 hours (instead of 1)
|
||||
```
|
||||
|
||||
### Ban Time Increment
|
||||
|
||||
Ban repeat offenders for longer:
|
||||
|
||||
```ini
|
||||
[caddy-auth]
|
||||
bantime.increment = true
|
||||
bantime.factor = 24
|
||||
bantime.maxtime = 604800 # Maximum 1 week ban
|
||||
```
|
||||
|
||||
### Email Notifications
|
||||
|
||||
Get notified when IPs are banned:
|
||||
|
||||
```ini
|
||||
[caddy-auth]
|
||||
action = iptables-multiport[name=CaddyAuth, port="http,https", protocol=tcp]
|
||||
sendmail-whois[name=CaddyAuth, dest=admin@yourdomain.com]
|
||||
```
|
||||
|
||||
### Per-Service Jails
|
||||
|
||||
Create separate jails for different services:
|
||||
|
||||
```ini
|
||||
[caddy-actualbudget]
|
||||
enabled = true
|
||||
port = http,https
|
||||
filter = caddy-auth
|
||||
logpath = /var/log/caddy/actualbudget-access.log
|
||||
maxretry = 3
|
||||
bantime = 7200
|
||||
|
||||
[caddy-keycloak]
|
||||
enabled = true
|
||||
port = http,https
|
||||
filter = caddy-auth
|
||||
logpath = /var/log/caddy/keycloak-access.log
|
||||
maxretry = 5
|
||||
bantime = 3600
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always backup before changes**
|
||||
2. **Test configuration before reloading** (`caddy validate`)
|
||||
3. **Monitor fail2ban logs** initially to tune settings
|
||||
4. **Use strong passwords** for admin interfaces
|
||||
5. **Keep services updated** (`docker compose pull && docker compose up -d`)
|
||||
6. **Regular backups** of configuration and data
|
||||
7. **Use HTTPS** via Caddy for all services
|
||||
8. **Implement rate limiting** in Caddy for API endpoints
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Common Commands
|
||||
|
||||
```bash
|
||||
# Caddy
|
||||
docker exec -w /etc/caddy caddy caddy reload
|
||||
docker exec -w /etc/caddy caddy caddy fmt --overwrite
|
||||
docker exec caddy caddy validate --config /etc/caddy/Caddyfile
|
||||
docker logs caddy --tail 50
|
||||
|
||||
# Fail2ban
|
||||
sudo systemctl restart fail2ban
|
||||
sudo fail2ban-client status caddy-auth
|
||||
sudo fail2ban-client set caddy-auth unbanip 1.2.3.4
|
||||
sudo tail -f /var/log/fail2ban.log
|
||||
|
||||
# Backup
|
||||
cp ~/docker/caddy/Caddyfile ~/docker/caddy/Caddyfile.backup
|
||||
```
|
||||
|
||||
### Service Ports
|
||||
|
||||
- **ActualBudget**: 5006
|
||||
- **Keycloak**: 8180
|
||||
- **Caddy**: 80 (HTTP), 443 (HTTPS)
|
||||
|
||||
## Support
|
||||
|
||||
For issues:
|
||||
- Caddy documentation: https://caddyserver.com/docs/
|
||||
- Fail2ban manual: https://www.fail2ban.org/wiki/index.php/MANUAL_0_8
|
||||
- ActualBudget docs: https://actualbudget.org/docs/
|
||||
- Keycloak docs: https://www.keycloak.org/documentation
|
||||
-224
@@ -1,224 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project. Versions follow `MAJOR.MINOR.PATCH`.
|
||||
|
||||
## [0.9.5] - 2026-06-03
|
||||
|
||||
### Changed
|
||||
- VERSION reset from 1.0.0 to 0.9.5 — versioning now tracks `setup_v<X.Y.Z>.sh`
|
||||
snapshot files. Each release creates a new numbered file (old files stay). The
|
||||
current `setup.sh` is always the live version; `setup_v0.9.5.sh` is the first
|
||||
named snapshot.
|
||||
|
||||
### Added
|
||||
- `setup_v0.9.5.sh` — first versioned snapshot of `setup.sh`. Future changes
|
||||
produce `setup_v0.9.6.sh`, etc. Previous snapshots are never removed.
|
||||
|
||||
## [1.0.0] - 2026-06-03
|
||||
|
||||
### Milestone: full parity with the monolith
|
||||
|
||||
Every service from `ubuntu-post-install-24.04-crowdsec.sh` is now a module.
|
||||
The modular system (`setup.sh` + `services/`) is the primary install path.
|
||||
The monolith is retained as a frozen evolution record.
|
||||
|
||||
### Added
|
||||
- `services/linux-to-sync.sh` *(extras)* — clones the private
|
||||
`outis1one/linux-to-sync` repository to `~/linux-to-sync` via SSH key or
|
||||
GitHub PAT (PAT is stripped from the remote URL after clone for security).
|
||||
`is_installed` marker checks `~/linux-to-sync/.git`.
|
||||
- Updated `MODULAR.md` migration table to show the completed module inventory
|
||||
grouped by category.
|
||||
|
||||
## [0.9.11] - 2026-06-03
|
||||
|
||||
### Added
|
||||
- **Utilities batch** — 8 service modules migrated from the monolith:
|
||||
- `services/mealie.sh` *(utilities)* — Recipe manager & meal planner. PUID/PGID baked;
|
||||
default creds noted (change immediately). Port 9925 → internal 9000.
|
||||
- `services/actualbudget.sh` *(utilities)* — Open-source personal finance (Actual Budget).
|
||||
Minimal container; bank sync via SimpleFIN optional. Port 5006.
|
||||
- `services/traccar.sh` *(utilities)* — GPS tracking server for phones, vehicles, assets.
|
||||
Ships a starter `config/traccar.xml` with H2 embedded DB. Port 8082 + 5000-5150 device
|
||||
protocols (TCP+UDP).
|
||||
- `services/fmd.sh` *(utilities)* — FindMyDevice server for Android. Generates a random
|
||||
admin password; mobile app from F-Droid (not Play Store). Port 8084.
|
||||
- `services/ddclient.sh` *(utilities)* — Dynamic DNS updater; no web UI. Ships a
|
||||
`config/ddclient.conf` template covering Cloudflare, DuckDNS, No-IP. Default start
|
||||
prompt is "n" — edit config first.
|
||||
- `services/wg-easy.sh` *(utilities)* — WireGuard VPN with web UI. Auto-detects public
|
||||
IP for `WG_HOST`; generates random password; requires `NET_ADMIN` + `SYS_MODULE` caps
|
||||
and `ip_forward` sysctl. Ports 51820/udp (VPN) + 51821/tcp (web).
|
||||
- `services/meshcentral.sh` *(utilities)* — Self-hosted remote device management server.
|
||||
Prompts for hostname (domain/IP for agent connections). Ports 4430 (HTTPS) + 4433 (agent).
|
||||
- `services/magicmirror.sh` *(utilities)* — Modular smart mirror / info dashboard.
|
||||
Multi-instance (1-3, ports 8081-8083); each instance in `~/docker/magicmirror/<N>/`.
|
||||
Optionally copies existing `config.js` and auto-clones `MMM-*` third-party modules
|
||||
from GitHub (tries MichMich → bugsounet → MagicMirrorOrg org order).
|
||||
- **Cameras batch** — 2 service modules:
|
||||
- `services/frigate.sh` *(cameras)* — AI-powered NVR with object detection. Auto-enables
|
||||
`/dev/dri/renderD128` for hardware-accelerated detection when present; ships a starter
|
||||
`config/config.yml` with camera examples. `privileged: true` + 1 GB tmpfs cache.
|
||||
Ports 5000 (web), 8554 (RTSP restream), 8555 (WebRTC). Default start prompt is "n" —
|
||||
edit config first.
|
||||
- `services/frigate-notify.sh` *(cameras)* — Push notification sidecar for Frigate events.
|
||||
Auto-detects local Frigate and ntfy installs to pre-fill config defaults. Supports ntfy,
|
||||
Pushover, Discord, Gotify, Telegram, and more. No web UI.
|
||||
|
||||
## [0.9.10] - 2026-06-03
|
||||
|
||||
### Added
|
||||
- **Media batch** — 6 service modules migrated from the monolith:
|
||||
- `services/jellyfin.sh` *(media)* — Free media server (movies, TV, music). Auto-detects
|
||||
`/dev/dri/renderD128` and enables VAAPI hardware transcoding with the render GID when
|
||||
present; falls back to CPU transcoding otherwise. Ports 8096, 1900/udp (DLNA),
|
||||
7359/udp (discovery).
|
||||
- `services/emby.sh` *(media)* — Emby media server. UID/GID baked from the install-time
|
||||
user; HW transcoding block left commented (uncomment `/dev/dri` once GPU confirmed).
|
||||
Ports 8096 (web) and 8920 (HTTPS).
|
||||
- `services/audiobookshelf.sh` *(media)* — Audiobook & podcast server. Separate audiobooks
|
||||
and podcasts paths; podcasts folder defaults to `./podcasts` inside the service dir.
|
||||
Port 13378.
|
||||
- `services/arm.sh` *(media)* — Automatic Ripping Machine for DVDs, Blu-rays, CDs.
|
||||
Detects optical drives at install time (`/dev/sr*`); runs with `privileged: true`.
|
||||
Ripped output split into movies/ and music/. Port 8080.
|
||||
- `services/lyrion.sh` *(media)* — Lyrion Music Server (formerly LMS) for Squeezebox
|
||||
devices, the Squeezer app, and Chromecast. Uses `network_mode: host` so UDP discovery
|
||||
works without manual port mapping. Port 9000.
|
||||
- `services/immich.sh` *(media)* — Self-hosted photo & video backup (like Google Photos).
|
||||
Full multi-container stack (immich-server, immich-machine-learning, valkey/redis,
|
||||
postgres). Two library strategies: (1) unified — all photos in one place with an
|
||||
auto-generated `import-photos.sh` helper that handles admin account creation, API key
|
||||
generation, storage template config, and CLI upload; (2) external — existing photos
|
||||
indexed read-only, new uploads separate. Port 2283.
|
||||
|
||||
## [0.9.9] - 2026-06-03
|
||||
|
||||
### Added
|
||||
- **New `extras` category** for non-Docker add-ons sourced from other repos —
|
||||
things that build/install on the host instead of running as a container.
|
||||
Inserted into `CATEGORY_ORDER` between `gaming` and `backup`.
|
||||
- `services/silent-send.sh` *(extras)* — installs the **Silent Send** browser
|
||||
extension (redacts PII before it's sent to AI chatbots). Installs the build
|
||||
toolchain (git, Node.js ≥18 via NodeSource, npm), clones
|
||||
`outis1one/silent-send` to `~/silent-send`, runs `npm install` so the Firefox
|
||||
build/sign tooling (`web-ext`) is ready, optionally builds a signed Firefox
|
||||
`.xpi` (with Mozilla API creds), and prints load-unpacked / build instructions
|
||||
per browser. README written to the checkout. No server/container.
|
||||
- `is_installed` marker for `silent-send` (checks `~/silent-send/.git`).
|
||||
|
||||
## [0.9.8] - 2026-06-03
|
||||
|
||||
### Added
|
||||
- `services/wolf-pair.sh` *(gaming)* — browser-based Moonlight pairing UI for
|
||||
Wolf. Builds a tiny Python HTTP container (`python:3.12-alpine` + `docker-cli`)
|
||||
that watches `docker logs wolf` for the current pairing secret and serves a
|
||||
PIN entry form on port 8090. Eliminates the `./manage.sh pin` CLI workflow —
|
||||
open `http://<server>:8090`, type the 4-digit PIN, done. Runs with
|
||||
`network_mode: host` so it can reach Wolf's `/pin/` API at `localhost:47989`;
|
||||
mounts the Docker socket read-only for log access. Optional Caddy subdomain.
|
||||
|
||||
## [0.9.7] - 2026-06-03
|
||||
|
||||
### Added
|
||||
- `services/caddy.sh` *(homelab)* — Caddy reverse proxy + automatic HTTPS, own
|
||||
`~/docker/caddy/` folder (compose + starter Caddyfile + README). Services add
|
||||
their site blocks to its Caddyfile.
|
||||
- `services/crowdsec.sh` *(homelab)* — system-level intrusion prevention
|
||||
(agent + firewall bouncer + Caddy log acquisition + optional ntfy ban alerts);
|
||||
README in `~/docker/crowdsec/`.
|
||||
- **Guided menu redesign** in `setup.sh`:
|
||||
- Prints the **required** set (essential packages incl. glow + a Docker check)
|
||||
up front and lets you **cancel** before anything changes.
|
||||
- Offers **Caddy first** (most services depend on it).
|
||||
- **Category menu loop**: pick a category → checklist (already-installed shown
|
||||
as `[installed]`) → install → back to the menu for the next category, until
|
||||
you choose Done. whiptail UI with a plain-text fallback.
|
||||
|
||||
### Changed
|
||||
- **Categories** reorganized: `base · homelab · utilities · media · cameras ·
|
||||
gaming · backup`. Moved ntfy, filebrowser, portainer, uptimekuma, watchtower
|
||||
to `utilities`. Within `homelab`, Caddy → CrowdSec → Authelia sort first.
|
||||
|
||||
## [0.9.6] - 2026-06-03
|
||||
|
||||
### Added
|
||||
- **Per-service README generation.** New `write_readme` helper in
|
||||
`lib/common.sh`; every module now writes a `README.md` into its
|
||||
`~/docker/<service>/` folder (what it is, access URL, start/stop, data
|
||||
location, reverse-proxy notes) — so each service folder is self-documenting.
|
||||
- Migrated 6 services from the monolith into modules (all in the `homelab`
|
||||
group, each with a README):
|
||||
- `authelia` — SSO + 2FA portal, ported from the `authelia-setup` repo + the
|
||||
monolith's working block: prompts for domain/SMTP, generates
|
||||
jwt/session/storage secrets + the admin Argon2 hash, writes
|
||||
compose/config/users, creates `caddy_net`, and injects the forward-auth
|
||||
snippet + portal block into the Caddyfile. Won't clobber an existing install.
|
||||
- `filebrowser` (8085), `ntfy` (8090), `uptimekuma` (3001),
|
||||
`portainer` (9443), `watchtower` (no web port).
|
||||
|
||||
### Notes
|
||||
- `homelab` group now: authelia, filebrowser, homeassistant, ntfy, portainer,
|
||||
uptimekuma, watchtower.
|
||||
- Remaining monolith services still to migrate: ActualBudget, ARM,
|
||||
AudioBookshelf, Caddy, CrowdSec, Emby, FindMyDevice, Frigate, Frigate-Notify,
|
||||
Immich, Jellyfin, Lyrion, MagicMirror, Mealie, MeshCentral, Traccar, ddclient,
|
||||
wg-easy.
|
||||
|
||||
## [0.9.5] - 2026-06-03
|
||||
|
||||
### Added
|
||||
- `services/minecraft.sh` *(gaming)* — full port of the standalone
|
||||
`setupminecraft.sh`, converted to the per-service-folder model. Each server
|
||||
is its own `~/docker/<instance>/` with a standalone compose, so multiple
|
||||
servers run side by side (port auto-bumps 25565→25566…). Preserves all the
|
||||
niceties: Fabric/Quilt/Paper/Vanilla/Forge flavours, the live Modrinth
|
||||
version/mod-availability picker, curated mods, Vanilla Tweaks datapacks,
|
||||
whitelist UUID pre-population, LuckPerms bootstrap, Chunky pre-gen, playit.gg
|
||||
tunnel, generated MINECRAFT_NETWORKING.md / CLIENT_MODS.md, and the
|
||||
client-mods download web page (its own folder + compose).
|
||||
|
||||
### Fixed
|
||||
- Minecraft compose env-block emission (trailing-newline bug from the original
|
||||
that glued `ports:` onto the last env line — now valid YAML).
|
||||
|
||||
### Notes
|
||||
- `gaming` group now: `js99er`, `minecraft`, `wolf`.
|
||||
- Still pending: `whitelist` Minecraft helper; migrating the ~65 monolith
|
||||
services into `services/`.
|
||||
|
||||
## [0.9.4] - 2026-06-03
|
||||
|
||||
The first versioned release. Introduces the **modular post-install system** so
|
||||
you can install the whole box at once *or* run a single service, with one
|
||||
source of truth (no per-service script duplication, nothing generated).
|
||||
|
||||
### Added
|
||||
- `setup.sh` dispatcher: interactive menu, run-one (`sudo ./setup.sh <name>`),
|
||||
`--list`, `--dry-run`, `--unattended`, `--version`.
|
||||
- `lib/common.sh`: shared helpers (logging, prompts, ownership, Caddy wiring)
|
||||
and a self-registration service registry — one implementation of each.
|
||||
- Service modules (each its own `~/docker/<name>/` folder + standalone compose):
|
||||
- `base` — essential CLI packages, now including **glow**.
|
||||
- `glow` — terminal markdown reader (charmbracelet), standalone too.
|
||||
- `homeassistant` — bridge/host networking choice, `trusted_proxies` pre-seed.
|
||||
- `js99er` *(gaming)* — self-hosted TI-99/4A emulator (Selkies launcher tie-in removed).
|
||||
- `wolf` *(gaming)* — Games-on-Whales Moonlight streaming (wolf-pair dropped; `pin` workflow kept).
|
||||
- `backup` — Kopia encrypted backups (paths adapted to `~/docker`).
|
||||
- `MODULAR.md` documenting the architecture, how to add a module, migration status.
|
||||
- Service groups: `base` / `homelab` / `gaming` / `backup`.
|
||||
- `glow` also added to the live `-crowdsec` monolith scripts' essential packages.
|
||||
|
||||
### Known gaps / next (0.9.5)
|
||||
- `minecraft` module (rich, multi-instance port of `setupminecraft.sh`) — not yet
|
||||
written; the background port hit a session limit.
|
||||
- `whitelist` Minecraft helper not yet shipped.
|
||||
- ~65 services still live only in the monolith, to migrate into `services/`.
|
||||
|
||||
### Earlier history (pre-versioning)
|
||||
- Removed Keycloak; standardized on Authelia for SSO.
|
||||
- Added `-no-keycloak` and `-crowdsec` script tiers (originals kept as the
|
||||
evolution record).
|
||||
- CrowdSec replaces fail2ban in the `-crowdsec` tier (SSH + Caddy, geo + IP
|
||||
reputation, optional ntfy ban alerts).
|
||||
- Home Assistant added to the `-crowdsec` tier.
|
||||
-112
@@ -1,112 +0,0 @@
|
||||
# HANDOFF — modular migration status
|
||||
|
||||
**Version:** 0.9.7 · **Branch:** `claude/happy-volta-RPhbD`
|
||||
**Read also:** `CHANGELOG.md` (per-version detail), `MODULAR.md` (architecture).
|
||||
|
||||
## Where we are
|
||||
|
||||
We're migrating a giant monolithic installer into a **modular system**:
|
||||
- `setup.sh` — the one dispatcher (menu + run-one). `lib/common.sh` — shared
|
||||
helpers + the service registry. `services/<name>.sh` — one file per service.
|
||||
- Run all: `sudo ./setup.sh` (required gate → Caddy offer → category menu loop).
|
||||
Run one: `sudo ./setup.sh <name>`. List: `./setup.sh --list`. `--version`.
|
||||
- Every service installs to its own `~/docker/<name>/` with its own
|
||||
`docker-compose.yml` **and a generated `README.md`** (via `write_readme`).
|
||||
- Nothing is generated/duplicated: a service = one committed file in `services/`.
|
||||
|
||||
The three monolith tiers still exist, frozen as history:
|
||||
`ubuntu-post-install-{24.04,26.04}.sh` (original, w/ Keycloak),
|
||||
`*-no-keycloak.sh`, `*-crowdsec.sh` (current "install everything" + glow).
|
||||
The `-crowdsec.sh` tier is the migration source of truth.
|
||||
|
||||
## Module status
|
||||
|
||||
**Done (16 modules in `services/`):**
|
||||
| Group | Modules |
|
||||
|-------|---------|
|
||||
| base | base, glow |
|
||||
| homelab | caddy, crowdsec, authelia, homeassistant |
|
||||
| utilities | filebrowser, ntfy, portainer, uptimekuma, watchtower |
|
||||
| gaming | wolf, minecraft, js99er |
|
||||
| backup | backup |
|
||||
|
||||
**Pending — migrate from `ubuntu-post-install-24.04-crowdsec.sh` (find by `# ---- NAME ----`):**
|
||||
| Target group | Services to migrate |
|
||||
|------|---------|
|
||||
| media | jellyfin, emby, audiobookshelf, immich, arm, lyrion |
|
||||
| cameras | frigate, frigate-notify |
|
||||
| utilities | actualbudget, mealie, traccar, findmydevice, magicmirror, wg-easy, ddclient |
|
||||
| (misc) | meshcentral (remote-mgmt server) |
|
||||
|
||||
Also still monolith-only (Phase-1 / system, not yet modularized): SSH config,
|
||||
Docker install, Samba, VPNs (Tailscale/NetBird/WireGuard), RustDesk, TeamViewer,
|
||||
MeshCentral agent, UFW. Decide later whether these become `required`/system
|
||||
modules.
|
||||
|
||||
## Final taxonomy (categories)
|
||||
|
||||
`base` (required) · `homelab` · `utilities` · `media` · `cameras` · `gaming` ·
|
||||
`backup`. Menu order is set in `setup.sh:CATEGORY_ORDER`. Within `homelab`,
|
||||
`SERVICE_PRIORITY` puts caddy → crowdsec → authelia first. `media`/`cameras`
|
||||
won't appear in the menu until they have ≥1 module (no empty categories).
|
||||
|
||||
Note: filebrowser/portainer/uptimekuma/watchtower were placed in `utilities`
|
||||
(weren't in the original taxonomy list) — move if desired by editing their
|
||||
`register_service ... <group> ...` line.
|
||||
|
||||
## OPEN ITEM — wolf-pair (action needed from you)
|
||||
|
||||
`wolf-pair` (the FQDN device-pairing page for Wolf/Moonlight) was **dropped**
|
||||
when Wolf was ported, because its source wasn't available. You said you worked
|
||||
hard on it and will **upload `wolf-pair/server.py` + `wolf-pair/Dockerfile`
|
||||
(and anything else it needs) in the next chat.**
|
||||
|
||||
To re-add it: create `services/wolf-pair.sh` (group `gaming`) that builds the
|
||||
wolf-pair container in `~/docker/wolf-pair/`, wires it to reach Wolf, opens its
|
||||
port, and offers a Caddy block for the pairing FQDN. Wolf's `manage.sh pin` is
|
||||
the current stopgap.
|
||||
|
||||
## Module contract (for consistency when adding/migrating)
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
register_service <name> <group> "Description" [port] # one line → appears in menu
|
||||
install_<name>() {
|
||||
require_docker || return 1 # docker services only
|
||||
local DIR="$DOCKER_DIR/<name>"
|
||||
[ "$DRY_RUN" = true ] && { echo "[DRY-RUN] Would create $DIR ..."; return 0; }
|
||||
mkdir -p "$DIR"; ensure_docker_dir_ownership "$DIR"; cd "$DIR" || return 1
|
||||
cat > docker-compose.yml << 'YAML'
|
||||
...
|
||||
YAML
|
||||
configure_caddy_for_service "Name" "PORT" "subdomain" # optional
|
||||
write_readme "$DIR" <<MD
|
||||
# Name
|
||||
...how to start/stop, access URL, data location...
|
||||
MD
|
||||
prompt_yn "Start now? (y/n):" "y" S && docker compose up -d
|
||||
}
|
||||
```
|
||||
Rules: **no `set -e`**; don't redefine `log_*`/colors (in common.sh); drop the
|
||||
monolith's `WHIPTAIL_USED`/`INSTALL_*`/`check_service_exists` wrappers; use
|
||||
`prompt_yn`/`prompt_text`; honor `DRY_RUN` with an early return before any
|
||||
prompt/curl/apt/docker. System (non-docker) modules: see `crowdsec`/`backup`.
|
||||
|
||||
Verify each: `bash -n services/<name>.sh`, `./setup.sh --list`,
|
||||
`sudo ./setup.sh --dry-run --unattended <name>` (must exit 0).
|
||||
|
||||
## Workflow reminders
|
||||
- Per-version: bump `VERSION`, add a `CHANGELOG.md` entry, commit, push to
|
||||
`claude/happy-volta-RPhbD`.
|
||||
- Your loop: build nice standalone `setup-*.sh` elsewhere → upload here → it gets
|
||||
"massaged" into a `services/<name>.sh` module (wrap in `install_`, use shared
|
||||
helpers, per-folder + README, register).
|
||||
|
||||
## Suggested next steps
|
||||
1. Add `wolf-pair` once you upload its files.
|
||||
2. Migrate **media** batch (jellyfin, emby, audiobookshelf, immich, arm, lyrion) → v0.9.8.
|
||||
3. Migrate **cameras** (frigate, frigate-notify) → v0.9.9.
|
||||
4. Migrate remaining **utilities** (actualbudget, mealie, traccar, findmydevice,
|
||||
magicmirror, wg-easy, ddclient).
|
||||
5. Decide how Phase-1/system items (VPNs, Samba, remote-access) fit (required vs
|
||||
their own category).
|
||||
@@ -1,678 +0,0 @@
|
||||
# Keycloak Setup Guide
|
||||
## Complete Manual and Automated Configuration Guide
|
||||
|
||||
This guide explains Keycloak concepts and how to configure it both automatically (via the script) and manually (via the web UI).
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
1. [What is Keycloak?](#what-is-keycloak)
|
||||
2. [Key Concepts](#key-concepts)
|
||||
3. [Automated Setup (via Script)](#automated-setup)
|
||||
4. [Manual Setup (via Web UI)](#manual-setup)
|
||||
5. [Configuring External Services](#configuring-external-services)
|
||||
6. [Reconfiguration & Adding Realms](#reconfiguration)
|
||||
7. [Common Use Cases](#common-use-cases)
|
||||
8. [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## What is Keycloak?
|
||||
|
||||
Keycloak is an **Identity and Access Management (IAM)** system that provides:
|
||||
- **Single Sign-On (SSO)**: Log in once, access all your services
|
||||
- **User Management**: Create, manage, and authenticate users in one place
|
||||
- **OAuth2/OIDC**: Industry-standard authentication for web apps
|
||||
- **Social Login**: Allow login via Google, GitHub, etc.
|
||||
- **Multi-Factor Authentication (MFA)**: Add extra security with 2FA/TOTP
|
||||
- **LDAP/Active Directory Integration**: Connect to existing user directories
|
||||
|
||||
**Think of Keycloak as:** A centralized login system for all your self-hosted services.
|
||||
|
||||
---
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### 1. **Realm**
|
||||
A **realm** is an isolated container for users, clients, and configuration.
|
||||
|
||||
**Analogy:** Think of a realm like a "company" or "organization" in Keycloak.
|
||||
|
||||
**Why you need it:**
|
||||
- The default `master` realm is for Keycloak admin only
|
||||
- You create a separate realm (e.g., `homelab`) for your actual users and applications
|
||||
- Realms are completely isolated - users in one realm can't access another
|
||||
|
||||
**Example:**
|
||||
- `master` realm: Only for Keycloak administrators
|
||||
- `homelab` realm: For your personal services (ActualBudget, Jellyfin, etc.)
|
||||
- `family` realm: Separate realm for family members (optional)
|
||||
|
||||
### 2. **OAuth2/OpenID Connect (OIDC) Client**
|
||||
A **client** is an application that uses Keycloak for authentication.
|
||||
|
||||
**Analogy:** Each service (ActualBudget, Jellyfin, etc.) is a "client" that asks Keycloak "Is this user allowed to log in?"
|
||||
|
||||
**Required information:**
|
||||
- **Client ID**: Name of the application (e.g., `actualbudget`)
|
||||
- **Client Secret**: Password for the application (auto-generated, 64-char hex)
|
||||
- **Redirect URIs**: Where Keycloak sends users after login
|
||||
- Example: `https://budget.yourdomain.com/*`
|
||||
- Must match EXACTLY or login will fail
|
||||
|
||||
**Flow:**
|
||||
1. User clicks "Login" in ActualBudget
|
||||
2. ActualBudget redirects to Keycloak: `https://auth.yourdomain.com/login`
|
||||
3. User logs in with username/password
|
||||
4. Keycloak redirects back to ActualBudget: `https://budget.yourdomain.com/callback`
|
||||
5. ActualBudget gets user info and logs them in
|
||||
|
||||
### 3. **Users**
|
||||
A **user** is a person who can log in to your services.
|
||||
|
||||
**User attributes:**
|
||||
- Username (required, unique)
|
||||
- Email (optional but recommended)
|
||||
- First name / Last name (optional)
|
||||
- Password (set via Credentials tab)
|
||||
- Email verified (set to true to skip verification)
|
||||
- Enabled (must be true for user to log in)
|
||||
|
||||
### 4. **Redirect URIs**
|
||||
**Critical concept:** The redirect URI is where Keycloak sends the user after successful login.
|
||||
|
||||
**Common mistakes:**
|
||||
- ❌ `http://localhost:5006` (won't work for external services)
|
||||
- ❌ `https://budget.example.com` (missing wildcard or path)
|
||||
- ✅ `https://budget.example.com/*` (correct - allows all paths)
|
||||
|
||||
**For external services (like Pikapod):**
|
||||
- Pikapod gives you a URL like: `https://actualbudget-abc123.pikapod.net`
|
||||
- Your redirect URI: `https://actualbudget-abc123.pikapod.net/*`
|
||||
- Your Keycloak URL: `https://auth.yourdomain.com` (must be publicly accessible)
|
||||
|
||||
---
|
||||
|
||||
## Automated Setup (via Script)
|
||||
|
||||
The script automates everything for you. Here's what it does:
|
||||
|
||||
### Step 1: Install Keycloak
|
||||
```bash
|
||||
./ubuntu-post-install.sh
|
||||
# Select KEYCLOAK in whiptail menu
|
||||
```
|
||||
|
||||
Prompts:
|
||||
- Admin password (for Keycloak admin console)
|
||||
- Database password (for PostgreSQL)
|
||||
|
||||
### Step 2: Automated Configuration
|
||||
```
|
||||
Configure Keycloak with initial realm and clients? (y/n): y
|
||||
```
|
||||
|
||||
This automatically:
|
||||
1. ✅ Waits for Keycloak to start (health check)
|
||||
2. ✅ Logs in using admin CLI (`kcadm.sh`)
|
||||
3. ✅ Creates a realm (e.g., `homelab`)
|
||||
4. ✅ Creates OAuth client for ActualBudget (if selected)
|
||||
5. ✅ Creates generic OAuth client template
|
||||
6. ✅ Saves all credentials to `~/docker/keycloak/*.txt`
|
||||
7. ✅ Optionally creates initial user
|
||||
|
||||
### Step 3: What Gets Created
|
||||
|
||||
**Realm:** `homelab` (or your custom name)
|
||||
|
||||
**ActualBudget OAuth Client:**
|
||||
- Client ID: `actualbudget`
|
||||
- Client Secret: (saved to `actualbudget-oauth.txt`)
|
||||
- Redirect URIs:
|
||||
- `http://localhost:5006/*` (local development)
|
||||
- `http://yourdomain.com:5006/*` (local with domain)
|
||||
- `https://yourdomain.com/*` (production - any subdomain)
|
||||
- `https://budget.yourdomain.com/*` (specific subdomain)
|
||||
|
||||
**Generic OAuth Client:**
|
||||
- Client ID: `generic-app`
|
||||
- Client Secret: (saved to `generic-oauth.txt`)
|
||||
- Can be cloned for other services
|
||||
|
||||
**Initial User:**
|
||||
- Username, email, password you provide
|
||||
- Immediately active
|
||||
- Can log in to all services
|
||||
|
||||
### Step 4: Configuration Files
|
||||
|
||||
All credentials saved to:
|
||||
```
|
||||
~/docker/keycloak/actualbudget-oauth.txt
|
||||
~/docker/keycloak/generic-oauth.txt
|
||||
```
|
||||
|
||||
These files contain:
|
||||
- Client ID
|
||||
- Client Secret
|
||||
- Authorization URL
|
||||
- Token URL
|
||||
- User Info URL
|
||||
- Instructions for configuring each service
|
||||
|
||||
---
|
||||
|
||||
## Manual Setup (via Web UI)
|
||||
|
||||
If you prefer to configure Keycloak manually, or want to add services later:
|
||||
|
||||
### Access Admin Console
|
||||
```
|
||||
URL: http://localhost:8180/admin
|
||||
Username: admin
|
||||
Password: [your admin password]
|
||||
```
|
||||
|
||||
### Step 1: Create a Realm
|
||||
|
||||
1. **Click dropdown** in top-left corner (shows "Master")
|
||||
2. **Click "Create Realm"**
|
||||
3. **Realm name:** `homelab` (or your choice)
|
||||
4. **Click "Create"**
|
||||
|
||||
**Settings to configure:**
|
||||
- **Login tab:**
|
||||
- ✅ User registration: OFF (you create users manually)
|
||||
- ✅ Forgot password: ON (allows password resets)
|
||||
- ✅ Remember me: ON (convenience)
|
||||
- ✅ Login with email: ON (users can use email instead of username)
|
||||
|
||||
- **Email tab:** (optional, for password resets)
|
||||
- Configure SMTP settings if you want email features
|
||||
|
||||
### Step 2: Create an OAuth2 Client (for ActualBudget)
|
||||
|
||||
1. **Switch to your realm** (`homelab`) via dropdown
|
||||
2. **Go to Clients** (left menu)
|
||||
3. **Click "Create client"**
|
||||
|
||||
**General Settings:**
|
||||
- **Client type:** OpenID Connect
|
||||
- **Client ID:** `actualbudget`
|
||||
- **Name:** `ActualBudget`
|
||||
- **Description:** `Personal Finance Management`
|
||||
- **Click "Next"**
|
||||
|
||||
**Capability config:**
|
||||
- ✅ Client authentication: ON (creates a secret)
|
||||
- ✅ Authorization: OFF (not needed)
|
||||
- ✅ Standard flow: ON (authorization code flow)
|
||||
- ✅ Direct access grants: ON (allows username/password)
|
||||
- ❌ Implicit flow: OFF (deprecated)
|
||||
- ❌ Service accounts: OFF (not needed for web apps)
|
||||
- **Click "Next"**
|
||||
|
||||
**Login settings:**
|
||||
|
||||
**Important: Adjust these for your setup!**
|
||||
|
||||
**For local ActualBudget:**
|
||||
```
|
||||
Root URL: http://localhost:5006
|
||||
Home URL: http://localhost:5006
|
||||
Valid redirect URIs:
|
||||
http://localhost:5006/*
|
||||
http://localhost:5006/callback
|
||||
|
||||
Valid post logout redirect URIs: +
|
||||
|
||||
Web origins:
|
||||
http://localhost:5006
|
||||
```
|
||||
|
||||
**For external ActualBudget (Pikapod, etc.):**
|
||||
```
|
||||
Root URL: https://actualbudget-abc123.pikapod.net
|
||||
Home URL: https://actualbudget-abc123.pikapod.net
|
||||
Valid redirect URIs:
|
||||
https://actualbudget-abc123.pikapod.net/*
|
||||
https://actualbudget-abc123.pikapod.net/callback
|
||||
|
||||
Valid post logout redirect URIs: +
|
||||
|
||||
Web origins:
|
||||
https://actualbudget-abc123.pikapod.net
|
||||
```
|
||||
|
||||
**For self-hosted with domain:**
|
||||
```
|
||||
Root URL: https://budget.yourdomain.com
|
||||
Home URL: https://budget.yourdomain.com
|
||||
Valid redirect URIs:
|
||||
https://budget.yourdomain.com/*
|
||||
https://budget.yourdomain.com/callback
|
||||
|
||||
Valid post logout redirect URIs: +
|
||||
|
||||
Web origins:
|
||||
https://budget.yourdomain.com
|
||||
```
|
||||
|
||||
4. **Click "Save"**
|
||||
|
||||
### Step 3: Get Client Secret
|
||||
|
||||
1. **Go to "Credentials" tab**
|
||||
2. **Copy "Client secret"** (you'll need this for ActualBudget)
|
||||
3. **Save it somewhere safe!**
|
||||
|
||||
### Step 4: Create a User
|
||||
|
||||
1. **Go to Users** (left menu)
|
||||
2. **Click "Create user"**
|
||||
|
||||
**User details:**
|
||||
- **Username:** `john` (required)
|
||||
- **Email:** `john@example.com` (optional but recommended)
|
||||
- **Email verified:** ✅ ON (skip email verification)
|
||||
- **First name:** `John`
|
||||
- **Last name:** `Doe`
|
||||
- **Enabled:** ✅ ON (user can log in)
|
||||
- **Click "Create"**
|
||||
|
||||
**Set password:**
|
||||
1. **Go to "Credentials" tab**
|
||||
2. **Click "Set password"**
|
||||
3. **Enter password** (twice)
|
||||
4. **Temporary:** ❌ OFF (user won't be forced to change it)
|
||||
5. **Click "Save"**
|
||||
6. **Confirm** in popup
|
||||
|
||||
### Step 5: Test Login
|
||||
|
||||
1. **Go to Realm Settings** → **Endpoints**
|
||||
2. **Click "OpenID Endpoint Configuration"** (opens JSON)
|
||||
3. **Find:** `authorization_endpoint`
|
||||
4. **Copy URL** and open in browser
|
||||
5. **Add:** `?client_id=actualbudget&response_type=code&redirect_uri=http://localhost:5006/callback`
|
||||
6. **Log in** with your user
|
||||
7. **You should see:** Redirect to callback URL (may error if ActualBudget not configured, but login works)
|
||||
|
||||
---
|
||||
|
||||
## Configuring External Services
|
||||
|
||||
### Keycloak MUST be Publicly Accessible
|
||||
|
||||
**Critical:** For external services like Pikapod, your Keycloak must be accessible from the internet.
|
||||
|
||||
### Requirements:
|
||||
1. ✅ **Domain name** (e.g., `yourdomain.com`)
|
||||
2. ✅ **DNS A record** pointing to your server
|
||||
3. ✅ **Caddy reverse proxy** with HTTPS
|
||||
4. ✅ **Port 80/443 open** in firewall
|
||||
5. ✅ **Keycloak accessible** at `https://auth.yourdomain.com`
|
||||
|
||||
### Setup Caddy for Keycloak
|
||||
|
||||
**Add to Caddyfile:**
|
||||
```caddy
|
||||
auth.yourdomain.com {
|
||||
log {
|
||||
output file /var/log/caddy/keycloak-access.log
|
||||
format json
|
||||
level INFO
|
||||
}
|
||||
|
||||
reverse_proxy localhost:8180
|
||||
|
||||
# Security headers
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-XSS-Protection "1; mode=block"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Reload Caddy:**
|
||||
```bash
|
||||
cd ~/docker/caddy
|
||||
docker exec -w /etc/caddy caddy caddy reload
|
||||
```
|
||||
|
||||
**Test:**
|
||||
```
|
||||
https://auth.yourdomain.com/admin
|
||||
```
|
||||
|
||||
### Configure DNS
|
||||
|
||||
**Add A record:**
|
||||
```
|
||||
auth.yourdomain.com → [Your Server IP]
|
||||
```
|
||||
|
||||
**Or use CNAME:**
|
||||
```
|
||||
auth → yourdomain.com
|
||||
```
|
||||
|
||||
### Example: ActualBudget on Pikapod
|
||||
|
||||
**Scenario:**
|
||||
- Keycloak: `https://auth.yourdomain.com` (your server)
|
||||
- ActualBudget: `https://actualbudget-abc123.pikapod.net` (Pikapod)
|
||||
|
||||
**In Keycloak:**
|
||||
|
||||
1. **Create client:** `actualbudget-pikapod`
|
||||
2. **Redirect URIs:**
|
||||
```
|
||||
https://actualbudget-abc123.pikapod.net/*
|
||||
https://actualbudget-abc123.pikapod.net/callback
|
||||
```
|
||||
3. **Web origins:**
|
||||
```
|
||||
https://actualbudget-abc123.pikapod.net
|
||||
```
|
||||
|
||||
**In ActualBudget (Pikapod):**
|
||||
|
||||
Settings → Authentication:
|
||||
```
|
||||
Client ID: actualbudget-pikapod
|
||||
Client Secret: [from Keycloak credentials tab]
|
||||
|
||||
Authorization URL: https://auth.yourdomain.com/realms/homelab/protocol/openid-connect/auth
|
||||
Token URL: https://auth.yourdomain.com/realms/homelab/protocol/openid-connect/token
|
||||
User Info URL: https://auth.yourdomain.com/realms/homelab/protocol/openid-connect/userinfo
|
||||
```
|
||||
|
||||
**Flow:**
|
||||
1. User visits `https://actualbudget-abc123.pikapod.net`
|
||||
2. Clicks "Login"
|
||||
3. Redirects to `https://auth.yourdomain.com/realms/homelab/...`
|
||||
4. User logs in
|
||||
5. Redirects back to `https://actualbudget-abc123.pikapod.net/callback`
|
||||
6. User is logged in!
|
||||
|
||||
---
|
||||
|
||||
## Reconfiguration & Adding Realms
|
||||
|
||||
You can re-run the script to add more realms or clients!
|
||||
|
||||
### Option 1: Re-run the Script
|
||||
|
||||
```bash
|
||||
cd ~/docker/keycloak
|
||||
docker compose down
|
||||
cd ~
|
||||
./ubuntu-post-install.sh
|
||||
# Select KEYCLOAK again
|
||||
# Choose "Configure Keycloak..." → Yes
|
||||
# Enter new realm name: "family"
|
||||
# Create new users
|
||||
```
|
||||
|
||||
**This creates:**
|
||||
- New realm with new users
|
||||
- New OAuth clients for that realm
|
||||
- Separate from your existing realm
|
||||
|
||||
### Option 2: Add Realm Manually
|
||||
|
||||
**Via Web UI:**
|
||||
1. Go to admin console
|
||||
2. Click realm dropdown
|
||||
3. "Create Realm"
|
||||
4. Name: `family`
|
||||
5. Repeat client/user creation steps
|
||||
|
||||
### Option 3: Use Script Helper
|
||||
|
||||
The script can be extended to add a helper:
|
||||
|
||||
```bash
|
||||
cd ~/docker/keycloak
|
||||
|
||||
# Login to admin CLI
|
||||
docker exec keycloak /opt/keycloak/bin/kcadm.sh config credentials \
|
||||
--server http://localhost:8080 \
|
||||
--realm master \
|
||||
--user admin \
|
||||
--password [YOUR_ADMIN_PASSWORD]
|
||||
|
||||
# Create new realm
|
||||
docker exec keycloak /opt/keycloak/bin/kcadm.sh create realms \
|
||||
-s realm=family \
|
||||
-s enabled=true
|
||||
|
||||
# Create new client
|
||||
docker exec keycloak /opt/keycloak/bin/kcadm.sh create clients -r family \
|
||||
-s clientId=my-new-service \
|
||||
-s enabled=true \
|
||||
-s clientAuthenticatorType=client-secret \
|
||||
-s secret=$(openssl rand -hex 32) \
|
||||
-s 'redirectUris=["https://service.yourdomain.com/*"]'
|
||||
|
||||
# Create new user
|
||||
docker exec keycloak /opt/keycloak/bin/kcadm.sh create users -r family \
|
||||
-s username=alice \
|
||||
-s email=alice@example.com \
|
||||
-s enabled=true
|
||||
|
||||
# Set password
|
||||
docker exec keycloak /opt/keycloak/bin/kcadm.sh set-password -r family \
|
||||
--username alice \
|
||||
--new-password 'AlicePassword123!'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### Use Case 1: All Local Services
|
||||
**Setup:**
|
||||
- Keycloak: `http://localhost:8180`
|
||||
- ActualBudget: `http://localhost:5006`
|
||||
- Jellyfin: `http://localhost:8096`
|
||||
|
||||
**Configuration:**
|
||||
- No domain needed
|
||||
- Use `localhost` URLs everywhere
|
||||
- Redirect URIs: `http://localhost:PORT/*`
|
||||
|
||||
### Use Case 2: Self-Hosted with Domain
|
||||
**Setup:**
|
||||
- Keycloak: `https://auth.yourdomain.com`
|
||||
- ActualBudget: `https://budget.yourdomain.com`
|
||||
- Jellyfin: `https://jellyfin.yourdomain.com`
|
||||
|
||||
**Configuration:**
|
||||
- Requires domain + Caddy
|
||||
- Use HTTPS URLs
|
||||
- Redirect URIs: `https://service.yourdomain.com/*`
|
||||
|
||||
### Use Case 3: Mixed (Local + External)
|
||||
**Setup:**
|
||||
- Keycloak: `https://auth.yourdomain.com` (self-hosted)
|
||||
- ActualBudget: `https://actualbudget-abc.pikapod.net` (Pikapod)
|
||||
- Jellyfin: `https://jellyfin.yourdomain.com` (self-hosted)
|
||||
|
||||
**Configuration:**
|
||||
- Keycloak MUST be publicly accessible
|
||||
- Each service gets its own client
|
||||
- ActualBudget redirect: `https://actualbudget-abc.pikapod.net/*`
|
||||
- Jellyfin redirect: `https://jellyfin.yourdomain.com/*`
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: "Invalid redirect URI"
|
||||
**Cause:** Redirect URI in Keycloak doesn't match what the app is using.
|
||||
|
||||
**Fix:**
|
||||
1. Check error message for actual redirect URI
|
||||
2. Add EXACT URI to Keycloak client settings
|
||||
3. Include wildcard: `https://domain.com/*`
|
||||
|
||||
### Issue: "Client not found"
|
||||
**Cause:** Client ID doesn't match.
|
||||
|
||||
**Fix:**
|
||||
1. Check client ID in Keycloak
|
||||
2. Ensure it matches exactly in application
|
||||
3. Case-sensitive!
|
||||
|
||||
### Issue: "Invalid client secret"
|
||||
**Cause:** Wrong secret or expired.
|
||||
|
||||
**Fix:**
|
||||
1. Go to Keycloak → Clients → Credentials
|
||||
2. Copy secret again (or regenerate)
|
||||
3. Update in application
|
||||
|
||||
### Issue: External service can't reach Keycloak
|
||||
**Cause:** Keycloak not publicly accessible.
|
||||
|
||||
**Fix:**
|
||||
1. Ensure Caddy is running: `docker ps | grep caddy`
|
||||
2. Check DNS: `dig auth.yourdomain.com`
|
||||
3. Test URL: `curl https://auth.yourdomain.com`
|
||||
4. Check firewall: `sudo ufw status` (80/443 open?)
|
||||
|
||||
### Issue: Login succeeds but redirect fails
|
||||
**Cause:** CORS or redirect URI mismatch.
|
||||
|
||||
**Fix:**
|
||||
1. Add domain to "Web Origins" in client settings
|
||||
2. Check redirect URI includes protocol (https://)
|
||||
3. Check for typos in domain name
|
||||
|
||||
### Issue: Can't login to Keycloak admin console
|
||||
**Cause:** Container not started or wrong password.
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
# Check if running
|
||||
docker ps | grep keycloak
|
||||
|
||||
# Check logs
|
||||
docker logs keycloak --tail 50
|
||||
|
||||
# Restart
|
||||
cd ~/docker/keycloak
|
||||
docker compose restart
|
||||
|
||||
# Reset admin password (if needed)
|
||||
docker exec keycloak /opt/keycloak/bin/kcadm.sh config credentials \
|
||||
--server http://localhost:8080 \
|
||||
--realm master \
|
||||
--user admin \
|
||||
--password NEW_PASSWORD_HERE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Important URLs
|
||||
|
||||
**Local:**
|
||||
```
|
||||
Admin Console: http://localhost:8180/admin
|
||||
Realm Endpoints: http://localhost:8180/realms/{realm-name}/.well-known/openid-configuration
|
||||
```
|
||||
|
||||
**Production:**
|
||||
```
|
||||
Admin Console: https://auth.yourdomain.com/admin
|
||||
Realm Endpoints: https://auth.yourdomain.com/realms/{realm-name}/.well-known/openid-configuration
|
||||
```
|
||||
|
||||
### OAuth URLs (for realm "homelab")
|
||||
|
||||
**Local:**
|
||||
```
|
||||
Authorization: http://localhost:8180/realms/homelab/protocol/openid-connect/auth
|
||||
Token: http://localhost:8180/realms/homelab/protocol/openid-connect/token
|
||||
User Info: http://localhost:8180/realms/homelab/protocol/openid-connect/userinfo
|
||||
Logout: http://localhost:8180/realms/homelab/protocol/openid-connect/logout
|
||||
```
|
||||
|
||||
**Production:**
|
||||
```
|
||||
Authorization: https://auth.yourdomain.com/realms/homelab/protocol/openid-connect/auth
|
||||
Token: https://auth.yourdomain.com/realms/homelab/protocol/openid-connect/token
|
||||
User Info: https://auth.yourdomain.com/realms/homelab/protocol/openid-connect/userinfo
|
||||
Logout: https://auth.yourdomain.com/realms/homelab/protocol/openid-connect/logout
|
||||
```
|
||||
|
||||
### Common Commands
|
||||
|
||||
```bash
|
||||
# Start Keycloak
|
||||
cd ~/docker/keycloak
|
||||
docker compose up -d
|
||||
|
||||
# Stop Keycloak
|
||||
docker compose down
|
||||
|
||||
# View logs
|
||||
docker logs keycloak -f
|
||||
|
||||
# Access shell
|
||||
docker exec -it keycloak bash
|
||||
|
||||
# Login to admin CLI
|
||||
docker exec keycloak /opt/keycloak/bin/kcadm.sh config credentials \
|
||||
--server http://localhost:8080 \
|
||||
--realm master \
|
||||
--user admin \
|
||||
--password YOUR_PASSWORD
|
||||
|
||||
# Export realm configuration (backup)
|
||||
docker exec keycloak /opt/keycloak/bin/kc.sh export \
|
||||
--dir /opt/keycloak/data/export \
|
||||
--realm homelab
|
||||
|
||||
# Copy export to host
|
||||
docker cp keycloak:/opt/keycloak/data/export ./backup/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**Keycloak provides:**
|
||||
- ✅ Single Sign-On for all your services
|
||||
- ✅ Centralized user management
|
||||
- ✅ OAuth2/OIDC authentication
|
||||
- ✅ Works with local and external services
|
||||
- ✅ Professional-grade security
|
||||
|
||||
**Automated setup does:**
|
||||
- ✅ Creates realm
|
||||
- ✅ Creates OAuth clients
|
||||
- ✅ Creates initial user
|
||||
- ✅ Saves all credentials
|
||||
- ✅ Ready to use immediately
|
||||
|
||||
**Manual setup allows:**
|
||||
- ✅ Full control over configuration
|
||||
- ✅ Multiple realms (family, work, etc.)
|
||||
- ✅ Custom client settings
|
||||
- ✅ Advanced features (LDAP, MFA, etc.)
|
||||
|
||||
**For external services:**
|
||||
- ✅ Keycloak must be publicly accessible
|
||||
- ✅ Use Caddy with HTTPS
|
||||
- ✅ Configure proper redirect URIs
|
||||
- ✅ Test OAuth flow before production
|
||||
|
||||
For questions or issues, check the Keycloak documentation: https://www.keycloak.org/documentation
|
||||
-101
@@ -1,101 +0,0 @@
|
||||
# Modular Post-Install (`setup.sh` + `lib/` + `services/`)
|
||||
|
||||
This is the new structure that gives you **one source of truth** *and* the
|
||||
ability to **run just the service you want** — without maintaining a pile of
|
||||
near-duplicate standalone scripts.
|
||||
|
||||
## Why
|
||||
|
||||
The full `ubuntu-post-install-*.sh` scripts are great as a "run once, set up the
|
||||
whole box" experience, but to add or update one service you edit a 300 KB file
|
||||
(in two or three places). The separate `setup-*.sh` scripts are easy to run for
|
||||
one service, but duplicate logic and drift apart.
|
||||
|
||||
The fix is **not** to generate per-service scripts from the monolith (that just
|
||||
triples the maintenance surface). It's to have **one implementation per service**
|
||||
in a module, shared helpers in a library, and a thin dispatcher with two entry
|
||||
points.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
setup.sh # dispatcher: menu, run-one, --list, --dry-run, --unattended
|
||||
lib/common.sh # shared helpers: logging, prompts, ownership, Caddy wiring,
|
||||
# the service registry. THE single source of truth.
|
||||
services/
|
||||
base.sh # essential CLI packages (incl. glow)
|
||||
homeassistant.sh # Home Assistant
|
||||
... # one file per service
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
sudo ./setup.sh # interactive menu (whiptail or text)
|
||||
sudo ./setup.sh homeassistant # install one service
|
||||
sudo ./setup.sh base glow # install several
|
||||
./setup.sh --list # list services, grouped
|
||||
sudo ./setup.sh --dry-run --unattended minecraft # preview, no prompts
|
||||
```
|
||||
|
||||
## Anatomy of a service module
|
||||
|
||||
Each `services/<name>.sh` does exactly two things: **register** itself and
|
||||
define **install_<name>**.
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
register_service myapp homelab "What it does" 1234 # name group description [port]
|
||||
|
||||
install_myapp() {
|
||||
require_docker || return 1
|
||||
local DIR="$DOCKER_DIR/myapp"
|
||||
[ "$DRY_RUN" = true ] && { echo "[DRY-RUN] Would create $DIR"; return 0; }
|
||||
mkdir -p "$DIR"; ensure_docker_dir_ownership "$DIR"; cd "$DIR" || return 1
|
||||
cat > docker-compose.yml << 'YAML'
|
||||
...
|
||||
YAML
|
||||
configure_caddy_for_service "MyApp" "1234" "myapp" # optional reverse proxy
|
||||
prompt_yn "Start now? (y/n):" "y" START && docker compose up -d
|
||||
}
|
||||
```
|
||||
|
||||
Helpers available from `lib/common.sh`: `log_info/success/warning/error`,
|
||||
`prompt_yn`, `prompt_text`, `run_cmd`, `ensure_docker_dir_ownership`,
|
||||
`generate_password`, `validate_password`, `configure_caddy_for_service`,
|
||||
`require_root`, `require_docker`. Globals: `DOCKER_DIR`, `ACTUAL_USER`,
|
||||
`ACTUAL_HOME`, `DRY_RUN`, `UNATTENDED`.
|
||||
|
||||
Every service installs to its **own folder** `~/docker/<name>/` with its **own
|
||||
`docker-compose.yml`** (the DoTheEvo `selfhosted-apps-docker` layout) — never a
|
||||
single shared compose file.
|
||||
|
||||
## Groups
|
||||
|
||||
`base` · `homelab` · `utilities` · `media` · `cameras` · `gaming` · `extras` ·
|
||||
`backup`. `extras` holds non-Docker add-ons pulled from other repos (e.g. the
|
||||
`silent-send` browser extension) — things that install/build on the host rather
|
||||
than running as a container, so this script stays a single entry point for them.
|
||||
The guided menu (`sudo ./setup.sh`) shows the **required** packages first with a
|
||||
cancel option, then offers **Caddy** (most services proxy through it), then a
|
||||
**category menu you loop through** — pick a category, tick services (already
|
||||
installed ones are marked `[installed]`), install, and you land back on the menu
|
||||
to pick the next category. Within `homelab`, Caddy → CrowdSec → Authelia are
|
||||
ordered first.
|
||||
|
||||
## Migration status
|
||||
|
||||
Migration is complete. `setup.sh` + `services/` now cover every service
|
||||
from `ubuntu-post-install-*-crowdsec.sh` plus several extras. The monolith
|
||||
is retained as a frozen evolution record.
|
||||
|
||||
| Group | Modules |
|
||||
|-------|---------|
|
||||
| `base` | `base`, `glow` |
|
||||
| `homelab` | `caddy`, `crowdsec`, `authelia`, `homeassistant` |
|
||||
| `utilities` | `actualbudget`, `ddclient`, `filebrowser`, `fmd`, `magicmirror`, `mealie`, `meshcentral`, `ntfy`, `portainer`, `traccar`, `uptimekuma`, `watchtower`, `wg-easy` |
|
||||
| `media` | `arm`, `audiobookshelf`, `emby`, `immich`, `jellyfin`, `lyrion` |
|
||||
| `cameras` | `frigate`, `frigate-audio`, `frigate-notify`, `sky-cam` |
|
||||
| `gaming` | `js99er`, `minecraft`, `wolf`, `wolf-pair` |
|
||||
| `extras` | `linux-to-sync`, `silent-send`, `sync-cc` |
|
||||
| `backup` | `backup` |
|
||||
@@ -1,244 +0,0 @@
|
||||
# Ubuntu Post-Install Script - New Structure
|
||||
|
||||
## Current Problems
|
||||
|
||||
1. ❌ Services prompt individually even when not selected in whiptail
|
||||
2. ❌ Too many things installed BEFORE whiptail menu (Samba, VPNs, fail2ban)
|
||||
3. ❌ No clear explanation to user about script phases
|
||||
4. ❌ No uninstall option
|
||||
5. ❌ Confusing for re-running on existing servers
|
||||
|
||||
## New Structure
|
||||
|
||||
### PHASE 1: Essential System Setup (Before Whiptail)
|
||||
**Purpose:** Install only what's REQUIRED for everything else to work
|
||||
|
||||
**What stays BEFORE whiptail:**
|
||||
- ✅ System updates (apt update/upgrade)
|
||||
- ✅ Essential packages (openssh-server, rsync, curl, wget, git, vim, htop, ncdu)
|
||||
- ✅ SSH key generation (optional)
|
||||
- ✅ SSH key import from GitHub/Launchpad (optional)
|
||||
- ✅ Docker & Docker Compose installation
|
||||
- ✅ Hard drive mounting/formatting
|
||||
|
||||
**What moves TO whiptail menu:**
|
||||
- 🔄 Samba file sharing
|
||||
- 🔄 fail2ban for SSH
|
||||
- 🔄 VPN services (Tailscale, WireGuard, NetBird)
|
||||
- 🔄 Remote desktop (RustDesk, TeamViewer)
|
||||
- 🔄 MeshCentral agent
|
||||
|
||||
### PHASE 2: Service Selection (Whiptail Menu)
|
||||
**Purpose:** Let user choose ALL optional services
|
||||
|
||||
**New whiptail menu structure:**
|
||||
|
||||
```
|
||||
┌─────────────────── Select Services ────────────────────┐
|
||||
│ ☐ INSTALL ☑ UNINSTALL │
|
||||
│ │
|
||||
│ === NETWORK & SECURITY === │
|
||||
│ [ ] SAMBA File sharing (SMB/CIFS) │
|
||||
│ [ ] FAIL2BAN_SSH Protect SSH from brute-force │
|
||||
│ [ ] TAILSCALE Easy VPN mesh network │
|
||||
│ [ ] NETBIRD Self-hosted VPN │
|
||||
│ [ ] WIREGUARD Manual VPN configuration │
|
||||
│ │
|
||||
│ === REMOTE ACCESS === │
|
||||
│ [ ] RUSTDESK Remote desktop (OSS) │
|
||||
│ [ ] TEAMVIEWER Remote desktop (commercial) │
|
||||
│ [ ] MESHCENTRAL Remote management agent │
|
||||
│ │
|
||||
│ === DOCKER SERVICES === │
|
||||
│ [ ] ACTUALBUDGET Personal finance │
|
||||
│ [ ] KEYCLOAK Identity management │
|
||||
│ [ ] CADDY Reverse proxy │
|
||||
│ [ ] FAIL2BAN_CADDY Protect Caddy services │
|
||||
│ [ ] JELLYFIN Media server │
|
||||
│ [ ] IMMICH Photo backup │
|
||||
│ [ ] AUDIOBOOKSHELF Audiobook server │
|
||||
│ [ ] MEALIE Recipe manager │
|
||||
│ [ ] UPTIMEKUMA Service monitoring │
|
||||
│ [ ] PORTAINER Docker management UI │
|
||||
│ [ ] WATCHTOWER Auto-update containers │
|
||||
│ ... (all other services) │
|
||||
│ │
|
||||
│ === MESHCENTRAL SERVER === │
|
||||
│ [ ] MESHCENTRAL_SRV Self-hosted remote mgmt server │
|
||||
│ │
|
||||
│ <Install Selected> <Uninstall Selected> │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### PHASE 3: Installation
|
||||
**Purpose:** Install/uninstall selected services in correct order
|
||||
|
||||
**Dependency-aware installation order:**
|
||||
1. Install Caddy first (if selected)
|
||||
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
|
||||
|
||||
### Re-Running The Script
|
||||
|
||||
**Behavior on already-configured server:**
|
||||
|
||||
**Phase 1 (Essential):**
|
||||
- Detects existing installations
|
||||
- Shows: "Docker is already installed: (version)"
|
||||
- Prompts: "Reinstall Docker? (y/n): **n**" (defaults to NO)
|
||||
- Skips if "n"
|
||||
|
||||
**Phase 2 (Services):**
|
||||
- Whiptail menu shows ALL services
|
||||
- Already-installed services could be marked with (installed)
|
||||
- User can:
|
||||
- Select new services to add
|
||||
- Select installed services + click "Uninstall Selected"
|
||||
- Skip menu to make no changes
|
||||
|
||||
## Implementation Changes
|
||||
|
||||
### 1. Add Intro Text
|
||||
|
||||
```bash
|
||||
# At beginning of script after argument parsing
|
||||
if [ "$UNATTENDED" != true ]; then
|
||||
cat << 'EOF'
|
||||
╔════════════════════════════════════════════════════════════════╗
|
||||
║ Ubuntu Post-Install Setup Script ║
|
||||
╚════════════════════════════════════════════════════════════════╝
|
||||
|
||||
This script installs and configures your Ubuntu server in TWO phases:
|
||||
|
||||
PHASE 1: ESSENTIAL SETUP (Required)
|
||||
• System updates & core packages
|
||||
• SSH configuration
|
||||
• Docker & Docker Compose
|
||||
• Hard drive mounting
|
||||
|
||||
PHASE 2: SERVICE SELECTION (Optional)
|
||||
• Interactive menu to select services
|
||||
• Install OR uninstall services
|
||||
• Safe to re-run on existing servers
|
||||
|
||||
Press ENTER to begin Phase 1...
|
||||
EOF
|
||||
read -p ""
|
||||
fi
|
||||
```
|
||||
|
||||
### 2. Reorganize Script Sections
|
||||
|
||||
**NEW ORDER:**
|
||||
1. Intro text
|
||||
2. Essential system setup (Phase 1)
|
||||
3. Whiptail menu with ALL services (Phase 2)
|
||||
4. Service installations based on selections
|
||||
|
||||
**REMOVED FROM PRE-WHIPTAIL:**
|
||||
- Lines 1709-1800: Samba installation → Move to whiptail
|
||||
- Lines 1596-1625: fail2ban (SSH) → Move to whiptail
|
||||
- Lines 1841-1888: NetBird → Move to whiptail
|
||||
- Lines 1889-1945: WireGuard → Move to whiptail
|
||||
- Lines 1947-1998: Tailscale → Move to whiptail
|
||||
- Lines 2000-2044: RustDesk → Move to whiptail
|
||||
- Lines 2046-2095: TeamViewer → Move to whiptail
|
||||
- Lines 2097-2153: MeshCentral Agent → Move to whiptail
|
||||
|
||||
### 3. Update Whiptail Menu
|
||||
|
||||
**Add these to the checklist:**
|
||||
```bash
|
||||
"SAMBA" "File sharing (Windows, Mac, Linux)" OFF \
|
||||
"FAIL2BAN_SSH" "Protect SSH from brute-force attacks" OFF \
|
||||
"TAILSCALE" "Easy VPN mesh network" OFF \
|
||||
"NETBIRD" "Self-hosted VPN alternative" OFF \
|
||||
"WIREGUARD" "Manual VPN configuration" OFF \
|
||||
"RUSTDESK" "Open-source remote desktop" OFF \
|
||||
"TEAMVIEWER" "Commercial remote desktop" OFF \
|
||||
"MESHCENTRAL_AGENT" "Remote management agent" OFF \
|
||||
```
|
||||
|
||||
### 4. Add Uninstall Functionality
|
||||
|
||||
**New buttons in whiptail:**
|
||||
```bash
|
||||
--extra-button --extra-label "Uninstall" \
|
||||
--ok-button "Install" --cancel-button "Skip"
|
||||
```
|
||||
|
||||
**Check return code:**
|
||||
- 0 = Install selected
|
||||
- 1 = Skip/Cancel
|
||||
- 3 = Uninstall selected
|
||||
|
||||
**Uninstall logic:**
|
||||
```bash
|
||||
if [ $WHIPTAIL_RETURN -eq 3 ]; then
|
||||
# Uninstall mode
|
||||
for service in $SELECTED_SERVICES; do
|
||||
uninstall_service "$service"
|
||||
done
|
||||
fi
|
||||
```
|
||||
|
||||
### 5. Fix Duplicate Prompts
|
||||
|
||||
**Current issue:**
|
||||
```bash
|
||||
if [ -z "$INSTALL_AUDIOBOOKSHELF" ]; then
|
||||
prompt_yn "Install Audiobookshelf? (y/n):" "n" INSTALL_AUDIOBOOKSHELF
|
||||
fi
|
||||
```
|
||||
|
||||
**Problem:** This runs even if user didn't select it in whiptail!
|
||||
|
||||
**Fix:** Only show prompt if whiptail wasn't used OR variable not set by whiptail
|
||||
|
||||
```bash
|
||||
# After whiptail, set a flag
|
||||
WHIPTAIL_USED=true
|
||||
|
||||
# In individual service sections
|
||||
if [ "$WHIPTAIL_USED" != true ] && [ -z "$INSTALL_AUDIOBOOKSHELF" ]; then
|
||||
prompt_yn "Install Audiobookshelf? (y/n):" "n" INSTALL_AUDIOBOOKSHELF
|
||||
fi
|
||||
```
|
||||
|
||||
**Or simpler:** Just remove all individual prompts! If whiptail is available, use it. If not, show ALL services as prompts.
|
||||
|
||||
## Benefits
|
||||
|
||||
1. ✅ Clear two-phase structure
|
||||
2. ✅ All optional services in ONE menu
|
||||
3. ✅ No duplicate prompts
|
||||
4. ✅ Uninstall functionality
|
||||
5. ✅ Safe to re-run
|
||||
6. ✅ User understands what's happening when
|
||||
7. ✅ Faster for users who know what they want
|
||||
|
||||
## Migration Path
|
||||
|
||||
**For existing users:**
|
||||
1. Script still works the same way
|
||||
2. New intro text explains structure
|
||||
3. Existing installations detected
|
||||
4. Can use uninstall to remove unwanted services
|
||||
|
||||
## Testing Scenarios
|
||||
|
||||
1. **Fresh install:** All prompts flow correctly
|
||||
2. **Re-run:** Detects existing, allows adding new services
|
||||
3. **Uninstall:** Removes selected services cleanly
|
||||
4. **Whiptail unavailable:** Falls back to individual prompts
|
||||
5. **Cancel menu:** Skips all service installations
|
||||
|
||||
---
|
||||
|
||||
**Implementation Priority:**
|
||||
1. ✅ Fix duplicate prompts (CRITICAL - doing now)
|
||||
2. ⏳ Add intro text (HIGH)
|
||||
3. ⏳ Move services to whiptail (HIGH)
|
||||
4. ⏳ Add uninstall functionality (MEDIUM)
|
||||
5. ⏳ Improve re-run detection (LOW - already works)
|
||||
@@ -1,59 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# SCRIPT FLOW EXPLANATION FOR USERS
|
||||
cat << 'EOF'
|
||||
╔════════════════════════════════════════════════════════════════╗
|
||||
║ Ubuntu Post-Install Setup Script ║
|
||||
╚════════════════════════════════════════════════════════════════╝
|
||||
|
||||
This script is divided into TWO main phases:
|
||||
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ PHASE 1: ESSENTIAL SYSTEM SETUP (Required) │
|
||||
├────────────────────────────────────────────────────────────────┤
|
||||
│ These are installed/configured FIRST: │
|
||||
│ ✓ System updates and essential packages │
|
||||
│ ✓ OpenSSH server (remote access) │
|
||||
│ ✓ rsync, curl, wget (core utilities) │
|
||||
│ ✓ SSH key configuration │
|
||||
│ ✓ Docker & Docker Compose (container platform) │
|
||||
│ ✓ Hard drive mounting (storage setup) │
|
||||
│ │
|
||||
│ These are REQUIRED for everything else to work. │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ PHASE 2: SERVICE SELECTION MENU (Optional) │
|
||||
├────────────────────────────────────────────────────────────────┤
|
||||
│ After Phase 1, you'll see a CHECKBOX MENU where you can: │
|
||||
│ • Select which services to INSTALL │
|
||||
│ • Select which services to UNINSTALL │
|
||||
│ • Skip services you don't want │
|
||||
│ │
|
||||
│ Services include: │
|
||||
│ • Self-hosted apps (ActualBudget, Keycloak, Jellyfin, etc.) │
|
||||
│ • Network services (Samba, VPNs, fail2ban) │
|
||||
│ • Monitoring tools (Uptime Kuma, Portainer, Watchtower) │
|
||||
│ • And many more... │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ RE-RUNNING THIS SCRIPT │
|
||||
├────────────────────────────────────────────────────────────────┤
|
||||
│ You can safely re-run this script on an already configured │
|
||||
│ server: │
|
||||
│ • Phase 1 will DETECT existing installations and skip them │
|
||||
│ • Phase 2 menu will show ALL services (installed and not) │
|
||||
│ • Select NEW services to add │
|
||||
│ • Or select UNINSTALL to remove services │
|
||||
│ │
|
||||
│ The script is IDEMPOTENT - safe to run multiple times! │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Press ENTER to continue with Phase 1 (Essential Setup)...
|
||||
EOF
|
||||
|
||||
read -p ""
|
||||
|
||||
# Now continue with the actual script...
|
||||
EOF
|
||||
@@ -1,78 +0,0 @@
|
||||
# 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`.
|
||||
|
||||
> New services are added to the **`-crowdsec`** tier only (the current tip of
|
||||
> the evolution); the original and `-no-keycloak` scripts are frozen as
|
||||
> historical snapshots. For example, **Home Assistant** (home-automation hub,
|
||||
> port 8123) is available in the `-crowdsec` variants. It ships with a
|
||||
> `trusted_proxies` config pre-seeded so it works behind the Caddy reverse
|
||||
> proxy out of the box, and the installer asks whether to use **bridge**
|
||||
> networking (port 8123 published — proxy-friendly, isolated) or **host**
|
||||
> networking (needed for LAN device auto-discovery: Chromecast, HomeKit,
|
||||
> mDNS, some Zigbee/Z-Wave/Bluetooth).
|
||||
|
||||
## 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
|
||||
- **Optional ntfy alerts on bans** — when configuring CrowdSec the script
|
||||
can wire up an [ntfy](https://ntfy.sh/) push notification (via CrowdSec's
|
||||
HTTP notification plugin). It writes `/etc/crowdsec/notifications/ntfy.yaml`
|
||||
and references it from the default profile in
|
||||
`/etc/crowdsec/profiles.yaml`. The same script can also install a
|
||||
self-hosted **ntfy server** (separate menu option), so alerts can stay on
|
||||
your own infrastructure.
|
||||
|
||||
### A note on "notification of failed attempts"
|
||||
|
||||
CrowdSec alerts fire on a **ban decision** — i.e. once an IP crosses the
|
||||
failed-attempt threshold for a scenario (e.g. `crowdsecurity/ssh-bf`), not on
|
||||
every individual failed login. That gives you one actionable "X banned for Y"
|
||||
push instead of a flood. To alert on a single failed login you'd lower the
|
||||
scenario threshold or write a custom scenario, but the ban-level alert is the
|
||||
recommended default.
|
||||
|
||||
Authelia, by contrast, only sends **email/SMTP** notifications (for password
|
||||
reset and 2FA device registration) — it has no built-in "failed login" push,
|
||||
which is why CrowdSec → ntfy is the path used here.
|
||||
|
||||
## 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
|
||||
```
|
||||
@@ -1,409 +0,0 @@
|
||||
# Security and Infrastructure Improvements
|
||||
|
||||
## Summary of Changes
|
||||
|
||||
This document describes the comprehensive security and infrastructure improvements made to the Ubuntu post-installation script.
|
||||
|
||||
## Issues Fixed
|
||||
|
||||
### 1. Docker Directory Ownership
|
||||
**Problem:** Docker directories were being created as root, causing permission issues when running Docker without sudo.
|
||||
|
||||
**Solution:**
|
||||
- Added `ensure_docker_dir_ownership()` helper function
|
||||
- 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
|
||||
|
||||
**Impact:** Docker containers can now be managed without requiring root/sudo for every command.
|
||||
|
||||
---
|
||||
|
||||
### 2. Keycloak Security Overhaul
|
||||
**Problem:** Weak default passwords, special characters causing issues, development mode in production.
|
||||
|
||||
**Solutions Implemented:**
|
||||
|
||||
#### Password Requirements
|
||||
- **Minimum length:** 12 characters (16+ recommended)
|
||||
- **Character set:** Letters and numbers ONLY (no special characters)
|
||||
- **Auto-generation:** Press ENTER to generate secure passwords automatically
|
||||
- **Validation:** Real-time password validation with retry loop
|
||||
|
||||
#### Production vs Development Mode
|
||||
- **Production mode:** Uses `start` command, requires hostname configuration
|
||||
- **Development mode:** Uses `start-dev` command, relaxed security for testing
|
||||
- **Hostname support:** Proper `KC_HOSTNAME` configuration for public deployment
|
||||
|
||||
#### Environment Variables
|
||||
- All credentials moved to `.env` file
|
||||
- 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:**
|
||||
- ✅ Keycloak (admin + database passwords)
|
||||
- ✅ ActualBudget (timezone and config)
|
||||
- ✅ Immich (already had .env)
|
||||
- ✅ FindMyDevice (already had .env)
|
||||
- ✅ wg-easy (already had .env)
|
||||
- ✅ Kopia (already had .env)
|
||||
|
||||
**Benefits:**
|
||||
- Passwords not visible in docker-compose.yml files
|
||||
- Easy to backup separately from compose files
|
||||
- Can be excluded from version control
|
||||
- Easier credential rotation
|
||||
|
||||
---
|
||||
|
||||
### 4. Caddy2 Reverse Proxy Integration
|
||||
|
||||
**Existing Integration:**
|
||||
All services already include Caddy2 reverse proxy configuration via the `configure_caddy_for_service()` function.
|
||||
|
||||
**Features:**
|
||||
- Automatic HTTPS via Let's Encrypt
|
||||
- HTTP/2 support
|
||||
- Security headers (HSTS, X-Frame-Options, etc.)
|
||||
- JSON logging for fail2ban
|
||||
- Automatic certificate renewal
|
||||
|
||||
**Example Caddy Configuration:**
|
||||
```
|
||||
photos.yourdomain.com {
|
||||
reverse_proxy localhost:2283
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
log {
|
||||
output file /var/log/caddy/photos-access.log
|
||||
format json
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Fail2ban Integration
|
||||
|
||||
**Existing fail2ban Labels:**
|
||||
All services already include fail2ban monitoring labels:
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
- "io.podman.annotations.label/fail2ban.enable=true"
|
||||
- "io.podman.annotations.label/fail2ban.filter=caddy-auth"
|
||||
```
|
||||
|
||||
**Services with fail2ban monitoring:**
|
||||
- ActualBudget
|
||||
- Keycloak
|
||||
- All other internet-facing services
|
||||
|
||||
**fail2ban Configuration:**
|
||||
- Filter: `/etc/fail2ban/filter.d/caddy-auth.conf`
|
||||
- Jail: `/etc/fail2ban/jail.d/caddy.conf`
|
||||
- Ban after: 5 failed attempts
|
||||
- Ban duration: 3600 seconds (1 hour)
|
||||
- Detection window: 600 seconds
|
||||
|
||||
**Detailed Setup:** See `CADDY-FAIL2BAN-SETUP.md` for complete configuration.
|
||||
|
||||
---
|
||||
|
||||
## Helper Functions Added
|
||||
|
||||
### `ensure_docker_dir_ownership(dir1 [dir2 ...])`
|
||||
Ensures Docker directories are owned by the actual user (not root).
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
mkdir -p "$SERVICE_DIR"
|
||||
ensure_docker_dir_ownership "$SERVICE_DIR"
|
||||
```
|
||||
|
||||
### `generate_password([length])`
|
||||
Generates secure alphanumeric passwords (no special characters).
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
PASSWORD=$(generate_password 20) # 20-character password
|
||||
```
|
||||
|
||||
### `validate_password(password [min_length])`
|
||||
Validates passwords for Keycloak compatibility.
|
||||
|
||||
**Validation Rules:**
|
||||
- Minimum length (default: 12 characters)
|
||||
- Alphanumeric only (a-zA-Z0-9)
|
||||
- Returns 0 if valid, 1 if invalid
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
if validate_password "$USER_PASSWORD" 12; then
|
||||
echo "Password accepted"
|
||||
fi
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Keycloak Setup Guide
|
||||
|
||||
### For ActualBudget on Pikapods
|
||||
|
||||
1. **Install Keycloak with production mode:**
|
||||
```bash
|
||||
sudo bash ubuntu-post-install.sh
|
||||
# Select Keycloak from menu
|
||||
# Choose production mode (y)
|
||||
# Enter hostname: auth.yourdomain.com
|
||||
# Press ENTER to auto-generate secure passwords
|
||||
```
|
||||
|
||||
2. **Configure Caddy2:**
|
||||
- Script automatically prompts for Caddy configuration
|
||||
- Enter your domain (e.g., auth.yourdomain.com)
|
||||
- Ensure DNS A record points to your server
|
||||
|
||||
3. **Configure DNS:**
|
||||
```
|
||||
auth.yourdomain.com → Your Server IP
|
||||
```
|
||||
|
||||
4. **Access Keycloak:**
|
||||
```
|
||||
https://auth.yourdomain.com
|
||||
```
|
||||
|
||||
5. **Set up ActualBudget OAuth:**
|
||||
- The script automatically creates an OAuth client for ActualBudget
|
||||
- Client details saved to: `~/docker/keycloak/actualbudget-oauth.txt`
|
||||
- Use these credentials in your Pikapod ActualBudget instance
|
||||
|
||||
6. **Configure ActualBudget on Pikapods:**
|
||||
- Go to your ActualBudget settings
|
||||
- Enable OpenID Connect
|
||||
- Enter your Keycloak details:
|
||||
- Issuer: `https://auth.yourdomain.com/realms/homelab`
|
||||
- Client ID: (from actualbudget-oauth.txt)
|
||||
- Client Secret: (from actualbudget-oauth.txt)
|
||||
|
||||
### For Other Self-Hosted Services
|
||||
|
||||
The script can create generic OAuth clients for other services. After Keycloak installation, you can:
|
||||
|
||||
1. Access Keycloak admin console
|
||||
2. Create new OAuth2/OIDC clients
|
||||
3. Configure redirect URIs for your services
|
||||
4. Use the client credentials in your service configuration
|
||||
|
||||
**Generic Client Template:**
|
||||
- Client ID: your-service-name
|
||||
- Client Type: Confidential
|
||||
- Standard Flow Enabled: Yes
|
||||
- Valid Redirect URIs: https://your-service.com/*
|
||||
|
||||
---
|
||||
|
||||
## Password Requirements Reference
|
||||
|
||||
### Keycloak Passwords
|
||||
- **Minimum:** 12 characters
|
||||
- **Recommended:** 16+ characters
|
||||
- **Format:** Alphanumeric only (a-zA-Z0-9)
|
||||
- **No special characters:** `!@#$%^&*()` etc. are NOT allowed
|
||||
- **Generation:** Press ENTER for auto-generated secure passwords
|
||||
|
||||
### Why No Special Characters?
|
||||
Keycloak has issues with special characters in certain authentication flows and database connection strings. Restricting to alphanumeric ensures compatibility.
|
||||
|
||||
### Password Strength with Alphanumeric Only
|
||||
- 12 characters: ~62^12 = 3.2 × 10^21 combinations
|
||||
- 16 characters: ~62^16 = 4.7 × 10^28 combinations
|
||||
- 20 characters: ~62^20 = 7.0 × 10^35 combinations
|
||||
|
||||
This is cryptographically secure for all practical purposes.
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
After running the updated script:
|
||||
|
||||
### Docker Ownership
|
||||
```bash
|
||||
# Check docker directory ownership
|
||||
ls -la ~/docker/
|
||||
# All directories should be owned by your user, not root
|
||||
|
||||
# Test docker without sudo
|
||||
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
|
||||
docker ps | grep caddy
|
||||
|
||||
# Check logs
|
||||
docker logs caddy
|
||||
|
||||
# Test HTTPS redirect
|
||||
curl -I http://yourdomain.com
|
||||
# Should redirect to HTTPS
|
||||
```
|
||||
|
||||
### fail2ban
|
||||
```bash
|
||||
# Check fail2ban status
|
||||
sudo fail2ban-client status caddy-auth
|
||||
|
||||
# Test ban
|
||||
# (Make 5 failed login attempts)
|
||||
sudo fail2ban-client status caddy-auth
|
||||
# Should show banned IP
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Guide
|
||||
|
||||
If you have existing services:
|
||||
|
||||
### Existing ActualBudget
|
||||
1. Backup existing data: `cp -r ~/docker/actualbudget ~/docker/actualbudget.backup`
|
||||
2. Run updated script and select "Reconfigure" when prompted
|
||||
3. New .env file will be created
|
||||
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
|
||||
|
||||
### Permission Denied Errors
|
||||
```bash
|
||||
# Fix ownership of all docker directories
|
||||
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
|
||||
docker logs caddy
|
||||
|
||||
# Common issues:
|
||||
# 1. DNS not pointing to server
|
||||
# 2. Ports 80/443 not open
|
||||
# 3. Firewall blocking Let's Encrypt
|
||||
|
||||
# Test DNS:
|
||||
dig auth.yourdomain.com
|
||||
|
||||
# Test port accessibility:
|
||||
sudo ufw status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Change default passwords:** Even with auto-generation, review and update if needed
|
||||
2. **Use production mode for Keycloak:** Never use development mode for internet-facing deployments
|
||||
3. **Enable fail2ban:** Monitor and ban malicious IPs
|
||||
4. **Regular updates:** Keep containers updated (use Watchtower in notify mode)
|
||||
5. **Backup .env files:** Store securely, separate from compose files
|
||||
6. **Use HTTPS everywhere:** Configure Caddy2 for all public services
|
||||
7. **Limit exposed ports:** Only expose necessary ports to the internet
|
||||
8. **Monitor logs:** Regular review of Caddy and fail2ban logs
|
||||
|
||||
---
|
||||
|
||||
## 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`
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
If you encounter issues:
|
||||
|
||||
1. Check logs: `docker logs <container-name>`
|
||||
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)
|
||||
@@ -1,547 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Caddy Setup Helper Script
|
||||
# This script helps manage Caddy configuration, backups, and fail2ban integration
|
||||
# for dockerized Caddy setups - FULLY AUTOMATED with error handling
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo "==============================================="
|
||||
echo " Caddy Configuration & Fail2ban Setup Helper"
|
||||
echo "==============================================="
|
||||
echo ""
|
||||
|
||||
# Function to print colored output
|
||||
print_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Function to ask yes/no questions
|
||||
ask_yn() {
|
||||
local prompt="$1"
|
||||
local default="${2:-n}"
|
||||
local response
|
||||
|
||||
if [ "$default" = "y" ]; then
|
||||
read -p "$prompt [Y/n]: " response
|
||||
response=${response:-y}
|
||||
else
|
||||
read -p "$prompt [y/N]: " response
|
||||
response=${response:-n}
|
||||
fi
|
||||
|
||||
[[ "$response" =~ ^[Yy]$ ]]
|
||||
}
|
||||
|
||||
# Track if we need to show manual instructions
|
||||
SHOW_MANUAL=false
|
||||
ERROR_MESSAGES=()
|
||||
|
||||
# ==============================
|
||||
# 1. CHECK IF CADDY IS INSTALLED
|
||||
# ==============================
|
||||
print_info "Checking for Caddy installation..."
|
||||
|
||||
CADDY_CONTAINER=""
|
||||
CADDYFILE_PATH=""
|
||||
|
||||
# Check if Docker is available
|
||||
if ! command -v docker &> /dev/null; then
|
||||
print_error "Docker is not installed or not in PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Try to find Caddy container
|
||||
if docker ps --format '{{.Names}}' | grep -iq "caddy"; then
|
||||
CADDY_CONTAINER=$(docker ps --format '{{.Names}}' | grep -i "caddy" | head -1)
|
||||
print_success "Found running Caddy container: $CADDY_CONTAINER"
|
||||
else
|
||||
print_warning "No running Caddy container found"
|
||||
if ! ask_yn "Is Caddy installed?" "n"; then
|
||||
print_info "Caddy is not installed. Please install Caddy first."
|
||||
echo ""
|
||||
echo "To install Caddy with Docker, see CADDY-FAIL2BAN-SETUP.md"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================
|
||||
# 2. LOCATE CADDYFILE
|
||||
# ==============================
|
||||
print_info "Locating Caddyfile..."
|
||||
|
||||
# Common Caddyfile locations
|
||||
POSSIBLE_PATHS=(
|
||||
"$HOME/docker/caddy/Caddyfile"
|
||||
"$HOME/docker/caddy/caddyfile"
|
||||
"$HOME/docker/caddy/config/Caddyfile"
|
||||
"/etc/caddy/Caddyfile"
|
||||
)
|
||||
|
||||
for path in "${POSSIBLE_PATHS[@]}"; do
|
||||
if [ -f "$path" ]; then
|
||||
CADDYFILE_PATH="$path"
|
||||
print_success "Found Caddyfile at: $CADDYFILE_PATH"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$CADDYFILE_PATH" ]; then
|
||||
print_warning "Caddyfile not found in common locations"
|
||||
read -p "Enter path to Caddyfile: " CUSTOM_PATH
|
||||
if [ -n "$CUSTOM_PATH" ] && [ -f "$CUSTOM_PATH" ]; then
|
||||
CADDYFILE_PATH="$CUSTOM_PATH"
|
||||
print_success "Using Caddyfile at: $CADDYFILE_PATH"
|
||||
else
|
||||
print_error "Cannot proceed without Caddyfile location"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
CADDY_DIR=$(dirname "$CADDYFILE_PATH")
|
||||
|
||||
# ==============================
|
||||
# 3. BACKUP CADDYFILE (ALWAYS FIRST!)
|
||||
# ==============================
|
||||
print_info "Creating backup of Caddyfile..."
|
||||
|
||||
BACKUP_DIR="$CADDY_DIR/backups"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
BACKUP_FILE="$BACKUP_DIR/Caddyfile.backup.$(date +%Y%m%d_%H%M%S)"
|
||||
cp "$CADDYFILE_PATH" "$BACKUP_FILE"
|
||||
print_success "Backup created: $BACKUP_FILE"
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo " BACKUP RESTORE INSTRUCTIONS"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "To restore this backup:"
|
||||
echo " cp $BACKUP_FILE $CADDYFILE_PATH"
|
||||
if [ -n "$CADDY_CONTAINER" ]; then
|
||||
echo " docker exec -w /etc/caddy $CADDY_CONTAINER caddy reload"
|
||||
echo " docker exec -w /etc/caddy $CADDY_CONTAINER caddy fmt --overwrite"
|
||||
fi
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
|
||||
# ==============================
|
||||
# 4. CHECK FOR FAIL2BAN INSTALLATION
|
||||
# ==============================
|
||||
print_info "Checking for fail2ban installation..."
|
||||
|
||||
FAIL2BAN_INSTALLED=false
|
||||
if command -v fail2ban-client &> /dev/null; then
|
||||
print_success "fail2ban is installed"
|
||||
FAIL2BAN_INSTALLED=true
|
||||
else
|
||||
print_warning "fail2ban is not installed"
|
||||
|
||||
if ask_yn "Would you like to install fail2ban now?" "y"; then
|
||||
print_info "Installing fail2ban..."
|
||||
|
||||
if sudo apt update && sudo apt install -y fail2ban; then
|
||||
print_success "fail2ban installed successfully"
|
||||
FAIL2BAN_INSTALLED=true
|
||||
else
|
||||
print_error "Failed to install fail2ban"
|
||||
ERROR_MESSAGES+=("Failed to install fail2ban - you may need to install it manually")
|
||||
SHOW_MANUAL=true
|
||||
fi
|
||||
else
|
||||
print_warning "Skipping fail2ban installation"
|
||||
SHOW_MANUAL=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================
|
||||
# 5. CREATE LOG DIRECTORY
|
||||
# ==============================
|
||||
if [ "$FAIL2BAN_INSTALLED" = true ]; then
|
||||
print_info "Checking Caddy log directory..."
|
||||
|
||||
LOG_DIR="/var/log/caddy"
|
||||
if [ ! -d "$LOG_DIR" ]; then
|
||||
if ask_yn "Create $LOG_DIR for Caddy logs?" "y"; then
|
||||
if sudo mkdir -p "$LOG_DIR" && sudo chmod 755 "$LOG_DIR"; then
|
||||
print_success "Log directory created: $LOG_DIR"
|
||||
else
|
||||
print_error "Failed to create log directory"
|
||||
ERROR_MESSAGES+=("Failed to create $LOG_DIR - create it manually with: sudo mkdir -p $LOG_DIR && sudo chmod 755 $LOG_DIR")
|
||||
SHOW_MANUAL=true
|
||||
fi
|
||||
fi
|
||||
else
|
||||
print_success "Log directory exists: $LOG_DIR"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================
|
||||
# 6. CHECK CADDY DOCKER COMPOSE FOR LOG VOLUME
|
||||
# ==============================
|
||||
if [ "$FAIL2BAN_INSTALLED" = true ] && [ -n "$CADDY_CONTAINER" ]; then
|
||||
print_info "Checking if Caddy container has log volume mounted..."
|
||||
|
||||
# Check if the container has /var/log/caddy mounted
|
||||
if docker inspect "$CADDY_CONTAINER" 2>/dev/null | grep -q "/var/log/caddy"; then
|
||||
print_success "Caddy container has log volume mounted"
|
||||
else
|
||||
print_warning "Caddy container does not have /var/log/caddy volume mounted"
|
||||
|
||||
# Check if there's a docker-compose.yml
|
||||
COMPOSE_FILE=""
|
||||
for file in "$CADDY_DIR/docker-compose.yml" "$CADDY_DIR/docker-compose.yaml"; do
|
||||
if [ -f "$file" ]; then
|
||||
COMPOSE_FILE="$file"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$COMPOSE_FILE" ]; then
|
||||
if ask_yn "Add /var/log/caddy volume to docker-compose.yml?" "y"; then
|
||||
# Backup docker-compose.yml
|
||||
cp "$COMPOSE_FILE" "$COMPOSE_FILE.backup.$(date +%Y%m%d_%H%M%S)"
|
||||
|
||||
# Check if volumes section exists
|
||||
if grep -q "volumes:" "$COMPOSE_FILE"; then
|
||||
# Add to existing volumes
|
||||
if ! grep -q "/var/log/caddy" "$COMPOSE_FILE"; then
|
||||
# Find the volumes section and add our volume
|
||||
sed -i '/volumes:/a\ - /var/log/caddy:/var/log/caddy' "$COMPOSE_FILE"
|
||||
print_success "Added log volume to docker-compose.yml"
|
||||
print_warning "You'll need to restart the Caddy container for this to take effect"
|
||||
|
||||
if ask_yn "Restart Caddy container now?" "n"; then
|
||||
cd "$CADDY_DIR"
|
||||
if docker compose down && docker compose up -d; then
|
||||
print_success "Caddy container restarted"
|
||||
# Update CADDY_CONTAINER name in case it changed
|
||||
CADDY_CONTAINER=$(docker ps --format '{{.Names}}' | grep -i "caddy" | head -1)
|
||||
else
|
||||
print_error "Failed to restart Caddy container"
|
||||
ERROR_MESSAGES+=("Failed to restart Caddy - restart manually with: cd $CADDY_DIR && docker compose restart")
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
else
|
||||
print_warning "Could not automatically add volume - docker-compose.yml format is unexpected"
|
||||
ERROR_MESSAGES+=("Add this volume manually to your Caddy service: /var/log/caddy:/var/log/caddy")
|
||||
SHOW_MANUAL=true
|
||||
fi
|
||||
fi
|
||||
else
|
||||
print_warning "No docker-compose.yml found - you may need to add the volume manually"
|
||||
ERROR_MESSAGES+=("Add log volume to Caddy container: /var/log/caddy:/var/log/caddy")
|
||||
SHOW_MANUAL=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================
|
||||
# 7. CREATE FAIL2BAN FILTER
|
||||
# ==============================
|
||||
if [ "$FAIL2BAN_INSTALLED" = true ]; then
|
||||
print_info "Checking fail2ban filter configuration..."
|
||||
|
||||
FILTER_FILE="/etc/fail2ban/filter.d/caddy-auth.conf"
|
||||
if [ -f "$FILTER_FILE" ]; then
|
||||
print_success "fail2ban filter already exists: $FILTER_FILE"
|
||||
else
|
||||
if ask_yn "Create fail2ban filter for Caddy?" "y"; then
|
||||
print_info "Creating fail2ban filter..."
|
||||
|
||||
FILTER_CONTENT='[Definition]
|
||||
failregex = ^.*"remote_ip":"<HOST>".*"status":(?:401|403|429).*$
|
||||
^.*"remote_addr":"<HOST>.*"status":(?:401|403|429).*$
|
||||
ignoreregex = ^.*"remote_ip":"(?:127\.0\.0\.1|::1)".*$
|
||||
datepattern = "ts":%%s'
|
||||
|
||||
if echo "$FILTER_CONTENT" | sudo tee "$FILTER_FILE" > /dev/null; then
|
||||
print_success "Created fail2ban filter: $FILTER_FILE"
|
||||
else
|
||||
print_error "Failed to create fail2ban filter"
|
||||
ERROR_MESSAGES+=("Failed to create $FILTER_FILE - create it manually (see CADDY-FAIL2BAN-SETUP.md)")
|
||||
SHOW_MANUAL=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================
|
||||
# 8. CREATE FAIL2BAN JAIL
|
||||
# ==============================
|
||||
if [ "$FAIL2BAN_INSTALLED" = true ]; then
|
||||
print_info "Checking fail2ban jail configuration..."
|
||||
|
||||
JAIL_FILE="/etc/fail2ban/jail.d/caddy.conf"
|
||||
if [ -f "$JAIL_FILE" ]; then
|
||||
print_success "fail2ban jail already exists: $JAIL_FILE"
|
||||
else
|
||||
if ask_yn "Create fail2ban jail for Caddy?" "y"; then
|
||||
print_info "Creating fail2ban jail..."
|
||||
|
||||
# Ask for custom settings
|
||||
echo ""
|
||||
print_info "Fail2ban jail settings (press Enter for defaults):"
|
||||
read -p " Max retries before ban [5]: " MAXRETRY
|
||||
MAXRETRY=${MAXRETRY:-5}
|
||||
|
||||
read -p " Find time window in seconds [600]: " FINDTIME
|
||||
FINDTIME=${FINDTIME:-600}
|
||||
|
||||
read -p " Ban duration in seconds [3600]: " BANTIME
|
||||
BANTIME=${BANTIME:-3600}
|
||||
|
||||
JAIL_CONTENT="[caddy-auth]
|
||||
enabled = true
|
||||
port = http,https
|
||||
filter = caddy-auth
|
||||
logpath = /var/log/caddy/access.log
|
||||
/var/log/caddy/*-access.log
|
||||
maxretry = $MAXRETRY
|
||||
findtime = $FINDTIME
|
||||
bantime = $BANTIME
|
||||
action = iptables-multiport[name=CaddyAuth, port=\"http,https\", protocol=tcp]
|
||||
backend = auto"
|
||||
|
||||
if echo "$JAIL_CONTENT" | sudo tee "$JAIL_FILE" > /dev/null; then
|
||||
print_success "Created fail2ban jail: $JAIL_FILE"
|
||||
else
|
||||
print_error "Failed to create fail2ban jail"
|
||||
ERROR_MESSAGES+=("Failed to create $JAIL_FILE - create it manually (see CADDY-FAIL2BAN-SETUP.md)")
|
||||
SHOW_MANUAL=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================
|
||||
# 9. TEST FAIL2BAN CONFIGURATION
|
||||
# ==============================
|
||||
if [ "$FAIL2BAN_INSTALLED" = true ]; then
|
||||
print_info "Testing fail2ban configuration..."
|
||||
|
||||
if sudo fail2ban-client -t &> /dev/null; then
|
||||
print_success "fail2ban configuration is valid"
|
||||
else
|
||||
print_error "fail2ban configuration has errors"
|
||||
ERROR_MESSAGES+=("fail2ban configuration is invalid - check with: sudo fail2ban-client -t")
|
||||
SHOW_MANUAL=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================
|
||||
# 10. RESTART FAIL2BAN
|
||||
# ==============================
|
||||
if [ "$FAIL2BAN_INSTALLED" = true ]; then
|
||||
if ask_yn "Restart fail2ban to apply changes?" "y"; then
|
||||
print_info "Restarting fail2ban..."
|
||||
|
||||
if sudo systemctl restart fail2ban; then
|
||||
print_success "fail2ban restarted successfully"
|
||||
|
||||
# Wait a moment for fail2ban to start
|
||||
sleep 2
|
||||
|
||||
# Check if caddy-auth jail is running
|
||||
if sudo fail2ban-client status caddy-auth &> /dev/null; then
|
||||
print_success "caddy-auth jail is active"
|
||||
echo ""
|
||||
print_info "Jail status:"
|
||||
sudo fail2ban-client status caddy-auth
|
||||
else
|
||||
print_warning "caddy-auth jail is not active"
|
||||
ERROR_MESSAGES+=("caddy-auth jail failed to start - check with: sudo fail2ban-client status")
|
||||
SHOW_MANUAL=true
|
||||
fi
|
||||
else
|
||||
print_error "Failed to restart fail2ban"
|
||||
ERROR_MESSAGES+=("Failed to restart fail2ban - check logs with: sudo journalctl -u fail2ban -n 50")
|
||||
SHOW_MANUAL=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================
|
||||
# 11. ADD SERVICE CONFIGURATIONS TO CADDYFILE
|
||||
# ==============================
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo " ADDING NEW SERVICES TO CADDYFILE"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
|
||||
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
|
||||
read -p "Enter domain for ActualBudget (e.g., budget.yourdomain.com): " AB_DOMAIN
|
||||
|
||||
if [ -n "$AB_DOMAIN" ]; then
|
||||
AB_CONFIG="
|
||||
# ActualBudget - Personal Finance
|
||||
$AB_DOMAIN {
|
||||
log {
|
||||
output file /var/log/caddy/actualbudget-access.log
|
||||
format json
|
||||
level INFO
|
||||
}
|
||||
|
||||
reverse_proxy localhost:5006
|
||||
|
||||
# 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 "$AB_CONFIG" >> "$CADDYFILE_PATH"; then
|
||||
print_success "Added ActualBudget configuration to Caddyfile"
|
||||
else
|
||||
print_error "Failed to add ActualBudget configuration"
|
||||
ERROR_MESSAGES+=("Add ActualBudget manually - see CADDY-FAIL2BAN-SETUP.md")
|
||||
fi
|
||||
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
|
||||
# ==============================
|
||||
if [ -n "$CADDY_CONTAINER" ]; then
|
||||
echo ""
|
||||
if ask_yn "Validate and reload Caddy configuration?" "y"; then
|
||||
print_info "Validating Caddyfile..."
|
||||
|
||||
# Format first
|
||||
if docker exec -w /etc/caddy "$CADDY_CONTAINER" caddy fmt --overwrite 2>/dev/null; then
|
||||
print_success "Caddyfile formatted"
|
||||
fi
|
||||
|
||||
# Validate
|
||||
if docker exec "$CADDY_CONTAINER" caddy validate --config /etc/caddy/Caddyfile 2>/dev/null; then
|
||||
print_success "Caddyfile is valid"
|
||||
|
||||
# Reload
|
||||
print_info "Reloading Caddy configuration..."
|
||||
if docker exec -w /etc/caddy "$CADDY_CONTAINER" caddy reload 2>/dev/null; then
|
||||
print_success "Caddy configuration reloaded successfully"
|
||||
else
|
||||
print_error "Failed to reload Caddy configuration"
|
||||
ERROR_MESSAGES+=("Failed to reload Caddy - check logs with: docker logs $CADDY_CONTAINER")
|
||||
SHOW_MANUAL=true
|
||||
fi
|
||||
else
|
||||
print_error "Caddyfile validation failed"
|
||||
ERROR_MESSAGES+=("Caddyfile has syntax errors - check with: docker exec $CADDY_CONTAINER caddy validate --config /etc/caddy/Caddyfile")
|
||||
SHOW_MANUAL=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================
|
||||
# 13. FINAL SUMMARY
|
||||
# ==============================
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo " SETUP COMPLETE"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
|
||||
if [ "$SHOW_MANUAL" = true ]; then
|
||||
print_warning "Some steps could not be completed automatically"
|
||||
echo ""
|
||||
echo "Issues encountered:"
|
||||
for msg in "${ERROR_MESSAGES[@]}"; do
|
||||
echo " - $msg"
|
||||
done
|
||||
echo ""
|
||||
print_info "See CADDY-FAIL2BAN-SETUP.md for manual setup instructions"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
print_success "Caddyfile backed up to: $BACKUP_FILE"
|
||||
|
||||
if [ "$FAIL2BAN_INSTALLED" = true ]; then
|
||||
print_success "fail2ban is installed and configured"
|
||||
echo ""
|
||||
echo "Useful commands:"
|
||||
echo " Check jail status: sudo fail2ban-client status caddy-auth"
|
||||
echo " View banned IPs: sudo fail2ban-client get caddy-auth banip"
|
||||
echo " Unban IP: sudo fail2ban-client set caddy-auth unbanip 1.2.3.4"
|
||||
echo " Test filter: sudo fail2ban-regex /var/log/caddy/access.log /etc/fail2ban/filter.d/caddy-auth.conf"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Caddyfile location: $CADDYFILE_PATH"
|
||||
echo "Backup location: $BACKUP_FILE"
|
||||
if [ -n "$CADDY_CONTAINER" ]; then
|
||||
echo "Caddy container: $CADDY_CONTAINER"
|
||||
echo "Reload Caddy: docker exec -w /etc/caddy $CADDY_CONTAINER caddy reload"
|
||||
echo "View Caddy logs: docker logs $CADDY_CONTAINER --tail 50"
|
||||
fi
|
||||
echo ""
|
||||
print_success "All done!"
|
||||
echo ""
|
||||
@@ -1,60 +0,0 @@
|
||||
# ActualBudget - Open-source personal finance management
|
||||
# https://actualbudget.org/
|
||||
#
|
||||
# DEPLOYMENT INSTRUCTIONS:
|
||||
# 1. Create directory: mkdir -p ~/docker/actualbudget
|
||||
# 2. Copy this file: cp docker-compose-actualbudget.yml ~/docker/actualbudget/docker-compose.yml
|
||||
# 3. Create data directory: mkdir -p ~/docker/actualbudget/data
|
||||
# 4. Start the service: cd ~/docker/actualbudget && docker compose up -d
|
||||
# 5. Access at: http://localhost:5006
|
||||
#
|
||||
# REVERSE PROXY SETUP (with Caddy):
|
||||
# Add to your Caddyfile:
|
||||
# budget.yourdomain.com {
|
||||
# reverse_proxy localhost:5006
|
||||
# }
|
||||
#
|
||||
# BANK ACCOUNT SYNC:
|
||||
# ActualBudget supports SimpleFIN (https://simplefin.org/) for bank synchronization.
|
||||
# You'll need a SimpleFIN account to sync with your bank accounts.
|
||||
# Setup instructions: https://actualbudget.org/docs/advanced/bank-sync
|
||||
|
||||
name: actualbudget
|
||||
|
||||
services:
|
||||
actualbudget:
|
||||
image: actualbudget/actual-server:latest
|
||||
container_name: actualbudget
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5006:5006"
|
||||
volumes:
|
||||
- ./data:/data
|
||||
environment:
|
||||
# Set your timezone
|
||||
- TZ=UTC
|
||||
# Uncomment and set these for HTTPS/production deployment:
|
||||
# - ACTUAL_UPLOAD_FILE_SYNC_SIZE_LIMIT_MB=20
|
||||
# - ACTUAL_UPLOAD_SYNC_ENCRYPTED_FILE_SYNC_SIZE_LIMIT_MB=50
|
||||
# - ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB=20
|
||||
labels:
|
||||
# Fail2ban support - logs HTTP requests
|
||||
- "io.podman.annotations.label/fail2ban.enable=true"
|
||||
- "io.podman.annotations.label/fail2ban.filter=caddy-auth"
|
||||
|
||||
# NOTES:
|
||||
# - Default port: 5006
|
||||
# - Data stored in: ./data
|
||||
# - First run: Create an account at http://localhost:5006
|
||||
# - Bank sync requires SimpleFIN: https://simplefin.org/
|
||||
# - Official docs: https://actualbudget.org/docs/
|
||||
#
|
||||
# BACKUP YOUR DATA:
|
||||
# Regular backups are important! ActualBudget stores data in SQLite.
|
||||
# docker compose down
|
||||
# cp -r data data-backup-$(date +%Y%m%d)
|
||||
# docker compose up -d
|
||||
#
|
||||
# UPDATES:
|
||||
# docker compose pull
|
||||
# docker compose up -d
|
||||
@@ -1,137 +0,0 @@
|
||||
# Keycloak - Open-source Identity and Access Management
|
||||
# https://www.keycloak.org/
|
||||
#
|
||||
# DEPLOYMENT INSTRUCTIONS:
|
||||
# 1. Create directory: mkdir -p ~/docker/keycloak
|
||||
# 2. Copy this file: cp docker-compose-keycloak.yml ~/docker/keycloak/docker-compose.yml
|
||||
# 3. Create .env file with credentials (see .env template below)
|
||||
# 4. Start the service: cd ~/docker/keycloak && docker compose up -d
|
||||
# 5. Access at: http://localhost:8180/admin (admin console)
|
||||
#
|
||||
# .ENV FILE TEMPLATE:
|
||||
# Create a file named .env in ~/docker/keycloak/ with:
|
||||
# KEYCLOAK_ADMIN=admin
|
||||
# KEYCLOAK_ADMIN_PASSWORD=<your-secure-password>
|
||||
# POSTGRES_DB=keycloak
|
||||
# POSTGRES_USER=keycloak
|
||||
# POSTGRES_PASSWORD=<your-db-password>
|
||||
# KC_DB=postgres
|
||||
# KC_DB_URL=jdbc:postgresql://postgres:5432/keycloak
|
||||
# KC_DB_USERNAME=keycloak
|
||||
# KC_DB_PASSWORD=<your-db-password>
|
||||
# KC_PROXY_HEADERS=xforwarded
|
||||
# KC_HTTP_ENABLED=true
|
||||
# KC_HOSTNAME_STRICT=false
|
||||
# KC_LOG_LEVEL=INFO
|
||||
# KC_HEALTH_ENABLED=true
|
||||
# KC_METRICS_ENABLED=true
|
||||
# # KC_HOSTNAME=auth.yourdomain.com # Uncomment for production
|
||||
#
|
||||
# REVERSE PROXY SETUP (with Caddy):
|
||||
# Add to your Caddyfile:
|
||||
# auth.yourdomain.com {
|
||||
# reverse_proxy localhost:8180
|
||||
# }
|
||||
#
|
||||
# PRODUCTION DEPLOYMENT:
|
||||
# For production, you should:
|
||||
# 1. Use a PostgreSQL database (see postgres service below)
|
||||
# 2. Enable HTTPS via reverse proxy
|
||||
# 3. Set KC_HOSTNAME to your domain
|
||||
# 4. Use strong admin password
|
||||
# 5. Configure proper realm and clients
|
||||
|
||||
name: keycloak
|
||||
|
||||
services:
|
||||
# PostgreSQL database for Keycloak (recommended for production)
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: keycloak-db
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- ./postgres-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U keycloak"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
keycloak:
|
||||
image: quay.io/keycloak/keycloak:latest
|
||||
container_name: keycloak
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- start-dev # Use 'start' for production mode
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "8180:8080" # HTTP port (use reverse proxy for HTTPS)
|
||||
# - "8787:8787" # Debug port (uncomment if needed)
|
||||
volumes:
|
||||
# Optional: Custom themes
|
||||
# - ./themes:/opt/keycloak/themes
|
||||
# Optional: Custom providers/extensions
|
||||
# - ./providers:/opt/keycloak/providers
|
||||
- ./data:/opt/keycloak/data
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
labels:
|
||||
# Fail2ban support
|
||||
- "io.podman.annotations.label/fail2ban.enable=true"
|
||||
- "io.podman.annotations.label/fail2ban.filter=caddy-auth"
|
||||
|
||||
# NOTES:
|
||||
# - Admin console: http://localhost:8180/admin
|
||||
# - Credentials: Stored in .env file
|
||||
# - Database: PostgreSQL (persistent data in ./postgres-data)
|
||||
# - Proxy: Uses KC_PROXY_HEADERS=xforwarded (v2 config, no deprecated warnings)
|
||||
#
|
||||
# FIRST-TIME SETUP:
|
||||
# 1. Create .env file with secure passwords (see template above)
|
||||
# 2. Start containers: docker compose up -d
|
||||
# 3. Login to admin console at http://localhost:8180/admin
|
||||
# 4. Create a realm (e.g., "homelab" or "myrealm")
|
||||
# 5. Create clients for your applications (OAuth2/OIDC)
|
||||
# 6. Add users or configure identity providers (LDAP, SAML, Social)
|
||||
#
|
||||
# COMMON USE CASES:
|
||||
# - Single Sign-On (SSO) for multiple applications
|
||||
# - OAuth2/OIDC provider for custom apps (ActualBudget, etc.)
|
||||
# - SAML 2.0 identity provider
|
||||
# - User federation with LDAP/Active Directory
|
||||
# - Multi-factor authentication (MFA/2FA)
|
||||
# - Social login (Google, GitHub, Facebook, etc.)
|
||||
#
|
||||
# PRODUCTION CHECKLIST:
|
||||
# [ ] Create .env file with secure passwords (12+ chars, alphanumeric only)
|
||||
# [ ] Set KC_HOSTNAME in .env to your domain (e.g., auth.yourdomain.com)
|
||||
# [ ] Use 'start' instead of 'start-dev' command in docker-compose.yml
|
||||
# [ ] Configure HTTPS via reverse proxy (Caddy/nginx)
|
||||
# [ ] Set KC_HOSTNAME_STRICT=true in .env for production
|
||||
# [ ] Configure DNS A record for your hostname
|
||||
# [ ] Set proper file permissions: chmod 600 .env
|
||||
# [ ] Configure backup strategy for PostgreSQL
|
||||
# [ ] Set up monitoring (metrics enabled via KC_METRICS_ENABLED=true)
|
||||
#
|
||||
# BACKUP:
|
||||
# docker compose down
|
||||
# tar -czf keycloak-backup-$(date +%Y%m%d).tar.gz postgres-data data
|
||||
# docker compose up -d
|
||||
#
|
||||
# RESTORE:
|
||||
# docker compose down
|
||||
# tar -xzf keycloak-backup-YYYYMMDD.tar.gz
|
||||
# docker compose up -d
|
||||
#
|
||||
# UPDATES:
|
||||
# docker compose pull
|
||||
# docker compose up -d
|
||||
#
|
||||
# DOCUMENTATION:
|
||||
# - Official docs: https://www.keycloak.org/documentation
|
||||
# - Getting started: https://www.keycloak.org/getting-started/getting-started-docker
|
||||
# - Server admin: https://www.keycloak.org/docs/latest/server_admin/
|
||||
@@ -1,49 +0,0 @@
|
||||
# Fail2ban filter for Caddy web server
|
||||
#
|
||||
# INSTALLATION:
|
||||
# 1. Copy this file to: /etc/fail2ban/filter.d/caddy-auth.conf
|
||||
# sudo cp fail2ban-caddy-filter.conf /etc/fail2ban/filter.d/caddy-auth.conf
|
||||
#
|
||||
# 2. Create jail configuration at: /etc/fail2ban/jail.d/caddy.conf
|
||||
# (See fail2ban-caddy-jail.conf in this directory)
|
||||
#
|
||||
# 3. Ensure Caddy is logging in JSON format to /var/log/caddy/access.log
|
||||
# (See caddy-setup-helper.sh for configuration examples)
|
||||
#
|
||||
# 4. Restart fail2ban:
|
||||
# sudo systemctl restart fail2ban
|
||||
#
|
||||
# 5. Check status:
|
||||
# sudo fail2ban-client status caddy-auth
|
||||
|
||||
[INCLUDES]
|
||||
before = common.conf
|
||||
|
||||
[Definition]
|
||||
|
||||
# Match failed authentication attempts and forbidden access
|
||||
# Caddy JSON log format: {"remote_ip":"1.2.3.4","status":401,...}
|
||||
failregex = ^.*"remote_ip":"<HOST>".*"status":(?:401|403|429).*$
|
||||
^.*"remote_addr":"<HOST>.*"status":(?:401|403|429).*$
|
||||
^.*"client_ip":"<HOST>".*"status":(?:401|403|429).*$
|
||||
|
||||
# Ignore localhost and common false positives
|
||||
ignoreregex = ^.*"remote_ip":"(?:127\.0\.0\.1|::1)".*$
|
||||
^.*"remote_addr":"(?:127\.0\.0\.1|::1)".*$
|
||||
|
||||
# Optional: Date/time pattern for log analysis
|
||||
# Most Caddy JSON logs include "ts" field with Unix timestamp
|
||||
datepattern = "ts":%%s
|
||||
|
||||
[Init]
|
||||
journalmatch = _SYSTEMD_UNIT=caddy.service
|
||||
|
||||
# NOTES:
|
||||
# - This filter looks for HTTP status codes:
|
||||
# 401 = Unauthorized (failed authentication)
|
||||
# 403 = Forbidden (access denied)
|
||||
# 429 = Too Many Requests (rate limiting)
|
||||
#
|
||||
# - Adjust the status codes based on your needs
|
||||
# - For more aggressive blocking, add: 404|500
|
||||
# - Test the filter: fail2ban-regex /var/log/caddy/access.log /etc/fail2ban/filter.d/caddy-auth.conf
|
||||
@@ -1,82 +0,0 @@
|
||||
# Fail2ban jail configuration for Caddy web server
|
||||
#
|
||||
# INSTALLATION:
|
||||
# 1. Copy this file to: /etc/fail2ban/jail.d/caddy.conf
|
||||
# sudo cp fail2ban-caddy-jail.conf /etc/fail2ban/jail.d/caddy.conf
|
||||
#
|
||||
# 2. Ensure the filter is installed:
|
||||
# sudo cp fail2ban-caddy-filter.conf /etc/fail2ban/filter.d/caddy-auth.conf
|
||||
#
|
||||
# 3. Create log directory if it doesn't exist:
|
||||
# sudo mkdir -p /var/log/caddy
|
||||
# sudo chown caddy:caddy /var/log/caddy # Or appropriate user
|
||||
#
|
||||
# 4. Restart fail2ban:
|
||||
# sudo systemctl restart fail2ban
|
||||
#
|
||||
# 5. Check status:
|
||||
# sudo fail2ban-client status caddy-auth
|
||||
|
||||
[caddy-auth]
|
||||
# Enable this jail
|
||||
enabled = true
|
||||
|
||||
# Ports to protect (HTTP and HTTPS)
|
||||
port = http,https
|
||||
|
||||
# Filter to use (must match filename in /etc/fail2ban/filter.d/)
|
||||
filter = caddy-auth
|
||||
|
||||
# Log file to monitor
|
||||
# Adjust this path if your Caddy logs are elsewhere
|
||||
logpath = /var/log/caddy/access.log
|
||||
/var/log/caddy/*-access.log
|
||||
|
||||
# For Docker Caddy, you might need to use Docker logs:
|
||||
# logpath = /var/lib/docker/containers/*-caddy*/*.log
|
||||
|
||||
# Maximum retry before ban
|
||||
# 5 attempts within findtime period will trigger a ban
|
||||
maxretry = 5
|
||||
|
||||
# Time window (seconds) to count failures
|
||||
# 600 = 10 minutes
|
||||
findtime = 600
|
||||
|
||||
# Ban duration (seconds)
|
||||
# 3600 = 1 hour
|
||||
# 86400 = 24 hours
|
||||
bantime = 3600
|
||||
|
||||
# Action to take when banning
|
||||
# iptables-multiport: Block on multiple ports
|
||||
action = iptables-multiport[name=CaddyAuth, port="http,https", protocol=tcp]
|
||||
# Optional: Send email notification
|
||||
# sendmail-whois[name=CaddyAuth, dest=admin@yourdomain.com]
|
||||
|
||||
# Backend to use for monitoring log file
|
||||
# auto = automatically detect (systemd journal or file polling)
|
||||
backend = auto
|
||||
|
||||
# OPTIONAL SETTINGS:
|
||||
|
||||
# Increase ban time on repeat offenders
|
||||
# First ban: 1 hour, second: 24 hours, third: 1 week
|
||||
# bantime.increment = true
|
||||
# bantime.factor = 24
|
||||
# bantime.maxtime = 604800 # 1 week max
|
||||
|
||||
# Find all jails using this ban
|
||||
# This enables ban synchronization across jails
|
||||
# banaction_allports = iptables-allports
|
||||
|
||||
# NOTES:
|
||||
# - Adjust maxretry, findtime, and bantime based on your security needs
|
||||
# - More aggressive: maxretry=3, findtime=300, bantime=86400
|
||||
# - More lenient: maxretry=10, findtime=1200, bantime=1800
|
||||
#
|
||||
# TESTING:
|
||||
# - Check if jail is running: sudo fail2ban-client status caddy-auth
|
||||
# - View banned IPs: sudo fail2ban-client get caddy-auth banip
|
||||
# - Unban an IP: sudo fail2ban-client set caddy-auth unbanip 1.2.3.4
|
||||
# - Test filter: fail2ban-regex /var/log/caddy/access.log /etc/fail2ban/filter.d/caddy-auth.conf
|
||||
@@ -1,184 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Fix Keycloak proxy configuration
|
||||
# This updates Keycloak to use v2 proxy headers (KC_PROXY_HEADERS)
|
||||
# instead of deprecated v1 (KC_PROXY)
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
KC_DIR="$HOME/docker/keycloak"
|
||||
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Keycloak Proxy Configuration Fix"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo "This script will:"
|
||||
echo " 1. Backup your current .env file"
|
||||
echo " 2. Replace deprecated KC_PROXY with KC_PROXY_HEADERS"
|
||||
echo " 3. Ensure docker-compose.yml uses env_file"
|
||||
echo " 4. Restart Keycloak with new configuration"
|
||||
echo ""
|
||||
|
||||
if [ ! -d "$KC_DIR" ]; then
|
||||
echo "❌ Error: Keycloak directory not found at $KC_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$KC_DIR"
|
||||
|
||||
# Backup existing configuration
|
||||
BACKUP_DIR="$KC_DIR/backups"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
|
||||
# Check if .env exists
|
||||
if [ -f ".env" ]; then
|
||||
echo "✓ Found existing .env file"
|
||||
cp .env "$BACKUP_DIR/.env.backup.$TIMESTAMP"
|
||||
echo "✓ Backed up .env to $BACKUP_DIR/.env.backup.$TIMESTAMP"
|
||||
|
||||
# Check if it has the old KC_PROXY setting
|
||||
if grep -q "KC_PROXY=" .env 2>/dev/null; then
|
||||
echo ""
|
||||
echo "Updating .env file..."
|
||||
|
||||
# Replace KC_PROXY with KC_PROXY_HEADERS
|
||||
sed -i 's/^KC_PROXY=.*/KC_PROXY_HEADERS=xforwarded/' .env
|
||||
|
||||
# Add KC_PROXY_HEADERS if it doesn't exist and KC_PROXY didn't either
|
||||
if ! grep -q "KC_PROXY_HEADERS=" .env 2>/dev/null; then
|
||||
echo "" >> .env
|
||||
echo "# Proxy settings (v2) - Trust X-Forwarded-* headers from Caddy2" >> .env
|
||||
echo "KC_PROXY_HEADERS=xforwarded" >> .env
|
||||
fi
|
||||
|
||||
echo "✓ Updated KC_PROXY to KC_PROXY_HEADERS=xforwarded"
|
||||
elif grep -q "KC_PROXY_HEADERS=" .env 2>/dev/null; then
|
||||
echo "✓ Already using KC_PROXY_HEADERS - no changes needed"
|
||||
else
|
||||
echo ""
|
||||
echo "Adding KC_PROXY_HEADERS to .env..."
|
||||
echo "" >> .env
|
||||
echo "# Proxy settings (v2) - Trust X-Forwarded-* headers from Caddy2" >> .env
|
||||
echo "KC_PROXY_HEADERS=xforwarded" >> .env
|
||||
echo "✓ Added KC_PROXY_HEADERS=xforwarded"
|
||||
fi
|
||||
else
|
||||
echo "⚠ No .env file found"
|
||||
echo ""
|
||||
echo "Please create a .env file with your Keycloak credentials."
|
||||
echo "See SECURITY-IMPROVEMENTS.md for the template."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check docker-compose.yml
|
||||
if [ -f "docker-compose.yml" ]; then
|
||||
cp docker-compose.yml "$BACKUP_DIR/docker-compose.yml.backup.$TIMESTAMP"
|
||||
echo "✓ Backed up docker-compose.yml to $BACKUP_DIR/docker-compose.yml.backup.$TIMESTAMP"
|
||||
|
||||
# Check if docker-compose.yml has hardcoded KC_PROXY
|
||||
if grep -q "KC_PROXY=" docker-compose.yml 2>/dev/null; then
|
||||
echo ""
|
||||
echo "⚠ Found KC_PROXY in docker-compose.yml"
|
||||
echo " Removing it (should be in .env file instead)..."
|
||||
|
||||
# Remove the KC_PROXY line from docker-compose.yml
|
||||
sed -i '/KC_PROXY=/d' docker-compose.yml
|
||||
echo "✓ Removed KC_PROXY from docker-compose.yml"
|
||||
fi
|
||||
|
||||
# Ensure it uses env_file
|
||||
if ! grep -q "env_file:" docker-compose.yml 2>/dev/null; then
|
||||
echo "⚠ docker-compose.yml doesn't use env_file"
|
||||
echo " You may need to update it manually to use 'env_file: - .env'"
|
||||
else
|
||||
echo "✓ docker-compose.yml uses env_file"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Show current configuration
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Current Configuration:"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
|
||||
if [ -f ".env" ]; then
|
||||
echo "Proxy Settings:"
|
||||
grep "KC_PROXY" .env | grep -v "^#" || echo " (none found)"
|
||||
echo ""
|
||||
|
||||
if grep -q "KC_HOSTNAME=" .env | grep -v "^#" 2>/dev/null; then
|
||||
echo "Hostname:"
|
||||
grep "KC_HOSTNAME=" .env | grep -v "^#"
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ask to restart
|
||||
echo ""
|
||||
read -p "Restart Keycloak with new configuration? (y/n): " RESTART
|
||||
|
||||
if [ "$RESTART" = "y" ] || [ "$RESTART" = "Y" ]; then
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Restarting Keycloak..."
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
|
||||
docker compose down
|
||||
echo "✓ Stopped Keycloak"
|
||||
|
||||
echo ""
|
||||
echo "Starting Keycloak (this may take a minute)..."
|
||||
docker compose up -d
|
||||
|
||||
# Wait for Keycloak to be ready
|
||||
echo ""
|
||||
echo "Waiting for Keycloak to be ready..."
|
||||
KC_READY=false
|
||||
for i in {1..60}; do
|
||||
if docker exec keycloak curl -sf http://localhost:8080/health/ready > /dev/null 2>&1; then
|
||||
KC_READY=true
|
||||
echo ""
|
||||
echo "✓ Keycloak is ready"
|
||||
break
|
||||
fi
|
||||
echo -n "."
|
||||
sleep 2
|
||||
done
|
||||
echo ""
|
||||
|
||||
if [ "$KC_READY" = true ]; then
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "✅ Keycloak successfully updated!"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo "Changes applied:"
|
||||
echo " • Deprecated KC_PROXY removed"
|
||||
echo " • New KC_PROXY_HEADERS=xforwarded configured"
|
||||
echo " • Configuration stored in .env file"
|
||||
echo ""
|
||||
echo "The 'Hostname v1 options [proxy]' warnings should be gone."
|
||||
echo ""
|
||||
echo "Check the logs:"
|
||||
echo " docker compose logs -f keycloak"
|
||||
echo ""
|
||||
else
|
||||
echo ""
|
||||
echo "⚠ Keycloak may still be starting. Check logs:"
|
||||
echo " docker compose logs -f keycloak"
|
||||
fi
|
||||
else
|
||||
echo ""
|
||||
echo "Skipping restart. To apply changes later, run:"
|
||||
echo " cd $KC_DIR && docker compose restart"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Backup location: $BACKUP_DIR/"
|
||||
echo " - .env.backup.$TIMESTAMP"
|
||||
echo " - docker-compose.yml.backup.$TIMESTAMP"
|
||||
echo ""
|
||||
-209
@@ -1,209 +0,0 @@
|
||||
#!/bin/bash
|
||||
# setup.sh — modular post-install dispatcher.
|
||||
#
|
||||
# One source of truth, multiple ways to run it:
|
||||
# sudo ./setup.sh guided install: required packages, then a
|
||||
# category menu you loop through
|
||||
# sudo ./setup.sh <service> ... install one or more services directly
|
||||
# ./setup.sh --list list available services (grouped)
|
||||
# ./setup.sh --version print version
|
||||
#
|
||||
# Flags:
|
||||
# --dry-run preview actions without making changes
|
||||
# --unattended use defaults, no prompts (pair with explicit service names)
|
||||
#
|
||||
# Every service lives in services/<name>.sh, registers itself with
|
||||
# register_service, and defines install_<name>. Adding a service = adding one
|
||||
# file; it appears in the menu automatically. Nothing is generated.
|
||||
set -uo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Category display order (groups not listed here are appended alphabetically).
|
||||
CATEGORY_ORDER=(base homelab utilities media cameras gaming extras backup)
|
||||
# Service ordering hint within a category (lower = earlier). Default 50.
|
||||
declare -A SERVICE_PRIORITY=( [caddy]=1 [crowdsec]=2 [authelia]=3 )
|
||||
|
||||
# ── Parse flags / collect service names ──────────────────────────────────────
|
||||
DRY_RUN=false; UNATTENDED=false; DO_LIST=false
|
||||
REQUESTED=()
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry-run) DRY_RUN=true ;;
|
||||
--unattended) UNATTENDED=true ;;
|
||||
--list|-l) DO_LIST=true ;;
|
||||
--version|-V) cat "$HERE/VERSION" 2>/dev/null || echo "unknown"; exit 0 ;;
|
||||
-h|--help) sed -n '2,18p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
-*) echo "Unknown flag: $arg" >&2; exit 1 ;;
|
||||
*) REQUESTED+=("$arg") ;;
|
||||
esac
|
||||
done
|
||||
export DRY_RUN UNATTENDED
|
||||
|
||||
# ── Load helpers + all service modules (they self-register) ──────────────────
|
||||
# shellcheck source=lib/common.sh
|
||||
source "$HERE/lib/common.sh"
|
||||
shopt -s nullglob
|
||||
for _mod in "$HERE"/services/*.sh; do source "$_mod"; done
|
||||
shopt -u nullglob
|
||||
|
||||
# ── Helpers over the registry ────────────────────────────────────────────────
|
||||
# Groups present, in CATEGORY_ORDER first, then any extras alphabetically.
|
||||
groups_present() {
|
||||
local g present=() seen=" "
|
||||
for name in "${SERVICE_ORDER[@]}"; do
|
||||
g="${SERVICE_GROUP[$name]}"
|
||||
case "$seen" in *" $g "*) : ;; *) present+=("$g"); seen="$seen$g " ;; esac
|
||||
done
|
||||
local out=()
|
||||
for g in "${CATEGORY_ORDER[@]}"; do
|
||||
printf '%s\n' "${present[@]}" | grep -qx "$g" && out+=("$g")
|
||||
done
|
||||
for g in "${present[@]}"; do
|
||||
printf '%s\n' "${CATEGORY_ORDER[@]}" | grep -qx "$g" || out+=("$g")
|
||||
done
|
||||
printf '%s\n' "${out[@]}"
|
||||
}
|
||||
|
||||
# Services in a group, ordered by SERVICE_PRIORITY then name.
|
||||
services_in_group() {
|
||||
local group="$1" name
|
||||
for name in "${SERVICE_ORDER[@]}"; do
|
||||
[ "${SERVICE_GROUP[$name]}" = "$group" ] && echo "${SERVICE_PRIORITY[$name]:-50} $name"
|
||||
done | sort -n -k1 | awk '{print $2}'
|
||||
}
|
||||
|
||||
# Best-effort "is it already installed?" for the [installed] marker.
|
||||
is_installed() {
|
||||
case "$1" in
|
||||
base) command -v ncdu >/dev/null 2>&1 ;;
|
||||
glow) command -v glow >/dev/null 2>&1 ;;
|
||||
crowdsec) command -v cscli >/dev/null 2>&1 ;;
|
||||
silent-send) [ -d "$ACTUAL_HOME/silent-send/.git" ] ;;
|
||||
linux-to-sync) [ -d "$ACTUAL_HOME/linux-to-sync/.git" ] ;;
|
||||
*) [ -e "$DOCKER_DIR/$1" ] ;;
|
||||
esac
|
||||
}
|
||||
|
||||
run_service() {
|
||||
local name="$1"
|
||||
if [ -z "${SERVICE_GROUP[$name]:-}" ]; then log_error "Unknown service: $name (try --list)"; return 1; fi
|
||||
declare -F "install_${name}" >/dev/null || { log_error "Service '$name' has no install_${name}"; return 1; }
|
||||
log_info "=== ${name} (${SERVICE_DESC[$name]}) ==="
|
||||
"install_${name}"
|
||||
}
|
||||
|
||||
list_services() {
|
||||
local g name
|
||||
while IFS= read -r g; do
|
||||
echo ""; echo "── ${g^^} ──"
|
||||
while IFS= read -r name; do
|
||||
printf " %-16s %s\n" "$name" "${SERVICE_DESC[$name]}"
|
||||
done < <(services_in_group "$g")
|
||||
done < <(groups_present)
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ── --list ───────────────────────────────────────────────────────────────────
|
||||
if [ "$DO_LIST" = true ]; then list_services; exit 0; fi
|
||||
|
||||
# ── Direct install: ./setup.sh caddy homeassistant ──────────────────────────
|
||||
if [ "${#REQUESTED[@]}" -gt 0 ]; then
|
||||
require_root
|
||||
rc=0; for name in "${REQUESTED[@]}"; do run_service "$name" || rc=1; done
|
||||
exit "$rc"
|
||||
fi
|
||||
|
||||
# ── Guided interactive flow ──────────────────────────────────────────────────
|
||||
require_root
|
||||
|
||||
# 1) Show the REQUIRED set and let the user cancel before anything happens.
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||
echo "║ Ubuntu Post-Install · v$(cat "$HERE/VERSION" 2>/dev/null || echo '?')"
|
||||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "REQUIRED (installed/verified first):"
|
||||
echo " • Essential CLI packages: net-tools, git, curl, wget, htop, tree,"
|
||||
echo " ncdu, zip/unzip, jq, rsync, and glow (markdown reader)"
|
||||
echo " • Docker presence check (needed by all containerized services)"
|
||||
echo ""
|
||||
echo "Then you'll get a category menu to pick optional services."
|
||||
echo ""
|
||||
PROCEED=""
|
||||
prompt_yn "Proceed with the required setup? (y/n):" "y" PROCEED
|
||||
if [ "$PROCEED" != "y" ] && [ "$PROCEED" != "Y" ]; then
|
||||
echo "Cancelled. Nothing was changed."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 2) Run required.
|
||||
run_service base
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
log_warning "Docker is not installed. Containerized services need it."
|
||||
echo " Install with: curl -fsSL https://get.docker.com | sh"
|
||||
fi
|
||||
|
||||
# 3) Offer Caddy first (most services proxy through it).
|
||||
if [ -n "${SERVICE_GROUP[caddy]:-}" ] && ! is_installed caddy; then
|
||||
echo ""
|
||||
OFFER_CADDY=""
|
||||
prompt_yn "Install Caddy now? It's the reverse proxy most services use. (y/n):" "y" OFFER_CADDY
|
||||
[ "$OFFER_CADDY" = "y" ] || [ "$OFFER_CADDY" = "Y" ] && run_service caddy
|
||||
fi
|
||||
|
||||
# 4) Category menu loop: pick a category → checklist → install → back to menu.
|
||||
have_whiptail=false
|
||||
command -v whiptail >/dev/null 2>&1 && have_whiptail=true
|
||||
|
||||
while true; do
|
||||
mapfile -t CATS < <(groups_present)
|
||||
|
||||
if [ "$have_whiptail" = true ]; then
|
||||
cat_items=()
|
||||
for g in "${CATS[@]}"; do
|
||||
n=$(services_in_group "$g" | wc -l)
|
||||
cat_items+=("$g" "$n service(s)")
|
||||
done
|
||||
cat_items+=("DONE" "Finish and exit")
|
||||
CHOSEN_CAT=$(whiptail --title "Service Categories" --menu \
|
||||
"Pick a category (services you install come back here):" 22 70 14 \
|
||||
"${cat_items[@]}" 3>&1 1>&2 2>&3) || break
|
||||
else
|
||||
echo ""; echo "Categories:"; i=1
|
||||
for g in "${CATS[@]}"; do echo " $i) $g"; i=$((i+1)); done
|
||||
echo " d) Done"
|
||||
read -rp "Pick a category [d]: " pick
|
||||
[ "$pick" = "d" ] || [ -z "$pick" ] && break
|
||||
CHOSEN_CAT="${CATS[$((pick-1))]:-}"
|
||||
[ -z "$CHOSEN_CAT" ] && { echo "Invalid."; continue; }
|
||||
fi
|
||||
[ "$CHOSEN_CAT" = "DONE" ] && break
|
||||
|
||||
mapfile -t SVCS < <(services_in_group "$CHOSEN_CAT")
|
||||
SELECTED=()
|
||||
if [ "$have_whiptail" = true ]; then
|
||||
svc_items=()
|
||||
for name in "${SVCS[@]}"; do
|
||||
tag="${SERVICE_DESC[$name]}"
|
||||
is_installed "$name" && tag="$tag [installed]"
|
||||
svc_items+=("$name" "$tag" "OFF")
|
||||
done
|
||||
CHOICE=$(whiptail --title "${CHOSEN_CAT^^}" --checklist \
|
||||
"Space to select, Enter to install. Already-installed are marked:" 22 78 14 \
|
||||
"${svc_items[@]}" 3>&1 1>&2 2>&3) || continue
|
||||
eval "SELECTED=($CHOICE)"
|
||||
else
|
||||
echo ""; echo "${CHOSEN_CAT^^}:"
|
||||
for name in "${SVCS[@]}"; do
|
||||
m=""; is_installed "$name" && m=" [installed]"
|
||||
printf " %-16s %s%s\n" "$name" "${SERVICE_DESC[$name]}" "$m"
|
||||
done
|
||||
read -rp "Enter service names to install (space-separated, blank to go back): " -a SELECTED
|
||||
fi
|
||||
|
||||
for name in "${SELECTED[@]}"; do run_service "$name"; done
|
||||
done
|
||||
|
||||
echo ""
|
||||
log_success "Done. Re-run 'sudo ./setup.sh' any time to add more."
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user