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
Steps to configure CORS headers in Apache:
- Enable mod_headers if it is not already loaded.
$ 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.
- Decide the exact URL scope and origin policy before editing the config.
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.
- Open or create a dedicated CORS snippet for Apache.
$ 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.
- Add a scoped CORS rule for the allowed origin and resource path.
<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.
- Enable the new snippet when using the apache2 layout.
$ 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.
- Test the Apache configuration before reloading the service.
$ sudo apachectl configtest Syntax OK
Related: How to test Apache configuration
- Reload Apache to apply the CORS policy.
$ sudo systemctl reload apache2 $ sudo systemctl reload httpd
Use the command that matches your platform's service name.
- Verify that an allowed origin receives the CORS headers on a normal request.
$ 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.
- Verify that the preflight OPTIONS request returns the same allow policy.
$ 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.
- Verify that a disallowed origin does not receive an Access-Control-Allow-Origin header.
$ 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.
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.
