diff --git a/.env.example b/.env.example index b03ff47..b9d30f2 100644 --- a/.env.example +++ b/.env.example @@ -36,9 +36,10 @@ TAILSCALE_TAGS=tag:nas # Enable Tailscale Funnel (public access) for HTTPS? Set to 'true' or 'false'. 'false' uses Serve (Tailnet only, recommended). ENABLE_FUNNEL_HTTPS=false -# --- Primary Hostname --- -# Primary hostname used by Traefik for routing. Derived from Tailscale settings by default. -HOSTNAME=${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN} +# --- Primary Application Hostname --- +# Primary hostname used by Traefik/Authelia. Derived from Tailscale settings by default. +# Renamed from HOSTNAME to avoid collision with host system environment variable. +APP_HOSTNAME=${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN} # --- Application Credentials --- # qBittorrent Web UI Credentials (change default!) @@ -62,18 +63,16 @@ HOMEPAGE_VAR_WEATHER_UNIT=metric # --- Authelia Settings --- # Generate strong random secrets for these using tools like `openssl rand -hex 32` -AUTHELIA_JWT_SECRET= # Example: your_strong_jwt_secret -AUTHELIA_SESSION_SECRET= # Example: your_strong_session_secret -AUTHELIA_STORAGE_ENCRYPTION_KEY= # Example: your_strong_storage_encryption_key -AUTHELIA_REDIS_PASSWORD= # Example: your_strong_redis_password +# These are all REQUIRED for Authelia to function properly +AUTHELIA_JWT_SECRET= # Secret used for JWT tokens (password reset, etc) +AUTHELIA_SESSION_SECRET= # Secret for encrypting session cookies +AUTHELIA_STORAGE_ENCRYPTION_KEY= # Secret for encrypting stored data +AUTHELIA_REDIS_PASSWORD= # Password for Redis session storage -# Google OIDC Provider Settings (Get from Google Cloud Console - https://console.cloud.google.com/apis/credentials) -AUTHELIA_GOOGLE_OIDC_CLIENT_ID= # Example: your-google-client-id.apps.googleusercontent.com -AUTHELIA_GOOGLE_OIDC_CLIENT_SECRET= # Example: GOCSPX-your-google-client-secret - -# Authelia Session Configuration -AUTHELIA_SESSION_DOMAIN=${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN} -AUTHELIA_DEFAULT_REDIRECT_URL=https://${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN}/home +# Note: The following variables are no longer needed with Authelia 4.38+ and the updated configuration +# They are preserved for backward compatibility but will be automatically mapped to the new structure +# AUTHELIA_SESSION_DOMAIN=${APP_HOSTNAME} +# AUTHELIA_DEFAULT_REDIRECT_URL=https://${APP_HOSTNAME}/home # --- API Keys & Integration Tokens (Optional - Mainly for Homepage Widgets) --- # Find API keys within each application's settings (usually Settings > General or Security) diff --git a/.gitignore b/.gitignore index fa2394e..487b297 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ *.env +*.bak .idea docker-compose.override.yml +/authelia/*.yml +!/authelia/*.example.yml /homepage/logs /homepage/*.yaml /homepage/*.css diff --git a/README.md b/README.md index 678c1d6..2aa21a4 100644 --- a/README.md +++ b/README.md @@ -4,263 +4,470 @@ This project provides a comprehensive, self-hosted media and utility server setu The core idea is to manage media libraries (movies, TV shows, music), automate downloads securely, provide easy access via a dashboard, and enable remote access through Tailscale. +## Table of Contents + +- [Docker Compose NAS](#docker-compose-nas) + - [Table of Contents](#table-of-contents) + - [Architecture Overview](#architecture-overview) + - [Features](#features) + - [Prerequisites](#prerequisites) + - [Required Setup Steps](#required-setup-steps) + - [Quick Start Guide](#quick-start-guide) + - [Configuration (`.env` Variables)](#configuration-env-variables) + - [Core System \& Paths](#core-system--paths) + - [Networking \& Access (Tailscale)](#networking--access-tailscale) + - [Authentication (Authelia)](#authentication-authelia) + - [Service Credentials](#service-credentials) + - [Homepage Customization \& Widgets](#homepage-customization--widgets) + - [Optional Features \& Services](#optional-features--services) + - [Detailed Setup \& Usage](#detailed-setup--usage) + - [Authelia User Management](#authelia-user-management) + - [(Optional) VPN Configuration](#optional-vpn-configuration) + - [(Optional) Traefik DNS Challenge](#optional-traefik-dns-challenge) + - [Service Access](#service-access) + - [Setup Script Commands (`update-setup.sh`)](#setup-script-commands-update-setupsh) + - [Managing Service Authentication](#managing-service-authentication-authelia-policies) + - [Optional Services](#optional-services) + - [Troubleshooting](#troubleshooting) + - [Middleware Not Found Errors](#middleware-not-found-errors) + - [SELinux Socket Permissions (Docker)](#selinux-socket-permissions-docker) + - [Authelia v4.38+ Configuration](#authelia-v438-configuration) + - [Tailscale Issues](#tailscale-issues) + - [File Permissions](#file-permissions) + - [Advanced Topics](#advanced-topics) + +## Architecture Overview + +This stack uses a combination of key services for routing, access, and security: + +- **[Tailscale](https://tailscale.com):** Provides a secure overlay network (WireGuard-based VPN) connecting your devices. It allows access to the NAS services from anywhere without opening firewall ports and handles HTTPS termination via its built-in `tailscale serve` or `tailscale funnel` features. All other services run within Tailscale's network namespace. +- **[Traefik](https://traefik.io):** Acts as a reverse proxy *within* the Tailscale network. It discovers services via Docker labels and routes incoming requests (from Tailscale) to the appropriate container based on paths (e.g., `/sonarr`, `/radarr`). +- **[Authelia](https://www.authelia.com):** Serves as the authentication gateway. Traefik forwards requests to Authelia for verification. If a user isn't logged in, they are redirected to the Authelia portal (`/`). Once authenticated, Authelia sets a session cookie (stored in Redis), and Traefik allows access to the requested service. You can configure which services require authentication via environment variables. + ## Features -This stack includes: +This stack includes the following services, categorized for clarity: -* **Reverse Proxy & Service Discovery:** [Traefik](https://traefik.io) automatically routes traffic to services. -* **Media Management:** - * [Sonarr](https://sonarr.tv): TV show management. - * [Radarr](https://radarr.video): Movie management. - * [Lidarr](https://lidarr.audio) (Optional): Music management. - * [Bazarr](https://www.bazarr.media/): Subtitle management. -* **Indexers & Downloads:** - * [Prowlarr](https://github.com/Prowlarr/Prowlarr): Indexer management for *arr apps. - * [qBittorrent](https://www.qbittorrent.org): Bittorrent client (can be configured to run through a VPN). - * [SABnzbd](https://sabnzbd.org/) (Optional): Usenet download client. -* **Media Server:** [Jellyfin](https://jellyfin.org) organizes and streams your media. -* **Request Management:** [Jellyseerr](https://github.com/FallenBagel/jellyseerr) allows users (including Jellyfin users) to request media. -* **Dashboard:** [Homepage](https://gethomepage.dev) provides a central dashboard to access all services. -* **Remote Access:** [Tailscale](https://tailscale.com) provides secure access to your services from anywhere without opening firewall ports. It handles HTTPS termination. -* **Utilities:** - * [Watchtower](https://containrrr.dev/watchtower/): Automatically updates running containers to the latest image. - * [Autoheal](https://github.com/willfarrell/docker-autoheal/): Monitors and restarts unhealthy containers. - * [Unpackerr](https://unpackerr.zip): Automatically extracts downloaded archives. -* **Other Optional Services:** AdGuard Home, Calibre-Web, Decluttarr, Tandoor Recipes, Joplin Server, Home Assistant, Immich Photos (enable via profiles). +**Core Infrastructure:** + +- **Reverse Proxy:** [Traefik](https://traefik.io) - Manages internal routing and service discovery. +- **Secure Remote Access:** [Tailscale](https://tailscale.com) - Provides VPN access and HTTPS. +- **Authentication:** [Authelia](https://www.authelia.com) & [Redis](https://redis.io) - Single sign-on portal and session management. +- **Dashboard:** [Homepage](https://gethomepage.dev) - Centralized access point (at `/home`). + +**Media Management (\*Arr Suite):** + +- [Sonarr](https://sonarr.tv): TV show management. +- [Radarr](https://radarr.video): Movie management. +- [Lidarr](https://lidarr.audio) (Optional): Music management. +- [Bazarr](https://www.bazarr.media/): Subtitle management. +- [Prowlarr](https://github.com/Prowlarr/Prowlarr): Indexer management for *arr apps. + +**Download Clients:** + +- [qBittorrent](https://www.qbittorrent.org): Bittorrent client. +- [SABnzbd](https://sabnzbd.org/) (Optional): Usenet download client. + +**Media Serving & Requests:** + +- [Jellyfin](https://jellyfin.org): Media server for streaming. +- [Jellyseerr](https://github.com/FallenBagel/jellyseerr): Media request management. + +**Utilities:** + +- [Watchtower](https://containrrr.dev/watchtower/): Automatic container updates. +- [Autoheal](https://github.com/willfarrell/docker-autoheal/): Automatic container restarts on failure. +- [Unpackerr](https://unpackerr.zip): Automated archive extraction. + +**Optional Services (Enabled via Profiles):** + +- [AdGuard Home](https://adguard.com/en/adguard-home/overview.html): Network-wide ad blocking. +- [Calibre-Web](https://github.com/janeczku/calibre-web): E-book library management. +- [Decluttarr](https://github.com/manimatter/decluttarr): Automated download cleanup. +- [Tandoor Recipes](https://docs.tandoor.dev/): Recipe management. +- [Joplin Server](https://joplinapp.org/): Note-taking synchronization server. +- [Home Assistant](https://www.home-assistant.io/): Home automation platform. +- [Immich](https://immich.app/): Self-hosted photo and video backup. +- [FlareSolverr](https://github.com/FlareSolverr/FlareSolverr): Bypasses Cloudflare challenges (e.g., for Prowlarr). ## Prerequisites -* **Linux Host:** Any recent Linux distribution capable of running Docker. Tested on Ubuntu Server 22.04. -* **Docker Engine:** Install the latest version of Docker Engine. [Official Installation Guide](https://docs.docker.com/engine/install/). -* **Docker Compose V2:** Ensure you have Docker Compose V2 (usually installed as a Docker plugin, invoked via `docker compose`). [Official Installation Guide](https://docs.docker.com/compose/install/). -* **User Permissions:** You'll need a user account that can run `docker` commands (usually by adding the user to the `docker` group) or run `docker compose` via `sudo`. -* **SELinux (If Enabled):** If your host uses SELinux (e.g., Fedora, CentOS, RHEL), you might need additional host configuration. See the [Troubleshooting](#selinux-socket-permissions) section. +- **Linux Host:** A system capable of running Docker (e.g., Ubuntu, Debian, Fedora). +- **Docker & Docker Compose:** Latest versions installed. See [Docker Engine Install](https://docs.docker.com/engine/install/) and [Docker Compose Install](https://docs.docker.com/compose/install/). +- **User Permissions:** Ability to run `docker` commands (user in `docker` group or use `sudo`). +- **Basic Linux Knowledge:** Familiarity with command line, text editors, and file permissions. +- **Tailscale Account:** Required for remote access. [Sign up here](https://tailscale.com/login). +- **(Optional) SELinux:** If enabled, see [Troubleshooting](#selinux-socket-permissions-docker). -## Quick Start +## Required Setup Steps + +These steps are **mandatory** for a working installation. Without properly completing these, the stack will not function correctly. + +1. **⚠️ Required: System User Information** + - Set `USER_ID` and `GROUP_ID` in `.env` (run `id -u` and `id -g` to get yours) + - Incorrect values will cause permission errors with mounted volumes + +2. **⚠️ Required: Directory Paths** + - Set `CONFIG_ROOT` (where service configurations will be stored) + - Set `DATA_ROOT` (where your media libraries will be stored) + - Set `DOWNLOAD_ROOT` (must be on same filesystem as DATA_ROOT for hardlinks) + +3. **⚠️ Required: Tailscale Configuration** + - Create a Tailscale account at [tailscale.com](https://tailscale.com) + - Generate an Auth Key in the [Tailscale Admin Console](https://login.tailscale.com/admin/settings/keys) + - Set `TAILSCALE_AUTHKEY` in `.env` + - Set `TAILSCALE_TAILNET_DOMAIN` to your Tailnet domain (e.g., `your-name.ts.net`) + +4. **⚠️ Required: Security Credentials** + - Generate four strong random secrets using `openssl rand -hex 32`: + + ```bash + echo "AUTHELIA_JWT_SECRET=$(openssl rand -hex 32)" + echo "AUTHELIA_SESSION_SECRET=$(openssl rand -hex 32)" + echo "AUTHELIA_STORAGE_ENCRYPTION_KEY=$(openssl rand -hex 32)" + echo "AUTHELIA_REDIS_PASSWORD=$(openssl rand -hex 32)" + ``` + + - Add these to your `.env` file as shown + +5. **⚠️ Required: Create Authelia Account** + - Create an Authelia account (only for yourself and those you trust!) + + ```bash + docker run --rm authelia/authelia:latest authelia hash-password 'your_secure_password' + ``` + +6. **ℹ️ Optional: Set up** + - Generate a password hash for Authelia: + + ```bash + docker run --rm authelia/authelia:latest authelia hash-password 'your_secure_password' + ``` + + - Replace the example hash in `authelia/users_database.yml` with your generated hash + +## Quick Start Guide + +After completing all [Required Setup Steps](#required-setup-steps) above, follow these steps to get up and running: + +1. **Clone Repository:** + + ```bash + git clone https://github.com/AdrienPoupa/docker-compose-nas.git + cd docker-compose-nas + ``` + +2. **Configure Environment:** + + ```bash + # Create an .env file from the example + cp .env.example .env + + # Edit the .env file and fill in ALL required values + nano .env + ``` + +3. **Configure Authelia Admin:** + + ```bash + # Generate a password hash + docker run --rm authelia/authelia:latest authelia hash-password 'your_secure_password' + + # Edit the users_database.yml with the generated hash + nano authelia/users_database.yml + ``` + +4. **Run the Setup Script:** + + ```bash + # Make the script executable + chmod +x ./update-setup.sh + + # Run the setup tool (use 'all' for initial setup) + ./update-setup.sh all + ``` + + This script will: + - Update your `.env` file while preserving existing values (`update-env`). + - Configure Authelia with your Tailscale domain settings (`update-authelia`). + - Set up service configurations and retrieve API keys (`update-services`). + + You can also run individual commands like `./update-setup.sh update-authelia`. Run `./update-setup.sh help` for all options. + +5. **Start the Stack:** + + ```bash + # Start containers + docker compose up -d + ``` + + *(Wait for containers to download and start)* + +6. **Access Your NAS:** + - Open `https://./` + - Log in with username `admin` and the password you set + - After login, you'll land on the Homepage dashboard at `/home` + +> ⚠️ **IMPORTANT:** If the stack fails to start, check the [Troubleshooting](#troubleshooting) section and verify you've properly completed all [Required Setup Steps](#required-setup-steps). + +## Configuration (`.env` Variables) + +This file controls essential settings. Copy `.env.example` to `.env` and modify the values. **Bold variables** are critical for initial setup. + +#### Core System & Paths + +| Variable | Description | Default | +| :----------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------- | +| **`USER_ID`** | Linux user ID for container permissions. Find with `id -u`. | `1000` | +| **`GROUP_ID`** | Linux group ID for container permissions. Find with `id -g`. | `1000` | +| **`TIMEZONE`** | Your local timezone (e.g., `America/New_York`, `Asia/Manila`). [List](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). | `America/New_York` | +| `CONFIG_ROOT` | Host base directory for service configurations. `.` = project subdirs. | `.` | +| `DATA_ROOT` | Host directory for media libraries (movies, TV, music, etc.). | `/mnt/data` | +| `DOWNLOAD_ROOT` | Host directory for downloads (in progress/completed). **Must be on same filesystem as `DATA_ROOT` for hardlinks.** | `/mnt/data/torrents` | +| `IMMICH_UPLOAD_LOCATION` | Host path for Immich uploads (if `immich` profile enabled). | `/mnt/data/photos` | + +#### Networking & Access (Tailscale) + +| Variable | Description | Default | +| :-------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------- | +| **`TAILSCALE_AUTHKEY`** | **Required.** Auth key from [Tailscale Admin Console](https://login.tailscale.com/admin/settings/keys). Use reusable or ephemeral. | *(None)* | +| **`TAILSCALE_TAILNET_DOMAIN`**| **Required.** Your unique Tailnet domain (e.g., `your-name.ts.net`). | `your-tailnet.ts.net` | +| `TAILSCALE_HOSTNAME` | Desired hostname for this NAS within Tailscale. | `tailscale-nas` | +| `TAILSCALE_TAGS` | Optional tags for the Tailscale node (e.g., `tag:nas`). | `tag:nas` | +| `ENABLE_FUNNEL_HTTPS` | Use Tailscale Funnel (`true` = public access via Tailscale domain) or Serve (`false` = Tailnet-only access, recommended). | `false` | +| `APP_HOSTNAME` | Primary hostname used by Traefik/Authelia. Defaults to Tailscale FQDN. Renamed from `HOSTNAME` to avoid host system conflicts. Can be overridden if using custom DNS. | `${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN}` | + +#### Authentication (Authelia) + +| Variable | Description | Default | +| :---------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------- | +| **`AUTHELIA_JWT_SECRET`** | **Required.** Random secret for Authelia (used for password reset JWT). **Generate your own!** | *(None - Example in file)* | +| **`AUTHELIA_SESSION_SECRET`** | **Required.** Random secret for session cookies. **Generate your own!** | *(None - Example in file)* | +| **`AUTHELIA_STORAGE_ENCRYPTION_KEY`** | **Required.** Random secret for encrypting data at rest (e.g., SQLite DB). **Generate your own!** | *(None - Example in file)* | +| **`AUTHELIA_REDIS_PASSWORD`** | **Required.** Password for the Redis database (used for session storage). **Generate your own!** | *(None - Example in file)* | + +#### Service Credentials + +| Variable | Description | Default | +| :----------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------- | +| `QBITTORRENT_USERNAME` | Username for qBittorrent Web UI. | `admin` | +| `QBITTORRENT_PASSWORD` | Password for qBittorrent Web UI. **Change default!** (May need to use temp password from logs on first run, then change in UI & `.env`). | `adminadmin` | +| `CALIBRE_USERNAME` | Username for Calibre-Web (if `calibre-web` profile enabled). | `admin` | +| `CALIBRE_PASSWORD` | Password for Calibre-Web (if `calibre-web` profile enabled). | `admin123` | +| `IMMICH_DB_PASSWORD` | Password for Immich's internal database (if `immich` profile enabled). | `postgres` | +| `ADGUARD_USERNAME` | Username for AdGuard Home (if `adguardhome` profile enabled). | *(None)* | +| `ADGUARD_PASSWORD` | Password for AdGuard Home (if `adguardhome` profile enabled). | *(None)* | + +#### Homepage Customization & Widgets + +| Variable | Description | Default | +| :------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------- | +| `HOMEPAGE_VAR_TITLE` | Title shown on the dashboard. | `Docker-Compose NAS` | +| `HOMEPAGE_VAR_SEARCH_PROVIDER` | Default search engine. [Options](https://gethomepage.dev/en/widgets/search/). | `google` | +| `HOMEPAGE_VAR_HEADER_STYLE` | Dashboard header style. [Options](https://gethomepage.dev/en/configs/settings/#header-style). | `boxed` | +| `HOMEPAGE_VAR_WEATHER_CITY` | City for weather widget. | *(None)* | +| `HOMEPAGE_VAR_WEATHER_LAT` | Latitude for weather widget. | *(None)* | +| `HOMEPAGE_VAR_WEATHER_LONG` | Longitude for weather widget. | *(None)* | +| `HOMEPAGE_VAR_WEATHER_UNIT` | Weather units (`metric` or `imperial`). | `metric` | +| `SONARR_API_KEY` | API Keys for various services, primarily used for Homepage widgets. Find keys in each app's settings. | *(None)* | +| `RADARR_API_KEY` | " | *(None)* | +| `LIDARR_API_KEY` | " (if `lidarr` profile enabled) | *(None)* | +| `BAZARR_API_KEY` | " | *(None)* | +| `PROWLARR_API_KEY` | " | *(None)* | +| `JELLYFIN_API_KEY` | " | *(None)* | +| `JELLYSEERR_API_KEY` | " | *(None)* | +| `SABNZBD_API_KEY` | " (if `sabnzbd` profile enabled) | *(None)* | +| `IMMICH_API_KEY` | " (if `immich` profile enabled) | *(None)* | +| `HOMEASSISTANT_ACCESS_TOKEN` | " (if `homeassistant` profile enabled) | *(None)* | + +#### Optional Features & Services + +| Variable | Description | Default | +| :---------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------- | +| `COMPOSE_PROFILES` | Comma-separated list of optional service profiles to enable (e.g., `lidarr,sabnzbd`). See [Optional Services](#optional-services). | *(None)* | +| `COMPOSE_PATH_SEPARATOR` | Path separator for `COMPOSE_FILE` (use `;` for Windows). | `:` | +| `COMPOSE_FILE` | Colon-separated list of compose files to use. Allows extending base config. | `docker-compose.yml:...` (See `.env.example`) | +| `DECLUTTARR_TEST_RUN` | Run Decluttarr in test mode (`True`/`False`)? (if `decluttarr` profile enabled). | `True` | +| `DECLUTTARR_...` | Other Decluttarr settings (see `.env.example`). | *(Varies)* | +| `PIA_USER` / `PIA_PASS` | Credentials for PIA VPN (if using default VPN setup for qBittorrent). | *(None)* | +| `PIA_LOCATION` | PIA server location (if using default VPN). [List](https://serverlist.piaservers.net/vpninfo/servers/v6). | `ca` | +| `PIA_LOCAL_NETWORK` | Your local network CIDR (e.g., `192.168.1.0/24`) to allow local access to VPN'd containers. | `192.168.0.0/16` | +| `DNS_CHALLENGE` | Enable Traefik DNS challenge for Let's Encrypt (`true`/`false`). **Not needed if using Tailscale for HTTPS.** | `true` | +| `DNS_CHALLENGE_PROVIDER` | Your DNS provider (e.g., `cloudflare`). [Providers](https://doc.traefik.io/traefik/https/acme/#providers). | `cloudflare` | +| `LETS_ENCRYPT_EMAIL` | Email for Let's Encrypt (if using DNS challenge). | *(None)* | +| `LETS_ENCRYPT_CA_SERVER` | Let's Encrypt server URL (if using DNS challenge). | `https://acme-v02.api.letsencrypt.org/directory` | +| `CLOUDFLARE_...` / `PROVIDER_...` | DNS provider API credentials (if using DNS challenge). | *(None)* | +| `HOMEASSISTANT_HOSTNAME` | Specific hostname for Home Assistant (if `homeassistant` profile enabled). | *(None)* | +| `IMMICH_HOSTNAME` | Specific hostname for Immich (if `immich` profile enabled). | *(None)* | +| `ADGUARD_HOSTNAME` | Specific hostname for AdGuard Home (if `adguardhome` profile enabled). | *(None)* | + +## Detailed Setup & Usage + +### Authelia User Management + +Authelia uses the `authelia/users_database.yml` file to manage users. + +- **Setting the Initial Admin Password:** + 1. As mentioned in the Quick Start, you **must** set a strong password for the default `admin` user. + 2. Generate a hash using Docker (replace `'your_secure_password'`): + + ```bash + docker run --rm authelia/authelia:latest authelia hash-password 'your_secure_password' + ``` + + 3. Copy the **entire output hash** (starting with `$argon2id...`). + 4. Open `authelia/users_database.yml` and replace the example `password:` value under `admin:` with your generated hash. + 5. Ensure the `admin` user belongs to the `admins` and `users` groups as shown in the example. + +- **Adding More Users:** + 1. Generate a password hash for the new user as shown above. + 2. Edit `authelia/users_database.yml`. + 3. Add a new entry under `users:`, following the format of the `admin` user: + + ```yaml + users: + admin: + # ... (admin details) ... + newuser: + displayname: "New User Name" + password: "paste_generated_hash_here" + email: newuser@example.com + groups: + - users # Add to 'admins' group if needed + ``` + + 4. Save the file and restart Authelia: `docker compose restart authelia`. + +- **Adding/Updating Users (Recommended Method):** + Use the setup script's interactive tool: -1. **Clone the Repository:** ```bash - git clone https://github.com/AdrienPoupa/docker-compose-nas.git - cd docker-compose-nas + ./update-setup.sh manage-accounts ``` -2. **Create Configuration File:** - Copy the example environment file: - ```bash - cp .env.example .env - ``` + This script handles password hashing and file formatting, reducing the chance of errors. It will prompt you for the username, display name, email, and groups, then generate a secure password hash. -3. **Edit `.env` File:** - Open the `.env` file with a text editor and configure it according to your system and preferences. **This is the most crucial step.** See the detailed [Configuration (`.env` File)](#configuration-env-file) section below for explanations of each variable. Minimally, you **must** set `USER_ID`, `GROUP_ID`, `TIMEZONE`, `HOSTNAME`, and `TAILSCALE_AUTHKEY`. +- **Enabling User Registration (Optional):** + 1. Edit `authelia/configuration.yml`. + 2. Find the commented-out `registration:` section near the bottom. + 3. Uncomment it and set `enable: true`. + 4. Save the file and restart Authelia (`docker compose restart authelia`). + 5. A "Register" link will now appear on the Authelia login page. -4. **Start the Stack:** - Run Docker Compose (use `sudo` if your user isn't in the `docker` group): - ```bash - docker compose up -d - ``` - This will pull the necessary images and start all the core services in the background. +- **Approving Registered Users:** + 1. When a user registers (if enabled), their details are added to `authelia/users_database.yml` but marked as `disabled: true`. + 2. To approve them, edit `authelia/users_database.yml`. + 3. Find the new user's entry. + 4. Change `disabled: true` to `disabled: false` (or simply remove the `disabled: true` line). + 5. Save the file. The user should now be able to log in. -5. **Run Initial Configuration Script:** - This script helps configure base URLs and API keys within the running *arr applications based on your `.env` file. - ```bash - ./update-config.sh - ``` - *(Note: You might need to make it executable first: `chmod +x ./update-config.sh`)* +### (Optional) VPN Configuration -6. **Access Services:** Once Tailscale is connected, you should be able to access your services via `https://..ts.net/` or `https:///`. If you set up DNS for your `HOSTNAME`, you can use `https:///`. The main dashboard is at `/`. +*(Details about configuring the PIA VPN or other VPN setups could go here if needed.)* -## Configuration (`.env` File) +### (Optional) Traefik DNS Challenge -This file controls all the essential settings for your Docker Compose stack. Copy `.env.example` to `.env` and edit the values. - ---- - -### **Core Settings (Required)** - -These are fundamental for basic operation and permissions. - -* `USER_ID`: The Linux user ID that the containers will run as. Find yours with `id -u`. - * *Default:* `1000` -* `GROUP_ID`: The Linux group ID that the containers will run as. Find yours with `id -g`. - * *Default:* `1000` - * **Note:** Using the correct IDs is crucial for file permissions, especially for accessing media files on the host. -* `TIMEZONE`: Your local timezone (e.g., `America/New_York`, `Europe/London`, `Asia/Manila`). Find yours from [this list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). - * *Default:* `America/New_York` -* `HOSTNAME`: **(Deprecated - Now derived)** The primary hostname used by Traefik for routing. This is now automatically constructed from `TAILSCALE_HOSTNAME` and `TAILSCALE_TAILNET_DOMAIN`. You generally don't need to set this directly unless overriding the default behavior. - * *Default:* `${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN}` - ---- - -### **Host Paths (Required)** - -Define where container data and configuration are stored on your host machine. - -* `CONFIG_ROOT`: The base directory on your host where configuration files for each service will be stored. Using `.` stores them in subdirectories within the project folder. - * *Default:* `.` -* `DATA_ROOT`: The main directory on your host containing your media libraries (movies, TV shows, music, books). - * *Default:* `/mnt/data` -* `DOWNLOAD_ROOT`: The directory on your host where the download client (qBittorrent/SABnzbd) will store downloads in progress and completed files *before* they are imported by *arr apps. - * *Default:* `/mnt/data/torrents` - * **Hardlink Note:** For efficient storage (avoiding duplicate files), it's highly recommended that `DOWNLOAD_ROOT` is on the **same filesystem** as `DATA_ROOT` (e.g., `/mnt/data/torrents` is inside `/mnt/data`). This allows instant moves via hardlinks instead of slow copies. - ---- - -### **Tailscale Access (Required)** - -Controls secure remote access via Tailscale. - -* `TAILSCALE_AUTHKEY`: **Required.** An authentication key from your Tailscale account. Generate one in the Tailscale Admin Console under Settings > Keys. You can use a reusable key or an ephemeral key (recommended for containers). - * *Default:* (None - **Must be set**) -* `TAILSCALE_HOSTNAME`: The desired hostname for this NAS within your Tailscale network. - * *Default:* `tailscale-nas` -* `TAILSCALE_TAILNET_DOMAIN`: **Required.** The domain of your Tailnet, including your Tailnet's unique name - * *Default:* `your-tailnet.ts.net` (**Must be set**) -* `TAILSCALE_TAGS`: Optional tags to apply to the Tailscale node (e.g., `tag:nas`). - * *Default:* `tag:nas` -* `ENABLE_FUNNEL_HTTPS`: Controls Tailscale's public accessibility. - * `true`: Enables Tailscale Funnel, making services accessible publicly via the Tailscale domain (`..ts.net`). Use with caution. - * `false`: Uses Tailscale Serve, making services accessible *only* to devices logged into your Tailnet. (Recommended) - * *Default:* `false` - ---- - -### **Homepage Widgets (Optional)** - -API keys needed *only* if you want to display real-time information from these services on the Homepage dashboard. Find the API keys within each application's settings (usually under Settings > General or Settings > Security). - -* `SONARR_API_KEY` -* `RADARR_API_KEY` -* `LIDARR_API_KEY` (If Lidarr profile is enabled) -* `BAZARR_API_KEY` -* `PROWLARR_API_KEY` -* `JELLYFIN_API_KEY` -* `JELLYSEERR_API_KEY` -* `SABNZBD_API_KEY` (If SABnzbd profile is enabled) -* `ADGUARD_USERNAME` / `ADGUARD_PASSWORD` (If AdGuard Home profile is enabled) -* `CALIBRE_USERNAME` / `CALIBRE_PASSWORD` (If Calibre-Web profile is enabled) - ---- - -### **Homepage Customization (Optional)** - -Control the appearance and behavior of the Homepage dashboard. - -* `HOMEPAGE_VAR_TITLE`: Title shown on the dashboard. - * *Default:* `Docker-Compose NAS` -* `HOMEPAGE_VAR_SEARCH_PROVIDER`: Default search engine. [See options](https://gethomepage.dev/en/widgets/search/). - * *Default:* `google` -* `HOMEPAGE_VAR_HEADER_STYLE`: Dashboard header style. [See options](https://gethomepage.dev/en/configs/settings/#header-style). - * *Default:* `boxed` -* `HOMEPAGE_VAR_WEATHER_CITY`, `_LAT`, `_LONG`, `_UNIT`: Configure the weather widget. - ---- - -### **Download Client Settings** - -Credentials for included download clients. - -* `QBITTORRENT_USERNAME`: Username for qBittorrent Web UI. - * *Default:* `admin` -* `QBITTORRENT_PASSWORD`: Password for qBittorrent Web UI. - * *Default:* `adminadmin` - * **Note:** On first run, qBittorrent might generate a temporary password shown in its logs (`docker compose logs qbittorrent`). Log in with that, change the password in qBittorrent settings, and update this `.env` variable accordingly. - ---- - -### **VPN Configuration (Example: PIA - Optional)** - -These variables are specific to the example `thrnz/docker-wireguard-pia` VPN container used for qBittorrent in the default setup. If you use a different VPN provider or container, you'll need different variables. **If you don't use the VPN, you can ignore these.** - -* `PIA_USER`: Private Internet Access username. -* `PIA_PASS`: Private Internet Access password. -* `PIA_LOCATION`: PIA server location code (e.g., `ca_montreal`, `us_east`). [See list](https://serverlist.piaservers.net/vpninfo/servers/v6). - * *Default:* `ca` -* `PIA_LOCAL_NETWORK`: Your local network CIDR (e.g., `192.168.1.0/24`). Allows local access to the qBittorrent UI when the VPN is active. - * *Default:* `192.168.0.0/16` - ---- - -### **Traefik DNS Challenge (Optional)** - -These settings are for enabling automatic HTTPS certificate generation via Let's Encrypt using the DNS-01 challenge method. **This is generally NOT needed** because Tailscale handles HTTPS termination by default in this setup. Only configure this if you have a specific reason to manage your own certificates via Traefik (e.g., accessing services without Tailscale). - -* `DNS_CHALLENGE`: Set to `true` to enable DNS challenge. - * *Default:* `true` (Consider setting to `false` if using Tailscale for HTTPS) -* `DNS_CHALLENGE_PROVIDER`: Your DNS provider supported by Traefik/Lego (e.g., `cloudflare`, `godaddy`). [See providers](https://doc.traefik.io/traefik/https/acme/#providers). - * *Default:* `cloudflare` -* `LETS_ENCRYPT_EMAIL`: Your email address for Let's Encrypt notifications. -* `LETS_ENCRYPT_CA_SERVER`: Let's Encrypt server URL (use staging for testing). - * *Default:* `https://acme-v02.api.letsencrypt.org/directory` (Production) -* Provider-Specific Variables (e.g., `CLOUDFLARE_EMAIL`, `CLOUDFLARE_DNS_API_TOKEN`, `CLOUDFLARE_ZONE_API_TOKEN`): Credentials required by your chosen `DNS_CHALLENGE_PROVIDER`. Refer to Traefik documentation. - ---- - -### **Compose Profiles & Files (Advanced)** - -* `COMPOSE_PROFILES`: Comma-separated list of optional service profiles to enable (e.g., `lidarr,sabnzbd,adguardhome`). See [Optional Services](#optional-services). -* `COMPOSE_FILE`: Colon-separated list of compose files to use. Allows extending the base configuration. - * *Default:* `docker-compose.yml` +*(Details about setting up DNS provider credentials for Let's Encrypt could go here if needed.)* ## Service Access -With the default Tailscale setup, services are securely accessible via HTTPS using your Tailscale node's name or IP, followed by the service path. Replace `` with your Tailscale device name (e.g., `tailscale-nas.your-tailnet.ts.net`) or its Tailscale IP address. +With the default Tailscale setup and Authelia enabled, services are securely accessible via HTTPS using your Tailscale node's name or IP. Authentication is controlled by the included `update-setup.sh` script. -* **Homepage:** `https:///home` -* **Sonarr:** `https:///sonarr` -* **Radarr:** `https:///radarr` -* **Lidarr:** `https:///lidarr` (If profile enabled) -* **Bazarr:** `https:///bazarr` -* **Jellyseerr:** `https:///jellyseerr` -* **Prowlarr:** `https:///prowlarr` -* **qBittorrent:** `https:///qbittorrent` -* **SABnzbd:** `https:///sabnzbd` (If profile enabled) -* **Jellyfin:** `https:///jellyfin` -* **Calibre-Web:** `https:///calibre` (If profile enabled) -* **AdGuard Home:** `http://:3000` (If profile enabled, access via IP/port initially) -* **Tandoor Recipes:** `https:///recipes` (If profile enabled) -* **Joplin Server:** `https:///joplin` (If profile enabled) -* **Home Assistant:** `http://:8123` (If profile enabled, access via IP/port initially) -* **Immich:** `http://:2283` (If profile enabled, access via IP/port initially) +- **Login Portal:** `https:///` (Redirects unauthenticated users here for secured services) +- **Homepage Dashboard:** `https:///home` (Requires login by default) +- **Sonarr:** `https:///sonarr` (Requires login by default) +- **Radarr:** `https:///radarr` (Requires login by default) +- **qBittorrent:** `https:///qbittorrent` (Requires login by default) +- **Jellyfin:** `https:///jellyfin` (Requires login by default) +- ...and so on. -**Note:** -* `` refers to the full Tailscale name (e.g., `tailscale-nas.your-tailnet.ts.net`). -* `` refers to the Tailscale IP address of the NAS. -* Some services (AdGuard, HA, Immich) might require initial setup via their direct IP and port before Tailscale/Traefik routing is fully effective or configured within the application. Authentication for most services will be handled by Authelia (configured later). +Replace `` with your Tailscale device name (e.g., `tailscale-nas.your-tailnet.ts.net`) or its Tailscale IP address. + +If you configure DNS for your `APP_HOSTNAME` variable to point to the Tailscale IP, you can use `https:///`. + +### Setup Script Commands (`update-setup.sh`) + +The `update-setup.sh` script provides various commands to manage your configuration. Run `./update-setup.sh help` to see all options. + +**Core Setup & Updates:** + +- `./update-setup.sh update-env`: Updates `.env` from `.env.example`, preserving existing values and highlighting new/deprecated keys. +- `./update-setup.sh update-authelia`: Updates `authelia/configuration.yml` from the example, applying domain settings from `.env` and attempting to preserve secrets (uses `yq` if available). +- `./update-setup.sh update-services`: Updates configurations for running *arr/qBittorrent/Bazarr containers (sets URL base, extracts API keys to `.env`). Restarts affected containers. +- `./update-setup.sh all`: Runs `update-env`, `update-authelia`, and `update-services` sequentially. Recommended for initial setup and major updates. + +**Authelia Policy Management:** + +- `./update-setup.sh manage-policies`: Starts an interactive menu to list or set Authelia access policies (`one_factor`, `two_factor`, `bypass`, `deny`) for specific services defined in `authelia/configuration.yml`. +- `./update-setup.sh list-policies`: Lists services defined in `authelia/configuration.yml` and their current access policy. +- `./update-setup.sh set-policy `: Directly sets the Authelia access policy for the specified `` to the given `` (e.g., `one_factor`, `two_factor`, `bypass`, `deny`). + +> **Important:** After changing Authelia policies using `manage-policies` or `set-policy`, you **must** restart Authelia for the changes to take effect: +> +> ```bash +> docker compose restart authelia +> ``` + +**User & File Management:** + +- `./update-setup.sh manage-accounts`: Starts an interactive tool to add or update users in `authelia/users_database.yml`. It generates password hashes and prompts for user details. +- `./update-setup.sh cleanup`: Interactively finds and deletes old backup files (`.bak`) created by the script. Allows keeping the most recent backup of each type. + +**Help:** + +- `./update-setup.sh help`: Displays the full list of commands and usage instructions. + +### Managing Service Authentication (Authelia Policies) + +Use the `update-setup.sh` script to easily control which services require Authelia login and what level of authentication is needed. This is done by managing *access control rules* within Authelia's configuration (`authelia/configuration.yml`). + +See the `Authelia Policy Management` commands in the [Setup Script Commands](#setup-script-commands-update-setupsh) section above for details on how to list and set policies like `one_factor`, `two_factor`, `bypass`, or `deny` for each service. ## Optional Services Several services are included but disabled by default. Enable them by adding their profile name to the `COMPOSE_PROFILES` variable in your `.env` file (separate multiple profiles with commas). Example: Enable Lidarr and SABnzbd + ```dotenv COMPOSE_PROFILES=lidarr,sabnzbd ``` Available Profiles: -* `lidarr`: Music management. -* `sabnzbd`: Usenet download client. -* `flaresolverr`: Bypasses Cloudflare challenges for Prowlarr. -* `adguardhome`: Network-wide ad blocking (see `adguardhome/README.md`). -* `calibre-web`: E-book library management. -* `decluttarr`: Automated download cleanup. -* `tandoor`: Recipe management (see `tandoor/README.md`). -* `joplin`: Note-taking server (see `joplin/README.md`). -* `homeassistant`: Home automation (see `homeassistant/README.md`). -* `immich`: Photo management (see `immich/README.md`). + +- `lidarr`: Music management. +- `sabnzbd`: Usenet download client. +- `flaresolverr`: Bypasses Cloudflare challenges for Prowlarr. +- `adguardhome`: Network-wide ad blocking (see `adguardhome/README.md`). +- `calibre-web`: E-book library management. +- `decluttarr`: Automated download cleanup. +- `tandoor`: Recipe management (see `tandoor/README.md`). +- `joplin`: Note-taking server (see `joplin/README.md`). +- `homeassistant`: Home automation (see `homeassistant/README.md`). +- `immich`: Photo management (see `immich/README.md`). ## Troubleshooting +### Middleware Not Found Errors + +If you see error messages like `middleware "authelia-auth@docker" does not exist` in the Traefik logs, please check Authelia logs for any fatal errors. It is likely due to a misconfigured `configuration.yml` in `authelia/configuration.yml` + +Make sure Traefik can access the Docker socket. See the [SELinux Socket Permissions](#selinux-socket-permissions-docker) section below for more details. + ### SELinux Socket Permissions (Docker) If you are running Docker on a host with SELinux enabled (like Fedora, CentOS, RHEL) and services like Traefik, Watchtower, or Autoheal fail with "permission denied" errors when trying to access `/var/run/docker.sock`: -1. **Check Audit Logs:** Immediately after seeing the error, check the SELinux audit log on the host: +1. **Check Audit Logs:** Immediately after seeing the error, check the SELinux audit log on the host: + ```bash sudo ausearch -m avc -ts recent ``` + Look for lines containing `denied`, `docker.sock`, and the name of the failing service (e.g., `traefik`, `watchtower`). -2. **Generate Custom Policy:** If denials are found, you may need to create a custom SELinux policy module using `audit2allow`. Pipe the denial messages into it: +2. **Generate Custom Policy:** If denials are found, you may need to create a custom SELinux policy module using `audit2allow`. Pipe the denial messages into it: + ```bash # Generate policy files (my-dockersock.te and my-dockersock.pp) sudo ausearch -m avc -ts recent | audit2allow -M my-dockersock @@ -268,20 +475,61 @@ If you are running Docker on a host with SELinux enabled (like Fedora, CentOS, R # Install the policy module sudo semodule -i my-dockersock.pp ``` + This allows the specific actions that were being denied. You might need to repeat this if different denials appear after applying the first policy. +### Authelia v4.38+ Configuration + +Authelia v4.38+ introduces significant changes to its configuration structure, particularly for session domains and authentication flows. The setup in this repository has been carefully configured to work with these changes: + +1. **Domain Configuration**: + - You must use your specific Tailnet domain (e.g., `example.ts.net`) for cookies, not just `ts.net` + - The domain `ts.net` is part of the [Public Suffix List](https://publicsuffix.org/), which means browsers restrict cookies on it for security reasons + - Authelia will refuse to start if you try to use a domain from this list + +2. **Required Secret Variables**: You must set these four variables in your `.env` file: + - `AUTHELIA_JWT_SECRET`: Used for password reset tokens + - `AUTHELIA_SESSION_SECRET`: Used for session cookie encryption + - `AUTHELIA_STORAGE_ENCRYPTION_KEY`: Used for database encryption + - `AUTHELIA_REDIS_PASSWORD`: Used for Redis authentication + + Generate strong random values for these with: `openssl rand -hex 32` + +3. **Automatic Domain Setup**: The `update-setup.sh` script automatically: + - Uses your specific Tailnet domain (e.g., `example.ts.net`) from your `.env` file + - Configures cookie domains properly to avoid Public Suffix List issues + - Sets up proper access control rules for both your domain and its subdomains + +If you encounter any of these common errors: + +```log +error: option 'domain' is not a valid cookie domain: the domain is part of the special public suffix list +error: option 'authelia_url' does not share a cookie scope with domain +error: can't be specified at the same time: option 'domain' and option 'cookies' +configuration key 'jwt_secret' is deprecated in 4.38.0 +``` + +Running the setup script should resolve them. After making changes to the configuration, restart Authelia with: + +```bash +docker compose restart authelia +``` + +See the [Authelia documentation](https://www.authelia.com/configuration/session/introduction/) for more details on the v4.38+ configuration structure. + ### Tailscale Issues -* **Authentication:** Ensure your `TAILSCALE_AUTHKEY` in `.env` is valid and hasn't expired (especially if using ephemeral keys). Check the `tailscale` container logs (`docker compose logs tailscale`) for authentication errors. -* **Connectivity:** Verify the `tailscale` container is running and connected to your Tailnet (`docker compose exec tailscale tailscale status`). -* **Funnel/Serve Command:** If you modified the Tailscale command, ensure the syntax for `tailscale funnel` or `tailscale serve` is correct. +- **Authentication:** Ensure your `TAILSCALE_AUTHKEY` in `.env` is valid and hasn't expired (especially if using ephemeral keys). Check the `tailscale` container logs (`docker compose logs tailscale`) for authentication errors. +- **Connectivity:** Verify the `tailscale` container is running and connected to your Tailnet (`docker compose exec tailscale tailscale status`). +- **Funnel/Serve Command:** If you modified the Tailscale command, ensure the syntax for `tailscale funnel` or `tailscale serve` is correct. ### File Permissions If services report permission errors when accessing `/config` or `/data` directories, double-check that: -* The `USER_ID` and `GROUP_ID` in your `.env` file match the owner/group of the corresponding `CONFIG_ROOT` and `DATA_ROOT` directories on your host. -* The host directories have appropriate read/write permissions for that user/group. -* If using SELinux, the `:Z` flag on the volume mounts in `docker-compose.yml` is correctly applied to allow the container to write to the host paths. + +- The `USER_ID` and `GROUP_ID` in your `.env` file match the owner/group of the corresponding `CONFIG_ROOT` and `DATA_ROOT` directories on your host. +- The host directories have appropriate read/write permissions for that user/group. +- If using SELinux, the `:Z` flag on the volume mounts in `docker-compose.yml` is correctly applied to allow the container to write to the host paths. ## Advanced Topics diff --git a/authelia/configuration.example.yml b/authelia/configuration.example.yml new file mode 100644 index 0000000..753d5af --- /dev/null +++ b/authelia/configuration.example.yml @@ -0,0 +1,140 @@ +# Authelia Configuration File v4.38+ +# Documentation: https://www.authelia.com/configuration/ + +# Server settings +server: + address: 'tcp://0.0.0.0:9091' + +# Logging configuration +log: + level: info + format: text + +# Session configuration for v4.38+ +session: + name: authelia_session + secret: ${AUTHELIA_SESSION_SECRET} + expiration: 1h + inactivity: 5m + redis: + host: redis + port: 6379 + password: ${AUTHELIA_SESSION_REDIS_PASSWORD} + database_index: 0 + cookies: + # Using your specific Tailscale domain (e.g. example.ts.net) not just ts.net + - domain: 'your-tailnet.ts.net' + authelia_url: 'https://tailscale-nas.your-tailnet.ts.net' + default_redirection_url: 'https://tailscale-nas.your-tailnet.ts.net/home' + same_site: lax + +# Regulation (brute force protection) +regulation: + max_retries: 3 + find_time: 2m + ban_time: 5m + +# Storage (for user preferences, etc. - encrypted using storage key) +storage: + encryption_key: ${AUTHELIA_STORAGE_ENCRYPTION_KEY} + local: + path: /config/db.sqlite3 + +# Authentication backend (using file-based user database) +authentication_backend: + file: + path: /config/users_database.yml + password: + algorithm: argon2id + iterations: 1 + memory: 1024 + parallelism: 8 + salt_length: 16 + key_length: 32 + +# Access control rules +access_control: + default_policy: deny # Deny access by default + rules: + # Rules are processed in order. First match wins. + # It's recommended to put more specific rules first. + + # 1. Bypass rules (No authentication required) + # Allow access to Authelia's own endpoints + - domain: '*.your-tailnet.ts.net' + path_regex: '^/auth.*' # Match /auth and anything after it + policy: bypass + # Allow access to the root path (will be redirected by Traefik later) + - domain: '*.your-tailnet.ts.net' + path: '/' + policy: bypass + # Allow access to API endpoints (as requested, review security implications) + - domain: '*.your-tailnet.ts.net' + path_regex: '^/api.*' # Match /api and anything after it + policy: bypass + + # 2. One-Factor Authentication Rules (Requires login) + # Add rules for each service you want to protect. + # The domain should match your Tailscale domain. + # The path should match the Traefik PathPrefix for the service. + - domain: '*.your-tailnet.ts.net' + path_regex: '^/sonarr.*' + policy: one_factor + - domain: '*.your-tailnet.ts.net' + path_regex: '^/radarr.*' + policy: one_factor + - domain: '*.your-tailnet.ts.net' + path_regex: '^/lidarr.*' + policy: one_factor + - domain: '*.your-tailnet.ts.net' + path_regex: '^/bazarr.*' + policy: one_factor + - domain: '*.your-tailnet.ts.net' + path_regex: '^/qbittorrent.*' + policy: one_factor + - domain: '*.your-tailnet.ts.net' + path_regex: '^/sabnzbd.*' + policy: one_factor + - domain: '*.your-tailnet.ts.net' + path_regex: '^/calibre.*' + policy: one_factor + - domain: '*.your-tailnet.ts.net' + path_regex: '^/home.*' # Protect the homepage + policy: one_factor + - domain: '*.your-tailnet.ts.net' + path_regex: '^/jellyseerr.*' + policy: one_factor + - domain: '*.your-tailnet.ts.net' + path_regex: '^/prowlarr.*' + policy: one_factor + - domain: '*.your-tailnet.ts.net' + path_regex: '^/flaresolverr.*' + policy: one_factor + # Add other services here following the pattern: + # - domain: '*.your-tailnet.ts.net' + # path_regex: '^/.*' + # policy: one_factor + + # 3. Default rule for the domain (optional, if you want a catch-all) + # This rule will apply if no path-specific rule above matches. + # You might want to deny or require one_factor for unmatched paths. + # Example: Deny any other path on the domain + # - domain: '*.your-tailnet.ts.net' + # policy: deny + # Example: Require login for any other path + # - domain: '*.your-tailnet.ts.net' + # policy: one_factor + +# Notifier configuration +notifier: + filesystem: + filename: /config/notification.txt + +# Identity Validation (includes JWT secret for password reset) +identity_validation: + reset_password: + jwt_secret: ${AUTHELIA_JWT_SECRET} + +# Identity Providers +identity_providers: + oidc: null diff --git a/authelia/users_database.example.yml b/authelia/users_database.example.yml new file mode 100644 index 0000000..461f16d --- /dev/null +++ b/authelia/users_database.example.yml @@ -0,0 +1,39 @@ +# Authelia User Database +# Documentation: https://www.authelia.com/configuration/security/authentication/file/ + +# To add users: +# 1. Generate a password hash: +# docker run authelia/authelia:latest authelia hash-password 'your_strong_password' +# 2. Add the user entry below. +# +# To approve registered users (if registration is enabled in configuration.yml): +# 1. New users will appear here, possibly commented out or with 'disabled: true'. +# 2. Uncomment the user or set 'disabled: false' to grant access. + +users: + # First user is typically considered the admin in access rules + admin: + displayname: "Admin User" + # Replace this hash with one generated for your desired password! + password: "$argon2id$v=19$m=102400,t=1,p=8$PBf/L9l3s7LwN6jX/B3tVg$9+q3kL8VAbpWj9Gv9Z6uA5bA4zT1fB2fH3aD5c6b7e8" # Example hash for 'password' + email: admin@example.com + groups: + - admins + - users + + # Example of a regular user + # user1: + # displayname: "Regular User" + # password: "..." # Generate hash + # email: user1@example.com + # groups: + # - users + + # Example of a registered user waiting for approval (if registration enabled) + # newuser: + # disabled: true + # displayname: "New User" + # password: "..." # Hash generated during registration + # email: newuser@example.com + # groups: + # - users diff --git a/docker-compose.yml b/docker-compose.yml index 2f3263f..d34d1c2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,18 +13,57 @@ services: - --experimental.plugins.rewrite-body.version=v1.2.0 - --experimental.plugins.rewriteHeaders.modulename=github.com/XciD/traefik-plugin-rewrite-headers - --experimental.plugins.rewriteHeaders.version=v0.0.3 - network_mode: service:tailscale # Add this line - # ports: # Remove this section - # - "80:80" - # - "443:443" + - --providers.docker.network=docker-compose-nas + - --providers.docker.endpoint=unix:///var/run/docker.sock + network_mode: service:tailscale volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - # extra_hosts: # Remove this section - # - host.docker.internal:172.17.0.1 healthcheck: test: ["CMD", "traefik", "healthcheck", "--ping"] interval: 30s retries: 10 + redis: + image: redis:alpine + container_name: redis + restart: always + environment: + - REDIS_PASSWORD=${AUTHELIA_REDIS_PASSWORD} + command: ["redis-server", "--requirepass", "${AUTHELIA_REDIS_PASSWORD}"] + volumes: + - ${CONFIG_ROOT:-.}/redis:/data:Z + healthcheck: + test: ["CMD", "redis-cli", "-a", "${AUTHELIA_REDIS_PASSWORD}", "ping"] + interval: 5s + timeout: 3s + retries: 5 + authelia: + image: authelia/authelia:latest + container_name: authelia + restart: always + user: ${USER_ID}:${GROUP_ID} + volumes: + - ${CONFIG_ROOT:-.}/authelia:/config:Z + environment: + - AUTHELIA_SESSION_SECRET=${AUTHELIA_SESSION_SECRET} + - AUTHELIA_STORAGE_ENCRYPTION_KEY=${AUTHELIA_STORAGE_ENCRYPTION_KEY} + - AUTHELIA_SESSION_REDIS_PASSWORD=${AUTHELIA_REDIS_PASSWORD} + - AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET=${AUTHELIA_JWT_SECRET} + - TZ=${TIMEZONE} + labels: + - traefik.enable=true + - traefik.http.routers.authelia.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/auth`) # Changed rule + - traefik.http.routers.authelia.entrypoints=web + # - traefik.http.routers.authelia.priority=100 # Removed priority + - traefik.http.services.authelia.loadbalancer.server.port=9091 + - traefik.http.middlewares.authelia-auth.forwardAuth.address=http://authelia:9091/api/verify # Simplified forwardAuth address + - traefik.http.routers.authelia.middlewares=https-proto@docker + - traefik.http.middlewares.authelia-auth.forwardAuth.trustForwardHeader=true + - traefik.http.middlewares.authelia-auth.forwardAuth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email + - homepage.group=Security + - homepage.name=Authelia + - homepage.icon=authelia.png + - homepage.href=/auth # Updated href + - homepage.description=Authentication Portal sonarr: image: lscr.io/linuxserver/sonarr container_name: sonarr @@ -42,8 +81,9 @@ services: retries: 10 labels: - traefik.enable=true - - traefik.http.routers.sonarr.rule=PathPrefix(`/sonarr`) + - traefik.http.routers.sonarr.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/sonarr`) # Added Host check - traefik.http.routers.sonarr.entrypoints=web + - traefik.http.routers.sonarr.middlewares=https-proto@docker,authelia-auth@docker - traefik.http.services.sonarr.loadbalancer.server.port=8989 - homepage.group=Media - homepage.name=Sonarr @@ -71,8 +111,9 @@ services: retries: 10 labels: - traefik.enable=true - - traefik.http.routers.radarr.rule=PathPrefix(`/radarr`) + - traefik.http.routers.radarr.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/radarr`) # Added Host check - traefik.http.routers.radarr.entrypoints=web + - traefik.http.routers.radarr.middlewares=https-proto@docker,authelia-auth@docker - traefik.http.services.radarr.loadbalancer.server.port=7878 - homepage.group=Media - homepage.name=Radarr @@ -100,8 +141,9 @@ services: retries: 10 labels: - traefik.enable=true - - traefik.http.routers.lidarr.rule=PathPrefix(`/lidarr`) + - traefik.http.routers.lidarr.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/lidarr`) # Added Host check - traefik.http.routers.lidarr.entrypoints=web + - traefik.http.routers.lidarr.middlewares=https-proto@docker,authelia-auth@docker - traefik.http.services.lidarr.loadbalancer.server.port=8686 - homepage.group=Media - homepage.name=Lidarr @@ -131,8 +173,9 @@ services: retries: 10 labels: - traefik.enable=true - - traefik.http.routers.bazarr.rule=Host(`${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN}`) && PathPrefix(`/bazarr`) + - traefik.http.routers.bazarr.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/bazarr`) # Added Host check - traefik.http.routers.bazarr.entrypoints=web + - traefik.http.routers.bazarr.middlewares=https-proto@docker,authelia-auth@docker - traefik.http.services.bazarr.loadbalancer.server.port=6767 - homepage.group=Download - homepage.name=Bazarr @@ -165,10 +208,10 @@ services: retries: 10 labels: - traefik.enable=true - - traefik.http.routers.jellyseerr.rule=PathPrefix(`/jellyseerr`) + - traefik.http.routers.jellyseerr.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/jellyseerr`) # Added Host check - traefik.http.routers.jellyseerr.entrypoints=web - traefik.http.services.jellyseerr.loadbalancer.server.port=5055 - - traefik.http.routers.jellyseerr.middlewares=jellyseerr-stripprefix,jellyseerr-rewrite,jellyseerr-rewriteHeaders + - traefik.http.routers.jellyseerr.middlewares=https-proto@docker,jellyseerr-stripprefix,jellyseerr-rewrite,jellyseerr-rewriteHeaders,authelia-auth@docker - traefik.http.middlewares.jellyseerr-stripprefix.stripPrefix.prefixes=/jellyseerr - traefik.http.middlewares.jellyseerr-rewriteHeaders.plugin.rewriteHeaders.rewrites[0].header=location - traefik.http.middlewares.jellyseerr-rewriteHeaders.plugin.rewriteHeaders.rewrites[0].regex=^/(.+)$ @@ -239,8 +282,9 @@ services: retries: 10 labels: - traefik.enable=true - - traefik.http.routers.prowlarr.rule=PathPrefix(`/prowlarr`) + - traefik.http.routers.prowlarr.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/prowlarr`) # Added Host check - traefik.http.routers.prowlarr.entrypoints=web + - traefik.http.routers.prowlarr.middlewares=https-proto@docker,authelia-auth@docker - traefik.http.services.prowlarr.loadbalancer.server.port=9696 - homepage.group=Download - homepage.name=Prowlarr @@ -262,8 +306,9 @@ services: - TZ=${TIMEZONE} labels: - traefik.enable=true - - traefik.http.routers.flaresolverr.rule=PathPrefix(`/flaresolverr`) + - traefik.http.routers.flaresolverr.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/flaresolverr`) # Added Host check - traefik.http.routers.flaresolverr.entrypoints=web + - traefik.http.routers.flaresolverr.middlewares=https-proto@docker,authelia-auth@docker - traefik.http.services.flaresolverr.loadbalancer.server.port=8191 profiles: - flaresolverr @@ -281,25 +326,20 @@ services: - ${DOWNLOAD_ROOT}:/data/torrents:Z restart: always healthcheck: - # Container may fail if the PIA's token expired, so mark as unhealthy when there is no internet connection - # see: https://github.com/qdm12/gluetun/issues/641#issuecomment-933856220 test: ["CMD", "curl", "--fail", "http://127.0.0.1:8080", "https://google.com"] interval: 30s retries: 10 labels: - traefik.enable=true - - traefik.http.routers.qbittorrent.rule=Host(`${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN}`) && PathPrefix(`/qbittorrent`) + - traefik.http.routers.qbittorrent.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/qbittorrent`) # Added Host check - traefik.http.routers.qbittorrent.entrypoints=web - traefik.http.services.qbittorrent.loadbalancer.server.port=8080 - - traefik.http.routers.qbittorrent.middlewares=qbittorrent-strip-slash,qbittorrent-stripprefix - # https://github.com/qbittorrent/qBittorrent/issues/5693#issuecomment-552146296 + - traefik.http.routers.qbittorrent.middlewares=https-proto@docker,qbittorrent-strip-slash,qbittorrent-stripprefix,authelia-auth@docker - traefik.http.middlewares.qbittorrent-stripprefix.stripPrefix.prefixes=/qbittorrent - # https://community.traefik.io/t/middleware-to-add-the-if-needed/1895/19 - traefik.http.middlewares.qbittorrent-strip-slash.redirectregex.regex=(^.*\/qbittorrent$$) - traefik.http.middlewares.qbittorrent-strip-slash.redirectregex.replacement=$$1/ - traefik.http.middlewares.qbittorrent-strip-slash.redirectregex.permanent=false - #- com.centurylinklabs.watchtower.depends-on=/vpn - homepage.group=Download - homepage.name=qBittorrent - homepage.icon=qbittorrent.png @@ -338,8 +378,9 @@ services: restart: always labels: - traefik.enable=true - - traefik.http.routers.sabnzbd.rule=PathPrefix(`/sabnzbd`) # Simplified rule + - traefik.http.routers.sabnzbd.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/sabnzbd`) # Added Host check - traefik.http.routers.sabnzbd.entrypoints=web + - traefik.http.routers.sabnzbd.middlewares=https-proto@docker,authelia-auth@docker - traefik.http.services.sabnzbd.loadbalancer.server.port=8080 - homepage.group=Download - homepage.name=Sabnzbd @@ -359,7 +400,7 @@ services: - PUID=${USER_ID} - PGID=${GROUP_ID} - TZ=${TIMEZONE} - - JELLYFIN_PublishedServerUrl=${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN}/jellyfin + - JELLYFIN_PublishedServerUrl=https://${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN}/jellyfin volumes: - ${CONFIG_ROOT:-.}/jellyfin:/config:Z - ${DATA_ROOT}:/data:Z @@ -373,8 +414,9 @@ services: retries: 10 labels: - traefik.enable=true - - traefik.http.routers.jellyfin.rule=PathPrefix(`/jellyfin`) + - traefik.http.routers.jellyfin.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/jellyfin`) # Added Host check - traefik.http.routers.jellyfin.entrypoints=web + - traefik.http.routers.jellyfin.middlewares=https-proto@docker # Only HTTPS, no auth - traefik.http.services.jellyfin.loadbalancer.server.port=8096 - homepage.group=Media - homepage.name=Jellyfin @@ -403,8 +445,8 @@ services: - traefik.http.middlewares.calibre-headers.headers.customRequestHeaders.X-Scheme=https - traefik.http.middlewares.calibre-headers.headers.customRequestHeaders.X-Script-Name=/calibre - traefik.http.middlewares.calibre-stripprefixregex.stripPrefixRegex.regex=/calibre - - traefik.http.routers.calibre.middlewares=calibre-headers,calibre-stripprefixregex - - traefik.http.routers.calibre.rule=Host(`${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN}`) && PathPrefix(`/calibre`) + - traefik.http.routers.calibre.middlewares=https-proto@docker,calibre-headers,calibre-stripprefixregex,authelia-auth@docker + - traefik.http.routers.calibre.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/calibre`) # Added Host check - traefik.http.routers.calibre.entrypoints=web - traefik.http.services.calibre.loadbalancer.server.port=8083 - homepage.group=Media @@ -467,7 +509,6 @@ services: - HOMEPAGE_VAR_WEATHER_LONG=${HOMEPAGE_VAR_WEATHER_LONG} - HOMEPAGE_VAR_WEATHER_TIME=${TIMEZONE} - HOMEPAGE_VAR_WEATHER_UNIT=${HOMEPAGE_VAR_WEATHER_UNIT} - # Explicitly allow the hostname constructed from Tailscale variables - HOMEPAGE_ALLOWED_HOSTS=${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN} volumes: - ${CONFIG_ROOT:-.}/homepage:/app/config:Z @@ -478,11 +519,17 @@ services: [sh, -c, "cp -n /app/config/tpl/*.yaml /app/config && node server.js"] labels: - traefik.enable=true - # Change path to /home and use specific Tailscale host - - traefik.http.routers.homepage.rule=Host(`${TAILSCALE_HOSTNAME}.${TAILSCALE_TAILNET_DOMAIN}`) && PathPrefix(`/home`) + - traefik.http.routers.homepage.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/home`) # Changed rule to root - traefik.http.routers.homepage.entrypoints=web - # Authelia middleware will be added in a later commit - - traefik.http.services.homepage.loadbalancer.server.port=3000 + # - traefik.http.routers.homepage.priority=10 # Removed priority + # Global middleware for setting HTTPS header + - traefik.http.middlewares.https-proto.headers.customrequestheaders.X-Forwarded-Proto=https + - traefik.http.routers.homepage.middlewares=https-proto@docker,authelia-auth@docker + - homepage.group=Dashboard + - homepage.name=Homepage + - homepage.icon=homepage.png + - homepage.href=/ # Updated href + - homepage.description=Service Dashboard watchtower: image: ghcr.io/containrrr/watchtower:latest container_name: watchtower @@ -502,23 +549,22 @@ services: tailscale: image: tailscale/tailscale:latest container_name: tailscale - hostname: ${TAILSCALE_HOSTNAME:-tailscale-nas} # Hostname for Tailscale access + hostname: ${TAILSCALE_HOSTNAME:-tailscale-nas} environment: - TS_AUTHKEY: ${TAILSCALE_AUTHKEY} # Needs to be set in .env - TS_EXTRA_ARGS: "--advertise-tags=${TAILSCALE_TAGS:-tag:nas}" # Keep tags if desired + TS_AUTHKEY: ${TAILSCALE_AUTHKEY} + TS_EXTRA_ARGS: "--advertise-tags=${TAILSCALE_TAGS:-tag:nas}" TS_STATE_DIR: "/var/lib/tailscale" TS_USERSPACE: "false" - # Switch to enable Funnel (public access) or Serve (Tailnet only) ENABLE_FUNNEL_HTTPS: ${ENABLE_FUNNEL_HTTPS:-false} volumes: - - ${CONFIG_ROOT:-.}/tailscale/state:/var/lib/tailscale:Z # Persist state - - /var/run/docker.sock:/var/run/docker.sock # Optional, keep if needed + - ${CONFIG_ROOT:-.}/tailscale/state:/var/lib/tailscale:Z + - /var/run/docker.sock:/var/run/docker.sock devices: - /dev/net/tun:/dev/net/tun cap_add: - NET_ADMIN - NET_RAW - extra_hosts: # Add this section + extra_hosts: - host.docker.internal:172.17.0.1 restart: always command: @@ -542,8 +588,6 @@ services: done echo " Tailscaled is running." - # --- Start Tailscale Funnel/Serve --- - # Check the ENABLE_FUNNEL_HTTPS variable if [ "${ENABLE_FUNNEL_HTTPS}" = "true" ]; then echo "ENABLE_FUNNEL_HTTPS is true. Setting up Funnel -> http://localhost:80..." tailscale funnel --bg http://localhost:80 @@ -553,10 +597,9 @@ services: tailscale serve --bg http://localhost:80 echo "Tailscale Serve configured." fi - # --- End Tailscale Funnel/Serve --- echo "Tailscale forwarding configured. Container will remain running." - wait # Wait indefinitely for background processes + wait networks: default: diff --git a/update-config.sh b/update-config.sh deleted file mode 100755 index b7ce725..0000000 --- a/update-config.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash - -# See https://stackoverflow.com/a/44864004 for the sed GNU/BSD compatible hack - -function update_arr_config { - echo "Updating ${container} configuration..." - until [ -f "${CONFIG_ROOT:-.}"/"$container"/config.xml ]; do sleep 1; done - sed -i.bak "s/<\/UrlBase>/\/$1<\/UrlBase>/" "${CONFIG_ROOT:-.}"/"$container"/config.xml && rm "${CONFIG_ROOT:-.}"/"$container"/config.xml.bak - CONTAINER_NAME_UPPER=$(echo "$container" | tr '[:lower:]' '[:upper:]') - sed -i.bak 's/^'"${CONTAINER_NAME_UPPER}"'_API_KEY=.*/'"${CONTAINER_NAME_UPPER}"'_API_KEY='"$(sed -n 's/.*\(.*\)<\/ApiKey>.*/\1/p' "${CONFIG_ROOT:-.}"/"$container"/config.xml)"'/' .env && rm .env.bak - echo "Update of ${container} configuration complete, restarting..." - docker compose restart "$container" -} - -function update_qbittorrent_config { - echo "Updating ${container} configuration..." - docker compose stop "$container" - until [ -f "${CONFIG_ROOT:-.}"/"$container"/qBittorrent/qBittorrent.conf ]; do sleep 1; done - sed -i.bak '/WebUI\\ServerDomains=*/a WebUI\\Password_PBKDF2="@ByteArray(ARQ77eY1NUZaQsuDHbIMCA==:0WMRkYTUWVT9wVvdDtHAjU9b3b7uB8NR1Gur2hmQCvCDpm39Q+PsJRJPaCU51dEiz+dTzh8qbPsL8WkFljQYFQ==)"' "${CONFIG_ROOT:-.}"/"$container"/qBittorrent/qBittorrent.conf && rm "${CONFIG_ROOT:-.}"/"$container"/qBittorrent/qBittorrent.conf.bak - echo "Update of ${container} configuration complete, restarting..." - docker compose start "$container" -} - -function update_bazarr_config { - echo "Updating ${container} configuration..." - until [ -f "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml ]; do sleep 1; done - sed -i.bak "s/base_url: ''/base_url: '\/$container'/" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak - sed -i.bak "s/use_radarr: false/use_radarr: true/" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak - sed -i.bak "s/use_sonarr: false/use_sonarr: true/" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak - until [ -f "${CONFIG_ROOT:-.}"/sonarr/config.xml ]; do sleep 1; done - SONARR_API_KEY=$(sed -n 's/.*\(.*\)<\/ApiKey>.*/\1/p' "${CONFIG_ROOT:-.}"/sonarr/config.xml) - sed -i.bak "/sonarr:/,/^radarr:/ { s/apikey: .*/apikey: $SONARR_API_KEY/; s/base_url: .*/base_url: \/sonarr/; s/ip: .*/ip: sonarr/ }" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak - until [ -f "${CONFIG_ROOT:-.}"/radarr/config.xml ]; do sleep 1; done - RADARR_API_KEY=$(sed -n 's/.*\(.*\)<\/ApiKey>.*/\1/p' "${CONFIG_ROOT:-.}"/radarr/config.xml) - sed -i.bak "/radarr:/,/^sonarr:/ { s/apikey: .*/apikey: $RADARR_API_KEY/; s/base_url: .*/base_url: \/radarr/; s/ip: .*/ip: radarr/ }" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak - sed -i.bak 's/^BAZARR_API_KEY=.*/BAZARR_API_KEY='"$(sed -n 's/.*apikey: \(.*\)*/\1/p' "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml | head -n 1)"'/' .env && rm .env.bak - echo "Update of ${container} configuration complete, restarting..." - docker compose restart "$container" -} - -for container in $(docker ps --format '{{.Names}}'); do - if [[ "$container" =~ ^(radarr|sonarr|lidarr|prowlarr)$ ]]; then - update_arr_config "$container" - elif [[ "$container" =~ ^(bazarr)$ ]]; then - update_bazarr_config "$container" - elif [[ "$container" =~ ^(qbittorrent)$ ]]; then - update_qbittorrent_config "$container" - fi -done diff --git a/update-setup.sh b/update-setup.sh new file mode 100755 index 0000000..50dc293 --- /dev/null +++ b/update-setup.sh @@ -0,0 +1,1370 @@ +#!/bin/bash + +# Combined update script for docker-compose-nas +# - Updates .env file from .env.example while preserving values +# - Updates Authelia configuration from example file +# - Configures services with correct paths and API keys +# - Manages Authelia accounts +# - Controls service authentication requirements +# Created: April 26, 2025 + +set -e + +# Color definitions +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +MAGENTA='\033[0;35m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' # No Color + +# Files +ENV_FILE=".env" +ENV_EXAMPLE=".env.example" +TIMESTAMP=$(date +"%Y%m%d-%H%M%S") +ENV_BACKUP=".env.${TIMESTAMP}.bak" +AUTHELIA_CONFIG="authelia/configuration.yml" +AUTHELIA_CONFIG_EXAMPLE="authelia/configuration.example.yml" +AUTHELIA_CONFIG_BACKUP="authelia/configuration.${TIMESTAMP}.bak" +COMPOSE_FILE="docker-compose.yml" +COMPOSE_BACKUP="docker-compose.${TIMESTAMP}.bak" + +# Check if yq is installed +check_yq() { + if ! command -v yq &> /dev/null; then + echo -e "${YELLOW}Warning: 'yq' is not installed. While not required, it provides better YAML handling.${NC}" + echo -e "${YELLOW}Installation instructions: https://github.com/mikefarah/yq#install${NC}" + return 1 + fi + return 0 +} + +# Print section header +print_header() { + echo -e "\n${CYAN}${BOLD}$1${NC}" + echo -e "${CYAN}$(printf '=%.0s' $(seq 1 ${#1}))${NC}" +} + +# Check if a file exists +check_file() { + if [ ! -f "$1" ]; then + echo -e "${RED}Error: $1 doesn't exist${NC}" + return 1 + fi + return 0 +} + +# Function to create a backup +create_backup() { + echo -e "${BLUE}Creating backup of $1 as $2...${NC}" + cp "$1" "$2" +} + + +################################################## +# PART 1: Update .env file from .env.example +################################################## + +update_env_file() { + print_header "Environment File Update Tool" + + # Check if files exist + if [ ! -f "$ENV_FILE" ]; then + echo -e "${RED}Error: $ENV_FILE doesn't exist${NC}" + echo -e "Creating a new $ENV_FILE from $ENV_EXAMPLE" + cp "$ENV_EXAMPLE" "$ENV_FILE" + echo -e "${GREEN}Done! Please review and fill in required values in $ENV_FILE${NC}" + return 0 + fi + + if ! check_file "$ENV_EXAMPLE"; then + return 1 + fi + + echo -e "${BLUE}This will update your $ENV_FILE based on the structure in $ENV_EXAMPLE${NC}" + echo -e "${BLUE}Your existing values will be preserved where possible${NC}" + echo -e "${BLUE}Backup will be created as: $ENV_BACKUP${NC}" + echo -e "${YELLOW}Continue? [y/N]:${NC}" + read -r answer + if [[ ! "$answer" =~ ^[Yy]$ ]]; then + echo -e "${RED}Environment update cancelled.${NC}" + return 0 + fi + + # Create backup of current .env + create_backup "$ENV_FILE" "$ENV_BACKUP" + + # Store current env values + echo -e "${BLUE}Reading current environment values...${NC}" + declare -A current_values + declare -A current_keys_present + while IFS='=' read -r key value; do + # Skip comments and empty lines + if [[ ! "$key" =~ ^#.*$ ]] && [[ ! -z "$key" ]]; then + # Clean up any comments after the value + value=$(echo "$value" | sed 's/[[:space:]]*#.*$//') + # Trim leading/trailing whitespace + key=$(echo "$key" | xargs) + value=$(echo "$value" | xargs) + # Store in associative array if key is not empty + if [[ ! -z "$key" ]]; then + current_values["$key"]="$value" + # Track that this key existed in original file, regardless of value + current_keys_present["$key"]=1 + fi + fi + done < "$ENV_FILE" + + # Create new env file from example + echo -e "${BLUE}Creating new $ENV_FILE from $ENV_EXAMPLE...${NC}" + cp "$ENV_EXAMPLE" "$ENV_FILE.new" + + # Track which keys from the current env have been used + declare -A used_keys + # Track new keys that need attention + new_keys=() + # Track keys with special warnings + special_keys=() + + # Process the template and fill in values from current env + while IFS= read -r line; do + if [[ "$line" =~ ^([A-Za-z0-9_]+)=(.*)$ ]]; then + key="${BASH_REMATCH[1]}" + default_value="${BASH_REMATCH[2]}" + + # Mark the key as used if it exists in the original file + if [[ -n "${current_keys_present[$key]}" ]]; then + used_keys["$key"]=1 + + # Replace the line with the current value if one exists + if [[ -n "${current_values[$key]}" ]]; then + sed -i "s|^$key=.*$|$key=${current_values[$key]}|" "$ENV_FILE.new" + fi + # If key doesn't exist in original file and has empty/placeholder value + elif [[ -z "$default_value" ]] || [[ "$default_value" == '""' ]] || [[ "$default_value" == "''" ]]; then + new_keys+=("$key") + + # Special attention for Authelia keys + if [[ "$key" == AUTHELIA_*_SECRET* ]] || [[ "$key" == AUTHELIA_*_KEY* ]]; then + special_keys+=("$key") + fi + fi + fi + done < "$ENV_FILE.new" + + # Create section for unused/deprecated keys at the bottom of the file + echo -e "\n\n# --- DEPRECATED OR UNUSED KEYS (Kept for Reference) ---" >> "$ENV_FILE.new" + echo -e "# Keys below were in your original .env but aren't in the current .env.example" >> "$ENV_FILE.new" + echo -e "# They may be deprecated or renamed. Review and remove if no longer needed\n" >> "$ENV_FILE.new" + + unused_keys_count=0 + for key in "${!current_values[@]}"; do + if [[ -z "${used_keys[$key]}" ]]; then + echo "$key=${current_values[$key]} # DEPRECATED/UNUSED - Review" >> "$ENV_FILE.new" + unused_keys_count=$((unused_keys_count + 1)) + fi + done + + # Replace the old file with the new one + mv "$ENV_FILE.new" "$ENV_FILE" + + # Generate summary + echo -e "\n${GREEN}${BOLD}Environment Update Complete!${NC}" + echo -e "${BLUE}Summary:${NC}" + echo -e " - ${CYAN}Original config backed up to: $ENV_BACKUP${NC}" + echo -e " - ${CYAN}Updated .env structure to match .env.example${NC}" + echo -e " - ${CYAN}Preserved ${#used_keys[@]} existing values${NC}" + + if [[ $unused_keys_count -gt 0 ]]; then + echo -e " - ${YELLOW}Found $unused_keys_count deprecated/unused keys${NC}" + echo -e " ${YELLOW}These have been moved to the bottom of the file with warnings${NC}" + fi + + if [[ ${#new_keys[@]} -gt 0 ]]; then + echo -e "\n${YELLOW}${BOLD}NEW KEYS NEEDING ATTENTION:${NC}" + echo -e "${YELLOW}The following keys are new and may need values set:${NC}" + for key in "${new_keys[@]}"; do + echo -e " - ${MAGENTA}$key${NC}" + done + fi + + if [[ ${#special_keys[@]} -gt 0 ]]; then + echo -e "\n${RED}${BOLD}IMPORTANT SECURITY KEYS:${NC}" + echo -e "${RED}The following keys require secure values:${NC}" + for key in "${special_keys[@]}"; do + echo -e " - ${MAGENTA}$key${NC}" + + # Specific advice for Authelia keys + if [[ "$key" == AUTHELIA_*_SECRET* ]] || [[ "$key" == AUTHELIA_*_KEY* ]]; then + echo -e " ${CYAN}Generate with: ${GREEN}openssl rand -hex 32${NC}" + fi + done + fi + + echo -e "\n${BLUE}Review your updated $ENV_FILE file and adjust any values as needed.${NC}" +} + + +################################################## +# PART 2: Update Authelia configuration +################################################## + +update_authelia_config() { + print_header "Authelia Configuration Update Tool" + + # Check if files exist + if ! check_file "$AUTHELIA_CONFIG_EXAMPLE"; then + echo -e "${RED}Error: Example configuration file '$AUTHELIA_CONFIG_EXAMPLE' doesn't exist${NC}" + return 1 + fi + if ! check_file "$ENV_FILE"; then + echo -e "${RED}Error: Environment file '$ENV_FILE' doesn't exist. Cannot retrieve domain settings.${NC}" + return 1 + fi + + # Get the tailnet domain and hostname from .env + local TAILNET_DOMAIN=$(grep -oP "^TAILSCALE_TAILNET_DOMAIN=\K.*" "$ENV_FILE" | tr -d '"' | tr -d "'") + local TAILSCALE_HOSTNAME=$(grep -oP "^TAILSCALE_HOSTNAME=\K.*" "$ENV_FILE" | tr -d '"' | tr -d "'") + local FULL_HOSTNAME="${TAILSCALE_HOSTNAME}.${TAILNET_DOMAIN}" + local WILDCARD_DOMAIN="*.${TAILNET_DOMAIN}" + + if [ -z "$TAILNET_DOMAIN" ] || [ -z "$TAILSCALE_HOSTNAME" ]; then + echo -e "${RED}Error: Could not read TAILSCALE_TAILNET_DOMAIN or TAILSCALE_HOSTNAME from $ENV_FILE${NC}" + return 1 + fi + + # If config file doesn't exist, create it from example + if [ ! -f "$AUTHELIA_CONFIG" ]; then + echo -e "${YELLOW}Authelia configuration file '$AUTHELIA_CONFIG' doesn't exist, creating from example...${NC}" + cp "$AUTHELIA_CONFIG_EXAMPLE" "$AUTHELIA_CONFIG" + echo -e "${GREEN}Created new Authelia configuration file.${NC}" + # Proceed to update the newly created file + else + echo -e "${BLUE}This will update your Authelia configuration '$AUTHELIA_CONFIG' based on '$AUTHELIA_CONFIG_EXAMPLE'${NC}" + echo -e "${BLUE}Your Tailscale domain settings from '$ENV_FILE' will be applied.${NC}" + echo -e "${BLUE}Backup will be created as: $AUTHELIA_CONFIG_BACKUP${NC}" + echo -e "${YELLOW}Continue? [y/N]:${NC}" + read -r answer + if [[ ! "$answer" =~ ^[Yy]$ ]]; then + echo -e "${RED}Authelia config update cancelled.${NC}" + return 0 + fi + # Create backup of current config only if it exists + create_backup "$AUTHELIA_CONFIG" "$AUTHELIA_CONFIG_BACKUP" + fi + + # Check for yq + if check_yq; then + echo -e "${BLUE}Using 'yq' to update Authelia configuration...${NC}" + # Create a temporary file from the example + local TEMP_CONFIG="${AUTHELIA_CONFIG}.tmp" + cp "$AUTHELIA_CONFIG_EXAMPLE" "$TEMP_CONFIG" + + # Preserve specific existing values if the original config exists + if [ -f "$AUTHELIA_CONFIG_BACKUP" ]; then # Use backup as source of truth for existing values + echo -e "${BLUE}Attempting to preserve existing secrets and notifier settings...${NC}" + local existing_jwt_secret=$(yq e '.identity_validation.reset_password.jwt_secret // ""' "$AUTHELIA_CONFIG_BACKUP") + local existing_session_secret=$(yq e '.session.secret // ""' "$AUTHELIA_CONFIG_BACKUP") + local existing_storage_key=$(yq e '.storage.encryption_key // ""' "$AUTHELIA_CONFIG_BACKUP") + local existing_redis_pass=$(yq e '.session.redis.password // ""' "$AUTHELIA_CONFIG_BACKUP") + local existing_notifier=$(yq e '.notifier // ""' "$AUTHELIA_CONFIG_BACKUP") + + # Update secrets in temp file if they existed in the backup + if [[ -n "$existing_jwt_secret" && "$existing_jwt_secret" != '""' && "$existing_jwt_secret" != "null" ]]; then + existing_jwt_secret="$existing_jwt_secret" \ + yq e -i '.identity_validation.reset_password.jwt_secret = strenv(existing_jwt_secret)' "$TEMP_CONFIG" + fi + + if [[ -n "$existing_session_secret" && "$existing_session_secret" != '""' && "$existing_session_secret" != "null" ]]; then + existing_session_secret="$existing_session_secret" \ + yq e -i '.session.secret = strenv(existing_session_secret)' "$TEMP_CONFIG" + fi + + if [[ -n "$existing_storage_key" && "$existing_storage_key" != '""' && "$existing_storage_key" != "null" ]]; then + existing_storage_key="$existing_storage_key" \ + yq e -i '.storage.encryption_key = strenv(existing_storage_key)' "$TEMP_CONFIG" + fi + + if [[ -n "$existing_redis_pass" && "$existing_redis_pass" != '""' && "$existing_redis_pass" != "null" ]]; then + existing_redis_pass="$existing_redis_pass" \ + yq e -i '.session.redis.password = strenv(existing_redis_pass)' "$TEMP_CONFIG" + fi + + if [[ -n "$existing_notifier" && "$existing_notifier" != '""' && "$existing_notifier" != "null" ]]; then + existing_notifier="$existing_notifier" \ + yq e -i '.notifier = strenv(existing_notifier)' "$TEMP_CONFIG" + fi + fi + + # Update domain settings from .env + echo -e "${BLUE}Applying Tailscale domain settings...${NC}" + + # Replace placeholder full hostname - using proper yq syntax instead of walk() + yq e -i "(.. | select(tag == \"!!str\" and . == \"tailscale-nas.your-tailnet.ts.net\")) = \"${FULL_HOSTNAME}\"" "$TEMP_CONFIG" + + # Replace placeholder tailnet domain - using proper yq syntax instead of walk() + yq e -i "(.. | select(tag == \"!!str\" and . == \"your-tailnet.ts.net\")) = \"${TAILNET_DOMAIN}\"" "$TEMP_CONFIG" + + # Move temp file to final location + mv "$TEMP_CONFIG" "$AUTHELIA_CONFIG" + + echo -e "${GREEN}Authelia configuration updated successfully!${NC}" + else + echo -e "${YELLOW}Warning: 'yq' is not installed. Using sed to update configuration.${NC}" + echo -e "${YELLOW}This is less reliable and may not preserve all settings.${NC}" + + # Create a new file from the example + cp "$AUTHELIA_CONFIG_EXAMPLE" "$AUTHELIA_CONFIG.new" + + # Replace placeholders manually + sed -i "s|tailscale-nas\.your-tailnet\.ts\.net|${FULL_HOSTNAME}|g" "$AUTHELIA_CONFIG.new" + sed -i "s|your-tailnet\.ts\.net|${TAILNET_DOMAIN}|g" "$AUTHELIA_CONFIG.new" + + # Move the new file to the final location + mv "$AUTHELIA_CONFIG.new" "$AUTHELIA_CONFIG" + + echo -e "${YELLOW}Authelia configuration updated with sed.${NC}" + fi + + echo -e "${GREEN}Authelia configuration update completed.${NC}" + echo -e "${BLUE}Remember to restart Authelia for changes to take effect:${NC}" + echo -e "${CYAN} docker compose restart authelia${NC}" +} + +################################################## +# PART 3: Update service configurations +################################################## + +update_service_configs() { + print_header "Service Configuration Update Tool" + + # Check if Docker is running + if ! docker ps &> /dev/null; then + echo -e "${RED}Error: Docker is not running or you don't have permission to use it.${NC}" + echo -e "${YELLOW}Please start Docker and ensure your user has permission to run Docker commands.${NC}" + return 1 + fi + + if ! check_file "$ENV_FILE"; then + echo -e "${RED}Error: Environment file '$ENV_FILE' doesn't exist. Cannot update services.${NC}" + return 1 + fi + + # Get CONFIG_ROOT from .env if defined + CONFIG_ROOT=$(grep -oP "^CONFIG_ROOT=\K.*" "$ENV_FILE" | tr -d '"' | tr -d "'" || echo ".") + + echo -e "${BLUE}This will update configurations for running containers and extract API keys.${NC}" + echo -e "${BLUE}Services may be restarted during this process.${NC}" + echo -e "${YELLOW}Continue? [y/N]:${NC}" + read -r answer + if [[ ! "$answer" =~ ^[Yy]$ ]]; then + echo -e "${RED}Service configuration update cancelled.${NC}" + return 0 + fi + + # Create a temporary .env file for updates + local ENV_TEMP="${ENV_FILE}.${TIMESTAMP}.tmp" + cp "$ENV_FILE" "$ENV_TEMP" + + # Service update tracker + declare -A updated_services + declare -A api_keys_updated + + # Function to restart a container if needed + restart_container() { + local service=$1 + if [ "${updated_services[$service]}" == "1" ]; then + echo -e "${YELLOW}Restarting $service to apply changes...${NC}" + if docker compose restart "$service"; then + echo -e "${GREEN}$service restarted successfully!${NC}" + else + echo -e "${RED}Failed to restart $service. Please restart manually.${NC}" + fi + fi + } + + # Function to extract API key from a container + extract_api_key() { + local service=$1 + local config_path=$2 + local api_key_pattern=$3 + local env_var_name=$4 + + # Skip if service is not running + if ! docker compose ps "$service" 2>/dev/null | grep -q "Up"; then + echo -e "${YELLOW}Service $service is not running, skipping API key extraction.${NC}" + return 0 + fi + + echo -e "${BLUE}Attempting to extract API key for $service...${NC}" + + # Try to get API key directly from the config file in the container + local api_key="" + if [ -n "$config_path" ] && [ -n "$api_key_pattern" ]; then + api_key=$(docker compose exec -T "$service" grep -oP "$api_key_pattern" "$config_path" 2>/dev/null | head -1 | sed 's/.*[":=]\s*\([^"]*\).*/\1/g') + fi + + # If API key was found and is not empty + if [ -n "$api_key" ]; then + echo -e "${GREEN}API key for $service extracted successfully!${NC}" + + # Check if the API key is already in the .env file + if grep -q "^$env_var_name=" "$ENV_TEMP"; then + local current_key=$(grep -oP "^$env_var_name=\K.*" "$ENV_TEMP" | tr -d '"' | tr -d "'") + + # Only update if key is different and not empty + if [ "$current_key" != "$api_key" ] && [ -n "$current_key" ]; then + echo -e "${YELLOW}API key for $service in .env differs from extracted key.${NC}" + echo -e "${YELLOW}Would you like to update it? [y/N]:${NC}" + read -r update_key + if [[ ! "$update_key" =~ ^[Yy]$ ]]; then + echo -e "${BLUE}Keeping existing API key in .env file.${NC}" + return 0 + fi + fi + + # Update the existing entry + sed -i "s|^$env_var_name=.*|$env_var_name=$api_key|" "$ENV_TEMP" + else + # Add the new API key to the .env file + echo -e "\n# API key for $service (extracted $(date +"%Y-%m-%d"))" >> "$ENV_TEMP" + echo "$env_var_name=$api_key" >> "$ENV_TEMP" + fi + + api_keys_updated["$service"]="1" + else + echo -e "${YELLOW}Could not extract API key for $service.${NC}" + fi + } + + # Update *arr services with the correct URL base + for service in "sonarr" "radarr" "lidarr" "prowlarr" "bazarr"; do + # Skip if service is not running + if ! docker compose ps "$service" 2>/dev/null | grep -q "Up"; then + echo -e "${YELLOW}Service $service is not running, skipping.${NC}" + continue + fi + + echo -e "${BLUE}Updating $service configuration...${NC}" + + local config_file="/config/config.xml" + + # Check if the service's config file exists + if ! docker compose exec "$service" test -f "$config_file"; then + echo -e "${YELLOW}Configuration file for $service not found, skipping.${NC}" + continue + fi + + # Check current URLBase setting + local current_urlbase=$(docker compose exec -T "$service" grep -oP '\K[^<]+' "$config_file" 2>/dev/null || echo "") + local desired_urlbase="/$service" + + # Update URLBase if needed + if [ "$current_urlbase" != "$desired_urlbase" ]; then + echo -e "${YELLOW}$service URL base needs to be updated from '$current_urlbase' to '$desired_urlbase'.${NC}" + + if [ -z "$current_urlbase" ]; then + # If URLBase tag doesn't exist, add it + docker compose exec "$service" sed -i "//i\\ $desired_urlbase" "$config_file" || \ + docker compose exec "$service" sed -i "//i\\ $desired_urlbase" "$config_file" + else + # If URLBase exists, update it + docker compose exec "$service" sed -i "s|[^<]*|$desired_urlbase|g" "$config_file" + fi + + if [ $? -eq 0 ]; then + echo -e "${GREEN}$service URL base updated to $desired_urlbase${NC}" + updated_services["$service"]="1" + else + echo -e "${RED}Failed to update $service URL base.${NC}" + fi + else + echo -e "${GREEN}$service URL base already set to $desired_urlbase${NC}" + fi + + # Extract API key based on service + case "$service" in + "sonarr") + extract_api_key "$service" "$config_file" '\K[^<]+' "SONARR_API_KEY" + ;; + "radarr") + extract_api_key "$service" "$config_file" '\K[^<]+' "RADARR_API_KEY" + ;; + "lidarr") + extract_api_key "$service" "$config_file" '\K[^<]+' "LIDARR_API_KEY" + ;; + "prowlarr") + extract_api_key "$service" "$config_file" '\K[^<]+' "PROWLARR_API_KEY" + ;; + "bazarr") + # Bazarr uses a different config format (YAML/settings.ini) + extract_api_key "$service" "/config/config/config.ini" 'auth_apikey\s*=\s*\K[^#\n]+' "BAZARR_API_KEY" + ;; + esac + done + + # Update qBittorrent WebUI URL + if docker compose ps "qbittorrent" 2>/dev/null | grep -q "Up"; then + echo -e "${BLUE}Updating qBittorrent configuration...${NC}" + + local qbit_config_dir="/config/qBittorrent" + local qbit_config_file="$qbit_config_dir/qBittorrent.conf" + + # Check if the config file exists + if docker compose exec "qbittorrent" test -f "$qbit_config_file"; then + # Check current WebUI\RootFolder setting + local current_root=$(docker compose exec -T "qbittorrent" grep -oP '^WebUI\\RootFolder\s*=\s*\K.*' "$qbit_config_file" 2>/dev/null || echo "") + local desired_root="/qbittorrent" + + # Update WebUI\RootFolder if needed + if [ "$current_root" != "$desired_root" ]; then + echo -e "${YELLOW}qBittorrent WebUI root needs to be updated from '$current_root' to '$desired_root'.${NC}" + + if docker compose exec "qbittorrent" grep -q "^WebUI\\\\RootFolder=" "$qbit_config_file"; then + # If setting exists, update it + docker compose exec "qbittorrent" sed -i "s|^WebUI\\\\RootFolder=.*|WebUI\\\\RootFolder=$desired_root|g" "$qbit_config_file" + else + # If setting doesn't exist, add it to the [Preferences] section + if docker compose exec "qbittorrent" grep -q "^\[Preferences\]" "$qbit_config_file"; then + docker compose exec "qbittorrent" sed -i "/^\[Preferences\]/a WebUI\\\\RootFolder=$desired_root" "$qbit_config_file" + else + # Add [Preferences] section if it doesn't exist + docker compose exec "qbittorrent" bash -c "echo -e '\n[Preferences]\nWebUI\\\\RootFolder=$desired_root' >> $qbit_config_file" + fi + fi + + if [ $? -eq 0 ]; then + echo -e "${GREEN}qBittorrent WebUI root updated to $desired_root${NC}" + updated_services["qbittorrent"]="1" + else + echo -e "${RED}Failed to update qBittorrent WebUI root.${NC}" + fi + else + echo -e "${GREEN}qBittorrent WebUI root already set to $desired_root${NC}" + fi + else + echo -e "${YELLOW}qBittorrent configuration file not found, skipping.${NC}" + fi + fi + + # Extract Jellyfin API key + if docker compose ps "jellyfin" 2>/dev/null | grep -q "Up"; then + # Jellyfin API key is in a different location + extract_api_key "jellyfin" "/config/config/system.xml" '\K[^<]+' "JELLYFIN_API_KEY" + fi + + # Extract Jellyseerr API key + if docker compose ps "jellyseerr" 2>/dev/null | grep -q "Up"; then + # Jellyseerr stores API key in settings.json + extract_api_key "jellyseerr" "/app/config/settings.json" '"apiKey":\s*"\K[^"]+' "JELLYSEERR_API_KEY" + fi + + # Check for optional services based on COMPOSE_PROFILES + local profiles=$(grep -oP "^COMPOSE_PROFILES=\K.*" "$ENV_FILE" | tr -d '"' | tr -d "'" || echo "") + + # Process profiles as a comma-separated list + IFS=',' read -ra profile_array <<< "$profiles" + for profile in "${profile_array[@]}"; do + profile=$(echo "$profile" | xargs) # Trim whitespace + + # Handle specific profiles + case "$profile" in + "sabnzbd") + if docker compose ps "sabnzbd" 2>/dev/null | grep -q "Up"; then + # Extract SABnzbd API key from its config + extract_api_key "sabnzbd" "/config/sabnzbd.ini" 'api_key\s*=\s*\K[0-9a-f]+' "SABNZBD_API_KEY" + fi + ;; + "immich") + if docker compose ps "immich-server" 2>/dev/null | grep -q "Up"; then + # Extract Immich API key from user database + echo -e "${BLUE}Attempting to extract Immich API key...${NC}" + local immich_api_key=$(docker compose exec -T immich-server /usr/local/bin/cli api-key --show 2>/dev/null | grep -oP '[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}') + + if [ -n "$immich_api_key" ]; then + echo -e "${GREEN}Immich API key extracted successfully!${NC}" + + # Update or add the API key to the .env file + if grep -q "^IMMICH_API_KEY=" "$ENV_TEMP"; then + sed -i "s|^IMMICH_API_KEY=.*|IMMICH_API_KEY=$immich_api_key|" "$ENV_TEMP" + else + echo -e "\n# API key for Immich (extracted $(date +"%Y-%m-%d"))" >> "$ENV_TEMP" + echo "IMMICH_API_KEY=$immich_api_key" >> "$ENV_TEMP" + fi + + api_keys_updated["immich"]="1" + else + echo -e "${YELLOW}Could not extract Immich API key.${NC}" + fi + fi + ;; + "homeassistant") + if docker compose ps "homeassistant" 2>/dev/null | grep -q "Up"; then + echo -e "${BLUE}Home Assistant detected.${NC}" + echo -e "${YELLOW}Home Assistant requires a Long-Lived Access Token for API access.${NC}" + echo -e "${YELLOW}This must be generated manually in the Home Assistant UI:${NC}" + echo -e "${CYAN} Profile > Long-Lived Access Tokens > Create Token${NC}" + + # Check if token already exists + if ! grep -q "^HOMEASSISTANT_ACCESS_TOKEN=" "$ENV_TEMP" || [ -z "$(grep -oP "^HOMEASSISTANT_ACCESS_TOKEN=\K.*" "$ENV_TEMP" | tr -d '"' | tr -d "'")" ]; then + echo -e "${YELLOW}Would you like to add a Home Assistant token now? [y/N]:${NC}" + read -r add_token + if [[ "$add_token" =~ ^[Yy]$ ]]; then + echo -e "${BLUE}Enter your Home Assistant Long-Lived Access Token:${NC}" + read -rs token + if [ -n "$token" ]; then + if grep -q "^HOMEASSISTANT_ACCESS_TOKEN=" "$ENV_TEMP"; then + sed -i "s|^HOMEASSISTANT_ACCESS_TOKEN=.*|HOMEASSISTANT_ACCESS_TOKEN=$token|" "$ENV_TEMP" + else + echo -e "\n# Home Assistant Access Token (added $(date +"%Y-%m-%d"))" >> "$ENV_TEMP" + echo "HOMEASSISTANT_ACCESS_TOKEN=$token" >> "$ENV_TEMP" + fi + api_keys_updated["homeassistant"]="1" + echo -e "${GREEN}Home Assistant token added!${NC}" + else + echo -e "${RED}No token provided. Skipping.${NC}" + fi + fi + else + echo -e "${GREEN}Home Assistant token already exists in .env${NC}" + fi + fi + ;; + esac + done + + # Apply changes to the .env file if any API keys were updated + if [ ${#api_keys_updated[@]} -gt 0 ]; then + echo -e "${YELLOW}API keys were extracted for these services:${NC}" + for service in "${!api_keys_updated[@]}"; do + echo -e " - ${CYAN}$service${NC}" + done + + echo -e "${YELLOW}Update .env file with these API keys? [Y/n]:${NC}" + read -r update_env + if [[ ! "$update_env" =~ ^[Nn]$ ]]; then + # Backup the current .env before replacing + if [ ! -f "$ENV_BACKUP" ]; then + create_backup "$ENV_FILE" "$ENV_BACKUP" + fi + + # Replace the .env file with the updated temp file + mv "$ENV_TEMP" "$ENV_FILE" + echo -e "${GREEN}API keys updated in $ENV_FILE${NC}" + else + echo -e "${YELLOW}API key updates were not saved to .env file.${NC}" + rm "$ENV_TEMP" + fi + else + echo -e "${BLUE}No API key updates were found.${NC}" + rm "$ENV_TEMP" + fi + + # Restart services that were updated + if [ ${#updated_services[@]} -gt 0 ]; then + echo -e "${YELLOW}The following services had their configurations updated:${NC}" + for service in "${!updated_services[@]}"; do + echo -e " - ${CYAN}$service${NC}" + done + + echo -e "${YELLOW}Would you like to restart these services now? [Y/n]:${NC}" + read -r restart_now + if [[ ! "$restart_now" =~ ^[Nn]$ ]]; then + for service in "${!updated_services[@]}"; do + restart_container "$service" + done + else + echo -e "${YELLOW}Remember to restart these services manually for changes to take effect:${NC}" + for service in "${!updated_services[@]}"; do + echo -e " ${CYAN}docker compose restart $service${NC}" + done + fi + fi + + echo -e "\n${GREEN}${BOLD}Service Configuration Update Complete!${NC}" + if [ ${#updated_services[@]} -gt 0 ] || [ ${#api_keys_updated[@]} -gt 0 ]; then + echo -e "${GREEN}Services were successfully updated.${NC}" + else + echo -e "${BLUE}No changes were needed.${NC}" + fi +} + + +################################################## +# PART 4: Authelia Policy Management +################################################## + +# Get the current policy for a service from Authelia config +# Usage: get_authelia_policy +# Output: policy (e.g., one_factor, bypass), not_found, or error +get_authelia_policy() { + local service=$1 + local config_file=$2 + + if ! check_file "$config_file"; then + echo "error: config file not found" + return 1 + fi + + # Use yq if available for more reliable YAML parsing + if check_yq; then + # Find the rule matching the path_regex for the service + # Note: This assumes path_regex is unique enough for the service (e.g., ^/service.*) + local policy=$(yq e ".access_control.rules[] | select(.path_regex == \"^/${service}.*\") | .policy" "$config_file" 2>/dev/null) + if [ -n "$policy" ] && [ "$policy" != "null" ]; then + echo "$policy" + else + # Check if a rule exists for the service path at all + local rule_exists=$(yq e ".access_control.rules[] | select(.path_regex == \"^/${service}.*\")" "$config_file" 2>/dev/null) + if [ -n "$rule_exists" ] && [ "$rule_exists" != "null" ]; then + echo "error: policy not found in rule" # Rule exists but policy couldn't be read + else + echo "not_found" # No rule found for this service path + fi + fi + else + # Fall back to grep if yq isn't available (less reliable) + # This is fragile and depends heavily on formatting + local policy_line=$(grep -A 1 "path_regex: '^/${service}.*'" "$config_file" | grep "policy:") + if [ -n "$policy_line" ]; then + echo "$policy_line" | awk '{print $2}' + else + echo "not_found" + fi + fi + return 0 +} + +# Set the policy for a service in Authelia config +# Usage: set_authelia_policy +set_authelia_policy() { + local service=$1 + local policy=$2 + local config_file=$3 + local backup_file="${config_file}.${TIMESTAMP}.bak" + + if ! check_file "$config_file"; then + echo -e "${RED}Error: Authelia configuration file '$config_file' not found.${NC}" + return 1 + fi + + if [[ "$policy" != "one_factor" && "$policy" != "two_factor" && "$policy" != "deny" && "$policy" != "bypass" ]]; then + echo -e "${RED}Error: Invalid policy '$policy'. Must be one of: one_factor, two_factor, deny, bypass.${NC}" + return 1 + fi + + echo -e "${BLUE}Setting policy for service '$service' to '$policy' in '$config_file'...${NC}" + + # Create backup if it doesn't exist for this run + if [ ! -f "$backup_file" ]; then + create_backup "$config_file" "$backup_file" + fi + + # Use yq if available + if check_yq; then + # Check if the rule exists first using the path_regex + local rule_index=$(yq e ".access_control.rules | map(.path_regex == \"^/${service}.*\") | indexOf(true)" "$config_file" 2>/dev/null) + + if [ "$rule_index" == "-1" ] || [ -z "$rule_index" ]; then + echo -e "${YELLOW}Warning: No rule found for service path '^/${service}.*' in '$config_file'.${NC}" + echo -e "${YELLOW}Attempting to add a new rule...${NC}" + + # Get the tailnet domain from .env for the new rule + local TAILNET_DOMAIN=$(grep -oP "^TAILSCALE_TAILNET_DOMAIN=\K.*" "$ENV_FILE" | tr -d '"' | tr -d "'") + if [ -z "$TAILNET_DOMAIN" ]; then + echo -e "${RED}Error: Could not read TAILSCALE_TAILNET_DOMAIN from $ENV_FILE. Cannot add rule.${NC}" + return 1 + fi + local WILDCARD_DOMAIN="*.${TAILNET_DOMAIN}" + + # Add the new rule to the access_control.rules array + # Places it before the generic domain rule if it exists, otherwise at the end + # This assumes a generic rule like "- domain: '*.domain.tld'" exists near the end + yq e -i ".access_control.rules |= select(.domain != \"${WILDCARD_DOMAIN}\" or .path_regex != null) + [{\"domain\": \"${WILDCARD_DOMAIN}\", \"path_regex\": \"^/${service}.*\", \"policy\": \"${policy}\"}] + select(.domain == \"${WILDCARD_DOMAIN}\" and .path_regex == null)" "$config_file" + + if [ $? -eq 0 ]; then + echo -e "${GREEN}Added new rule for '$service' with policy '$policy'.${NC}" + return 0 + else + echo -e "${RED}Error: Failed to add new rule for '$service' using yq.${NC}" + return 1 + fi + else + # Rule exists, update the policy at the found index + yq e -i "(.access_control.rules[$rule_index].policy) = \"$policy\"" "$config_file" + if [ $? -eq 0 ]; then + echo -e "${GREEN}Policy for '$service' updated to '$policy'.${NC}" + return 0 + else + echo -e "${RED}Error: Failed to update policy for '$service' using yq.${NC}" + return 1 + fi + fi + else + # Fallback to sed (much less reliable, especially for adding rules) + echo -e "${YELLOW}Warning: 'yq' not found. Using 'sed' which is less reliable for YAML manipulation.${NC}" + # Check if rule exists (simple grep) + if grep -q "path_regex: '^/${service}.*'" "$config_file"; then + # Attempt to find the line number of path_regex and update the policy line below it + local line_num=$(grep -n "path_regex: '^/${service}.*'" "$config_file" | head -n 1 | cut -d: -f1) # Use head -n 1 just in case + if [ -n "$line_num" ]; then + # Assuming policy is the next line (fragile!) + local policy_line_num=$((line_num + 1)) + # Check if the next line actually contains 'policy:' + if sed -n "${policy_line_num}p" "$config_file" | grep -q "policy:"; then + sed -i "${policy_line_num}s/policy:.*/policy: $policy/" "$config_file" + if [ $? -eq 0 ]; then + echo -e "${GREEN}Policy for '$service' updated to '$policy' (using sed).${NC}" + return 0 + else + echo -e "${RED}Error: Failed to update policy line using sed.${NC}" + return 1 + fi + else + echo -e "${RED}Error: Could not reliably find policy line for '$service' using sed (expected on line $policy_line_num).${NC}" + return 1 + fi + else + echo -e "${RED}Error: Could not find line number for service '$service' using sed.${NC}" + return 1 + fi + else + echo -e "${RED}Error: Rule for service '$service' not found. Cannot add rule using sed.${NC}" + return 1 + fi + fi +} + +# List services and their Authelia policy status +list_authelia_services() { + print_header "Authelia Service Policy Status" + + if ! check_file "$AUTHELIA_CONFIG"; then + echo -e "${RED}Error: Authelia configuration file '$AUTHELIA_CONFIG' not found.${NC}" + return 1 + fi + + echo -e "${BLUE}Checking service policies in $AUTHELIA_CONFIG...${NC}" + echo -e "${CYAN}SERVICE\t\tPOLICY${NC}" + echo -e "${CYAN}-------\t\t------${NC}" + + local service_count=0 + local processed_services="" # Track processed services + + # Use yq to get all rules with path_regex if available + if check_yq; then + # Extract path_regex and policy for rules that have path_regex + local rules_data=$(yq e '.access_control.rules[] | select(has("path_regex")) | {"path": .path_regex, "policy": .policy}' "$AUTHELIA_CONFIG") + + # Process each rule found + while IFS= read -r line; do + # Extract service name from path_regex (e.g., "^/sonarr.*" -> "sonarr") + local path_regex=$(echo "$line" | grep -oP 'path: \K.*' | tr -d '"' | tr -d "'") + local policy=$(echo "$line" | grep -oP 'policy: \K.*' | tr -d '"' | tr -d "'") + + if [[ "$path_regex" =~ ^\^/([a-zA-Z0-9_-]+)\.\* ]]; then + local service="${BASH_REMATCH[1]}" + + # Skip duplicates if multiple rules somehow match the same pattern start + if [[ "$processed_services" == *"$service"* ]]; then + continue + fi + processed_services="$processed_services $service" + + printf "${BOLD}%-20s${NC}" "$service" + case "$policy" in + "one_factor"|"two_factor") + echo -e "${GREEN}${policy}${NC}" + ;; + "bypass") + echo -e "${YELLOW}${policy}${NC}" + ;; + "deny") + echo -e "${RED}${policy}${NC}" + ;; + *) + echo -e "${MAGENTA}Unknown ($policy)${NC}" + ;; + esac + service_count=$((service_count + 1)) + fi + # Use yq -N to prevent splitting lines with spaces + done <<< "$(echo "$rules_data" | yq -N e '.' -)" + + else + # Fallback to grep (less reliable) + echo -e "${YELLOW}Warning: yq not found, using grep (may miss services or show duplicates).${NC}" + # Find lines with path_regex, then try to get the policy line after + grep -n "path_regex: '^/.*" "$AUTHELIA_CONFIG" | while IFS=: read -r line_num line_content; do + if [[ "$line_content" =~ path_regex:\ \'^\/([a-zA-Z0-9_-]+)\.\*\' ]]; then + local service="${BASH_REMATCH[1]}" + # Skip duplicates + if [[ "$processed_services" == *"$service"* ]]; then + continue + fi + processed_services="$processed_services $service" + + # Try to get policy from the next line (very fragile) + local policy_line_num=$((line_num + 1)) + local policy=$(sed -n "${policy_line_num}p" "$AUTHELIA_CONFIG" | grep "policy:" | awk '{print $2}') + + printf "${BOLD}%-20s${NC}" "$service" + if [ -n "$policy" ]; then + case "$policy" in + "one_factor"|"two_factor") echo -e "${GREEN}${policy}${NC}" ;; + "bypass") echo -e "${YELLOW}${policy}${NC}" ;; + "deny") echo -e "${RED}${policy}${NC}" ;; + *) echo -e "${MAGENTA}Unknown ($policy)${NC}" ;; + esac + else + echo -e "${RED}Unknown (could not read policy)${NC}" + fi + service_count=$((service_count + 1)) + fi + done + fi + + if [ $service_count -eq 0 ]; then + echo -e "${YELLOW}No services with path_regex rules found in $AUTHELIA_CONFIG.${NC}" + echo -e "${YELLOW}Only services explicitly defined with path_regex rules are listed.${NC}" + fi + return 0 +} + + +cleanup_backups() { + print_header "Backup Files Cleanup" + + echo -e "${BLUE}Searching for backup files...${NC}" + + local env_backups=$(find . -maxdepth 1 -name ".env.*.bak" | sort) + local compose_backups=$(find . -maxdepth 1 -name "docker-compose.*.bak" | sort) + local authelia_backups=$(find ./authelia -maxdepth 1 -name "configuration.*.bak" 2>/dev/null | sort) + + local env_count=$(echo "$env_backups" | grep -c "^") + local compose_count=$(echo "$compose_backups" | grep -c "^") + local authelia_count=$(echo "$authelia_backups" | grep -c "^") + + echo -e "${CYAN}Found:${NC}" + echo -e " - ${CYAN}$env_count .env backup files${NC}" + echo -e " - ${CYAN}$compose_count docker-compose.yml backup files${NC}" + echo -e " - ${CYAN}$authelia_count Authelia configuration backup files${NC}" + + local total_count=$((env_count + compose_count + authelia_count)) + + if [ $total_count -eq 0 ]; then + echo -e "${GREEN}No backup files found.${NC}" + return 0 + fi + + echo -e "${YELLOW}Do you want to delete all backup files? [y/N]:${NC}" + read -r answer + if [[ ! "$answer" =~ ^[Yy]$ ]]; then + echo -e "${YELLOW}Would you like to keep the most recent backup of each type? [Y/n]:${NC}" + read -r keep_recent + + if [[ "$keep_recent" =~ ^[Nn]$ ]]; then + echo -e "${RED}Backup cleanup cancelled.${NC}" + return 0 + fi + + if [ $env_count -gt 1 ]; then + local keep_env=$(echo "$env_backups" | tail -1) + local del_env=$(echo "$env_backups" | grep -v "$keep_env") + echo -e "${BLUE}Keeping most recent .env backup: ${CYAN}$keep_env${NC}" + echo -e "${BLUE}Deleting ${CYAN}$((env_count - 1))${BLUE} older .env backups${NC}" + for file in $del_env; do + rm "$file" + done + fi + + if [ $compose_count -gt 1 ]; then + local keep_compose=$(echo "$compose_backups" | tail -1) + local del_compose=$(echo "$compose_backups" | grep -v "$keep_compose") + echo -e "${BLUE}Keeping most recent docker-compose.yml backup: ${CYAN}$keep_compose${NC}" + echo -e "${BLUE}Deleting ${CYAN}$((compose_count - 1))${BLUE} older docker-compose.yml backups${NC}" + for file in $del_compose; do + rm "$file" + done + fi + + if [ $authelia_count -gt 1 ]; then + local keep_authelia=$(echo "$authelia_backups" | tail -1) + local del_authelia=$(echo "$authelia_backups" | grep -v "$keep_authelia") + echo -e "${BLUE}Keeping most recent Authelia config backup: ${CYAN}$keep_authelia${NC}" + echo -e "${BLUE}Deleting ${CYAN}$((authelia_count - 1))${BLUE} older Authelia config backups${NC}" + for file in $del_authelia; do + rm "$file" + done + fi + + echo -e "${GREEN}Cleanup completed with most recent backups retained.${NC}" + else + for file in $env_backups $compose_backups $authelia_backups; do + rm "$file" + done + echo -e "${GREEN}All $total_count backup files have been deleted.${NC}" + fi +} + +# Interactive menu for managing Authelia policies +manage_authelia_policies() { + print_header "Authelia Policy Management" + + if ! check_file "$AUTHELIA_CONFIG"; then + echo -e "${RED}Error: Authelia configuration file '$AUTHELIA_CONFIG' not found.${NC}" + return 1 + fi + + while true; do + echo -e "\n${BLUE}Choose an option:${NC}" + echo -e " ${CYAN}1. ${NC}List services and their current Authelia policy" + echo -e " ${CYAN}2. ${NC}Set Authelia policy for a service" + echo -e " ${CYAN}3. ${NC}Return to main menu" + echo + + local choice + echo -e "${YELLOW}Enter your choice [1-3]: ${NC}" + read -r choice + + case "$choice" in + 1) + list_authelia_services + ;; + 2) + echo -e "${BLUE}Enter the service name to set the policy for (e.g., sonarr, radarr):${NC}" + read -r service + if [ -z "$service" ]; then + echo -e "${RED}No service name provided.${NC}" + continue + fi + + echo -e "${BLUE}Select the desired policy for '$service':${NC}" + echo -e " ${CYAN}1. ${GREEN}one_factor${NC} (Requires login)" + echo -e " ${CYAN}2. ${GREEN}two_factor${NC} (Requires login + 2FA)" + echo -e " ${CYAN}3. ${YELLOW}bypass${NC} (No login required)" + echo -e " ${CYAN}4. ${RED}deny${NC} (Access denied)" + echo -e " ${CYAN}5. ${NC}Cancel" + + local policy_choice + local policy="" + while true; do + echo -e "${YELLOW}Enter policy choice [1-5]: ${NC}" + read -r policy_choice + case "$policy_choice" in + 1) policy="one_factor"; break ;; + 2) policy="two_factor"; break ;; + 3) policy="bypass"; break ;; + 4) policy="deny"; break ;; + 5) policy=""; break ;; # Cancel + *) echo -e "${RED}Invalid choice.${NC}" ;; + esac + done + + if [ -z "$policy" ]; then + echo -e "${YELLOW}Policy change cancelled.${NC}" + continue + fi + + # Call the function to set the policy + set_authelia_policy "$service" "$policy" "$AUTHELIA_CONFIG" + + # Check the return status of set_authelia_policy + if [ $? -eq 0 ]; then + echo -e "\n${YELLOW}Remember to restart Authelia for the policy change to take effect:${NC}" + echo -e " ${CYAN}docker compose restart authelia${NC}" + else + echo -e "${RED}Failed to set policy. Please check errors above.${NC}" + # Optionally offer to restore backup here + fi + ;; + 3) + return 0 + ;; + *) + echo -e "${RED}Invalid choice. Please try again.${NC}" + ;; + esac + echo # Add a newline for better readability between menu iterations + done +} + + +################################################## +# PART 5: Authelia Account Management +################################################## + +# Function to generate a secure, random passphrase +generate_passphrase() { + # Generate a random passphrase using common words and numbers + local words=("apple" "banana" "orange" "grape" "melon" "cherry" "lemon" "peach" "plum" "kiwi" + "red" "blue" "green" "yellow" "purple" "cyan" "magenta" "white" "black" "grey" + "dog" "cat" "bird" "fish" "horse" "tiger" "lion" "bear" "wolf" "fox" + "river" "ocean" "mountain" "forest" "desert" "island" "valley" "canyon" "lake" "hill") + + local w1=${words[$((RANDOM % ${#words[@]}))]} + local w2=${words[$((RANDOM % ${#words[@]}))]} + local w3=${words[$((RANDOM % ${#words[@]}))]} + local num=$((1000 + RANDOM % 9000)) # 4-digit number + + echo "${w1^}${w2^}${w3^}${num}" # Capitalize first letter of each word +} + +manage_authelia_accounts() { + print_header "Authelia Account Management" + + local users_file="${CONFIG_ROOT:-.}/authelia/users_database.yml" + + if [ ! -f "$users_file" ]; then + echo -e "${RED}Error: users_database.yml not found at $users_file${NC}" + echo -e "${YELLOW}Would you like to create a new users database file? [y/N]:${NC}" + read -r answer + if [[ "$answer" =~ ^[Yy]$ ]]; then + cat > "$users_file" </dev/null) + + if [ -z "$password_hash" ]; then + echo -e "${RED}Error: Failed to generate password hash. Is the authelia container available?${NC}" + echo -e "${YELLOW}Trying direct docker run method...${NC}" + password_hash=$(docker run --rm authelia/authelia:latest authelia crypto hash generate argon2 --password "$password" 2>/dev/null) + + if [ -z "$password_hash" ]; then + echo -e "${RED}Error: Both methods failed to generate a password hash. Skipping user.${NC}" + continue + fi + fi + + password_hash=$(echo "$password_hash" | sed 's/^Digest: //') + + if [[ ! "$password_hash" =~ ^\$argon2id.*$ ]]; then + echo -e "${RED}Error: Generated hash does not have the expected format. Actual value:${NC}" + echo -e "${YELLOW}$password_hash${NC}" + echo -e "${RED}Skipping user creation.${NC}" + continue + fi + + echo -e "${GREEN}Password hash generated successfully.${NC}" + + if grep -q "^[[:space:]]*${username}:" "$users_file"; then + sed -i "/^[[:space:]]*${username}:/,/^[[:space:]]*[a-zA-Z0-9_-]\+:/ s/^/# UPDATED: /" "$users_file" + sed -i "0,/^# UPDATED: [[:space:]]*[a-zA-Z0-9_-]\+:/ s/^# UPDATED: //" "$users_file" + fi + + cat >> "$users_file" < ${NC} Set Authelia policy for a service (e.g., 'one_factor', 'bypass')." + echo -e " ${CYAN}cleanup${NC} Interactively clean up old backup files (.bak)." + echo -e " ${CYAN}all${NC} Run 'update-env', 'update-authelia', and 'update-services'." + echo -e " ${CYAN}help${NC} Show this help message." + echo -e "" + echo -e "${BLUE}Examples:${NC}" + echo -e " $0 update-authelia" + echo -e " $0 set-policy sonarr one_factor" + echo -e " $0 set-policy radarr bypass" + echo -e " $0 manage-policies" + echo -e " $0 all" + echo -e "" + echo -e "${YELLOW}Note:${NC} Some commands require Docker to be running and may restart containers." + echo -e "${YELLOW}Policy changes require an Authelia restart ('docker compose restart authelia').${NC}" +} + +# Check if any arguments were provided +if [ $# -eq 0 ]; then + show_help + exit 0 +fi + +# Process command line arguments +case "$1" in + update-env) + update_env_file + ;; + update-authelia) + update_authelia_config + ;; + update-services) + update_service_configs + ;; + manage-accounts) + manage_authelia_accounts # Interactive + ;; + manage-policies) + manage_authelia_policies # Interactive + ;; + list-policies) + list_authelia_services + ;; + set-policy) + if [ -z "$2" ] || [ -z "$3" ]; then + echo -e "${RED}Error: Service name and policy are required.${NC}" >&2 + echo -e "Usage: $0 set-policy ${NC}" >&2 + echo -e "Valid policies: one_factor, two_factor, bypass, deny" >&2 + exit 1 + fi + if ! check_file "$AUTHELIA_CONFIG"; then exit 1; fi + # Backup is handled within set_authelia_policy if needed + set_authelia_policy "$2" "$3" "$AUTHELIA_CONFIG" + if [ $? -eq 0 ]; then + echo -e "\n${YELLOW}Remember to restart Authelia for the policy change to take effect:${NC}" + echo -e " ${CYAN}docker compose restart authelia${NC}" + fi + ;; + cleanup) + cleanup_backups + ;; + all) + print_header "Running All Updates" + update_env_file + update_authelia_config + update_service_configs + echo -e "\n${GREEN}${BOLD}All core updates completed!${NC}" + echo -e "${BLUE}Review output for any required actions (e.g., setting new .env variables).${NC}" + echo -e "${BLUE}Consider running 'manage-accounts' if needed.${NC}" + echo -e "${YELLOW}Remember to restart relevant services or the full stack if necessary.${NC}" + ;; + help|-h|--help) + show_help + ;; + *) + echo -e "${RED}Unknown command: $1${NC}" >&2 + echo -e "${BLUE}Run '$0 help' for usage information.${NC}" >&2 + exit 1 + ;; +esac + +exit 0