How to Configure CORS in Nginx

Configure CORS in NGINX for APIs, fonts, localhost dev, credentials, preflight checks, duplicate headers, and rollback.

PublishedAuthorJoshua JamesRead time13 minGuide typeNginx

CORS becomes a server problem as soon as a browser app at one origin needs to read an API, font, image, or JSON response from another origin. To configure CORS in NGINX correctly, the response must match the requesting origin, method, requested headers, and credential mode instead of sending a broad wildcard everywhere.

The safest NGINX pattern is narrow: place CORS headers only on the location that needs cross-origin browser access, use * only for public non-credentialed content, use a map-based allowlist for APIs with cookies or authorization headers, test the syntax, reload NGINX, and verify the delivered headers with curl. If NGINX is not installed yet, start with the relevant install workflow for Ubuntu, Debian, or Fedora.

Configure CORS in NGINX

NGINX adds CORS response headers with the NGINX add_header directive. The always parameter matters because CORS headers otherwise disappear from many error responses, which can make a browser show a generic CORS failure instead of the real 401, 403, 404, or 500 response.

CORS pieceUse in NGINXImportant boundary
Access-Control-Allow-OriginAllow one origin, a mapped allowlist value, or *.Use * only when browsers do not send credentials.
Access-Control-Allow-CredentialsSend true only for trusted origins that need cookies, HTTP auth, or client certificates.Browsers reject credentialed responses that use Access-Control-Allow-Origin: *.
Access-Control-Allow-MethodsAnswer browser preflight checks with the methods the endpoint supports.Do not expose verbs the application route cannot safely handle.
Access-Control-Allow-HeadersAllow request headers such as Authorization and Content-Type.Prefer an explicit list over reflecting arbitrary requested headers.
Access-Control-Expose-HeadersExpose response headers that browser JavaScript needs to read, such as Content-Range or X-Request-ID.Only name headers the client actually uses.
Vary: OriginSend it when the allowed origin changes by request.Prevents shared caches from reusing one origin’s response for another origin.
mapCreate allowlist variables with the NGINX map directive in the http context.Place map outside every server block.

CORS is not authentication. It controls whether browser JavaScript can read a response; it does not stop non-browser clients, same-origin code, server-side requests, or direct requests from reaching the endpoint.

Match the CORS Pattern to the Request

Choose the narrowest pattern that matches the browser request. Most NGINX CORS problems come from using one broad snippet for assets, APIs, cookies, development ports, and proxied applications that actually need different boundaries.

Request typeUseful NGINX patternAvoid
Public fonts, images, JSON files, or downloadsWildcard origin on the exact static-file location, with no credentials header.Applying the wildcard to private API, admin, account, or billing routes.
One trusted frontend calling one APIFixed exact origin such as https://app.example.com, explicit methods, explicit headers, and credentials only when needed.Reflecting every origin or using * with cookies or authorization.
Several trusted frontends or controlled development portsmap-based allowlist keyed on $http_origin, with Vary: Origin.Regexes that trust every subdomain, every localhost port, or every preview URL without review.
Reverse-proxied app that already sends CORSPick one owner. Let the app own CORS, or hide upstream CORS headers and add the final policy in NGINX.Letting both layers send duplicate Access-Control-Allow-Origin values.
Cookie-backed session APIExact origin allowlist, Access-Control-Allow-Credentials: true, normal application auth, and correct cookie attributes.Treating CORS as an authorization rule or expecting it to fix blocked third-party cookies.

Choose the CORS Owner Layer

Configure CORS at one layer. If the application framework already sets complete CORS headers, let the app own them and avoid adding a second set in NGINX. If NGINX is the public origin, reverse proxy, or static-file layer that should own the final browser response, keep the headers in the matching server or location block.

Managed layers can change the final result. Cloudflare, another CDN, Kubernetes Ingress, NGINX Proxy Manager, Plesk, cPanel, a load balancer, or an application gateway may add, strip, or cache response headers before the browser sees them. Verify the public edge response after changing origin NGINX.

Back Up the Active NGINX Configuration

Back up the main configuration before changing a working host. This protects the top-level include structure, but it is not a substitute for backing up the site file you will edit.

sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.backup

Check which site include paths the active configuration loads. Debian and Ubuntu packages commonly use sites-enabled, while Fedora, RHEL-family systems, nginx.org packages, and container images commonly use conf.d. Keep the owning file marker with the include line so you know which file controls the layout:

sudo nginx -T 2>/dev/null | awk '
  /^# configuration file / { file=$0 }
  /^[[:space:]]*include[[:space:]].*(sites-enabled|conf\.d)/ {
    print file
    print
  }
'

Relevant output often includes the owning file and one or more include paths:

# configuration file /etc/nginx/nginx.conf:
include /etc/nginx/conf.d/*.conf;
# configuration file /etc/nginx/nginx.conf:
include /etc/nginx/sites-enabled/*;

After you identify the file that contains the server or location block you will edit, back up that file too. Set site_file to the real path from your configuration dump:

site_file=/etc/nginx/conf.d/api.example.com.conf
sudo cp -a "$site_file" "$site_file.bak"

If no site include appears, inspect the http block in /etc/nginx/nginx.conf and place the CORS map and server configuration where that package layout actually loads site files. The NGINX server blocks guide covers the common layout split in more depth.

Configure Public CORS for Static Files

Public static files such as fonts, public images, open JSON feeds, or downloadable assets can use the wildcard origin when they do not depend on cookies or authorization headers. Keep this pattern away from private dashboards, user APIs, billing data, admin endpoints, and account-specific responses.

Add the headers to the location that serves public assets:

server {
    listen 80;
    server_name static.example.com;

    root /var/www/static.example.com;

    location /assets/ {
        add_header Access-Control-Allow-Origin "*" always;
        add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
        add_header Access-Control-Allow-Headers "Content-Type" always;
        add_header Access-Control-Max-Age 600 always;

        if ($request_method = OPTIONS) {
            return 204;
        }

        try_files $uri =404;
    }
}

The OPTIONS branch returns an empty 204 preflight response before static-file lookup. Keep the branch limited to return; do not copy unrelated NGINX directives into an if block just because the condition is convenient.

Allow CORS for Fonts Served by NGINX

Font CORS errors are common when CSS loads from one origin and font files load from another, such as www.example.com using fonts from static.example.com. Public font files can use wildcard CORS when they do not depend on cookies or authorization headers.

Add the headers to the font-serving location, or merge them into the existing static-file location that already handles font extensions:

location ~* \.(eot|otf|ttf|woff|woff2)$ {
    add_header Access-Control-Allow-Origin "*" always;
    add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
    add_header Access-Control-Allow-Headers "Content-Type" always;
    add_header Access-Control-Max-Age 86400 always;

    if ($request_method = OPTIONS) {
        return 204;
    }

    try_files $uri =404;
}

If the same site also sets cache headers for fonts, keep the CORS headers and cache headers in the location that wins the request. The NGINX browser caching guide covers static-asset cache headers and extension-based locations.

Configure CORS for One Trusted Frontend

If one browser app calls one API, a fixed exact origin is clearer than a multi-origin map. This pattern fits a single production frontend such as https://app.example.com calling https://api.example.com.

server {
    listen 80;
    server_name api.example.com;

    location /api/ {
        add_header Access-Control-Allow-Origin "https://app.example.com" always;
        add_header Access-Control-Allow-Credentials "true" always;
        add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" always;
        add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Requested-With" always;
        add_header Access-Control-Max-Age 600 always;

        if ($request_method = OPTIONS) {
            return 204;
        }

        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Browsers only accept the response when the request’s Origin matches the fixed value. If you need production, admin, staging, or localhost origins at the same endpoint, use the allowlist map pattern instead of adding several fixed Access-Control-Allow-Origin lines.

Configure Allowlisted CORS for an API

Credentialed API requests need a specific allowed origin, not *. Use an NGINX map in the http context so trusted origins receive CORS headers and untrusted origins receive no Access-Control-Allow-Origin value.

Match the browser origin exactly. The origin includes the scheme, hostname, and non-default port, but not the URL path, so https://app.example.com and http://localhost:5173 are separate allowlist entries.

Place the maps in a file that is loaded from the http context, such as a site file under conf.d or sites-enabled when those paths are included from http:

map $http_origin $cors_allowed_origin {
    default "";
    "https://app.example.com" $http_origin;
    "https://admin.example.com" $http_origin;
}

map $http_origin $cors_allow_credentials {
    default "";
    "https://app.example.com" "true";
    "https://admin.example.com" "true";
}

map $http_origin $cors_allow_methods {
    default "";
    "https://app.example.com" "GET, POST, PUT, PATCH, DELETE, OPTIONS";
    "https://admin.example.com" "GET, POST, PUT, PATCH, DELETE, OPTIONS";
}

map $http_origin $cors_allow_headers {
    default "";
    "https://app.example.com" "Authorization, Content-Type, X-Requested-With";
    "https://admin.example.com" "Authorization, Content-Type, X-Requested-With";
}

map $http_origin $cors_max_age {
    default "";
    "https://app.example.com" "600";
    "https://admin.example.com" "600";
}

The empty default "" values are intentional. They keep the CORS header values empty for origins that do not match the allowlist, so rejected origins do not receive browser approval headers.

Add Local Development Origins Deliberately

Local frontend tools often use origins such as http://localhost:5173, http://localhost:3000, or http://127.0.0.1:5173. Add only the exact development ports you use, and keep those entries in development or staging configuration unless production browsers must reach them.

For a map-based policy, add the development origins to every map that controls the same location. The result value must match the map’s purpose:

map $http_origin $cors_allowed_origin {
    default "";
    "https://app.example.com" $http_origin;
    "http://localhost:5173" $http_origin;
    "http://127.0.0.1:5173" $http_origin;
}

map $http_origin $cors_allow_credentials {
    default "";
    "https://app.example.com" "true";
    "http://localhost:5173" "true";
    "http://127.0.0.1:5173" "true";
}

map $http_origin $cors_allow_methods {
    default "";
    "https://app.example.com" "GET, POST, PUT, PATCH, DELETE, OPTIONS";
    "http://localhost:5173" "GET, POST, PUT, PATCH, DELETE, OPTIONS";
    "http://127.0.0.1:5173" "GET, POST, PUT, PATCH, DELETE, OPTIONS";
}

Repeat the same origin keys in the header and max-age maps if those maps are separate. Do not allow every localhost port by default; a broad localhost pattern can approve unrelated local web apps or ports you did not intend to trust.

Use those variables in the API location. Replace the upstream target with the application listener that NGINX already reaches:

server {
    listen 80;
    server_name api.example.com;

    location /api/ {
        add_header Access-Control-Allow-Origin $cors_allowed_origin always;
        add_header Access-Control-Allow-Credentials $cors_allow_credentials always;
        add_header Access-Control-Allow-Methods $cors_allow_methods always;
        add_header Access-Control-Allow-Headers $cors_allow_headers always;
        add_header Access-Control-Max-Age $cors_max_age always;
        add_header Vary "Origin" always;

        if ($request_method = OPTIONS) {
            return 204;
        }

        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

This pattern keeps disallowed origins from receiving Access-Control-Allow-Origin or Access-Control-Allow-Credentials. The request can still reach the application, so sensitive endpoints must keep normal authentication and authorization checks in the application or upstream service.

Do not blindly reflect every Origin value into Access-Control-Allow-Origin when credentials are enabled. That pattern can let any website read credentialed responses from browsers that already hold a session.

Avoid Duplicate CORS Headers from Upstreams

If the upstream application also sends CORS headers, browsers can reject the response because they receive duplicate or conflicting values. Choose one owner. When NGINX must own the final CORS policy for a proxied location, hide upstream CORS headers before adding the NGINX-controlled set:

location /api/ {
    proxy_hide_header Access-Control-Allow-Origin;
    proxy_hide_header Access-Control-Allow-Credentials;
    proxy_hide_header Access-Control-Allow-Methods;
    proxy_hide_header Access-Control-Allow-Headers;
    proxy_hide_header Access-Control-Expose-Headers;
    proxy_hide_header Access-Control-Max-Age;

    add_header Access-Control-Allow-Origin $cors_allowed_origin always;
    add_header Access-Control-Allow-Credentials $cors_allow_credentials always;
    add_header Access-Control-Allow-Methods $cors_allow_methods always;
    add_header Access-Control-Allow-Headers $cors_allow_headers always;
    add_header Access-Control-Max-Age $cors_max_age always;
    add_header Vary "Origin" always;

    if ($request_method = OPTIONS) {
        return 204;
    }

    proxy_pass http://127.0.0.1:3000;
}

The NGINX proxy_hide_header directive applies to proxied responses only. It does not remove headers generated by NGINX itself or headers added by a CDN after the origin response leaves the server.

Test and Reload NGINX After CORS Changes

Test the full NGINX configuration before applying the new headers:

sudo nginx -t

A successful syntax test prints the standard OK and successful lines:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Reload NGINX only after the syntax test passes:

sudo systemctl reload nginx

On systems that do not use systemd for NGINX, use the service manager or control panel that owns the running NGINX process. A reload keeps existing worker processes serving active requests while new workers pick up the updated configuration.

Verify NGINX CORS Headers with curl

Start with an allowed actual request. Use the same origin your browser application uses:

curl -sS -D - -o /dev/null \
  -H "Origin: https://app.example.com" \
  https://api.example.com/api/users

Relevant response headers should include the allowed origin, credentials when used, and Vary: Origin:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin

Check the preflight path with the method and request headers your browser sends before the real request:

curl -sS -D - -o /dev/null -X OPTIONS \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Authorization, Content-Type" \
  https://api.example.com/api/users

A working preflight response usually returns 204 with the allowed origin, method list, header list, and optional cache lifetime:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, X-Requested-With
Access-Control-Max-Age: 600
Vary: Origin

Confirm disallowed origins do not receive CORS approval. Filtering the response keeps the check focused on the status line, CORS headers, and cache variation:

curl -sS -D - -o /dev/null \
  -H "Origin: https://evil.example" \
  https://api.example.com/api/users | grep -iE '^(HTTP/|access-control|vary:)'

With the allowlist pattern, a disallowed origin can still receive the normal HTTP response, but it should not receive Access-Control-Allow-Origin or Access-Control-Allow-Credentials:

HTTP/1.1 200 OK
Vary: Origin

If you test before public DNS is ready, send the request to the local listener with a host header instead. This proves the target NGINX server block without depending on external DNS:

curl --noproxy '*' -sS -D - -o /dev/null \
  -H "Host: api.example.com" \
  -H "Origin: https://app.example.com" \
  http://127.0.0.1/api/users

For HTTPS server blocks, keep the real scheme and SNI with --resolve instead of changing the request to plain HTTP:

curl --noproxy '*' --resolve api.example.com:443:127.0.0.1 \
  -sS -D - -o /dev/null \
  -H "Origin: https://app.example.com" \
  https://api.example.com/api/users

This keeps TLS certificate selection and HTTPS-only server blocks in the test path while still bypassing public DNS.

Adjust CORS for Common NGINX Workloads

Reverse Proxy APIs

For a reverse proxy API, apply CORS at the proxy location only when NGINX should own the final browser response. Keep normal proxy headers such as Host, X-Real-IP, and X-Forwarded-Proto beside the CORS block so the upstream application still sees the original request context. The NGINX reverse proxy guide covers the base proxy workflow.

Expose Pagination, Request-ID, and Range Headers

Browsers can read only the normal CORS-safelisted response headers unless the response names additional headers with Access-Control-Expose-Headers. Add it when client code needs pagination, rate-limit, request-id, or range metadata from response headers. Common examples include Content-Range, X-Total-Count, Link, RateLimit-Remaining, and X-Request-ID.

For an allowlisted API, map the exposed header list with the other http-context CORS maps:

map $http_origin $cors_expose_headers {
    default "";
    "https://app.example.com" "Content-Range, X-Total-Count, Link, RateLimit-Remaining, X-Request-ID";
    "https://admin.example.com" "Content-Range, X-Total-Count, Link, RateLimit-Remaining, X-Request-ID";
}

Then add the response header in the same CORS-enabled location:

add_header Access-Control-Expose-Headers $cors_expose_headers always;

For public non-credentialed static responses, a fixed value can be enough. Do not expose headers by habit. Keep secrets, internal upstream names, session identifiers, and diagnostic-only values out of browser-readable response headers. If the browser Network panel shows a response header but JavaScript cannot read it, check this exposed-header list before changing the origin allowlist.

WebSocket and Socket.IO Endpoints

WebSocket handshakes use an Origin header, but normal CORS preflight rules are not a replacement for application-side WebSocket origin checks. Configure allowed origins in the WebSocket application, then keep the NGINX upgrade headers and timeout settings correct. Use the NGINX WebSocket proxy guide for the upgrade-header pattern.

Security Headers and Nested Locations

NGINX inherits add_header directives from the previous level only when the current level defines no add_header directives of its own. If a CORS location also needs security headers, include the full security header snippet again or define all required headers at that level. NGINX 1.29.3 and newer can change this behavior with add_header_inherit, but many packaged builds do not have that directive yet, so repeated headers remain the portable pattern. The NGINX security headers guide explains the inheritance trap in more detail.

Location Matching and Path Scope

CORS headers apply only to the location that handles the request. If /api/ works but /api/v2/ or a static-file regex location does not, inspect which location wins. Use the NGINX location block priority guide when prefix, exact, and regex locations overlap.

Troubleshoot NGINX CORS Errors

SymptomLikely causeFirst check
Browser reports missing Access-Control-Allow-OriginThe matched location does not add CORS headers, the origin is not in the allowlist, or an error response lacks always.Check the same URL with curl -D - and the real Origin.
Browser rejects wildcard with credentialsThe response combines * with cookies, authorization, or credential mode.Use an explicit origin allowlist and keep Access-Control-Allow-Credentials: true off wildcard responses.
Preflight returns 404, 405, or 502OPTIONS is not handled at the NGINX location or upstream route.Send a manual OPTIONS request with Access-Control-Request-Method.
JavaScript cannot read pagination or request-id headersThe response lacks Access-Control-Expose-Headers for those header names.Compare browser code needs with the exposed-header list in the CORS location.
Cookie login still fails after CORS headers look correctThe cookie is missing, blocked by SameSite, missing Secure, or blocked by browser third-party cookie policy.Inspect Set-Cookie plus browser storage warnings, not only CORS headers.
Only some paths failA different location block wins or a nested add_header resets inheritance.Inspect the merged config with sudo nginx -T.
Duplicate CORS headers appearBoth NGINX and the upstream application add CORS headers.Choose one owner and use proxy_hide_header only when NGINX owns the policy.

Fix Missing CORS Headers on Error Responses

If normal 200 responses include CORS headers but 401, 403, 404, or 500 responses do not, check for missing always parameters:

sudo nginx -T 2>/dev/null | awk '
  /^# configuration file / { file=$0; shown=0 }
  /^[[:space:]]*add_header[[:space:]]+Access-Control/ {
    if (!shown) { print file; shown=1 }
    print
  }
'

The file marker shows which included file owns the matching lines. Every CORS add_header line that should appear on error responses should end with always;. Update the matching lines, then test and reload NGINX.

Fix Wildcard Origin with Credentials

Browsers reject credentialed CORS when the response uses Access-Control-Allow-Origin: *. The MDN CORS documentation describes the same boundary: credentialed requests need an explicit origin value, and dynamic origin responses should include Vary: Origin.

Replace wildcard CORS on private API routes with the allowlist map pattern. Keep wildcard CORS only on public resources that do not use cookies, authorization headers, TLS client certificates, or account-specific response bodies.

Fix Exact Origin Mismatches

If the correct NGINX location handles the request but the allowlisted response is still missing, compare the browser’s Origin header with the map keys. The scheme, hostname, and non-default port must match exactly. Paths such as /login or /api do not belong in a CORS origin.

curl -sS -D - -o /dev/null \
  -H "Origin: http://localhost:5173" \
  https://api.example.com/api/users | grep -iE '^(HTTP/|access-control|vary:)'

If that origin is trusted, add the exact value to each CORS map that controls origin, credentials, methods, headers, and max age. Avoid using null or broad development-origin patterns as a production shortcut; serve the frontend from a normal trusted origin instead.

Fix Failed Preflight OPTIONS Requests

Preflight errors usually mean the browser’s OPTIONS request did not receive the same CORS approval as the real request. Reproduce the preflight from the terminal:

curl -sS -D - -o /dev/null -X OPTIONS \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Authorization, Content-Type" \
  https://api.example.com/api/users

If the response is 404 or 405, add the OPTIONS return branch in the matching NGINX location or let the upstream application handle preflight completely. If the response is 502, troubleshoot the upstream route before changing CORS headers.

Fix CORS Headers Missing from One Path

When one path works and another path fails, NGINX is probably using a different location block. Print the merged configuration and inspect the matching locations:

sudo nginx -T 2>/dev/null | awk '
  /^# configuration file / { file=$0; shown=0 }
  /^[[:space:]]*(location[[:space:]]|add_header[[:space:]]+Access-Control|proxy_pass[[:space:]])/ {
    if (!shown) { print file; shown=1 }
    print
  }
'

Move the CORS headers into the location that actually serves the failing URL, or split the route so API, static, PHP, and proxy paths do not compete for the same request. For deeper status-code checks, use the NGINX access and error logs guide to confirm which location or upstream handled the request.

Fix Duplicate Access-Control-Allow-Origin Headers

Duplicate Access-Control-Allow-Origin headers usually mean an upstream app and NGINX both add CORS. Verify the public response first:

curl -sS -D - -o /dev/null \
  -H "Origin: https://app.example.com" \
  https://api.example.com/api/users | grep -i '^access-control-allow-origin:'

If two values appear, remove CORS from one layer. When NGINX owns the final policy for a reverse proxy, add the relevant proxy_hide_header lines before the NGINX add_header lines. When the app owns CORS, delete the duplicate NGINX CORS block and reload.

Fix Headers That JavaScript Cannot Read

If the browser Network panel shows Content-Range, X-Total-Count, Link, or X-Request-ID but frontend code reads null, the response probably needs Access-Control-Expose-Headers. Verify the delivered header list from the same origin the browser uses:

curl -sS -D - -o /dev/null \
  -H "Origin: https://app.example.com" \
  https://api.example.com/api/users | grep -iE '^(HTTP/|access-control-expose-headers:|content-range:|x-total-count:|link:|ratelimit-remaining:|x-request-id:)'

If the application sends the response header but Access-Control-Expose-Headers does not name it, add the header name to the exposed-header map or fixed CORS block, then test and reload NGINX.

Fix Cookies Missing After CORS Succeeds

A response can pass CORS and still fail to create or send cookies. For cross-site session cookies, the application usually needs cookie attributes such as SameSite=None and Secure, and the browser can still apply third-party cookie restrictions.

Confirm the response sends both the expected CORS headers and the application cookie:

curl -sS -D - -o /dev/null \
  -H "Origin: https://app.example.com" \
  https://api.example.com/api/session | grep -iE '^(HTTP/|access-control|set-cookie:|vary:)'

If Set-Cookie is missing, fix the upstream application route first. If Set-Cookie is present but the browser rejects it, inspect the browser storage or console message for cookie attribute and third-party cookie policy details before changing the NGINX CORS block again.

Roll Back NGINX CORS Changes

Remove only the file or lines you created for CORS. If you added a separate conf.d file, set cors_file to that file and remove only that path:

cors_file=/etc/nginx/conf.d/cors.conf
if sudo test -f "$cors_file"; then
    sudo rm "$cors_file"
    sudo nginx -t && sudo systemctl reload nginx
fi

If you edited an existing site file, restore the site-specific backup instead of replacing the entire NGINX configuration. Set site_file to the same path you backed up before the change:

site_file=/etc/nginx/conf.d/api.example.com.conf
if sudo test -f "$site_file.bak"; then
    sudo cp -a "$site_file.bak" "$site_file"
    sudo nginx -t && sudo systemctl reload nginx
fi

Use the matching sites-available, sites-enabled, or conf.d site path from your configuration dump. Verify the original endpoint after reload with the same curl request that failed or succeeded before the CORS change.

Conclusion

NGINX is serving CORS correctly when the target location sends the expected headers for trusted origins, preflight requests return a clean 204, disallowed origins receive no browser approval, and syntax tests pass before every reload. Keep wildcard CORS limited to public non-credentialed assets, use allowlists for APIs, and verify the public edge when a CDN or proxy sits in front of NGINX.

Share this guide

Help another Linux user troubleshoot faster

Share this guide with someone troubleshooting Linux systems or saving it for later.

Follow LinuxCapable

Want more LinuxCapable guides in Google?

Add LinuxCapable as a preferred source so Google can show our tutorials more often in Top Stories and mark them as preferred in AI Mode and AI Overviews when relevant.

Add LinuxCapable as a preferred source on Google
Search LinuxCapable

Need another guide?

Search LinuxCapable for package installs, commands, troubleshooting, and follow-up guides related to what you just read.

Found this guide useful?

Support LinuxCapable to keep tutorials free and up to date.

Buy me a coffeeBuy me a coffee
Before commenting, please review our Comments Policy.
Formatting tips for your comment

You can use basic HTML to format your comment. Useful tags currently allowed in published comments:

You type Result
<code>command</code> command
<strong>bold</strong> bold
<em>italic</em> italic
<a href="https://example.com">link</a> link
<blockquote>quote</blockquote> quote block

Add to the discussion

Questions, fixes, command output, and version notes help keep this guide current.

Verify before posting: