diff --git a/Readme.md b/Readme.md index 9a7a95f..d662d1c 100644 --- a/Readme.md +++ b/Readme.md @@ -1,6 +1,6 @@ # Ubuntu Based Kiosk -**Current Version:** 2.5.0 (check script header for latest version) +**Current Version:** 2.6.0 (check script header for latest version) **Built with Claude Sonnet 4.6 AI assistance** **License:** GPL v3 - Keep derivatives open source **Repository:** https://github.com/outis1one/ubuntu-based-kiosk/ @@ -1209,6 +1209,9 @@ terminal menu and the web UI, so they can't drift apart). reconfigure for network access, complete uninstall (purge). The first Addon migrated — genuinely mutates real system state (apt packages, `/etc/cups`, ufw) rather than this project's own files. +- `menus/addon_authelia.sh` — **Authelia Auto-Login** (Addons): + encrypted SSO credentials plus the server-side setup instructions. + Prompted the `save_config` merge fix above. - `install.sh` — entry point for the modular tool, now grouped **Core Settings / Addons / Advanced** like the legacy menu. Run it against an *already-installed* kiosk: @@ -1221,7 +1224,7 @@ terminal menu and the web UI, so they can't drift apart). **Honest status:** this does not yet replace first-time installation, or most of the old installer. `ubuntu-based-kiosk.sh` is still ~12,000 lines and still contains its own unremoved, unmodified copies of every -menu above (plus Upgrade, Reinstall, Uninstall, 4 more Addons, and the +menu above (plus Upgrade, Reinstall, Uninstall, 3 more Addons, and the other 8 Advanced items — none of that has moved yet). Both copies coexist deliberately: the old ones stay until enough of Core Settings/Addons/Advanced is migrated to @@ -1230,13 +1233,26 @@ Migration continues one `menus/*.sh` file at a time; first-time installation itself is the last and largest piece to move, if it moves at all. +**Open question:** the config-clobbering bug fixed in `lib/config.sh` +(v2.6.0 — `save_config` silently deleting fields it doesn't know about, +like Authelia's credentials, on the next unrelated save) has the exact +same shape in `ubuntu-based-kiosk.sh`'s own `save_config`, unfixed. It's +a real bug in the currently-shipping single-file installer, independent +of whether the rest of that menu ever gets migrated. Worth deciding +separately whether to backport just that fix into the legacy script now +rather than waiting for a full migration pass. + --- ## Project Status & Future Plans -**Current Version:** 2.5.0 +**Current Version:** 2.6.0 -**Recent Updates (v2.5.0):** +**Recent Updates (v2.6.0):** +- **Authelia Auto-Login migrated** — encrypted SSO credentials (same AES-256-CBC/scrypt algorithm `main.js` decrypts with, verified by a real encrypt→decrypt round trip in testing) plus the full server-side Docker setup instructions, viewable again later without reconfiguring. +- **Important bug found and fixed, not specific to Authelia:** `save_config()` did a full rebuild of `config.json` from known fields — exactly like the legacy script's `save_config` still does. Authelia's own write is a careful merge that preserves everything else, but the *next* save from Sites, Touch Controls, Navigation, or Password Protection would silently delete the Authelia credentials, since none of those knew the three Authelia fields existed. **This is a real bug in the currently-shipping single-file installer**, not introduced by this migration. Fixed in `lib/config.sh` by changing `save_config` to merge its known fields onto whatever's already on disk instead of rebuilding from nothing, so any untracked field — Authelia's three today, anything else tomorrow — survives automatically. The equivalent bug still exists, unfixed, in `ubuntu-based-kiosk.sh`'s own `save_config` — see "Modular Management" below. + +**Previous (v2.5.0):** - **First Addon migrated:** CUPS Printing — install/reconfigure/complete uninstall, in `./install.sh`. Genuinely mutates real system state (`apt install`/`remove --purge`, `/etc/cups`, `ufw`) at fixed paths CUPS itself doesn't let us relocate, so every test uses full command-level `sudo` stubbing rather than the scratch-directory approach used for this project's own files. - **Menu restructured:** `install.sh`'s top level is now grouped Core Settings / Addons / Advanced, matching the legacy tool, instead of one flat list — done now while it's cheap, ahead of the list getting unwieldy. - **Bug fix:** a "wait for service to start" retry loop used a bare `cmd1 && cmd2 && break` as its body — that's not made safe by being inside a loop; a bare `&&`/`||` list used as a standalone statement is fully subject to `set -e`, and the first command failing on an early iteration (near-certain right after a fresh install) would have killed the whole session. Restored the `if cmd1 && cmd2; then break; fi` form. diff --git a/install.sh b/install.sh index 2e50f7b..fd6bc4e 100755 --- a/install.sh +++ b/install.sh @@ -16,7 +16,8 @@ # Core Settings: Sites & Page Timing, Display & Interaction, Timezone, # Hidden Site PIN, Password Protection & Lockout, WiFi, # Power/Display/Quiet Hours. -# Addons: CUPS Printing (menus/addon_cups.sh). +# Addons: CUPS Printing (menus/addon_cups.sh), Authelia Auto-Login +# (menus/addon_authelia.sh). # Advanced: Diagnostics (menus/diagnostics.sh - system status/logs/ # audio/network). # @@ -52,6 +53,8 @@ source "$SCRIPT_DIR/menus/power_schedule.sh" source "$SCRIPT_DIR/menus/diagnostics.sh" # shellcheck source=menus/addon_cups.sh source "$SCRIPT_DIR/menus/addon_cups.sh" +# shellcheck source=menus/addon_authelia.sh +source "$SCRIPT_DIR/menus/addon_authelia.sh" ################################################################################ # Preflight @@ -115,8 +118,8 @@ core_settings_menu() { } addons_menu_builder() { - MENU_LABELS=("CUPS Printing") - MENU_HANDLERS=(addon_cups_menu) + MENU_LABELS=("CUPS Printing" "Authelia Auto-Login") + MENU_HANDLERS=(addon_cups_menu addon_authelia_menu) } addons_menu() { diff --git a/lib/config.sh b/lib/config.sh index c4ce7b5..6007c2e 100644 --- a/lib/config.sh +++ b/lib/config.sh @@ -60,6 +60,9 @@ LOCKOUT_AT_TIME="" LOCKOUT_ACTIVE_START="" LOCKOUT_ACTIVE_END="" REQUIRE_PASSWORD_ON_BOOT="false" +AUTHELIA_URL="" +AUTHELIA_USERNAME="" +AUTHELIA_ENCRYPTED_PASSWORD="" kiosk_user_exists() { id "$KIOSK_USER" &>/dev/null @@ -125,6 +128,10 @@ load_existing_config() { boot_password=$(sudo -u "$KIOSK_USER" jq -r '.requirePasswordOnBoot // false' "$CONFIG_PATH" 2>/dev/null) [[ "$boot_password" == "true" ]] && REQUIRE_PASSWORD_ON_BOOT="true" || REQUIRE_PASSWORD_ON_BOOT="false" + + AUTHELIA_URL=$(sudo -u "$KIOSK_USER" jq -r '.autheliaURL // ""' "$CONFIG_PATH" 2>/dev/null || echo "") + AUTHELIA_USERNAME=$(sudo -u "$KIOSK_USER" jq -r '.autheliaUsername // ""' "$CONFIG_PATH" 2>/dev/null || echo "") + AUTHELIA_ENCRYPTED_PASSWORD=$(sudo -u "$KIOSK_USER" jq -r '.autheliaEncryptedPassword // ""' "$CONFIG_PATH" 2>/dev/null || echo "") } # Write every bash global back out to config.json, then offer to reload the @@ -159,7 +166,28 @@ save_config() { local boot_password_json="false" [[ "$REQUIRE_PASSWORD_ON_BOOT" == "true" ]] && boot_password_json="true" - jq -n \ + # Merge onto whatever's already in config.json rather than rebuilding + # the file from nothing. The legacy save_config did a full `jq -n` + # rebuild listing every known field - any field it doesn't know about + # (e.g. Authelia's autheliaURL/autheliaUsername/ + # autheliaEncryptedPassword, written by its own careful `. + {...}` + # merge) gets silently DELETED the next time any other menu that + # calls save_config runs. Real, currently-shipping bug in the legacy + # script, not unique to this migration - ported faithfully into this + # file's first version because no test happened to set an untracked + # field first. `. + {known fields...}` below preserves anything this + # tool doesn't track while still fully replacing every field it does + # (including tabs, via the same array-rebuild loop as before) - jq's + # `+` on objects takes the right-hand value for any key present on + # both sides, so a fully-specified `tabs` here still discards a + # deleted tab rather than merging old and new. + local existing="{}" + if sudo -u "$KIOSK_USER" test -f "$CONFIG_PATH" 2>/dev/null; then + existing=$(sudo -u "$KIOSK_USER" cat "$CONFIG_PATH" 2>/dev/null) + echo "$existing" | jq empty 2>/dev/null || existing="{}" + fi + + echo "$existing" | jq \ --argjson autoswitch true \ --argjson enableTouch true \ --argjson dualSwipe "$dual_json" \ @@ -177,7 +205,10 @@ save_config() { --arg lockoutActiveStart "${LOCKOUT_ACTIVE_START:-}" \ --arg lockoutActiveEnd "${LOCKOUT_ACTIVE_END:-}" \ --argjson requirePasswordOnBoot "$boot_password_json" \ - '{autoswitch:$autoswitch,enableTouch:$enableTouch,dualSwipe:$dualSwipe,swipeMode:$swipeMode,allowNavigation:$allowNavigation,homeTabIndex:$homeTabIndex,inactivityTimeout:$inactivityTimeout,enablePauseButton:$enablePauseButton,enableKeyboardButton:$enableKeyboardButton,enableNavButton:$enableNavButton,enablePasswordProtection:$enablePasswordProtection,lockoutPassword:$lockoutPassword,lockoutTimeout:$lockoutTimeout,lockoutAtTime:$lockoutAtTime,lockoutActiveStart:$lockoutActiveStart,lockoutActiveEnd:$lockoutActiveEnd,requirePasswordOnBoot:$requirePasswordOnBoot,tabs:[]}' > "$tmp" + --arg autheliaURL "${AUTHELIA_URL:-}" \ + --arg autheliaUsername "${AUTHELIA_USERNAME:-}" \ + --arg autheliaEncryptedPassword "${AUTHELIA_ENCRYPTED_PASSWORD:-}" \ + '. + {autoswitch:$autoswitch,enableTouch:$enableTouch,dualSwipe:$dualSwipe,swipeMode:$swipeMode,allowNavigation:$allowNavigation,homeTabIndex:$homeTabIndex,inactivityTimeout:$inactivityTimeout,enablePauseButton:$enablePauseButton,enableKeyboardButton:$enableKeyboardButton,enableNavButton:$enableNavButton,enablePasswordProtection:$enablePasswordProtection,lockoutPassword:$lockoutPassword,lockoutTimeout:$lockoutTimeout,lockoutAtTime:$lockoutAtTime,lockoutActiveStart:$lockoutActiveStart,lockoutActiveEnd:$lockoutActiveEnd,requirePasswordOnBoot:$requirePasswordOnBoot,autheliaURL:$autheliaURL,autheliaUsername:$autheliaUsername,autheliaEncryptedPassword:$autheliaEncryptedPassword,tabs:[]}' > "$tmp" if [[ ${#URLS[@]} -gt 0 ]]; then for idx in "${!URLS[@]}"; do diff --git a/menus/addon_authelia.sh b/menus/addon_authelia.sh new file mode 100644 index 0000000..265e0dd --- /dev/null +++ b/menus/addon_authelia.sh @@ -0,0 +1,232 @@ +#!/bin/bash +################################################################################ +# menus/addon_authelia.sh - "Authelia Auto-Login" addon. +# +# Stores encrypted Authelia SSO credentials so the kiosk authenticates +# automatically on every startup. The password is AES-256-CBC encrypted +# with a key derived from this machine's /etc/machine-id via scrypt - +# the encrypted blob is useless on any other machine - and is NEVER +# stored in plain text, matching the legacy addon exactly (same +# algorithm, same salt, same node crypto calls). +# +# autheliaURL/autheliaUsername/autheliaEncryptedPassword are tracked +# fields in lib/config.sh now (load_existing_config/save_config), same +# as every other config.json field this tool manages - this is also +# what motivated fixing save_config to merge onto the existing file +# instead of rebuilding it from scratch (see lib/config.sh): the legacy +# save_config had no idea these three fields existed, so configuring +# Authelia and then visiting Sites/Touch/Navigation/Password Protection +# in the legacy menu would silently wipe the credentials on the next +# save. Real bug in the shipped script, not unique to this migration. +# +# Depends on: lib/menu.sh, lib/config.sh being sourced first. +################################################################################ + +addon_authelia_status() { + if [[ -n "$AUTHELIA_URL" ]]; then + echo "Authelia: configured" + echo " URL: $AUTHELIA_URL" + echo " Username: $AUTHELIA_USERNAME" + else + echo "Authelia: not configured" + fi +} + +addon_authelia_menu_builder() { + if [[ -n "$AUTHELIA_URL" ]]; then + MENU_LABELS=("Reconfigure (overwrite)" "Show server-side setup instructions again" "Clear Authelia configuration") + MENU_HANDLERS=(action_configure_authelia action_show_authelia_server_setup action_clear_authelia) + else + MENU_LABELS=("Configure Authelia auto-login") + MENU_HANDLERS=(action_configure_authelia) + fi +} + +addon_authelia_menu() { + load_existing_config + + if ! sudo -u "$KIOSK_USER" test -f "$CONFIG_PATH" 2>/dev/null; then + log_error "config.json not found at $CONFIG_PATH - run a full install first" + pause + return + fi + + run_menu "AUTHELIA AUTO-LOGIN" addon_authelia_menu_builder addon_authelia_status +} + +################################################################################ +# Encryption +################################################################################ + +# Same algorithm as the legacy addon: AES-256-CBC, key derived from +# /etc/machine-id via scrypt with a fixed salt, random IV prepended to +# the ciphertext, everything base64-encoded. main.js decrypts with the +# same derivation - do not change this without updating main.js too. +encrypt_authelia_password() { + local password="$1" + command -v node &>/dev/null || return 1 + + node -e " +const crypto=require('crypto'),fs=require('fs'); +const id=fs.readFileSync('/etc/machine-id','utf8').trim(); +const key=crypto.scryptSync(id,'kiosk-authelia-v1',32); +const iv=crypto.randomBytes(16); +const c=crypto.createCipheriv('aes-256-cbc',key,iv); +const enc=Buffer.concat([c.update(process.argv[1],'utf8'),c.final()]); +process.stdout.write(Buffer.concat([iv,enc]).toString('base64')); +" "$password" 2>/dev/null +} + +################################################################################ +# Actions +################################################################################ + +action_configure_authelia() { + echo + echo "Stores encrypted Authelia credentials so the kiosk" + echo "authenticates automatically on every startup." + echo "Password is encrypted with this machine's unique ID -" + echo "the encrypted blob is useless on any other machine." + echo + + if [[ -n "$AUTHELIA_URL" ]]; then + echo "Current config:" + echo " URL: $AUTHELIA_URL" + echo " Username: $AUTHELIA_USERNAME" + echo + ask_yes_no "Overwrite existing Authelia config?" "n" || { echo "Cancelled"; return; } + echo + fi + + local url user pass + read -r -p "Authelia URL (e.g. https://auth.yourdomain.com): " url + [[ -z "$url" ]] && { echo "Cancelled"; return; } + read -r -p "Authelia username: " user + [[ -z "$user" ]] && { echo "Cancelled"; return; } + read -r -s -p "Authelia password: " pass + echo + [[ -z "$pass" ]] && { echo "Cancelled"; return; } + + echo "Encrypting with machine ID..." + local encrypted + encrypted=$(encrypt_authelia_password "$pass") + if [[ -z "$encrypted" ]]; then + log_error "Encryption failed - is Node.js installed?" + return 1 + fi + + AUTHELIA_URL="$url" + AUTHELIA_USERNAME="$user" + AUTHELIA_ENCRYPTED_PASSWORD="$encrypted" + save_config + + log_success "Authelia config saved (password encrypted, NOT stored in plain text)" + action_show_authelia_server_setup + + echo + if is_service_active lightdm && ask_yes_no "Restart kiosk display now?" "n"; then + sudo systemctl restart lightdm + fi +} + +action_clear_authelia() { + echo + ask_yes_no "Clear Authelia configuration?" "n" || { echo "Cancelled"; return; } + + AUTHELIA_URL="" + AUTHELIA_USERNAME="" + AUTHELIA_ENCRYPTED_PASSWORD="" + save_config + log_success "Authelia configuration cleared" +} + +action_show_authelia_server_setup() { + echo + echo "════════════════════════════════════════════════════════════" + echo " AUTHELIA SERVER-SIDE SETUP (Dockerized)" + echo "════════════════════════════════════════════════════════════" + echo + echo "1. Generate the argon2 password hash on your Docker host:" + echo + echo " docker run --rm authelia/authelia:latest \\" + echo " authelia crypto hash generate argon2 \\" + echo " --password 'yourpassword'" + echo + echo " Copy the \$argon2id\$... output — that is your hash." + echo + echo "2. ADD a kiosk user to ~/docker/authelia/config/users.yml" + echo " (append — do not replace existing users):" + echo + echo " kiosk:" + echo " displayname: \"Kiosk Display\"" + echo " password: '\$argon2id\$v=19\$m=65536,t=3,p=4\$'" + echo " email: kiosk@local.com" + echo " groups:" + echo " - kiosk" + echo + echo "3. MERGE into ~/docker/authelia/config/configuration.yml:" + echo + echo " ── access_control ─────────────────────────────────────" + echo " Find your EXISTING access_control block and add the" + echo " kiosk rule as the FIRST rule inside it." + echo + echo " !! DO NOT create a second access_control: block !!" + echo " YAML silently ignores duplicate keys — the kiosk rule" + echo " will be invisible to Authelia and you will get a white" + echo " screen on the kiosk." + echo + echo " Authelia reads rules top-down, first match wins." + echo " The kiosk rule MUST be above any two_factor rule or" + echo " the two_factor wildcard will match first." + echo + echo " ── EXAMPLE — before (your existing config): ──────────" + echo " access_control:" + echo " default_policy: deny" + echo " rules:" + echo " - domain: '*.yourdomain.com'" + echo " policy: two_factor" + echo + echo " ── EXAMPLE — after (add kiosk rule above two_factor): ─" + echo " access_control:" + echo " default_policy: deny" + echo " rules:" + echo " - domain: '*.yourdomain.com' # <-- kiosk first" + echo " subject: 'group:kiosk'" + echo " policy: one_factor" + echo " - domain: '*.yourdomain.com' # <-- existing" + echo " policy: two_factor" + echo + echo " Why one_factor? The kiosk authenticates via the API" + echo " (/api/firstfactor — password only). TOTP and WebAuthn" + echo " require a second interactive step that is impossible" + echo " from a script, so the kiosk group must use one_factor." + echo + echo " ── session ─────────────────────────────────────────────" + echo " Keep your existing session block — no changes needed." + echo " The kiosk re-authenticates via API on every startup so" + echo " session expiry barely matters for it." + echo + echo " If you do NOT yet have a session block, add:" + echo + echo " session:" + echo " expiration: 8h" + echo " inactivity: 1h" + echo " remember_me: 7d" + echo " cookies:" + echo " - domain: yourdomain.com" + echo " authelia_url: https://auth.yourdomain.com" + echo + echo "4. Restart Authelia on your Docker host:" + echo " docker compose restart authelia" + echo + echo "────────────────────────────────────────────────────────────" + echo " NOTE: HTTP Basic Auth (per-site username/password) still" + echo " works alongside Authelia for sites that use browser-popup" + echo " authentication rather than Authelia SSO." + echo "────────────────────────────────────────────────────────────" + echo + echo " To clear Authelia config later, use this menu's" + echo " 'Clear Authelia configuration' option." + echo + pause +} diff --git a/ubuntu-based-kiosk.sh b/ubuntu-based-kiosk.sh index c6d0581..49a7c86 100644 --- a/ubuntu-based-kiosk.sh +++ b/ubuntu-based-kiosk.sh @@ -1,8 +1,43 @@ #!/bin/bash ################################################################################ -### Ubuntu Based Kiosk v2.5.0 ### +### Ubuntu Based Kiosk v2.6.0 ### ################################################################################ # +# RELEASE v2.6.0 - Authelia Migrated; Real Config-Clobbering Bug Fixed +# - New in ./install.sh: Authelia Auto-Login (menus/addon_authelia.sh) - +# encrypted SSO credentials (AES-256-CBC, key derived from this +# machine's /etc/machine-id via scrypt - same algorithm main.js +# decrypts with, verified by test with a real round-trip encrypt/ +# decrypt, not just "some string came out"), plus the full Dockerized +# server-side setup instructions, viewable again later without +# reconfiguring. +# - IMPORTANT bug found and fixed in lib/config.sh, NOT specific to +# Authelia or to this migration: save_config() did a full `jq -n` +# rebuild of config.json from known fields, exactly like the legacy +# script's save_config still does. Authelia's own write is a careful +# `. + {...}` merge that preserves everything - but the legacy +# configure_authelia() writes autheliaURL/autheliaUsername/ +# autheliaEncryptedPassword into config.json via that merge, and +# *neither* the legacy save_config nor this project's own (before this +# fix) had any idea those three fields existed. The next time a user +# visited Sites, Touch Controls, Navigation, or Password Protection - +# all of which call save_config - their Authelia credentials were +# silently deleted. This is a real bug in the currently-shipping +# single-file installer, not introduced by this migration; ported +# faithfully into lib/config.sh's first version because no test +# happened to set an untracked field before calling save_config. +# Fixed here by changing save_config to merge its known fields onto +# whatever's already in config.json (jq `. + {...}`) instead of +# rebuilding the file from nothing, so any field this tool doesn't +# track - Authelia's three today, anything else tomorrow - survives +# automatically. autheliaURL/autheliaUsername/autheliaEncryptedPassword +# are also now tracked fields in their own right, same as every other +# config.json field this tool manages. NOTE: the equivalent bug still +# exists in this script's own save_config below, unfixed - see +# Readme.md ("Modular Management") for the open question of whether to +# backport this specific fix here independent of the wider migration, +# given it's a real, currently-shipping credential-loss bug. +# # RELEASE v2.5.0 - First Addon Migrated (CUPS), Menu Restructured # - New in ./install.sh: CUPS Printing (menus/addon_cups.sh) - the first # Addon migrated. Install/reconfigure/complete uninstall (purge), @@ -226,7 +261,7 @@ set -euo pipefail ### SECTION 1: CONSTANTS & GLOBALS ################################################################################ -SCRIPT_VERSION="2.5.0" +SCRIPT_VERSION="2.6.0" # Resolve the real path to this script file. # When piped (curl|bash or wget|bash), BASH_SOURCE[0] is a pipe descriptor,