Skip to content

0004. A client-side password gate, and why it is not access control

  • Status: Accepted
  • Date: 2026-09-14
  • Deciders: Konrad engagement team
  • Needs client validation: No for the gate itself. Yes for what replaces it — open question 4 is the client decision this does not settle.
  • Domain: security

Context

The knowledge base has to be showable — screen-shared in a working session, opened from a link in a thread, handed to a colleague — before open question 1 resolves the handoff format and open question 4 resolves what access control sits in front of a deployment. Those are client decisions on a client's timeline, and the material is Manulife target state architecture. Until they land, a forwarded link should not render.

The constraint is the output. npm run build produces a static dist/: prerendered HTML, the markdown compiled into JS chunks, and ~800 KB Archify documents under artifacts/. There is no server in the repository to check a credential, and the diagram artifacts are deliberately standalone — the repository README treats a bare folder of them, opened from file://, as the handoff path that always works.

Decision

A password screen in the browser, checked against a PBKDF2-SHA256 digest of SITE_PASSWORD. The password is read from the environment or a gitignored .env at build time; only the digest ships. npm run build fails without it, so an ungated dist/ cannot be produced by forgetting.

scripts/lib/gate.mjs emits both halves. A <head> script marks <html> before the body is parsed — which is what hides the page; an overlay then asks for the password. The site draws that overlay from a Vue component and each artifact from injected markup, sharing one stylesheet, one derivation, and one sessionStorage unlock across the origin, so unlocking a diagram unlocks the site. Artifacts are gated through injectInto(), the same seam the dock and the brand skin already pass through. An artifact opened from file:// is exempt: that origin has no crypto.subtle to check a password with, so gating there would permanently lock the handoff path rather than ask for anything.

This is obfuscation, not access control. The content is in the HTML the server already sent; curl retrieves any page without meeting the gate, and devtools reveals it in one step. It stops a forwarded link from rendering to someone who was not meant to have it. It does not protect the material from anyone who looks. The digest raises the cost of recovering the password from the bundle; it does not make the password secret.

Alternatives considered

OptionProsConsWhy not
HTTP Basic auth in a Cloudflare Pages FunctionReal: no bytes leave the edge without the credential. Covers the artifacts for free. Matches the host the repository README already namesProtects only a deployment. Nothing is deployed, and the host is not chosen — it is part of open question 4Right answer, wrong week. Adopt it when open question 4 resolves; this ADR is superseded then
Cloudflare Access (SSO)Real, and identity-based rather than a shared password — auditable, revocable per personNeeds a Cloudflare account, a domain, and an identity provider decision that is the client's to makeSame dependency, larger. It is the recommendation to put to the client, not something we can adopt unilaterally
Basic auth on a local Node server in front of dist/Real, and works today for a tunnel or a LAN demo with no hosting decisionDoes nothing for a static host, and adds a serving path the repo does not otherwise haveSolves sharing, not publishing. The gate has to travel with the files
Encrypt each page at build time and decrypt in the browserContent genuinely absent until the password is entered — the one option that survives curlBreaks the SPA router, the built-in search index, and validateArtifactLinks(), which scans built HTML. Roughly the weight of the diagram pipeline, for a stopgapCost far exceeds a stopgap's budget, and it still ends in a bundle an attacker holds offline
Nothing until open question 4 resolvesNo false confidence, no codeLeaves every link fully open for as long as the client takes to answerThe risk is asymmetric. A gate that stops the accidental reader is worth having while the real answer is pending

Consequences

A link can be shared with someone who is expected to have the password without the page rendering to anyone else who receives it. The build cannot silently produce an ungated dist/.

The gate must not be described to anyone as protecting the content, and it does not close open question 4 — the answer there is still server-side auth in front of a deployment, and this ADR should be superseded when that lands. Anything whose exposure would be a genuine incident does not belong in this repository while this is the only gate; the cost options in D3.2 are the material to watch.

Two rebuild costs come with it. A changed password invalidates every cached diagram render, because the digest is part of the version-history cache signature — that is deliberate, since cut versions are otherwise immutable and would keep the old gate forever. And npm run build now needs the secret available, which is a CI configuration step whenever there is a CI.

Dependencies and open questions

  • Open question 4 — what access control sits in front of a deployment. Unchanged by this decision.
  • Open question 1 — the handoff format. If it resolves toward handing over files rather than hosting, the gate matters only for the working site, and the file:// exemption is what keeps the handover usable.
  • Distributing the password is out of band and unsolved by anything here. A shared password has no revocation short of changing it and rebuilding.

Internal working knowledge base — not for external distribution.