Cross-origin requests are blocked by default, so a frontend on one origin cannot read responses from another unless the server explicitly opts in. Configuring CORS headers in Apache enables controlled sharing of APIs, fonts, images, and other resources across origins, without turning browser security into a production fire drill.

Browsers send an Origin request header and enforce access based on response headers such as Access-Control-Allow-Origin. Requests that use non-simple methods (for example PUT or DELETE) or custom headers (for example Authorization) trigger a preflight OPTIONS request, and that preflight response must include matching Access-Control-Allow-Methods and Access-Control-Allow-Headers values for the browser to proceed.

Overly broad rules can expose private endpoints, and a wildcard origin (*) cannot be used with Access-Control-Allow-Credentials: true. CORS is enforced by browsers rather than the server, so backend authentication and authorization still need to be correct. Commands and paths below follow the Ubuntu and Debian apache2 layout (/etc/apache2, a2enmod, a2enconf, apache2 service).

Steps to configure CORS headers in Apache:

  1. Enable the headers module in Apache.
    $ sudo a2enmod headers
    Enabling module headers.
    To activate the new configuration, you need to run:
      systemctl restart apache2
  2. Select the smallest URL scope that should send CORS headers.

    Use a narrow scope such as an API prefix (for example /api/) or a static directory (for example fonts), instead of setting CORS globally for every response.

  3. Define the exact allowed origin pattern for the browser Origin header.

    Prefer explicit origins such as https://app.internal.example; comma-separated origins are invalid for Access-Control-Allow-Origin, and Access-Control-Allow-Credentials: true requires a non-wildcard origin.

  4. Create a scoped CORS snippet in /etc/apache2/conf-available/cors.conf.
    $ sudo tee /etc/apache2/conf-available/cors.conf >/dev/null <<'EOF'
    <IfModule mod_headers.c>
        # Allow only the listed origin(s).
        # Single origin example:
        SetEnvIfNoCase Origin "^https://app\.internal\.example$" ORIGIN_ALLOWED=$0
    
        # Multiple origin example (uncomment and adjust):
        # SetEnvIfNoCase Origin "^https://(app|admin)\.internal\.example$" ORIGIN_ALLOWED
    
        <LocationMatch "^/api(/|$)">
            # Avoid duplicate headers when an upstream app/proxy also sets CORS.
            Header always unset Access-Control-Allow-Origin
            Header always unset Access-Control-Allow-Methods
            Header always unset Access-Control-Allow-Headers
            Header always unset Access-Control-Allow-Credentials
            Header always unset Access-Control-Max-Age
            Header always unset Access-Control-Expose-Headers
    
            # Core CORS headers (sent only when Origin matches the allowlist above).
            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 cookies or HTTP authentication are required.
            # Header always set Access-Control-Allow-Credentials "true" env=ORIGIN_ALLOWED
    
            # Expose custom response headers to browser JavaScript when needed.
            # Header always set Access-Control-Expose-Headers "Content-Length, Content-Range" env=ORIGIN_ALLOWED
        </LocationMatch>
    </IfModule>
    EOF

    Adjust the Origin regex and the /api scope to match the real application domain(s) and endpoint path(s).

  5. Enable the cors configuration snippet.
    $ sudo a2enconf cors
    Enabling conf cors.
    To activate the new configuration, you need to run:
      systemctl reload apache2
  6. Validate the Apache configuration syntax.
    $ sudo apachectl configtest
    Syntax OK
  7. Reload the apache2 service.
    $ sudo systemctl reload apache2
  8. Verify the response returns CORS headers for an allowed origin.
    $ curl -i -H 'Origin: https://app.internal.example' http://127.0.0.1/api/
    HTTP/1.1 200 OK
    Date: Sat, 10 Jan 2026 05:52:59 GMT
    Server: Apache/2.4.58 (Ubuntu)
    Access-Control-Allow-Origin: https://app.internal.example
    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 #####
  9. Verify a preflight OPTIONS request returns the expected allow headers.
    $ curl -i -X OPTIONS \
      -H 'Origin: https://app.internal.example' \
      -H 'Access-Control-Request-Method: POST' \
      -H 'Access-Control-Request-Headers: Authorization, Content-Type' \
      http://127.0.0.1/api/
    HTTP/1.1 200 OK
    Date: Sat, 10 Jan 2026 05:52:59 GMT
    Server: Apache/2.4.58 (Ubuntu)
    Access-Control-Allow-Origin: https://app.internal.example
    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 #####

    A 404 or 405 on the OPTIONS request usually indicates the backend endpoint does not handle preflight requests for that path.

  10. Verify a disallowed origin does not receive an Access-Control-Allow-Origin header.
    $ curl -i -H 'Origin: https://evil.example' http://127.0.0.1/api/
    HTTP/1.1 200 OK
    Date: Sat, 10 Jan 2026 05:52:59 GMT
    Server: Apache/2.4.58 (Ubuntu)
    ##### snipped #####