fix: deploy-server.sh cleans up legacy posterg configs and prunes old xamxam backups

This commit is contained in:
Pontoporeia
2026-04-30 11:11:54 +02:00
parent 68e30abb56
commit ab51bf3a66
4 changed files with 239 additions and 0 deletions

View File

@@ -49,8 +49,15 @@
- [x] Update `app/migrations/run.php`, `app/tests/README.md`, `app/storage/README.md` - [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] 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`: 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` - [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 ## CSS refactor
- [x] Move semantic HTML element baseline styles into common.css - [x] Move semantic HTML element baseline styles into common.css

View File

@@ -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"}

207
docs/LDAP_AUTH_PLAN.md Normal file
View File

@@ -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 <ldap-host> 636 # LDAPS (preferred)
nc -zv <ldap-host> 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 `<input type="text" name="username">` 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.

View File

@@ -48,6 +48,21 @@ chown -R www-data:xamxam /var/www/xamxam/storage/cache
chmod -R 2775 /var/www/xamxam/storage/cache chmod -R 2775 /var/www/xamxam/storage/cache
ok "Cache dirs: created and owned by www-data:xamxam" 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 <username>"
fi
# ── Step 2: Nginx config ────────────────────────────────────────────────────── # ── Step 2: Nginx config ──────────────────────────────────────────────────────
printf "\n📋 Step 2: Deploying nginx configuration...\n" printf "\n📋 Step 2: Deploying nginx configuration...\n"
echo "--------------------------------------------" echo "--------------------------------------------"
@@ -72,6 +87,15 @@ if [ -L "/etc/nginx/sites-enabled/posterg" ]; then
ok "Removed legacy sites-enabled/posterg symlink" ok "Removed legacy sites-enabled/posterg symlink"
fi 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 if [ ! -L "/etc/nginx/sites-enabled/xamxam" ]; then
ln -s /etc/nginx/sites-available/xamxam /etc/nginx/sites-enabled/xamxam ln -s /etc/nginx/sites-available/xamxam /etc/nginx/sites-enabled/xamxam
ok "Created sites-enabled symlink" ok "Created sites-enabled symlink"