fix(peertube): let OIDC discovery reach a plaintext onion issuer

openid-client v6 refuses a non-HTTPS issuer, and an onion-exclusive Keycloak has
none: the onion publishes only HiddenServicePort 80, and https there is blocked
on four separate things (see TODO.md). PeerTube's plugin therefore threw
OAUTH_HTTP_REQUEST_FORBIDDEN during discovery, never built a client, and answered
2326 logins with "OpenID provider unavailable" — while `test -d` on the plugin
directory reported the deploy as ok.

Two changes, one honest and one not:

The gate at the end of enable.yml now fails when the container log carries
"Initial OpenID discovery failed", so a plugin that registered without building a
client stops the deploy instead of surfacing hours later in the browser suite.

The sed over the installed dist is a hack and is marked as one in TODO.md. It
widens the trigger of the escape hatch upstream already implements — the plugin
gates `openid.allowInsecureRequests` on NODE_ENV alone, and setting NODE_ENV=dev
in production would swap PeerTube's whole runtime profile. The `grep -q` after
the substitution is what keeps it tolerable: an upstream release that moves the
line fails the deploy loudly rather than silently doing nothing.

Both are driven by one variable, PEERTUBE_OIDC_INSECURE_ISSUER, which requires
SSO enabled AND tor enabled for peertube AND an .onion issuer, so no deploy
without Tor can execute the patch or ever fail on it. Verified with Ansible's own
templar across seven cases, including https://onionsoup.example.com/ to catch a
too-greedy match. The sed itself was run against the real 2.0.1 tarball: exactly
one hit, `node --check` clean, and the patched expression exercised in four
environment combinations.

Exit path in TODO.md: the plugin is an official Framasoft one (AGPL-3.0 in the
repo root, the npm package merely ships no license field), and the proper fix is
a `registerSetting({ name: 'allow-insecure-issuer' })` OR-ed into the same
condition. Issue text and patch are written; upstream filing is waiting on a
framagit account.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-08-10 21:53:31 +02:00
1 parent ec58997771
commit e64ed1cd93
4 files changed
+69

No files matched your search

