40

Two Planes of Authorization: Who/What vs Where/How

When a workload reaches out to a customer's database or connector, two very different systems must both say "yes" — an **auth service** that decides *who* the user is and *what* they may do, and an **egress proxy** that decides *where* the workload may connect and *how*. Why these are separate jobs, why collapsing them into one is a security bug, and how the four authorization layers combine as an AND.

The two questions, and why one system can't answer both

When a running service in your platform wants to pull data from a customer's warehouse, there are really two independent questions to answer, and they belong to two different worlds. The first is about people and entitlement: is this human user, in this tenant, allowed to ask for this? The second is about the network: is this workload allowed to open a socket to that exact destination, over an approved transport? An auth service is the component that holds identity and business rules (users, tenants, roles); an egress proxy — here an Envoy sidecar that all outbound traffic is forced through — is the component that enforces network policy (which hosts, which ports, mutual TLS). They answer different questions and neither has the context to answer the other's.

Auth service:
    "Who is this user, and are they allowed to request this action?"

Egress proxy (Envoy):
    "Which external destination may this workload connect to, over what transport?"

The trap this whole day guards against is assuming one of these is enough. A user being entitled to Acme's data does not mean a compromised worker should be free to dial any host on the internet; and a network path being open to Acme's connector does not prove that the specific human on the other end is allowed to read Acme's finance tables. You need both answers, from both systems.

The split of responsibility

The cleanest way to see the design is a table of who decides what. Four components each own one authorization concern, and — this is the key discipline — no component reaches into another's job. The auth service never opens a socket; the egress proxy never reasons about job titles. Each row below is a distinct plane of control, and a request only succeeds when every plane independently allows it. "mTLS" here means mutual TLS: both sides present certificates, so the proxy proves the workload's identity to the destination, not just the destination's to the workload.

ComponentPrimary jobTypical decisions
Auth serviceIdentity and application authorizationUser, tenant, entitlement, connector access, allowed warehouse role
Egress proxy (Envoy)Network and transport enforcementDestination allowlist, mTLS, TLS policy, ports, timeouts, circuit breaking
Query connectorOperation-level authorizationIs this SQL / semantic query allowed at all?
WarehouseData authorizationWhich schemas, tables, rows and columns are visible?

Rule of thumb: map every access decision to exactly one of these four owners before you build anything — identity to the auth service, network to egress, operation to the connector, data to the warehouse. The moment one component starts making another's decision (egress judging entitlement, or the auth service dialing the warehouse), you have coupled two lifecycles that should stay independent, and you have created a place where a single mistake bypasses a whole plane.

The request flow, end to end

It helps to trace one real request from the user all the way to the data, so you can see each plane firing in turn rather than in the abstract. Follow Alice, a finance analyst at tenant Acme, asking a question that needs data from Acme's warehouse. Notice that the edge proxy (the inbound gateway that terminates the user's session) validates her session token and then asks the auth service — it does not decide entitlement itself — and that the egress proxy near the bottom is a completely separate enforcement point from that edge.

Read top to bottom, the request passes through identity (auth service), then operation (the query service and connector), then network (egress proxy), then data (warehouse) — four checkpoints, four different systems. If any one of them says no, the request stops there. That layered "any layer can veto" shape is exactly what makes the design safe, and the rest of the day looks at the two planes people most often get wrong: the auth service and the egress proxy.

What the auth service should decide

The auth service is the authority on identity and entitlement — it answers everything about the human and the tenant, and nothing about sockets. Its questions are all of the form "is this principal, in this tenant, eligible for this?" It leans on your identity provider (SSO for who is logged in, SCIM for who is still an active employee) so that a deactivated user loses access without anyone touching the query path. Critically, its output is a decision, not an action: it tells the rest of the system what is allowed, and lets those systems carry it out.

Can Alice use warehouse tools at all?
Can Alice use Acme's production connector?
Which customer connection applies?
Which warehouse role is Alice eligible for?
Is Alice still active according to SSO / SCIM?

The concrete output is best modeled as a short-lived, signed authorization context — a small token the auth service issues and the query service consumes. Signed so it can't be forged, short-lived so a stolen copy expires quickly, and carrying the decision so downstream services don't each re-derive entitlement:

{
  "allow": true,
  "tenant_id": "acme",
  "principal_id": "usr_789",
  "connection_id": "acme-prod",
  "allowed_role": "FINANCE_ANALYST"
}

Rule of thumb: the auth service should produce a signed decision, never open a socket to the warehouse. Its currency is who and what — principal, tenant, connector eligibility, warehouse role — expressed as a signed context with a short lifetime. If you ever find it making an outbound database connection, it has stepped out of its plane and into egress's job, and you've lost the separation that lets each be reasoned about on its own.

What the egress proxy should decide

The egress proxy sits on the outbound edge of the workload and enforces the network and transport plane — and deliberately knows almost nothing about business context. Its questions are all about destinations and transport: is this exact host:port on the allowlist, is the calling workload's identity valid, must the connection use mTLS, what timeouts and connection limits apply. It is the thing that turns "the query service is only ever supposed to talk to Acme's connector" from a hope into an enforced fact.

