You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
We run a small studio that ships Astro + Cloudflare sites for clients, with EmDash as the CMS. Deployment is automated: an agent builds the site, creates D1/R2, applies the seed through POST /_emdash/api/setup and deploys. The first human login happens on the public domain, not on localhost, because the passkey origin has to be the final domain (emdash:site_url is write-once).
That leaves a window we cannot close: between wrangler deploy and the moment we open /_emdash/admin ourselves, any visitor who reaches /_emdash/admin/setup first becomes the site's administrator. countUsers() > 0 is the only gate, so the window is real until someone claims it.
This is not theoretical exposure. A brand-new hostname shows up in Certificate Transparency logs within minutes, and scanners follow CT feeds. /_emdash/ is a fixed, well-known prefix, so "is this an unclaimed EmDash?" is one request. For a studio shipping many sites, every launch reopens the window.
We know about #744 (nonce binding for POST /_emdash/api/setup/admin) and #1135. #744 closes a narrower hole: an attacker overwriting the admin email during the legitimate ceremony. It does not stop an attacker who simply completes the whole wizard first.
What we do today
Nothing satisfying:
Deploy and immediately claim the site by hand, minimizing but not eliminating the window.
We tried an outer middleware that 403s /_emdash/api/setup and /_emdash/admin/setup for every IP except the studio's, released through a Worker secret. It works, but it is our code guarding core behavior, and we would rather not carry it.
What we would like
Any one of these would solve it, in our order of preference:
Optional setup secret. If EMDASH_SETUP_TOKEN is present in the environment, the wizard and the setup API require it (query parameter or header). Absent, behavior is exactly as today, so nothing changes for local development or for the current tutorial path.
Create the first admin from the CLI.emdash users create --email … --role admin --url https://site against a remote instance, authenticated by a token the deployer already holds. The person then signs in with a magic link or registers a passkey on a normal login page, and the wizard never renders publicly.
Closed by default when a seed already ran. If the database has content applied through POST /_emdash/api/setup but no user, treat the site as "provisioned, awaiting claim" and require the secret above.
A related nicety for automated deploys: when the seed is already applied, the wizard still asks for site title, tagline and "include example content". For us the answer is always "already done" and showing it to the client is confusing. A way to mark the install as seeded so the wizard goes straight to identity would help.
Happy to send a PR for option 1 if the maintainers think the shape is right. Is there guidance we are missing for the automated, remote-first deployment case?
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Context
We run a small studio that ships Astro + Cloudflare sites for clients, with EmDash as the CMS. Deployment is automated: an agent builds the site, creates D1/R2, applies the seed through
POST /_emdash/api/setupand deploys. The first human login happens on the public domain, not on localhost, because the passkey origin has to be the final domain (emdash:site_urlis write-once).That leaves a window we cannot close: between
wrangler deployand the moment we open/_emdash/adminourselves, any visitor who reaches/_emdash/admin/setupfirst becomes the site's administrator.countUsers() > 0is the only gate, so the window is real until someone claims it.This is not theoretical exposure. A brand-new hostname shows up in Certificate Transparency logs within minutes, and scanners follow CT feeds.
/_emdash/is a fixed, well-known prefix, so "is this an unclaimed EmDash?" is one request. For a studio shipping many sites, every launch reopens the window.We know about #744 (nonce binding for
POST /_emdash/api/setup/admin) and #1135. #744 closes a narrower hole: an attacker overwriting the admin email during the legitimate ceremony. It does not stop an attacker who simply completes the whole wizard first.What we do today
Nothing satisfying:
outermiddleware that 403s/_emdash/api/setupand/_emdash/admin/setupfor every IP except the studio's, released through a Worker secret. It works, but it is our code guarding core behavior, and we would rather not carry it.What we would like
Any one of these would solve it, in our order of preference:
EMDASH_SETUP_TOKENis present in the environment, the wizard and the setup API require it (query parameter or header). Absent, behavior is exactly as today, so nothing changes for local development or for the current tutorial path.emdash users create --email … --role admin --url https://siteagainst a remote instance, authenticated by a token the deployer already holds. The person then signs in with a magic link or registers a passkey on a normal login page, and the wizard never renders publicly.POST /_emdash/api/setupbut no user, treat the site as "provisioned, awaiting claim" and require the secret above.A related nicety for automated deploys: when the seed is already applied, the wizard still asks for site title, tagline and "include example content". For us the answer is always "already done" and showing it to the client is confusing. A way to mark the install as seeded so the wizard goes straight to identity would help.
Happy to send a PR for option 1 if the maintainers think the shape is right. Is there guidance we are missing for the automated, remote-first deployment case?
All reactions