Cron and worker processes often run as a privileged “system” identity: they can touch every tenant’s rows because the job must sweep the whole table. That operational convenience is not an authorization model for the effects those jobs cause.
If a nightly job emails invoices, purges data, or calls a partner API, each action still belongs to a tenant (or resource). Running as SYSTEM without proving which tenant/resource the effect is for — and that the effect is allowed for that resource — turns a housekeeping script into a cross-tenant blast radius.
Patterns that hold up:
- Carry an explicit tenant/resource context into every side effect. The job runner may be privileged; the effect still names the object it touches.
- Authorize (or at least enforce the same tenant scope your online APIs use) before each write, send, or export — don’t rely on “the SQL already filtered the batch.” Filters drift; policy should not.
- Prefer short-lived, per-tenant credentials or scoped tokens for outbound calls instead of one god-mode worker secret.
- Log tenant id + resource id + job name together. “System did cleanup” without tenancy makes incident review impossible.
Quick check: inject a row from tenant B into tenant A’s batch input. If the job processes it because the worker is “system,” you never had authorization — only a trust in the query.
Privileged runners are for scheduling. Authorization is still about the object being changed.
Top comments (5)
If the nightly job emails invoices, the destination address is still tenant-scoped data. Resolve the tenant first, then send only to the normalised email on that tenant's row, and treat MX / Null MX as a pre-send check (timeout = skip / inconclusive, not verified).
Good call on treating the recipient address as tenant-scoped data. I’d also make the job re-check the tenant and authorization immediately before enqueueing or sending, so a queued invoice can’t outlive a membership or routing change; logging the tenant and decision (without the address) makes the skip path auditable.
That is a good guard against queued work outliving the authorization that created it. Logging the tenant and decision without the address also gives you an audit trail without copying sensitive contact data.
Exactly. Re-checking at enqueue and send closes the stale-authorization window, and keeping the address out of the audit event preserves the useful trail without duplicating contact data.
Exactly. Re-checking the tenant and authorization immediately before enqueueing keeps a queued invoice from outliving a membership or routing change, and logging the tenant and decision without the address makes the skip path auditable.