May query-executor connect to acme-connector.internal:443?
Is the destination on the allowlist?
Is the client workload identity valid?
Must mTLS be used?
What timeouts and connection limits apply?

Its policy is a tight allowlist keyed by source workload identity — everything not explicitly permitted is denied, including other customers' endpoints and raw database ports:

source_workload: query-executor

allowed_destination:
  - acme-private-connector:443   # mTLS required

denied:
  - "*.internal.acme:5432"       # raw DB port
  - arbitrary_public_ips
  - all_other_customer_endpoints

Rule of thumb: the egress proxy should decide where and how, never who — it must not try to judge whether Alice is a finance analyst, because it lacks the business context and the authoritative identity state to do so. Give it a default-deny allowlist scoped to the exact connector endpoint, require mTLS, and let it be gloriously ignorant of users and roles. Its ignorance is the point: a plane that only understands destinations can't be tricked by a bug in your entitlement logic.

Why you need both — the two dangerous single-plane designs

It is tempting to save a component and rely on just one plane, so it's worth walking through why each shortcut fails — because each failure is a real, exploitable hole, not a theoretical tidiness concern. Two teams reach for the same two shortcuts, and both are dangerous in a way that only shows up under attack.

The first shortcut is auth service only: check the user, then let the query worker have unrestricted network egress. The problem is that entitlement says nothing about destinations. A server-side request forgery (SSRF — an attacker tricking your server into making requests it shouldn't) or a compromised worker can now dial an arbitrary database or internet host, and the auth "yes" for Alice does nothing to stop it.

The second shortcut is egress proxy only: lock the network to Acme's connector, and skip per-user entitlement. Now the network path is safe, but any user of the platform who can reach that route may ride it into Acme's data — network permission is not proof that the human is entitled to that tenant's tables.

Rule of thumb: user authorization without network enforcement leaves you open to SSRF and compromised workers; network enforcement without user authorization lets anyone who reaches the route touch the data. Neither plane substitutes for the other — they fail in opposite directions, which is exactly why you keep both.

Defense in depth: the four planes as an AND

The reason to keep all four components is not caution for its own sake — it is that they compose as a logical AND, and an AND has a property a single check can never have: every term must hold, so a mistake in one plane is caught by the others. This is what "defense in depth" means concretely — overlapping, independent controls where no single failure grants access. A request is permitted only when the whole conjunction is true.

Stated as a formula, the four planes are independent conditions that must all pass — losing any one term collapses the whole result to "deny":

request_allowed =
        auth_service_allows_user
    AND query_policy_allows_operation
    AND egress_proxy_allows_destination
    AND warehouse_allows_data

The practical recommendation follows directly: use the auth service to produce a short-lived, signed authorization context (the who and what); have the query service consume that context and enforce the operation; and have the egress proxy independently restrict the workload to the exact customer connector endpoint (the where and how). Two systems, two questions, enforced independently — so a bug in either one is contained by the other.

Auth service controls:
    who and what

Egress proxy controls:
    where and how

Rule of thumb: design the access path as a conjunction of independent planes, not a single gate — identity AND operation AND network AND data. Independence is what buys you depth: because the egress proxy can't be talked out of its allowlist by an entitlement bug, and the auth service can't be bypassed by an open network path, each plane covers exactly the failure the others can't see.

Key takeaways

  • Reaching a customer's data poses two different questionswho is the user and what may they do (auth service) and where may the workload connect and how (egress proxy) — and no single system has the context to answer both.
  • Four planes own four concerns: auth service (identity/entitlement), query connector (operation), egress proxy (network/transport), warehouse (data). Keep each in its lane — the auth service never opens a socket; the egress proxy never judges entitlement.
  • The auth service's output is a short-lived, signed authorization context (allow, tenant, principal, connection, role) — a decision, consumed by the query service, not an action.
  • The egress proxy enforces a default-deny allowlist keyed to the source workload, pinned to the exact connector host:443, requiring mTLS — deliberately ignorant of users and roles.
  • Both single-plane shortcuts are exploitable: auth-only leaves you open to SSRF / compromised workers dialing arbitrary hosts; egress-only lets any user ride an open route into a tenant's data. They fail in opposite directions.
  • Access is a logical AND of four independent planes — identity AND operation AND network AND data — which is what "defense in depth" means: a mistake in one plane is caught by the others because none can override another.

Checklist

  • [ ] I can state the two distinct questions and say which system (auth service vs egress proxy) answers each.
  • [ ] I can fill in the four-plane responsibility table and name what each component must not do.
  • [ ] I can trace a request end to end (edge → auth service → query service → egress proxy → connector → warehouse) and name the plane at each hop.
  • [ ] I can describe the auth service's output as a short-lived signed authorization context and explain why it is a decision, not a socket.
  • [ ] I can write an egress allowlist that pins the workload to one connector endpoint with mTLS and default-deny.
  • [ ] I can explain why auth-only (SSRF / compromised worker) and egress-only (any user rides the route) each fail, and in opposite directions.
  • [ ] I can express access as the AND of four independent planes and explain why that independence is what makes it defense in depth.