CORS on Caddyserver version 2

⚠️ Security Warning: Using Access-Control-Allow-Origin: * allows any website to access your resources. Always specify exact origins in production.

Secure Configuration (Recommended)

For production use with a single trusted origin:

sitename.com {
  # Secure configuration for single trusted origin
  file_server
  encode zstd gzip

  # Handle preflight requests
  @cors_preflight {
    method OPTIONS
  }

  header @cors_preflight {
    Access-Control-Allow-Origin "https://example.com"
    Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
    Access-Control-Allow-Headers "Content-Type, Authorization"
    Access-Control-Max-Age "86400"
    Vary "Origin"
  }

  respond @cors_preflight 204

  # For actual requests
  header {
    Access-Control-Allow-Origin "https://example.com"
    Vary "Origin"
  }
}

Multiple Origins

To support multiple specific origins, use regex matching with Caddy's matcher system:

sitename.com {
  # Validate multiple origins using regex
  @cors_origin_match {
    header_regexp origin Origin ^https?://(www\.)?(example\.com|app\.example\.com)$
  }

  # Preflight for matched origins
  @cors_preflight {
    method OPTIONS
  }

  header @cors_preflight {
    Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
    Access-Control-Allow-Headers "Content-Type, Authorization"
    Access-Control-Max-Age "86400"
  }

  header @cors_origin_match {
    Access-Control-Allow-Origin "{http.request.header.Origin}"
    Vary "Origin"
  }

  respond @cors_preflight 204

  # Your backend
  reverse_proxy localhost:8080
}

Configuration with Credentials

For authenticated APIs that require cookies or authentication headers:

api.example.com {
  # CORS with credentials for authenticated API
  @cors_allowed {
    header Origin https://example.com
  }

  @cors_preflight {
    method OPTIONS
  }

  header @cors_preflight {
    Access-Control-Allow-Origin "https://example.com"
    Access-Control-Allow-Methods "GET, POST, PUT, DELETE"
    Access-Control-Allow-Headers "Content-Type, Authorization"
    Access-Control-Allow-Credentials "true"
    Access-Control-Max-Age "86400"
    Vary "Origin"
  }

  respond @cors_preflight 204

  header @cors_allowed {
    Access-Control-Allow-Origin "https://example.com"
    Access-Control-Allow-Credentials "true"
    Vary "Origin"
  }

  # API backend
  reverse_proxy localhost:3000
}

Path-Specific CORS

Apply different CORS policies to different paths:

example.com {
  # Public API - open CORS
  handle /api/public/* {
    header {
      Access-Control-Allow-Origin "*"
    }
    reverse_proxy localhost:8080
  }

  # Private API - restricted CORS
  handle /api/private/* {
    @cors_origin {
      header Origin https://example.com
    }

    header @cors_origin {
      Access-Control-Allow-Origin "https://example.com"
      Access-Control-Allow-Credentials "true"
      Vary "Origin"
    }

    reverse_proxy localhost:8080
  }

  # Static files - no CORS
  handle {
    file_server
  }
}

Insecure Configuration (Not Recommended)

Only use this for completely public APIs where you want to allow access from any origin:

sitename.com {
  file_server
  encode zstd gzip

  header {
    Access-Control-Allow-Headers *
    Access-Control-Allow-Methods *
    Access-Control-Allow-Origin *
  }
  @options {
    method OPTIONS
  }
  respond @options 204
}

Warning: This configuration allows unrestricted access from any website. It provides no origin validation and should only be used for truly public data.

Key Points

Instructions for Caddy Version 1 (older version) can be found here.

Testing Your CORS Configuration

For comprehensive testing instructions including curl commands, browser DevTools usage, and troubleshooting common CORS errors, see the CORS Testing Guide.

Who’s behind this

Monsur Hossain and Michael Hausenblas

Contribute

The content on this site stays fresh thanks to help from users like you! If you have suggestions or would like to contribute, fork us on GitHub.

Buy the book

Save 39% on CORS in Action with promotional code hossainco at manning.com/hossain