+47
View File
@@ -20,6 +20,53 @@ failure class from that run is committed (hlth-csp onion-sibling skip,
onion-aware Playwright request timeouts + lint guard, joomla server-side OIDC
reachability). The items below are the ones that are NOT fixed yet.
- **https on the onion IdP — investigated and REJECTED, do not re-derive.** OIDC
Discovery / RFC 8414 want an https issuer, and an onion-exclusive Keycloak has
none, so strict client libraries refuse it. Serving TLS on the onion is blocked
on four independent things, in this order: (1) `roles/svc-net-tor/templates/torrc.j2`
publishes only `HiddenServicePort 80`, and no role declares 443 in
`ports.public`/`ports.local`, so `lookup('tor_ports')` can never emit it —
flipping TLS on first makes Keycloak unreachable over Tor entirely, because
`nginx_ssl_header.j2` is one if/else and port 80 disappears; (2)
`roles/sys-front-tls-selfsigned/tasks/00_core.yml` writes an unguarded CN, which
overflows X.509's 64-byte cap on a 62-char v3 onion — `group_vars/all/02_tls.yml`
already guards exactly this for the root CA and nowhere else; (3)
`nginx_ssl_header.j2` emits HSTS in the same branch, and browsers offer no
click-through on an HSTS host with an untrusted chain, so a self-signed interim
locks real users out while staying invisible to Playwright's
`ignoreHTTPSErrors`; (4) it fixes zero libcurl consumers, because RFC 7686
refusal happens at name resolution and is scheme-independent. Unblocking needs a
CA that issues for v3 `.onion` (HARICA, CA/B Forum ballot 144) — procurement,
not code. Until then the per-consumer fixes stand: SOCKS proxy for libcurl
clients, and the `PEERTUBE_OIDC_ALLOW_INSECURE_ISSUER` dist patch for
openid-client v6.
- **peertube OIDC dist patch — a hack, kept deliberately, remove when upstream
lands.** `roles/web-app-peertube/tasks/oidc/enable.yml` runs a `sed` over the
npm-installed `dist/main.js` of `peertube-plugin-auth-openid-connect` so the
plugin honours `PEERTUBE_OIDC_ALLOW_INSECURE_ISSUER` (set in `templates/env.j2`
only when the issuer is an onion). It exists because openid-client v6 refuses a
non-HTTPS issuer and the plugin gates its own escape hatch on `NODE_ENV` alone,
which would swap PeerTube's whole runtime profile. Patching a third party's
compiled output is fragile by construction; the `grep -q` after the `sed` is
what keeps it honest — an upstream release that moves the line fails the deploy
loudly instead of silently doing nothing.
Exit path, in order: (1) file the issue and MR upstream — the plugin is an
official Framasoft one by Chocobozzz, AGPL-3.0 per the repo root `LICENSE`
(the npm package just ships no license field), and the fix is a
`registerSetting({ name: 'allow-insecure-issuer', type: 'input-checkbox',
default: false })` OR-ed into the existing condition; issue text and patch are
written and waiting. Currently blocked: the framagit account is not approved
yet. (2) Until it merges, run a fork — `git subtree split --prefix=
peertube-plugin-auth-openid-connect` gives a standalone repo (the 16 official
plugins sit flat in the root with no shared build, so nothing is missing except
the root `LICENSE`, which must be copied), built with `npm ci && npm run build`
because `library: ./dist/main.js` and `.npmignore` excludes `src`, and installed
with `--plugin-path`: `--npm-name` cannot take a git URL because
`getTypeFromNpmName` requires a `peertube-plugin-*` name, while `--plugin-path`
only needs the *directory* to carry that name. (3) Delete the sed task, the env
var and this entry once the setting is released.
- **dual-family providers** — the one real remaining design gap. On a CI onion
node, tor-enabled providers (e.g. mastodon) are onion-exclusive: their domain
list contains only the `.onion` canonical, no clearnet sibling. The
@@ -56,6 +56,15 @@
changed_when: true
delegate_to: "{{ PEERTUBE_HOST_NODE }}"
- name: "🩹 Let the OIDC plugin accept the plaintext onion issuer"
ansible.builtin.shell: |
# nocheck: container-exec-resolver runtime-resolved CID, delegated to the hosting node
container exec {{ PEERTUBE_LOCAL_CID | quote }} sh -c 'f=/data/plugins/node_modules/{{ PEERTUBE_OIDC_PLUGIN }}/dist/main.js; sed -i "s#const discoverInsecureConfig = process.env.NODE_ENV#const discoverInsecureConfig = process.env.PEERTUBE_OIDC_ALLOW_INSECURE_ISSUER || process.env.NODE_ENV#" "$f"; grep -q PEERTUBE_OIDC_ALLOW_INSECURE_ISSUER "$f"'
when: PEERTUBE_OIDC_INSECURE_ISSUER | bool
changed_when: true
delegate_to: "{{ PEERTUBE_HOST_NODE }}"
notify: container recreate
- name: "🔄 Update the settings column of the auth-openid-connect plugin"
database_query:
mask_values: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
@@ -101,3 +110,12 @@
until: _peertube_oidc_verified.rc == 0
changed_when: false
delegate_to: "{{ PEERTUBE_HOST_NODE }}"
- name: "🛑 Fail if the OIDC plugin registered but never built a client"
shell: |
# nocheck: container-exec-resolver runtime-resolved CID, delegated to the hosting node
container logs {{ PEERTUBE_LOCAL_CID | quote }} 2>&1 | grep -q 'Initial OpenID discovery failed'
register: _peertube_oidc_discovery_failed
failed_when: _peertube_oidc_discovery_failed.rc == 0
changed_when: false
delegate_to: "{{ PEERTUBE_HOST_NODE }}"
+3
View File
@@ -54,3 +54,6 @@ PEERTUBE_SMTP_DISABLE_STARTTLS={{ 'false' if email.start_tls else 'true' }}
{% endif %}
PEERTUBE_ADMIN_EMAIL={{ email['from'] }}
{% if PEERTUBE_OIDC_INSECURE_ISSUER | bool %}
PEERTUBE_OIDC_ALLOW_INSECURE_ISSUER=1
{% endif %}
+1
View File
@@ -7,6 +7,7 @@ PEERTUBE_CONTAINER_ADDRESS: "{{ lookup('container_address', application_id, enti
PEERTUBE_OIDC_PLUGIN: "peertube-plugin-auth-openid-connect"
PEERTUBE_OIDC_ENABLED: "{{ lookup('config', application_id, 'services.sso.enabled') }}"
PEERTUBE_OIDC_INSECURE_ISSUER: "{{ PEERTUBE_OIDC_ENABLED | bool and (lookup('config', application_id, 'services.tor.enabled') | bool) and (OIDC.CLIENT.ISSUER_URL is search('\\.onion(/|:|$)')) }}"
PEERTUBE_LIVE_ENABLED: "{{ lookup('config', application_id, 'services.peertube.live.enabled') | bool }}"
PEERTUBE_RTMP_PORT: "{{ lookup('config', application_id, 'services.peertube.ports.internal.rtmp') }}"