Add a personal Etherpad instance at pads.mathewcsims.uk - #57
Closed
mathewcsims wants to merge 3 commits into
Closed
Conversation
A LAN/tailnet-gated document-authoring space with a pad directory at /list,
behind per-user accounts. Family on the tailnet can be given accounts later.
It has both a LAN gate and its own login, and that combination is the point
rather than belt-and-braces: the Caddy gate admits any device on the LAN or
the tailnet, which is the availability wanted, but it is not an identity — it
cannot tell one tailnet device from another, and these are personal drafts.
Etherpad's requireAuthentication supplies the identity, with bcrypt accounts
via ep_hash_auth. This is the repo's first app to pair the LAN-gate idiom with
an app-level login for that reason.
Deliberately a shared space: requireAuthorization is false, so every account
can open every pad. Etherpad has no per-pad ACL. ep_mypads was the only plugin
that ever offered one and it is a dead end — on Etherpad 3.x its storage layer
passes callbacks to ueberdb2 v6, which is promise-only, so startup hangs
silently with no error; and its pad passwords ride in the URL rather than a
session. If per-person separation is ever needed it means separate instances.
OIDC via Infomaniak was planned and dropped. It would have added a cloud
dependency to a LAN-only service, an IK-AUTH registration and a client secret,
and it made admin rights awkward: ep_openid_connect can only set is_admin from
an IdP claim, and Infomaniak advertises no roles or groups claim to key on.
With local accounts is_admin is just a flag, and hash_adm:false keeps
file-based accounts non-admin unless they get an explicit .adm file.
Two things were found by running it rather than reading about it, and both are
recorded where they will be re-encountered:
- Bind-mounting a host directory over Etherpad's var/ hides
installed_plugins.json and silently disables every build-time plugin. The
only symptom is "Loaded 1 plugins" and an empty "Installed plugins:" line.
The database therefore lives at pads/data/ with an absolute dbSettings path,
not Etherpad's default var/*.sq3.
- bcrypt hashes must be $2a$ or $2b$. htpasswd emits $2y$, which node's bcrypt
does not understand: it returns false for every password with no error and
no log line, so the account simply never authenticates. Verified directly
against the bcrypt build inside the image ($2y$ false, $2a$ true, same
password and cost). Both scripts rewrite the prefix.
Verified live before committing: both plugins load; /health is public 200
while /, /list and /admin are 401; a wrong password is refused; a file-based
user authenticates with no restart and is correctly refused at /admin-auth
(403 against the admin's 200); pad content survives a container restart; and
/list lists pads. The Caddyfile passes the same caddy validate CI runs.
Note /admin/* serves its HTML shell to any authenticated user — Etherpad gates
on req.path.startsWith('/admin-auth') — so a non-admin sees an empty dashboard
whose API calls are refused. Documented so it is not mistaken for a hole.
Not yet done, and deliberately left: the Pass item (agent tokens are read-only
for item creation, so that script must be run under Mathew's own session), the
DNS records, and copying the Caddyfile to the Pi.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ditor iframes The first deploy left every pad hanging on "Loading..." behind Etherpad's generic red error box. The cause was in the Etherpad container log, which collects the client-side exception: SecurityError: Failed to read a named property 'removeEventListener' from 'Window': Blocked a frame with origin "https://pads.mathewcsims.uk" from accessing a cross-origin frame. Etherpad's editor is nested same-origin iframes (ace_outer inside ace_inner), and security_headers sets X-Frame-Options: DENY, which blocks all framing including same-origin. The browser replaces each editor frame with an opaque-origin error document, so the parent's first access to it throws. Same fix as the Ghost block, and the simple `header` form is enough here rather than Owl's `header_down` workaround: Etherpad sends no X-Frame-Options of its own, verified with curl straight at :9001, so there is no upstream value for reverse_proxy's Add-not-Set copy to double up with. Confirmed by A/B test before deploying rather than by reasoning: the same image and credentials behind two local proxies differing only in this header. DENY reproduced the stuck "Loading..." and the identical error box; SAMEORIGIN loaded the pad. Production now returns SAMEORIGIN for this host while every other site still returns DENY. This was called out as a risk in the plan's Phase 0 and then asserted to be fine without being tested — the local verification ran against the container directly, which never sees the header Caddy adds. The SETUP.md note now says explicitly to test through the proxy, because this whole class of bug is invisible otherwise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Etherpad computes maxAge as `1000 * 60 * 60 * 6` — commented "6 hours", i.e.
milliseconds — then emits it verbatim into
`Cache-Control: public, max-age=${settings.maxAge}` (static.ts:53,
specialpages.ts:116). Cache-Control's max-age is in SECONDS, so the intended
6 hours ships as 21,600,000 seconds: 250 days.
That is not cosmetic. It bit within minutes of this instance going live: the
X-Frame-Options DENY -> SAMEORIGIN fix could not reach a browser that had
already cached /static/empty.html, because a response cached with a 250-day
max-age is reused without revalidation. Nothing server-side can invalidate
it — the browser simply never asks again — so a correct fix looked like a
failed one.
Setting maxAge to 21600 (seconds) gives the 6 hours upstream intended, and
means any future header or asset change propagates the same day.
Verified after deploy: Cache-Control: max-age=21600, Expires now tomorrow
morning rather than April 2027.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Owner
Author
|
Closing unmerged. Etherpad has been torn down entirely — it is not suitable for the use cases this was built for. See the discussion: its access control is HTTP Basic only (no logout, browser-cached credentials), it has no per-pad ACL, and its built-in OIDC provider cannot serve pad login. Nothing from this branch is deployed. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A LAN/tailnet-gated document-authoring space with a pad directory at /list,
behind per-user accounts. Family on the tailnet can be given accounts later.
It has both a LAN gate and its own login, and that combination is the point
rather than belt-and-braces: the Caddy gate admits any device on the LAN or
the tailnet, which is the availability wanted, but it is not an identity — it
cannot tell one tailnet device from another, and these are personal drafts.
Etherpad's requireAuthentication supplies the identity, with bcrypt accounts
via ep_hash_auth. This is the repo's first app to pair the LAN-gate idiom with
an app-level login for that reason.
Deliberately a shared space: requireAuthorization is false, so every account
can open every pad. Etherpad has no per-pad ACL. ep_mypads was the only plugin
that ever offered one and it is a dead end — on Etherpad 3.x its storage layer
passes callbacks to ueberdb2 v6, which is promise-only, so startup hangs
silently with no error; and its pad passwords ride in the URL rather than a
session. If per-person separation is ever needed it means separate instances.
OIDC via Infomaniak was planned and dropped. It would have added a cloud
dependency to a LAN-only service, an IK-AUTH registration and a client secret,
and it made admin rights awkward: ep_openid_connect can only set is_admin from
an IdP claim, and Infomaniak advertises no roles or groups claim to key on.
With local accounts is_admin is just a flag, and hash_adm:false keeps
file-based accounts non-admin unless they get an explicit .adm file.
Two things were found by running it rather than reading about it, and both are
recorded where they will be re-encountered:
Bind-mounting a host directory over Etherpad's var/ hides
installed_plugins.json and silently disables every build-time plugin. The
only symptom is "Loaded 1 plugins" and an empty "Installed plugins:" line.
The database therefore lives at pads/data/ with an absolute dbSettings path,
not Etherpad's default var/*.sq3.
bcrypt hashes must be$2a$ or $2b$ . htpasswd emits $2y$ , which node's bcrypt$2y$ false, $2a$ true, same
does not understand: it returns false for every password with no error and
no log line, so the account simply never authenticates. Verified directly
against the bcrypt build inside the image (
password and cost). Both scripts rewrite the prefix.
Verified live before committing: both plugins load; /health is public 200
while /, /list and /admin are 401; a wrong password is refused; a file-based
user authenticates with no restart and is correctly refused at /admin-auth
(403 against the admin's 200); pad content survives a container restart; and
/list lists pads. The Caddyfile passes the same caddy validate CI runs.
Note /admin/* serves its HTML shell to any authenticated user — Etherpad gates
on req.path.startsWith('/admin-auth') — so a non-admin sees an empty dashboard
whose API calls are refused. Documented so it is not mistaken for a hole.
Not yet done, and deliberately left: the Pass item (agent tokens are read-only
for item creation, so that script must be run under Mathew's own session), the
DNS records, and copying the Caddyfile to the Pi.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
🤖 Generated with Claude Code