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.

Steps to configure CORS headers in Apache:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. Test the Apache configuration before reloading the service.
    $ sudo apachectl configtest
    Syntax OK
  7. 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.

  8. 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.

  9. 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.

  10. 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.