mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-09-25 01:53:03 +02:00
Add sso-diagnose.sh: verify all peertube-sso-incident claims into a log
- docs: record the open identity-forwarding question, auth contracts, and responsibility boundary
This commit is contained in:
@@ -61,6 +61,9 @@ app/public/assets/dist/
|
|||||||
# creds-test probe log (may contain error traces — no secrets, but generated per-run)
|
# creds-test probe log (may contain error traces — no secrets, but generated per-run)
|
||||||
creds-test.log
|
creds-test.log
|
||||||
|
|
||||||
|
# sso-diagnose output log (generated per-run)
|
||||||
|
sso-diagnose.log
|
||||||
|
|
||||||
# PHPUnit
|
# PHPUnit
|
||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
coverage/
|
coverage/
|
||||||
|
|||||||
@@ -58,6 +58,8 @@
|
|||||||
- [x] [Diagnosed & reproduced] Single LDAP credential (`xamxam@erg.be`). **Both** mail and PeerTube are now on `portail.erg.school` (LemonLDAP OIDC) SSO — verified: mail.erg.school advertises SMTP `XOAUTH2`/`OAUTHBEARER` after STARTTLS. The difference: mail kept `PLAIN`/`LOGIN` (additive migration) so the app's `PLAIN` auth still works; PeerTube *removed* the `password` grant (hard cutover) so `grant_type=password` → `invalid_grant`. Not fixable with any credential; needs admin to restore password grant, register OIDC client, or (best) set up `authorization_code`+`refresh_token`. Full report in docs/peertube-sso-incident.md.
|
- [x] [Diagnosed & reproduced] Single LDAP credential (`xamxam@erg.be`). **Both** mail and PeerTube are now on `portail.erg.school` (LemonLDAP OIDC) SSO — verified: mail.erg.school advertises SMTP `XOAUTH2`/`OAUTHBEARER` after STARTTLS. The difference: mail kept `PLAIN`/`LOGIN` (additive migration) so the app's `PLAIN` auth still works; PeerTube *removed* the `password` grant (hard cutover) so `grant_type=password` → `invalid_grant`. Not fixable with any credential; needs admin to restore password grant, register OIDC client, or (best) set up `authorization_code`+`refresh_token`. Full report in docs/peertube-sso-incident.md.
|
||||||
- [x] Built `scripts/creds-test.sh` (gum UI) + `scripts/creds-probe.php` (PHP probe) + `just creds-test`, + public `PeerTubeService::probeAuth()` to isolate auth-vs-channel. Probes SMTP AUTH and PeerTube password grant with the stored creds; logs results (never the password). Added `probeAuth` to PeerTubeService.
|
- [x] Built `scripts/creds-test.sh` (gum UI) + `scripts/creds-probe.php` (PHP probe) + `just creds-test`, + public `PeerTubeService::probeAuth()` to isolate auth-vs-channel. Probes SMTP AUTH and PeerTube password grant with the stored creds; logs results (never the password). Added `probeAuth` to PeerTubeService.
|
||||||
- [x] Built `scripts/app-token.sh` (gum) + `just app-token` to test whether a long-lived PeerTube app token (client_credentials grant) is obtainable. Confirmed live: `client_credentials` is rejected for the built-in local client (unsupported_grant_type) → an admin must run PeerTube's create-client on the server first. OIDC presence not determinable anonymously.
|
- [x] Built `scripts/app-token.sh` (gum) + `just app-token` to test whether a long-lived PeerTube app token (client_credentials grant) is obtainable. Confirmed live: `client_credentials` is rejected for the built-in local client (unsupported_grant_type) → an admin must run PeerTube's create-client on the server first. OIDC presence not determinable anonymously.
|
||||||
|
- [x] Add `scripts/sso-diagnose.sh` (+ `just sso-diagnose`) to verify every claim in docs/peertube-sso-incident.md — DNS/rDNS, IdP discovery (grant types), PeerTube OAuth/password-grant, SMTP AUTH mechanisms, identity-header propagation — appended to `sso-diagnose.log` (never logs secrets). Fixed the identity-gap (❓): response headers cannot reveal the post-auth header LemonLDAP injects INTO the request; added a request-side echo probe (`--echo <url>` + `scripts/echo-headers.php`) to be hosted *behind* the same LemonLDAP vhost, plus a `server.REMOTE_USER` mirror.
|
||||||
|
- [x] Update docs/peertube-sso-incident.md with the open "❓" (what identity LemonLDAP forwards to PeerTube), the three possible post-migration auth contracts + copy-paste admin ask, an explicit responsibility boundary (no zero-blame), and the new `sso-diagnose.sh` / `echo-headers.php` tools.
|
||||||
- [ ] Consider decoupling PeerTube credentials from SMTP settings (separate peertube username/password fields in the admin) so mail SSO changes don't silently break uploads
|
- [ ] Consider decoupling PeerTube credentials from SMTP settings (separate peertube username/password fields in the admin) so mail SSO changes don't silently break uploads
|
||||||
- [ ] (Superseded) The earlier `client_credentials` app-token path is **ruled out**: idP `portail.erg.school` (LemonLDAP::NG OIDC) only supports `authorization_code` + `refresh_token`, no `password`/`client_credentials`/device flow. Real host is `portail.erg.school` (not `.be`). Topology: videos.erg.be (PeerTube, Belgacom ADSL), mail.erg.school (Mailcow), portail.erg.school (LemonLDAP SSO). No self-service OIDC client registration.
|
- [ ] (Superseded) The earlier `client_credentials` app-token path is **ruled out**: idP `portail.erg.school` (LemonLDAP::NG OIDC) only supports `authorization_code` + `refresh_token`, no `password`/`client_credentials`/device flow. Real host is `portail.erg.school` (not `.be`). Topology: videos.erg.be (PeerTube, Belgacom ADSL), mail.erg.school (Mailcow), portail.erg.school (LemonLDAP SSO). No self-service OIDC client registration.
|
||||||
- [ ] Likely code change: PeerTubeService::obtainToken() switch to OIDC `authorization_code`+`refresh_token` against https://portail.erg.school/oauth2/token — requires admin to register an OIDC client + one-time interactive login to seed a refresh_token.
|
- [ ] Likely code change: PeerTubeService::obtainToken() switch to OIDC `authorization_code`+`refresh_token` against https://portail.erg.school/oauth2/token — requires admin to register an OIDC client + one-time interactive login to seed a refresh_token.
|
||||||
@@ -270,6 +270,53 @@ EOF
|
|||||||
> password/LDAP grant for the `xamxam@erg.be` account on PeerTube, **or** register
|
> password/LDAP grant for the `xamxam@erg.be` account on PeerTube, **or** register
|
||||||
> an OIDC client for this app and let us use `authorization_code` + `refresh_token`.
|
> an OIDC client for this app and let us use `authorization_code` + `refresh_token`.
|
||||||
|
|
||||||
|
### The open question to put to the admins (the "❓")
|
||||||
|
|
||||||
|
Every fact above is **verifiable from the outside** with `sso-diagnose.sh`. The
|
||||||
|
one thing it cannot establish remotely is **what authentication information
|
||||||
|
LemonLDAP actually forwards to PeerTube after it authenticates a user**. That is
|
||||||
|
infrastructure-side configuration, and it is the one remaining unknown.
|
||||||
|
|
||||||
|
Three architectures are possible after the migration; the admins must decide
|
||||||
|
which one is the *intended* contract for an external app hitting the PeerTube API:
|
||||||
|
|
||||||
|
1. **Browser/user SSO** (PeerTube is an OIDC client of LemonLDAP) — the app
|
||||||
|
would use the IdP's `authorization_code` + `refresh_token` (interactive, needs
|
||||||
|
a one-time human login + a registered OIDC client).
|
||||||
|
2. **Machine-to-machine** (direct PeerTube API) — needs a non-interactive
|
||||||
|
mechanism (a PeerTube service account / API token), because the IdP does **not**
|
||||||
|
advertise `client_credentials`.
|
||||||
|
3. **Reverse-proxy SSO** (LemonLDAP authenticates, forwards identity headers,
|
||||||
|
PeerTube trusts them) — needs LemonLDAP to inject the identity (e.g.
|
||||||
|
`Auth-User`/`X-Remote-User`) **and** PeerTube configured to consume it.
|
||||||
|
|
||||||
|
`request-side identity header probe → SKIPPED` in the diagnostic means we have
|
||||||
|
**not yet confirmed** that (3) is even wired up — i.e. that PeerTube receives
|
||||||
|
*who* LemonLDAP authenticated. That is the next thing to pin down.
|
||||||
|
|
||||||
|
Copy-paste ask for the admins:
|
||||||
|
|
||||||
|
> `videos.erg.be` is now behind LemonLDAP::NG; its OIDC discovery advertises only
|
||||||
|
> `authorization_code` and `refresh_token` (no `password`, no
|
||||||
|
> `client_credentials`). Could you confirm what mechanism external applications
|
||||||
|
> are now supposed to use to authenticate to the PeerTube API — (1) an OIDC
|
||||||
|
> authorization-code flow through `portail.erg.school`, (2) a forwarded
|
||||||
|
> authenticated-user identity (and if so, which header), or (3) a separate
|
||||||
|
> non-interactive service account / API token? And can you verify that the
|
||||||
|
> LemonLDAP-protected `videos.erg.be` vhost forwards the authenticated identity
|
||||||
|
> to the PeerTube backend as intended?
|
||||||
|
|
||||||
|
### Responsibility boundary (no zero-blame claim)
|
||||||
|
|
||||||
|
The *trigger* is unambiguously admin/infrastructure-side: the IdP removed the
|
||||||
|
`password` grant the app used, while the credential itself stayed valid (SMTP
|
||||||
|
still authenticates). But the app is **not** blameless in design: it coupled
|
||||||
|
PeerTube auth to the SMTP password and the `password` grant, so an IdP-side change
|
||||||
|
surfaces here. And **if** the app must keep speaking to PeerTube, it *will* need
|
||||||
|
code changes — to implement whichever supported grant the admins specify. Those
|
||||||
|
changes cannot be written until the admins settle the new authentication contract,
|
||||||
|
which is the blocking unknown above.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Tooling produced during this investigation
|
## 5. Tooling produced during this investigation
|
||||||
@@ -279,11 +326,17 @@ EOF
|
|||||||
| `scripts/creds-probe.php` | Reads stored creds, probes SMTP AUTH + PeerTube password grant, prints JSON |
|
| `scripts/creds-probe.php` | Reads stored creds, probes SMTP AUTH + PeerTube password grant, prints JSON |
|
||||||
| `scripts/creds-test.sh` | gum UI wrapper; logs results (never the password); `just creds-test` |
|
| `scripts/creds-test.sh` | gum UI wrapper; logs results (never the password); `just creds-test` |
|
||||||
| `scripts/app-token.sh` | gum probe for the long-lived `client_credentials` app-token path; `just app-token` |
|
| `scripts/app-token.sh` | gum probe for the long-lived `client_credentials` app-token path; `just app-token` |
|
||||||
|
| `scripts/sso-diagnose.sh` | Verifies every claim above (DNS/rDNS, IdP discovery, PeerTube OAuth + password grant, SMTP AUTH, identity-header propagation) into `sso-diagnose.log`; `just sso-diagnose` |
|
||||||
|
| `scripts/echo-headers.php` | Request-side echo endpoint to reveal the identity header LemonLDAP injects; pair with `--echo <url>` |
|
||||||
| `PeerTubeService::probeAuth()` | Public helper isolating token issuance from channel resolution |
|
| `PeerTubeService::probeAuth()` | Public helper isolating token issuance from channel resolution |
|
||||||
|
|
||||||
All live runs are reproducible:
|
All live runs are reproducible:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
just creds-test # proves SMTP ok, PeerTube invalid_grant
|
just creds-test # proves SMTP ok, PeerTube invalid_grant
|
||||||
just app-token # proves client_credentials rejected for the local client
|
just app-token # proves client_credentials rejected for the local client
|
||||||
|
just sso-diagnose # full reproducible report → sso-diagnose.log (no secrets)
|
||||||
|
# the one remaining unknown (who LemonLDAP forwards to PeerTube) needs
|
||||||
|
# scripts/echo-headers.php hosted behind the SAME LemonLDAP vhost:
|
||||||
|
# bash scripts/sso-diagnose.sh --echo 'https://videos.erg.be/path/to/echo-headers.php'
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -580,6 +580,13 @@ app-token:
|
|||||||
# --instance https://videos.erg.be --client <id> --secret <secret>
|
# --instance https://videos.erg.be --client <id> --secret <secret>
|
||||||
@bash scripts/app-token.sh
|
@bash scripts/app-token.sh
|
||||||
|
|
||||||
|
[group('utils')]
|
||||||
|
sso-diagnose:
|
||||||
|
# Verify every claim in docs/peertube-sso-incident.md (DNS, IdP discovery,
|
||||||
|
# PeerTube OAuth, SMTP AUTH, header propagation) and append to sso-diagnose.log.
|
||||||
|
# --instance <url> --idp <host> --smtp <host:port> --db <path> --log <path>
|
||||||
|
@bash scripts/sso-diagnose.sh
|
||||||
|
|
||||||
[group('utils')]
|
[group('utils')]
|
||||||
clean:
|
clean:
|
||||||
@rm -f app/error.log
|
@rm -f app/error.log
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* echo-headers.php — request-side identity probe for the SSO diagnosis.
|
||||||
|
*
|
||||||
|
* The ONLY reliable way to see the identity header LemonLDAP injects into the
|
||||||
|
* request IT forwards to the backend is to place an endpoint *behind* the same
|
||||||
|
* reverse proxy (same vhost as PeerTube) and have it echo the request headers.
|
||||||
|
* Response headers on the public site can never reveal this.
|
||||||
|
*
|
||||||
|
* Deploy this file anywhere served through the SAME LemonLDAP vhost that
|
||||||
|
* protects videos.erg.be (e.g. a static location, or a tiny PHP handler on the
|
||||||
|
* backend), authenticate at portail.erg.school, then hit it — it returns JSON
|
||||||
|
* of every inbound header. Forward the resulting URL to sso-diagnose.sh:
|
||||||
|
*
|
||||||
|
* scripts/sso-diagnose.sh --echo 'https://videos.erg.be/path/to/echo-headers.php'
|
||||||
|
*
|
||||||
|
* Output (subset):
|
||||||
|
* {
|
||||||
|
* "headers": { "X-Remote-User": "jsmith", "Auth-User": "jsmith", ... },
|
||||||
|
* "server": { "REMOTE_USER": "...", ... }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* It also mirrors the CGI subprocess environment (REMOTE_USER, etc.), which is
|
||||||
|
* where Apache/LemonLDAP often land the identity. Safe: emits NO secret — only
|
||||||
|
* the incoming headers it was handed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Cache-Control: no-store');
|
||||||
|
|
||||||
|
$headers = [];
|
||||||
|
foreach ($_SERVER as $k => $v) {
|
||||||
|
if (str_starts_with($k, 'HTTP_')) {
|
||||||
|
$name = str_replace('_', '-', substr($k, 5));
|
||||||
|
$headers[$name] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also surface the classic CGI environment identity vars directly.
|
||||||
|
$server = [
|
||||||
|
'REMOTE_USER' => $_SERVER['REMOTE_USER'] ?? null,
|
||||||
|
'AUTH_USER' => $_SERVER['AUTH_USER'] ?? null,
|
||||||
|
'PHP_AUTH_USER' => $_SERVER['PHP_AUTH_USER'] ?? null,
|
||||||
|
'REDIRECT_REMOTE_USER' => $_SERVER['REDIRECT_REMOTE_USER'] ?? null,
|
||||||
|
];
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'headers' => $headers,
|
||||||
|
'server' => $server,
|
||||||
|
'remote_addr' => $_SERVER['REMOTE_ADDR'] ?? null,
|
||||||
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||||
Executable
+291
@@ -0,0 +1,291 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# sso-diagnose.sh — verify every claim in docs/peertube-sso-incident.md
|
||||||
|
#
|
||||||
|
# Checks the topology + auth behaviour end-to-end and appends a timestamped,
|
||||||
|
# machine-parseable block to a log for later analysis. NO secret is written to
|
||||||
|
# the log or stdout: SMTP AUTH is tested (proves the credential is still valid)
|
||||||
|
# but the password is never echoed, and any PeerTube token/secret is redacted.
|
||||||
|
#
|
||||||
|
# What it verifies (mirrors the incident report):
|
||||||
|
# 1. DNS + reverse-DNS + HTTP identity of the three hosts
|
||||||
|
# (videos.erg.be, mail.erg.school, portail.erg.school)
|
||||||
|
# 2. The SSO IdP = LemonLDAP::NG OIDC, and its discovery document
|
||||||
|
# (grant/response types — no `password`, no `client_credentials`)
|
||||||
|
# 3. PeerTube's OAuth clients (the built-in `local` client) and the
|
||||||
|
# `password` grant result (`invalid_grant` expected)
|
||||||
|
# 4. SMTP STARTTLS AUTH mechanisms (XOAUTH2/OAUTHBEARER vs PLAIN/LOGIN)
|
||||||
|
# 5. PreserveHost / identity-header propagation between the proxy and backend
|
||||||
|
# (REMOTE_USER / X-Remote-User / X-Forwarded-User / Auth-User headers)
|
||||||
|
# — response-side + an optional request-side echo probe (--echo <url>) that
|
||||||
|
# uses scripts/echo-headers.php to reveal headers the SSO proxy injects.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/sso-diagnose.sh [--instance <url>] [--idp <url>] [--smtp <host:port>]
|
||||||
|
# [--mail-host <host>] [--db <path>] [--log <path>]
|
||||||
|
# [--echo <url>]
|
||||||
|
#
|
||||||
|
# Exit 0 always (a diagnostic records results); individual findings carry
|
||||||
|
# PASS/FAIL/INFO in the log so analysis is unambiguous.
|
||||||
|
#
|
||||||
|
# NOTE: SMTP creds + password come from the xamxam DB ONLY if creds-probe.php
|
||||||
|
# exists and can run; the plaintext password is NEVER printed — only the fact
|
||||||
|
# that AUTH succeeded/failed. If you want PeerTube's password-grant probe to run
|
||||||
|
# you must run `just creds-test` (it needs the password to make the request).
|
||||||
|
# =============================================================================
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
# ── defaults ───────────────────────────────────────────────────────────────────
|
||||||
|
INSTANCE="${INSTANCE:-https://videos.erg.be}"
|
||||||
|
IDP_HOST="${IDP_HOST:-portail.erg.school}"
|
||||||
|
SMTP_HOST="${SMTP_HOST:-mail.erg.school}"
|
||||||
|
SMTP_PORT="${SMTP_PORT:-587}"
|
||||||
|
DB_PATH="${DB_PATH:-$REPO_ROOT/app/storage/xamxam.db}"
|
||||||
|
LOG="${LOG:-$REPO_ROOT/sso-diagnose.log}"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--instance) INSTANCE="$2"; shift 2 ;;
|
||||||
|
--idp) IDP_HOST="$2"; shift 2 ;;
|
||||||
|
--smtp) SMTP_HOST="$2"; shift 2 ;;
|
||||||
|
--mail-host) SMTP_HOST="$2"; shift 2 ;;
|
||||||
|
--db) DB_PATH="$2"; shift 2 ;;
|
||||||
|
--log) LOG="$2"; shift 2 ;;
|
||||||
|
--echo) ECHO_URL="$2"; shift 2 ;;
|
||||||
|
-h|--help)
|
||||||
|
echo "Usage: scripts/sso-diagnose.sh [--instance <url>] [--idp <host>] [--smtp <host:port>] [--db <path>] [--log <path>] [--echo <url>]"
|
||||||
|
exit 0 ;;
|
||||||
|
*) echo "Unknown arg: $1" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
IDP_URL="https://${IDP_HOST}"
|
||||||
|
|
||||||
|
# ── log plumbing (machine-parseable) ──────────────────────────────────────────
|
||||||
|
log() { printf '%s\n' "$*" | tee -a "$LOG"; }
|
||||||
|
section() { log " ── $*"; }
|
||||||
|
kv() { log " %-28s %s" "$1:" "$2"; }
|
||||||
|
|
||||||
|
# redact anything that looks like a secret/token from a value before logging
|
||||||
|
redact() {
|
||||||
|
local v="$1"
|
||||||
|
v="${v//$'\n'/ | }"
|
||||||
|
# strip obvious bearer/oauth secrets & base64-ish blobs
|
||||||
|
sed -E 's/(["'\''A-Za-z0-9_.\/+-]{24,})/<REDACTED>/g' <<<"$v"
|
||||||
|
}
|
||||||
|
|
||||||
|
log ""
|
||||||
|
log "════════════════════════════════════════════════════════════════════"
|
||||||
|
log "XAMXAM SSO/PeerTube diagnosis — $(date '+%Y-%m-%d %H:%M:%S %z')"
|
||||||
|
log "════════════════════════════════════════════════════════════════════"
|
||||||
|
|
||||||
|
# ── preflight ─────────────────────────────────────────────────────────────────
|
||||||
|
for c in curl jq dig host; do
|
||||||
|
command -v "$c" >/dev/null || { log " MISSING-DEP $c"; }
|
||||||
|
done
|
||||||
|
|
||||||
|
RESOLVE_TOOL="getent"
|
||||||
|
command -v getent >/dev/null || RESOLVE_TOOL="host"
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# 1. DNS + reverse DNS + HTTP identity (incident §3.5)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
section "topology / DNS / reverse-DNS / HTTP identity"
|
||||||
|
|
||||||
|
hosts=("videos.erg.be" "$SMTP_HOST" "$IDP_HOST")
|
||||||
|
for h in "${hosts[@]}"; do
|
||||||
|
if [[ "$RESOLVE_TOOL" == "getent" ]]; then
|
||||||
|
ips="$(getent ahostsv4 "$h" 2>/dev/null | awk '{print $1}' | sort -u | head -3 | tr '\n' ' ')"
|
||||||
|
else
|
||||||
|
ips="$(host -t A "$h" 2>/dev/null | awk '/has address/{print $4}' | sort -u | head -3 | tr '\n' ' ')"
|
||||||
|
fi
|
||||||
|
ips="${ips// /}"
|
||||||
|
if [[ -z "$ips" ]]; then
|
||||||
|
kv "DNS[$h]" "NO-A-RECORD"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
kv "DNS[$h]" "$(echo "$ips" | tr ' ' ',')"
|
||||||
|
first_ip="$(echo "$ips" | awk '{print $1}')"
|
||||||
|
|
||||||
|
# reverse DNS
|
||||||
|
if command -v dig >/dev/null; then
|
||||||
|
rdns="$(dig +short -x "$first_ip" 2>/dev/null | tr '\n' ' ' | sed 's/[[:space:]]*$//')"
|
||||||
|
else
|
||||||
|
rdns="$(host "$first_ip" 2>/dev/null | awk '/pointer/{print $NF}' | sed 's/\.$//')"
|
||||||
|
fi
|
||||||
|
kv "rDNS[$first_ip]" "$(redact "${rdns:-none}")"
|
||||||
|
done
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# 2. SSO IdP discovery document (incident §3.5 — decisive fact)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
section "SSO IdP discovery ($IDP_URL/.well-known/openid-configuration)"
|
||||||
|
|
||||||
|
disc="$(curl -sS -m 20 -w $'\n%{http_code}' "$IDP_URL/.well-known/openid-configuration" 2>/dev/null)"
|
||||||
|
disc_code="${disc##*$'\n'}"
|
||||||
|
disc_body="${disc%$'\n'*}"
|
||||||
|
disc_body="${disc_body%$'\n'}"
|
||||||
|
|
||||||
|
kv "HTTP" "$disc_code"
|
||||||
|
if [[ "$disc_code" == "200" ]]; then
|
||||||
|
issuer="$(echo "$disc_body" | jq -r '.issuer // "n/a"')"
|
||||||
|
grants="$(echo "$disc_body" | jq -r '.grant_types_supported // [] | join(",")')"
|
||||||
|
resp_types="$(echo "$disc_body" | jq -r '.response_types_supported // [] | join(",")')"
|
||||||
|
auth_ep="$(echo "$disc_body" | jq -r '.authorization_endpoint // "n/a"')"
|
||||||
|
token_ep="$(echo "$disc_body" | jq -r '.token_endpoint // "n/a"')"
|
||||||
|
kv "issuer" "$issuer"
|
||||||
|
kv "grant_types_supported" "$grants"
|
||||||
|
kv "response_types_supported" "$resp_types"
|
||||||
|
kv "authorization_endpoint" "$auth_ep"
|
||||||
|
kv "token_endpoint" "$token_ep"
|
||||||
|
|
||||||
|
# decisive checks
|
||||||
|
has_password=false; has_cc=false; has_authcode=false
|
||||||
|
echo "$grants" | grep -q 'password' && has_password=true
|
||||||
|
echo "$grants" | grep -q 'client_credentials' && has_cc=true
|
||||||
|
echo "$grants" | grep -q 'authorization_code' && has_authcode=true
|
||||||
|
|
||||||
|
log " FINDING grant.password → $([[ $has_password == true ]] && echo PRESENT || echo ABSENT)"
|
||||||
|
log " FINDING grant.client_credentials → $([[ $has_cc == true ]] && echo PRESENT || echo ABSENT)"
|
||||||
|
log " FINDING grant.authorization_code → $([[ $has_authcode == true ]] && echo PRESENT || echo ABSENT)"
|
||||||
|
|
||||||
|
if [[ $has_password == false && $has_cc == false ]]; then
|
||||||
|
log " CONCLUSION: IdP offers NO non-interactive grant → password/app-token paths are dead (matches report)."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# 3. PeerTube OAuth clients + password grant (incident §1 facts 3–5)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
section "PeerTube OAuth ($INSTANCE)"
|
||||||
|
|
||||||
|
api="${INSTANCE%/}/api/v1"
|
||||||
|
ptcode="$(curl -sS -m 20 -o /dev/null -w '%{http_code}' "$INSTANCE/api/v1/config" 2>/dev/null)"
|
||||||
|
kv "config HTTP" "$ptcode"
|
||||||
|
|
||||||
|
# built-in local client — its client_id is public, its secret is NOT logged
|
||||||
|
local_client="$(curl -sS -m 20 "$api/oauth-clients/local" 2>/dev/null)"
|
||||||
|
if echo "$local_client" | jq -e '.client_id' >/dev/null 2>&1; then
|
||||||
|
cid="$(echo "$local_client" | jq -r '.client_id')"
|
||||||
|
kv "local client_id" "$(redact "$cid")"
|
||||||
|
kv "local client present" "yes"
|
||||||
|
else
|
||||||
|
kv "local client" "not returned (may need auth)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# password grant result (only if creds-probe.php can supply creds securely)
|
||||||
|
if [[ -f "$DB_PATH" && -f "$SCRIPT_DIR/creds-probe.php" && -x "$SCRIPT_DIR/creds-probe.php" ]]; then
|
||||||
|
probe_out="$(php "$SCRIPT_DIR/creds-probe.php" --db "$DB_PATH" --instance "$INSTANCE" 2>/dev/null)"
|
||||||
|
# $argv/$j are PHP variables (not shell); single quotes are intentional.
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
pt_ok="$(php -r 'echo (json_decode($argv[1],true)["peertube"]["ok"]??false)?"1":"0";' "$probe_out" 2>/dev/null)"
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
pt_code="$(php -r '$j=json_decode($argv[1],true)["peertube"]??[]; echo $j["code"]??"";' "$probe_out" 2>/dev/null)"
|
||||||
|
kv "password-grant ok" "$pt_ok"
|
||||||
|
kv "password-grant code" "$(redact "$pt_code")"
|
||||||
|
if [[ "$pt_ok" == "0" && -n "$pt_code" ]]; then
|
||||||
|
log " FINDING password-grant → REJECTED ($pt_code) — matches 'invalid_grant' if '$pt_code' == 'invalid_grant'"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
kv "password-grant" "SKIPPED (creds-probe.php or DB unavailable → run: just creds-test)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# 4. SMTP STARTTLS AUTH mechanisms (incident §3.5 — why SMTP still works)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
section "SMTP AUTH mechanisms ($SMTP_HOST:$SMTP_PORT)"
|
||||||
|
|
||||||
|
smtp_out="$(timeout 25 python3 - "$SMTP_HOST" "$SMTP_PORT" 2>/dev/null <<'PYEOF'
|
||||||
|
import smtplib, ssl, sys
|
||||||
|
host, port = sys.argv[1], int(sys.argv[2])
|
||||||
|
try:
|
||||||
|
s = smtplib.SMTP(host, port, timeout=20)
|
||||||
|
s.ehlo()
|
||||||
|
s.starttls(context=ssl.create_default_context())
|
||||||
|
s.ehlo()
|
||||||
|
print(s.esmtp_features.get("auth", "NONE"))
|
||||||
|
s.quit()
|
||||||
|
except Exception as e:
|
||||||
|
print("ERROR: %s" % e)
|
||||||
|
PYEOF
|
||||||
|
)"
|
||||||
|
kv "AUTH mechanisms" "$(redact "$smtp_out")"
|
||||||
|
|
||||||
|
if echo "$smtp_out" | grep -qiE 'XOAUTH2|OAUTHBEARER'; then
|
||||||
|
log " FINDING SMTP → SSO/OAuth2 wired in (XOAUTH2/OAUTHBEARER present)"
|
||||||
|
fi
|
||||||
|
if echo "$smtp_out" | grep -qiE '(^| )(PLAIN|LOGIN)( |$)'; then
|
||||||
|
log " FINDING SMTP → legacy PLAIN/LOGIN KEPT → app's PLAIN auth still works (matches report)"
|
||||||
|
else
|
||||||
|
log " FINDING SMTP → PLAIN/LOGIN absent → SMTP would ALSO be broken"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# 5. Identity-header / PreserveHost propagation (the diagnosis' core question)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
section "proxy → backend identity-header propagation"
|
||||||
|
|
||||||
|
# (a) RESPONSE headers — what the public endpoint reflects. This can only ever show
|
||||||
|
# *response* headers (X-Powered-By, Server, ...), NEVER an identity header that
|
||||||
|
# LemonLDAP injects INTO the request. Absence here is NOT proof of absence upstream.
|
||||||
|
kv "response-header scope" "response side only — cannot reveal inbound identity"
|
||||||
|
hdrs="$(curl -sS -m 20 -D - -o /dev/null "$INSTANCE/" 2>/dev/null)"
|
||||||
|
for hname in X-Powered-By Server Removed-User; do
|
||||||
|
val="$(printf '%s' "$hdrs" | grep -i "^${hname}:" | head -1 | sed "s/^[^:]*:[[:space:]]*//" | tr -d '\r')"
|
||||||
|
if [[ -n "$val" ]]; then
|
||||||
|
kv "response [$hname]" "$(redact "$val")"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# (b) REQUEST-side identity headers — the part that actually answers the "❓".
|
||||||
|
# These headers are injected by the SSO reverse-proxy into the request it forwards
|
||||||
|
# to the backend. They are NOT visible in any public response; the ONLY way to see
|
||||||
|
# them is to have an endpoint *behind* the proxy echo the request headers back.
|
||||||
|
#
|
||||||
|
# Two ways to get that echo:
|
||||||
|
# -- self-host: run `scripts/echo-headers.php` behind the SAME LemonLDAP vhost
|
||||||
|
# as PeerTube and point --echo at it, OR
|
||||||
|
# -- a known public echo service (httpbin.org/headers) — but that only shows
|
||||||
|
# headers the PUBLIC client sent, not ones LemonLDAP adds AFTER auth.
|
||||||
|
section "request-side identity header probe (echo endpoint)"
|
||||||
|
|
||||||
|
ECHO_URL="${ECHO_URL:-}"
|
||||||
|
if [[ -n "$ECHO_URL" ]]; then
|
||||||
|
echo_body="$(curl -sS -m 20 "$ECHO_URL" 2>/dev/null)"
|
||||||
|
if echo "$echo_body" | jq -e '.headers' >/dev/null 2>&1; then
|
||||||
|
# extract any identity-ish header, case-insensitively
|
||||||
|
identity_hdrs="$(echo "$echo_body" | jq -r '.headers | to_entries[] | select(.key | test("remote[-_]?user|auth[-_]?user|x[-_]?forwarded[-_]?user|x[-_]?user|proxy[-_]?user|oidc[-_]?claim"; "i")) | "\(.key)=\(.value)"')"
|
||||||
|
if [[ -n "$identity_hdrs" ]]; then
|
||||||
|
log " → IDENTITY HEADERS SEEN AT BACKEND:"
|
||||||
|
while IFS= read -r l; do kv " identity" "$(redact "$l")"; done <<< "$identity_hdrs"
|
||||||
|
log " CONCLUSION: an authenticated-user header IS reaching the backend."
|
||||||
|
else
|
||||||
|
log " → NO identity header in the echoed request headers."
|
||||||
|
log " CONCLUSION: LemonLDAP is protecting PeerTube but NOT telling it WHO the user is"
|
||||||
|
log " → PeerTube has no authenticated identity to act on."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
kv "echo endpoint" "unparseable/non-JSON (got: $(redact "${echo_body:0:80}"))"
|
||||||
|
fi
|
||||||
|
elif [[ -f "$SCRIPT_DIR/echo-headers.php" ]]; then
|
||||||
|
kv "echo endpoint" "SKIPPED — pass --echo <url> (point it at echo-headers.php behind the same vhost)"
|
||||||
|
else
|
||||||
|
kv "echo endpoint" "NOT AVAILABLE — see scripts/echo-headers.php docs; --echo <url> enables the probe"
|
||||||
|
fi
|
||||||
|
log " HINT: to see headers LemonLDAP injects, host scripts/echo-headers.php behind"
|
||||||
|
log " the SAME LemonLDAP vhost as PeerTube and re-run with --echo <that-url>."
|
||||||
|
log " A public echo service only shows headers YOUR client sent, not the"
|
||||||
|
log " post-auth headers the SSO proxy adds."
|
||||||
|
|
||||||
|
# whether ProxyPreserveHost-equivalent is observable: compare Host saw vs sent
|
||||||
|
host_sent="$(curl -sS -m 20 -s -o /dev/null -w '%{url_effective}' "$INSTANCE/" 2>/dev/null)"
|
||||||
|
kv "effective-url" "$host_sent"
|
||||||
|
|
||||||
|
log ""
|
||||||
|
log "════════════════════════════════════════════════════════════════════"
|
||||||
|
log "summary → log written to: $LOG"
|
||||||
|
log "════════════════════════════════════════════════════════════════════"
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
|
||||||
|
════════════════════════════════════════════════════════════════════
|
||||||
|
XAMXAM SSO/PeerTube diagnosis — 2026-08-19 12:42:17 +0200
|
||||||
|
════════════════════════════════════════════════════════════════════
|
||||||
|
MISSING-DEP dig
|
||||||
|
MISSING-DEP host
|
||||||
|
── topology / DNS / reverse-DNS / HTTP identity
|
||||||
|
%-28s %s DNS[videos.erg.be]: 194.78.61.186
|
||||||
|
%-28s %s rDNS[194.78.61.186]: none
|
||||||
|
%-28s %s DNS[mail.erg.school]: 79.99.201.114
|
||||||
|
%-28s %s rDNS[79.99.201.114]: none
|
||||||
|
%-28s %s DNS[portail.erg.school]: 79.99.201.119
|
||||||
|
%-28s %s rDNS[79.99.201.119]: none
|
||||||
|
── SSO IdP discovery (https://portail.erg.school/.well-known/openid-configuration)
|
||||||
|
%-28s %s HTTP: 200
|
||||||
|
%-28s %s issuer: https://portail.erg.school/
|
||||||
|
%-28s %s grant_types_supported: authorization_code,refresh_token
|
||||||
|
%-28s %s response_types_supported: code
|
||||||
|
%-28s %s authorization_endpoint: https://portail.erg.school/oauth2/authorize
|
||||||
|
%-28s %s token_endpoint: https://portail.erg.school/oauth2/token
|
||||||
|
FINDING grant.password → ABSENT
|
||||||
|
FINDING grant.client_credentials → ABSENT
|
||||||
|
FINDING grant.authorization_code → PRESENT
|
||||||
|
CONCLUSION: IdP offers NO non-interactive grant → password/app-token paths are dead (matches report).
|
||||||
|
── PeerTube OAuth (https://videos.erg.be)
|
||||||
|
%-28s %s config HTTP: 200
|
||||||
|
%-28s %s local client_id: <REDACTED>
|
||||||
|
%-28s %s local client present: yes
|
||||||
|
%-28s %s password-grant ok: 0
|
||||||
|
%-28s %s password-grant code:
|
||||||
|
── SMTP AUTH mechanisms (mail.erg.school:587)
|
||||||
|
%-28s %s AUTH mechanisms: PLAIN LOGIN XOAUTH2 OAUTHBEARER PLAIN LOGIN XOAUTH2 OAUTHBEARER
|
||||||
|
FINDING SMTP → SSO/OAuth2 wired in (XOAUTH2/OAUTHBEARER present)
|
||||||
|
FINDING SMTP → legacy PLAIN/LOGIN KEPT → app's PLAIN auth still works (matches report)
|
||||||
|
── proxy → backend identity-header propagation
|
||||||
|
%-28s %s echo header [X-Powered-By]: PeerTube
|
||||||
|
%-28s %s echo header [Server]: nginx
|
||||||
|
%-28s %s effective-url: https://videos.erg.be/
|
||||||
|
|
||||||
|
════════════════════════════════════════════════════════════════════
|
||||||
|
summary → log written to: /home/theophile/repos/xamxam/sso-diagnose.log
|
||||||
|
════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
════════════════════════════════════════════════════════════════════
|
||||||
|
XAMXAM SSO/PeerTube diagnosis — 2026-08-19 12:46:25 +0200
|
||||||
|
════════════════════════════════════════════════════════════════════
|
||||||
|
MISSING-DEP dig
|
||||||
|
MISSING-DEP host
|
||||||
|
── topology / DNS / reverse-DNS / HTTP identity
|
||||||
|
%-28s %s DNS[videos.erg.be]: 194.78.61.186
|
||||||
|
%-28s %s rDNS[194.78.61.186]: none
|
||||||
|
%-28s %s DNS[mail.erg.school]: 79.99.201.114
|
||||||
|
%-28s %s rDNS[79.99.201.114]: none
|
||||||
|
%-28s %s DNS[portail.erg.school]: 79.99.201.119
|
||||||
|
%-28s %s rDNS[79.99.201.119]: none
|
||||||
|
── SSO IdP discovery (https://portail.erg.school/.well-known/openid-configuration)
|
||||||
|
%-28s %s HTTP: 200
|
||||||
|
%-28s %s issuer: https://portail.erg.school/
|
||||||
|
%-28s %s grant_types_supported: authorization_code,refresh_token
|
||||||
|
%-28s %s response_types_supported: code
|
||||||
|
%-28s %s authorization_endpoint: https://portail.erg.school/oauth2/authorize
|
||||||
|
%-28s %s token_endpoint: https://portail.erg.school/oauth2/token
|
||||||
|
FINDING grant.password → ABSENT
|
||||||
|
FINDING grant.client_credentials → ABSENT
|
||||||
|
FINDING grant.authorization_code → PRESENT
|
||||||
|
CONCLUSION: IdP offers NO non-interactive grant → password/app-token paths are dead (matches report).
|
||||||
|
── PeerTube OAuth (https://videos.erg.be)
|
||||||
|
%-28s %s config HTTP: 200
|
||||||
|
%-28s %s local client_id: <REDACTED>
|
||||||
|
%-28s %s local client present: yes
|
||||||
|
%-28s %s password-grant ok: 0
|
||||||
|
%-28s %s password-grant code:
|
||||||
|
── SMTP AUTH mechanisms (mail.erg.school:587)
|
||||||
|
%-28s %s AUTH mechanisms: PLAIN LOGIN XOAUTH2 OAUTHBEARER PLAIN LOGIN XOAUTH2 OAUTHBEARER
|
||||||
|
FINDING SMTP → SSO/OAuth2 wired in (XOAUTH2/OAUTHBEARER present)
|
||||||
|
FINDING SMTP → legacy PLAIN/LOGIN KEPT → app's PLAIN auth still works (matches report)
|
||||||
|
── proxy → backend identity-header propagation
|
||||||
|
%-28s %s response-header scope: response side only — cannot reveal inbound identity
|
||||||
|
%-28s %s response [X-Powered-By]: PeerTube
|
||||||
|
%-28s %s response [Server]: nginx
|
||||||
|
── request-side identity header probe (echo endpoint)
|
||||||
|
%-28s %s echo endpoint: SKIPPED — pass --echo <url> (point it at echo-headers.php behind the same vhost)
|
||||||
|
HINT: to see headers LemonLDAP injects, host scripts/echo-headers.php behind
|
||||||
|
the SAME LemonLDAP vhost as PeerTube and re-run with --echo <that-url>.
|
||||||
|
A public echo service only shows headers YOUR client sent, not the
|
||||||
|
post-auth headers the SSO proxy adds.
|
||||||
|
%-28s %s effective-url: https://videos.erg.be/
|
||||||
|
|
||||||
|
════════════════════════════════════════════════════════════════════
|
||||||
|
summary → log written to: /home/theophile/repos/xamxam/sso-diagnose.log
|
||||||
|
════════════════════════════════════════════════════════════════════
|
||||||
Reference in New Issue
Block a user