From ab51bf3a661551f1c9126fac61562705bbd7fe1c Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Thu, 30 Apr 2026 11:11:54 +0200 Subject: [PATCH] fix: deploy-server.sh cleans up legacy posterg configs and prunes old xamxam backups --- TODO.md | 7 + app/storage/logs/form-submissions.log | 1 + docs/LDAP_AUTH_PLAN.md | 207 ++++++++++++++++++++++++++ scripts/deploy-server.sh | 24 +++ 4 files changed, 239 insertions(+) create mode 100644 app/storage/logs/form-submissions.log create mode 100644 docs/LDAP_AUTH_PLAN.md diff --git a/TODO.md b/TODO.md index c5a79be..5ab86e4 100644 --- a/TODO.md +++ b/TODO.md @@ -49,8 +49,15 @@ - [x] Update `app/migrations/run.php`, `app/tests/README.md`, `app/storage/README.md` - [x] Replace all remaining "Post-ERG" branding with "XAMXAM" (scripts, PHP source, schema, docs) - [x] `deploy-server.sh`: remove legacy `sites-enabled/posterg` symlink to fix duplicate `limit_req_zone` nginx error +- [x] `deploy-server.sh`: auto-migrate `.htpasswd-posterg` → `.htpasswd-xamxam` if new file absent - [x] Rename local `storage/posterg.db` → `storage/xamxam.db` +## LDAP auth migration (pending client access) + +- [ ] Get LDAP server hostname, port, service-account DN+password, base DN, user attr, group DN from client +- [ ] Verify TCP reachability from XAMXAM VM to LDAP server (port 636) +- [ ] See `docs/LDAP_AUTH_PLAN.md` for full phase-by-phase plan + ## CSS refactor - [x] Move semantic HTML element baseline styles into common.css diff --git a/app/storage/logs/form-submissions.log b/app/storage/logs/form-submissions.log new file mode 100644 index 0000000..131219b --- /dev/null +++ b/app/storage/logs/form-submissions.log @@ -0,0 +1 @@ +{"source":"partage","action":"submit","status":"success","thesis_id":15,"identifier":"2025-012","author":"Emma Renard","share_slug":"20260429-DZESJT6X","timestamp":"2026-04-30T09:20:16+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"} diff --git a/docs/LDAP_AUTH_PLAN.md b/docs/LDAP_AUTH_PLAN.md new file mode 100644 index 0000000..ee40ada --- /dev/null +++ b/docs/LDAP_AUTH_PLAN.md @@ -0,0 +1,207 @@ +# LDAP Authentication — Migration Plan + +## Context + +The admin panel currently uses a two-layer auth stack: + +1. **nginx `auth_basic`** — browser password prompt, credentials stored in + `/etc/nginx/.htpasswd-xamxam`, managed manually with `htpasswd`. +2. **PHP `AdminAuth`** — session guard with bcrypt hash stored in the SQLite + database (`site_settings.admin_password_hash`). + +The client runs an org-wide LDAP service already used for other internal tools. +The goal is to replace both layers with a single LDAP-backed PHP login, so that +staff use their existing org credentials and account lifecycle (onboarding, +offboarding, password resets) is handled centrally. + +**Chosen approach: Option 3 — PHP LDAP auth, nginx `auth_basic` removed.** +No nginx module compilation required. The existing `AdminAuth` session +architecture stays intact; only the credential-verification back-end changes. + +--- + +## Network prerequisite (blocker) + +XAMXAM runs in a VM that may not have direct TCP access to the LDAP server +(port 389 plain / port 636 LDAPS). This must be confirmed before any +implementation work starts. + +**Action required (client):** +- Confirm the LDAP server hostname / IP and port (prefer 636 LDAPS). +- Open a firewall rule from the XAMXAM VM to the LDAP server on that port. +- Provide a **read-only service-account** DN and password for the bind + (e.g. `cn=xamxam-svc,ou=services,dc=erg,dc=be`). This account only needs + permission to search the directory — never to write. +- Confirm the LDAP server type (OpenLDAP / Active Directory / 389-DS / other) + and the base DN for staff accounts (e.g. `ou=staff,dc=erg,dc=be`). +- Confirm the attribute that holds the login name (`uid` on OpenLDAP, + `sAMAccountName` on AD). +- Confirm whether a group membership check is required (i.e. only members of + `cn=xamxam-admins,ou=groups,dc=erg,dc=be` may log in), or whether any valid + staff account is sufficient. + +**Verify TCP reachability from the VM before writing any code:** +```bash +# On the XAMXAM server +nc -zv 636 # LDAPS (preferred) +nc -zv 389 # plain LDAP (fallback, only on a trusted LAN) +``` + +--- + +## TODO + +### Phase 1 — Server preparation + +- [ ] Confirm network access (see blocker above). +- [ ] Install the PHP LDAP extension on the server: + ```bash + sudo apt install php8.4-ldap + sudo systemctl restart php8.4-fpm + ``` +- [ ] Verify the extension loaded: + ```bash + php -m | grep ldap + ``` +- [ ] Store LDAP connection parameters in the database (`site_settings` table) + or in a server-side env file — **never in the repository**: + - `ldap_host` — e.g. `ldaps://ldap.erg.be` + - `ldap_port` — `636` + - `ldap_bind_dn` — service-account DN + - `ldap_bind_password` — service-account password + - `ldap_base_dn` — search base for user accounts + - `ldap_user_attr` — login attribute (`uid` / `sAMAccountName`) + - `ldap_group_dn` — (optional) required group DN; empty = no group check + +### Phase 2 — New `LdapAuth` class + +Create `app/src/LdapAuth.php`: + +``` +LdapAuth::verify(string $username, string $password): bool +``` + +Internal steps: +1. Load connection parameters from `Database::getSetting()`. +2. Open connection: `ldap_connect($host, $port)`. +3. Set options: `LDAP_OPT_PROTOCOL_VERSION = 3`, + `LDAP_OPT_REFERRALS = 0`, + `LDAP_OPT_NETWORK_TIMEOUT = 3` (fail fast — don't stall page loads). +4. Service-account bind: `ldap_bind($conn, $bind_dn, $bind_password)`. +5. Search for the user: + `ldap_search($conn, $base_dn, "($attr=$username)", ['dn'])`. +6. Extract the user DN from search results. +7. If group check is configured: verify membership with a second search + against the group DN before proceeding. +8. Attempt user bind with the supplied password: + `ldap_bind($conn, $user_dn, $password)` — this is the actual + credential verification; LDAP does the password check. +9. `ldap_unbind($conn)`. +10. Return `true` on success, `false` on any failure. + +Error handling: +- Catch `ldap_error()` / `ldap_errno()` on every step. +- Log failures to the PHP error log (never expose LDAP error strings to + the browser). +- On LDAP server unreachable: fail **closed** (deny access, show a + "service temporarily unavailable" message — do not fall through to a + bypass). + +### Phase 3 — Modify `AdminAuth` + +`AdminAuth` currently verifies credentials in two places: + +| Location | Change | +|---|---| +| `AdminAuth::login()` | Replace `password_verify($password, $hash)` with `LdapAuth::verify($username, $password)` | +| `AdminAuth::requireLogin()` — nginx Basic Auth passthrough (`$_SERVER['PHP_AUTH_PW']`) | Remove entirely (nginx `auth_basic` will be gone) | +| `AdminAuth::getStoredHash()` | Can be removed or kept as dead code path | +| `AdminAuth::setPasswordHash()` / `removePasswordHash()` | Retire (no longer used) | + +The session logic (`SESSION_KEY`, `session_regenerate_id`, cookie hardening, +`logout()`) is unchanged — it is auth-method-agnostic. + +The login form (`/admin/login.php`) gains a `username` field alongside +`password`. The `account.php` password-change page is retired (password +management happens in the LDAP directory, not here). + +### Phase 4 — Modify the login form + +`app/public/admin/login.php` and `app/templates/admin/login.php`: + +- Add `` before the password field. +- Remove the "change password" link (password is managed in LDAP). +- POST handler calls `AdminAuth::login($username, $password)` with both args. + +### Phase 5 — Remove nginx `auth_basic` + +In `nginx/xamxam.conf`, inside `location ^~ /admin/`: + +```nginx +# Remove these two lines: +auth_basic "Admin Access - XAMXAM"; +auth_basic_user_file /etc/nginx/.htpasswd-xamxam; +``` + +The rate-limiting zone (`limit_req zone=admin`) stays — it still guards +against brute-force on the PHP login form. + +Update `scripts/deploy-server.sh` and `scripts/manage-admin-users.sh` to +note that htpasswd management is no longer required. + +Clean up the server: +```bash +sudo rm /etc/nginx/.htpasswd-xamxam +``` + +### Phase 6 — Admin UI: retire password management page + +- Remove or repurpose `app/public/admin/account.php` and + `app/public/admin/actions/account.php`. +- Remove the "Compte" nav link from the admin header. +- The `site_settings` rows `admin_password_hash` can be left in the DB + (harmless) or cleared with a migration. + +### Phase 7 — Testing + +- [ ] LDAP server reachable from VM (Phase 1 smoke test). +- [ ] Valid staff credentials → session created, redirected to `/admin/`. +- [ ] Invalid password → denied, error shown, no session. +- [ ] Unknown username → denied (same error message — no username enumeration). +- [ ] LDAP server unreachable → denied with "service unavailable", not a + PHP fatal. +- [ ] Group check (if configured): non-member staff → denied. +- [ ] Session expiry / logout → redirected to login form. +- [ ] Brute-force: 20+ rapid login attempts → nginx rate limit kicks in (429). +- [ ] Verify `/etc/nginx/.htpasswd-xamxam` no longer exists on server. + +--- + +## What does NOT change + +- The PHP session layer (`AdminAuth::startSession`, `isAuthenticated`, + `logout`, cookie parameters) — untouched. +- The CSRF protection on all action handlers. +- The nginx rate-limiting zone for `/admin/`. +- All other nginx security rules (file blocking, security headers, etc.). +- The `just manage-admin-users` recipe can be removed from the justfile. + +--- + +## Security notes + +- **Use LDAPS (port 636) exclusively.** Plain LDAP on port 389 transmits + the user's password in cleartext on the wire. Even on a trusted LAN this + is not acceptable. +- **Service account must be read-only.** It must not have write permission + to any part of the directory. +- **Do not store the service-account password in the repository.** Use + `Database::setSetting()` (already encrypted at rest via filesystem + permissions) or an env variable set in the server environment. +- **Never log the user's password or the service-account password.** +- **Fail closed.** If `ldap_connect` or the service-account bind fails, deny + access. Do not fall back to a local password. +- **Sanitise the username** before using it in the LDAP filter: + escape special characters per RFC 4515 to prevent LDAP injection + (`(uid=*)(|(uid=*))`-style attacks). PHP's `ldap_escape()` with + `LDAP_ESCAPE_FILTER` flag handles this. diff --git a/scripts/deploy-server.sh b/scripts/deploy-server.sh index 9696554..3b0715b 100755 --- a/scripts/deploy-server.sh +++ b/scripts/deploy-server.sh @@ -48,6 +48,21 @@ chown -R www-data:xamxam /var/www/xamxam/storage/cache chmod -R 2775 /var/www/xamxam/storage/cache ok "Cache dirs: created and owned by www-data:xamxam" +# ── Step 1b: htpasswd file ────────────────────────────────────────────────────── +printf "\n📋 Step 1b: Checking htpasswd file...\n" +echo "--------------------------------------" + +if [ -f "/etc/nginx/.htpasswd-xamxam" ]; then + ok "htpasswd file exists: /etc/nginx/.htpasswd-xamxam" +elif [ -f "/etc/nginx/.htpasswd-posterg" ]; then + cp /etc/nginx/.htpasswd-posterg /etc/nginx/.htpasswd-xamxam + chmod 644 /etc/nginx/.htpasswd-xamxam + ok "Migrated .htpasswd-posterg → .htpasswd-xamxam" +else + warn "No htpasswd file found — admin panel will return 403 until one is created" + warn "Run: sudo htpasswd -c /etc/nginx/.htpasswd-xamxam " +fi + # ── Step 2: Nginx config ────────────────────────────────────────────────────── printf "\n📋 Step 2: Deploying nginx configuration...\n" echo "--------------------------------------------" @@ -72,6 +87,15 @@ if [ -L "/etc/nginx/sites-enabled/posterg" ]; then ok "Removed legacy sites-enabled/posterg symlink" fi +# Remove legacy posterg config and all its backups from sites-available +for f in /etc/nginx/sites-available/posterg /etc/nginx/sites-available/posterg.backup.*; do + [ -f "$f" ] && rm "$f" && ok "Removed legacy $f" +done + +# Keep only the 2 most recent xamxam backups, delete older ones +ls -t /etc/nginx/sites-available/xamxam.backup.* 2>/dev/null | tail -n +3 | xargs -r rm -- +ok "Pruned old xamxam config backups (kept 2 most recent)" + if [ ! -L "/etc/nginx/sites-enabled/xamxam" ]; then ln -s /etc/nginx/sites-available/xamxam /etc/nginx/sites-enabled/xamxam ok "Created sites-enabled symlink"