Setting AUTH_ALLOWED_EMAIL_DOMAIN makes the CLI stop shipping
AUTH_ALLOWED_EMAILS entirely, so every address outside the domain loses
sign-in.
How I ran into it, on the docker target: I added the domain so colleagues at our
own domain wouldn't need a redeploy each time, kept the existing address list in
.env, and ran check then up. Both passed. The list was simply gone from the
auth container afterwards — and my own address is a gmail one, so the next
sign-in would have been mine, failing. I only noticed because I inspected the
container's env.
The auth broker ORs the two (plugins/auth/src/server.ts), so both are meant to
coexist; the CLI treats them as mutually exclusive:
// secrets.js
function requirementFor(config, spec) {
if (conditionMatches(config, spec.required.when)) return true;
return spec.required.optionalOtherwise ? false : null;
}
// computedSecrets: if (required === null) continue;
AUTH_ALLOWED_EMAILS is declared required: { when: env-absent AUTH_ALLOWED_EMAIL_DOMAIN } with no optionalOtherwise, so once the domain is
set requirementFor returns null and the secret is dropped before
computedSecrets ever returns it — not made optional, removed. fly.js stages
from that same list, so it is never fly secrets set on the app.
On fly this splits by deploy order rather than failing outright:
- domain added to an existing app — the old
AUTH_ALLOWED_EMAILS secret is
still on the app, since nothing unsets it (only SECURITY_SCREEN_PROXY_TOKEN,
FLY_DEPLOY_API_TOKEN and FLY_BASE_IMAGE get an explicit unset), so it keeps
working
- fresh app from the same config — never staged, so only the domain applies
and outside addresses are locked out
Same repo, same .env, different behaviour, and nothing reports it: qm check
passes, qm up succeeds, and the staging output lists what was staged, not what
was dropped. The failure lands on whoever is outside the domain — often the
operator's own personal address.
The portal copy has it too: AUTH_ALLOWED_EMAILS also aliases to
OIDC_ALLOWED_EMAILS for the portal, gated on the same condition, so an OIDC
deployment loses its out-of-domain addresses the same way.
Workaround, if it helps anyone: put both under env.auth in qm.config.jsonc
so they bypass the secret schema. On fly, a stale secret of the same name will
still win over [env].
Suggested fix
Give both AUTH_ALLOWED_EMAILS specs optionalOtherwise: true, mirroring
ANTHROPIC_API_KEY and OPENROUTER_API_KEY (cli/src/secrets.ts:45,52):
{
name: "AUTH_ALLOWED_EMAILS",
service: "auth",
required: {
when: { kind: "env-absent", service: "auth", name: "AUTH_ALLOWED_EMAIL_DOMAIN" },
optionalOtherwise: true,
},
}
With that, requirementFor returns false (optional) instead of null
(dropped) when the domain is set, so a list the operator kept in .env is still
staged and shipped — which is what the broker's OR already expects. The same
change on the portal spec keeps OIDC_ALLOWED_EMAILS in step.
A regression test would set AUTH_ALLOWED_EMAIL_DOMAIN alongside a provided
AUTH_ALLOWED_EMAILS and assert the list still appears in computedSecrets
and reaches both the auth and portal services.
Setting
AUTH_ALLOWED_EMAIL_DOMAINmakes the CLI stop shippingAUTH_ALLOWED_EMAILSentirely, so every address outside the domain losessign-in.
How I ran into it, on the docker target: I added the domain so colleagues at our
own domain wouldn't need a redeploy each time, kept the existing address list in
.env, and rancheckthenup. Both passed. The list was simply gone from theauth container afterwards — and my own address is a gmail one, so the next
sign-in would have been mine, failing. I only noticed because I inspected the
container's env.
The auth broker ORs the two (
plugins/auth/src/server.ts), so both are meant tocoexist; the CLI treats them as mutually exclusive:
AUTH_ALLOWED_EMAILSis declaredrequired: { when: env-absent AUTH_ALLOWED_EMAIL_DOMAIN }with nooptionalOtherwise, so once the domain isset
requirementForreturns null and the secret is dropped beforecomputedSecretsever returns it — not made optional, removed.fly.jsstagesfrom that same list, so it is never
fly secrets seton the app.On fly this splits by deploy order rather than failing outright:
AUTH_ALLOWED_EMAILSsecret isstill on the app, since nothing unsets it (only
SECURITY_SCREEN_PROXY_TOKEN,FLY_DEPLOY_API_TOKENandFLY_BASE_IMAGEget an explicit unset), so it keepsworking
and outside addresses are locked out
Same repo, same
.env, different behaviour, and nothing reports it:qm checkpasses,
qm upsucceeds, and the staging output lists what was staged, not whatwas dropped. The failure lands on whoever is outside the domain — often the
operator's own personal address.
The portal copy has it too:
AUTH_ALLOWED_EMAILSalso aliases toOIDC_ALLOWED_EMAILSfor the portal, gated on the same condition, so an OIDCdeployment loses its out-of-domain addresses the same way.
Workaround, if it helps anyone: put both under
env.authinqm.config.jsoncso they bypass the secret schema. On fly, a stale secret of the same name will
still win over
[env].Suggested fix
Give both
AUTH_ALLOWED_EMAILSspecsoptionalOtherwise: true, mirroringANTHROPIC_API_KEYandOPENROUTER_API_KEY(cli/src/secrets.ts:45,52):With that,
requirementForreturnsfalse(optional) instead ofnull(dropped) when the domain is set, so a list the operator kept in
.envis stillstaged and shipped — which is what the broker's OR already expects. The same
change on the portal spec keeps
OIDC_ALLOWED_EMAILSin step.A regression test would set
AUTH_ALLOWED_EMAIL_DOMAINalongside a providedAUTH_ALLOWED_EMAILSand assert the list still appears incomputedSecretsand reaches both the auth and portal services.