AI-Assisted NGINX CORS Configuration Without the Wildcard Trap
Configure CORS in NGINX with AI as a drafting aid: preflight OPTIONS handling, why Access-Control-Allow-Origin wildcard breaks credentials, and validating headers with curl.
- #nginx
- #ai
- #cors
- #security
CORS is the one configuration topic where everyone copy-pastes from the internet, the page “works,” and they never realize they’ve either left a hole open or set up a config that breaks the moment they add credentials. I’ve seen add_header Access-Control-Allow-Origin *; shipped on an authenticated API more times than I can count, and I’ve watched preflight requests fail because the OPTIONS method was never handled. AI is genuinely useful for drafting CORS headers — it knows the spec better than most of us remember it — but the validation is on you, because the failure modes are silent in the wrong direction.
This guide covers doing CORS in NGINX correctly, using AI to draft and explain while you verify with curl.
What CORS actually is
CORS isn’t a security feature you turn on; it’s a relaxation of the browser’s same-origin policy that you opt into. By default, a browser blocks JavaScript on app.example.com from reading a response from api.example.com. CORS headers are how the server tells the browser “it’s OK, let this origin read me.” Two consequences fall out of that:
- CORS is enforced by the browser, not NGINX. It does nothing for non-browser clients, so it’s not access control.
- Because the browser enforces it, the headers have to be exactly right or the browser blocks the response and your frontend sees an opaque error.
The preflight you can’t skip
For any request that isn’t a “simple” GET/POST — anything with a custom header, a JSON content type, or a method like PUT/DELETE — the browser first sends an OPTIONS preflight asking permission. If NGINX doesn’t answer that OPTIONS correctly, the real request never happens, and your console shows a CORS error that looks like the server is down.
This is the part AI drafts well but you must test. Here’s the prompt I use:
Draft an NGINX location config that adds CORS headers for an API at /api/. Allow only https://app.example.com, allow GET/POST/PUT/DELETE, allow Content-Type and Authorization headers, and support credentials. Handle the OPTIONS preflight with a 204. Comment why each header is there. Output only config.
The result, after I read it carefully:
location /api/ {
# Echo a single allowed origin — NOT a wildcard (see below)
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Max-Age 3600 always;
# Answer the preflight directly; don't proxy OPTIONS to the backend
if ($request_method = OPTIONS) {
return 204;
}
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
Two things worth checking in any generated version:
alwayson eachadd_header— without it, NGINX drops the header on error responses (4xx/5xx), so a CORS error gets a response with no CORS headers and the browser shows a confusing failure.- The
OPTIONSshort-circuit returns 204 with the headers and skips the backend. This is one of the few legitimate uses ofifat the location level (areturnis on the safe list), but it’s worth knowing why it’s here.
The wildcard trap
This is the mistake I see most. Access-Control-Allow-Origin: * is convenient and seems harmless, but the moment you also set Access-Control-Allow-Credentials: true, the browser refuses the combination — the spec forbids wildcard-plus-credentials. So people either ship a wildcard that silently breaks authenticated requests, or they ship a wildcard on an API that should never have been world-readable.
The correct pattern when you support credentials is to echo back a specific, validated origin. If you genuinely need to allow several origins, use a map to validate the incoming Origin against an allowlist rather than reflecting it blindly:
map $http_origin $cors_origin {
default "";
"https://app.example.com" $http_origin;
"https://admin.example.com" $http_origin;
}
server {
location /api/ {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials "true" always;
# ... methods, headers, preflight as above
}
}
Reflecting $http_origin unconditionally — which AI will sometimes suggest as “support all origins” — is effectively a wildcard that also passes credentials, which is the worst of both worlds. Validate against the map so only listed origins get echoed. When I review generated CORS config, this is the first thing I check.
Validate, then prove the headers
The config gate first:
sudo nginx -t
sudo nginx -s reload
Then prove the preflight and the actual response carry the right headers. nginx -t can’t tell you whether the browser will accept the combination — only the headers can:
# Simulate the preflight
curl -i -X OPTIONS https://api.example.com/api/users \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: PUT"
# Expect 204 with Access-Control-Allow-* headers
# Confirm a disallowed origin is NOT echoed back
curl -i -X OPTIONS https://api.example.com/api/users \
-H "Origin: https://evil.example.com"
# Access-Control-Allow-Origin should be empty, not the evil origin
The second test is the one people skip and the one that matters: it proves your allowlist actually rejects origins it shouldn’t trust, rather than reflecting whatever shows up.
Where AI fits
AI drafted the headers correctly, remembered always, and explained the credentials-versus-wildcard rule when I asked. What it didn’t do unprompted was reach for the map-based allowlist instead of reflecting $http_origin, or write the negative test that proves a bad origin is rejected. Those are exactly the judgment calls that keep CORS from becoming a quiet hole. Draft with AI, validate with nginx -t, and curl both a good and a bad origin before you call it done.
More patterns live in the AI for NGINX category, the map and geo conditional routing prompt is the cleaner way to build the origin allowlist, and the prompt library has the security-audit prompt worth running over any CORS config you generate.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.