Skip to content

Setting AUTH_ALLOWED_EMAIL_DOMAIN silently drops AUTH_ALLOWED_EMAILS, locking out every address outside the domain #328

Description

@ianTPE

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions