skip to content

Dozens of services each carry their own CORS allow-list, edited by whoever last needed a partner unblocked. How would you make that grant surface governable?

level: principalimportance: should knowfreq 38%

answer

  1. no one can answer who may read
  2. grants outlive the relationship
  3. owned data, not per-service reflex
  4. entries lapse unless renewed
  5. prove by sending, not by reviewing

basics

~20 s

Make the allowed set declared data with an owner, a reason and an expiry per entry, consumed by services rather than authored in them, and prove the estate's behaviour by sending unrelated origins at every route rather than by reading configuration.

solid answer

~50 s

The defect here is not a wrong comparison in one service; it is that no one can answer "who may read our data today?". Treat the allowed set as **owned data**: one declared list, versioned and reviewed, where each entry names the resource it grants, the origin, an owner, a reason and a date it lapses. Services consume that list and select from it; none of them author it, which removes the pressure that turns selection into echoing whatever arrived. Grant per resource rather than per service, so a public widget endpoint and an endpoint returning attendee records are separate decisions. Then verify by behaviour, not by review: periodically send an unrelated origin at every route and flag anything that echoes it or wildcards. The cost is a central artifact that can bottleneck delivery and rot if it lives far from deployment, so keep it close to the services and make lapsing the default.

code

json · 13 lines
json
{
  "resource": "api.conf-schedule.example/v1/sessions",
  "allowedOrigins": [
    "https://sponsor-a.example",
    "https://sponsor-b.example"
  ],
  "credentialed": false,
  "returns": "public-schedule",
  "owner": "schedule-platform",
  "reason": "sponsor sites embed the public session widget",
  "reviewedAt": "2026-09-01",
  "lapsesAt": "2027-03-01"
}

go deeper

for a junior

Take away the habit rather than the process: every allowed origin should be written down somewhere with a reason, not just typed into the service that was failing.

for a middle

Be able to say why per-service editing produces reflection — the team is asked to grant an origin they cannot know at build time, and echoing is the shortest route.

for a senior

Describe the behavioural check: send an unregistered origin at every route, flag echoes and wildcards on non-public resources, and run it continuously rather than at release.

for a principal

Own the tradeoff. A central list buys an answer to who may read our data and costs delivery speed and blast radius; scope entries by resource and make lapsing the default so the policy survives contact with deadlines.

## The failure is organisational, not syntactic Each individual allow-list in this estate may be written correctly. The problem is the shape of the whole: the set of origins that may read the organisation's data is spread across dozens of independently edited values, each added under time pressure by whoever was unblocking a partner that week, with no record of why and no date it stops being true. The symptoms of that shape are recognisable: - Nobody can answer **who may read our data today** without reading dozens of files, and the answer changes between reading and deploying. - Entries survive the relationship that created them; the partner left, the grant did not. - Each team meets the same problem independently and solves it differently, so the estate contains an exact comparison here, a suffix test there, and an echo in the service that was hardest to onboard. - Widening is invisible. Making a pattern looser is a smaller-looking change than adding a name, so review pressure runs in exactly the wrong direction. ## Making the allowed set data The move is to stop treating the grant as behaviour and start treating it as a record. Each entry should carry: 1. **The resource it grants**, not merely the service — a public schedule endpoint and an attendee-records endpoint are different decisions and should not share a value. 2. **The origin**, written in full as it appears on the wire. 3. **An owner** — a team that will be asked about it, not the person who typed it. 4. **A reason**, in a sentence, so the next reader can judge whether it still holds. 5. **An expiry**, so the entry lapses unless somebody renews it. Default to removal; entries that matter get renewed and the rest disappear without an audit. Services then **consume** the list and select from it per request. This matters more than it looks: the pressure that produces reflection is a team being asked to grant an origin they cannot know at build time. If the list is data they load rather than code they edit, the per-request selection stays a selection. ## Proving it, rather than reviewing it Configuration review cannot establish that no service reflects, because reflection is often not in any configuration — it is a line in request handling, or an inherited default in a middleware layer nobody has looked at. The check that works is behavioural: - Send a request carrying an origin nobody has ever registered, at every route in the estate, and read the grant that comes back. - Flag anything that **echoes** the value sent, anything that answers the wildcard on a route that is not reachable anonymously from outside, and anything granting the literal token that names a class rather than a party. - Run it continuously rather than at release, because the change that introduces it is usually an urgent one made outside the normal path. This also gives the inventory the list alone cannot: the difference between what was declared and what the estate actually does. ## What the central artifact costs Be honest about the tradeoff, because it is the part an interviewer is listening for: | The cost | What it looks like | What blunts it | |---|---|---| | Bottleneck | Every partner onboarding waits on one reviewer | Delegate by resource class; only sensitive resources need the slow path | | Rot | The list drifts from what services do | Keep it beside the services and verify behaviourally | | False centralisation | Teams route around a list that does not fit their case | Allow per-resource entries rather than one estate-wide value | | Blast radius | One wrong edit widens many services at once | Scope entries to resources, and make review proportional to what the resource returns | ## What to decide per resource The governing question is not how many origins are allowed but **what each one can read**. A grant on an endpoint returning a public schedule is cheap at any width; a grant on an endpoint returning attendee records is expensive at width one. Classifying resources by what they return is what lets the slow review path apply to the few endpoints that deserve it, and it is the difference between a policy that holds and one that teams quietly work around.

  • Why can a configuration review not establish that no service in the estate reflects?
    Because reflection frequently appears nowhere in configuration. It is a line in request handling, or a permissive default in a shared middleware layer, and both produce a runtime grant that no reviewed file mentions. The only reliable evidence is behavioural: send an origin nobody registered and see whether it comes back in the grant header.
  • What do you grant per resource rather than per service, and why does it matter?
    Endpoints differ by what they return. A public widget endpoint and one returning attendee records deserve different origins, different credentialed decisions and different review depth. Granting per service forces the widest need onto the most sensitive endpoint, which is how a sensitive response ends up carrying a grant that was justified by a public one.
  • How do you stop a central allow-list from becoming a bottleneck teams route around?
    Make review proportional to what the resource returns: public-data entries go through a fast path with an owner and a lapse date, while entries on sensitive resources take the slow one. Keep the list beside the services so changing it is part of the ordinary deployment, not a separate errand, and let unrenewed entries lapse instead of requiring a cleanup project.

A visitor register with a sponsor and an expiry beside each name, kept in one place, versus a stack of sticky notes at every reception desk in the building.

saying these in an interview costs you the question

  • One broad pattern in every service is simpler than a maintained list.
  • Reviewing configuration files proves that no service reflects an origin.
  • Grants never need to expire once a partner has been onboarded.
  • The count of allowed origins matters more than what they can read.
  • A central list removes the need to decide anything per resource.