Olympus Docs
CookbookDefensive security

Email allowlist vs denylist

Restricting signups by domain

For closed user groups (internal tools, beta, B2B), restrict signup to specific email domains. Two approaches.

Allowlist (preferred)

Only specific domains allowed:

olympus.registration.allowed_domains: 
  - "your-corp.com"
  - "subsidiary.com"
export async function POST(req: Request) {
  const { traits } = await req.json();
  const allowed = await getAllowedDomains();
  const domain = traits.email.split("@")[1];
  if (!allowed.includes(domain)) {
    return Response.json({
      reject: true,
      error: "domain_not_allowed",
      message: `Registration restricted to: ${allowed.join(", ")}`,
    });
  }
  return Response.json({ ok: true });
}

Pros: secure by default. Anyone not on the list can't signup. Cons: must explicitly add domains.

Denylist

Block specific bad domains:

olympus.registration.blocked_domains:
  - "mailinator.com"
  - "guerrillamail.com"
  - "10minutemail.com"
  # ... ~5000 disposable

Pros: most users allowed. Cons: maintenance burden (new disposable services appear).

Best for: open consumer signup where you just want to block obvious abuse.

Hybrid

For B2B with marketing site:

  • Marketing free trial: open (denylist disposables).
  • Production access: allowlist customer domains.

Two separate Kratos providers / paths, different hooks.

Implementation

Allowlist hook

# kratos.yml
selfservice:
  flows:
    registration:
      before:
        hooks:
          - hook: web_hook
            config:
              url: http://your-backend/internal/check-allowlist
              response: { ignore: false, parse: true }

Denylist via JSON Schema pattern

"email": {
  "type": "string",
  "format": "email",
  "not": {
    "pattern": ".*@(mailinator|guerrillamail|10minutemail|tempmail)\\.(com|net|org)$"
  }
}

Schema-level reject. Faster (no webhook).

Limitation: list is hardcoded in schema. Updates require config change + restart.

Dynamic via webhook

const blockedDomains = await getBlockedDomainsFromVault();
const domain = traits.email.split("@")[1];
if (blockedDomains.has(domain)) {
  return Response.json({ reject: true, ... });
}

Updateable in real-time via Athena settings.

Disposable email lists

Public lists:

Pull periodically:

# /etc/cron.weekly/update-disposables
curl https://raw.githubusercontent.com/disposable/disposable-email-domains/master/domains.txt \
  > /etc/olympus/disposable-domains.txt
podman-compose restart ciam-kratos

Catch-all detection

Some companies use catch-all email (*@bigcorp.com all delivered). Could be abused.

Detection (heuristic, not reliable):

  • VRFY SMTP command (largely disabled).
  • Send to random@domain, check if accepted.

Hard. For typical Olympus deployments: not worth implementing.

Plus-addressing in allowlist

alice@bigcorp.com and alice+test@bigcorp.com both valid for bigcorp.com. Allowlist matches the domain, both pass.

If you want to count them as ONE user, normalize at registration:

const normalized = traits.email.replace(/\+[^@]+(@)/, "$1");

Stores alice@bigcorp.com regardless of input.

TLD allowlist

For region-specific:

olympus.registration.allowed_tlds: [".com", ".org", ".net", ".edu", ".de", ".fr"]

Blocks .example or .test (typos).

Sub-domain considerations

bigcorp.com allowed. Does eng.bigcorp.com count?

Decide:

  • Strict: only exact match (@bigcorp.com, not @eng.bigcorp.com).
  • Inclusive: any subdomain (*.bigcorp.com).
function isAllowed(email: string, allowed: string[]) {
  const domain = email.split("@")[1];
  return allowed.some(d => domain === d || domain.endsWith(`.${d}`));
}

Most B2B: inclusive (any subdomain of customer's domain).

Communication to user

When blocked, be clear:

"We're sorry, registration is currently restricted to specific companies.
If you believe your domain should be allowed, contact us at support@your-domain.com."

Not just "rejected." Help them understand and request access.

Logging

INSERT INTO blocked_signups (email, reason, ip, attempted_at)
VALUES ($email, 'domain_not_allowed', $ip, NOW());

Review periodically:

  • Whose domain is being blocked most? Maybe should be allowlisted.
  • Same IP blocked many times? Suspicious, investigate.

On this page