Cross-origin browser requests fail unless the target response explicitly opts into sharing with the requesting origin. Setting CORS headers in Apache is the clean way to expose an API, font path, or other browser-consumed resource to a separate frontend without weakening same-origin protections for the rest of the site.
Apache does not have a single CORS switch. Instead, mod_headers adds the response headers, and the rule should be scoped to the exact URL space that needs browser access. When the allowed origin changes per request, SetEnvIfNoCase can match the incoming Origin header and feed that value into Access-Control-Allow-Origin, while Vary: Origin tells caches that the response depends on that request header.
CORS only controls what browsers expose to JavaScript; it does not replace authentication, authorization, or CSRF protection. Keep the allowlist as narrow as possible, do not combine Access-Control-Allow-Credentials: true with a wildcard origin, and avoid sending the same CORS header from both Apache and the upstream application because browsers reject duplicate Access-Control-Allow-Origin values.
Related: How to add a custom response header in Apache
Related: How to secure Apache web server
$ sudo a2enmod headers Enabling module headers. To activate the new configuration, you need to run: service apache2 restart
On Debian and Ubuntu, a2enmod headers enables the module. On RHEL, AlmaLinux, Rocky Linux, and Fedora, mod_headers is commonly already loaded and the service name is usually httpd.
Browsers accept only one Access-Control-Allow-Origin value per response. Use one explicit origin, or a controlled allowlist that echoes back the matched request origin. If the response must carry cookies or HTTP authentication, enable credentials only for those explicit origins.
Keep the rule on the narrowest practical path, such as /api/ or a font directory, instead of sending CORS headers for the entire site.
$ sudo vi /etc/apache2/conf-available/cors.conf $ sudo vi /etc/httpd/conf.d/cors.conf
Use the first path on Debian or Ubuntu. The second path is the common httpd include directory on RHEL-family systems.
<IfModule mod_headers.c> SetEnvIfNoCase Origin "^https://app\.example\.com$" ORIGIN_ALLOWED=$0 <LocationMatch "^/api(/|$)"> Header onsuccess unset Access-Control-Allow-Origin Header always unset Access-Control-Allow-Origin Header onsuccess unset Access-Control-Allow-Methods Header always unset Access-Control-Allow-Methods Header onsuccess unset Access-Control-Allow-Headers Header always unset Access-Control-Allow-Headers Header onsuccess unset Access-Control-Max-Age Header always unset Access-Control-Max-Age Header onsuccess unset Access-Control-Allow-Credentials Header always unset Access-Control-Allow-Credentials Header onsuccess unset Access-Control-Expose-Headers Header always unset Access-Control-Expose-Headers Header always set Access-Control-Allow-Origin "%{ORIGIN_ALLOWED}e" env=ORIGIN_ALLOWED Header always set Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" env=ORIGIN_ALLOWED Header always set Access-Control-Allow-Headers "Authorization, Content-Type" env=ORIGIN_ALLOWED Header always set Access-Control-Max-Age "86400" env=ORIGIN_ALLOWED Header always merge Vary "Origin" env=ORIGIN_ALLOWED # Enable only when the browser must send cookies or HTTP auth. # Header always set Access-Control-Allow-Credentials "true" env=ORIGIN_ALLOWED # Expose non-simple response headers to browser JavaScript when needed. # Header always set Access-Control-Expose-Headers "Content-Length, Content-Range" env=ORIGIN_ALLOWED </LocationMatch> </IfModule>
Replace https://app.example.com and ^/api(/|$) with the real frontend origin and path scope for your application.
If a proxied application or framework already emits CORS headers, keep the policy in one layer only. Duplicate Access-Control-Allow-Origin headers cause browser failures even when the values look correct.
$ sudo a2enconf cors Enabling conf cors. To activate the new configuration, you need to run: service apache2 reload
Skip this step on RHEL-family systems because files under /etc/httpd/conf.d/ are loaded automatically.
$ sudo apachectl configtest Syntax OK
Related: How to test Apache configuration
$ sudo systemctl reload apache2 $ sudo systemctl reload httpd
Use the command that matches your platform's service name.
$ curl -i -H 'Origin: https://app.example.com' http://127.0.0.1:8080/api/ HTTP/1.1 200 OK Date: Wed, 08 Apr 2026 04:16:46 GMT Server: Apache/2.4.58 (Ubuntu) Access-Control-Allow-Origin: https://app.example.com Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS Access-Control-Allow-Headers: Authorization, Content-Type Access-Control-Max-Age: 86400 Vary: Origin ##### snipped #####
Replace 127.0.0.1:8080 and /api/ with the real hostname, scheme, port, and path for the resource you just scoped.
$ curl -i -X OPTIONS \ -H 'Origin: https://app.example.com' \ -H 'Access-Control-Request-Method: POST' \ -H 'Access-Control-Request-Headers: Authorization, Content-Type' \ http://127.0.0.1:8080/api/ HTTP/1.1 200 OK Date: Wed, 08 Apr 2026 04:16:46 GMT Server: Apache/2.4.58 (Ubuntu) Access-Control-Allow-Origin: https://app.example.com Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS Access-Control-Allow-Headers: Authorization, Content-Type Access-Control-Max-Age: 86400 Vary: Origin Allow: POST,OPTIONS,HEAD,GET Content-Length: 0 ##### snipped #####
If the preflight request returns 404 or 405, the backend route usually needs explicit OPTIONS handling even though Apache is already adding the CORS headers.
$ curl -i -H 'Origin: https://evil.example' http://127.0.0.1:8080/api/ HTTP/1.1 200 OK Date: Wed, 08 Apr 2026 04:16:46 GMT Server: Apache/2.4.58 (Ubuntu) ##### snipped #####
The success state is simple: allowed origins receive the expected CORS headers, disallowed origins do not, and apachectl configtest stays clean before each reload.