design/cors-configuration.md
This document proposes a way to enable CORS (Cross-origin resource sharing) policies and therefore, allow access to resources in a cluster from a webpage deployed at a different domain.
Nowadays, most modern browsers don't allow requests to a domain from a webpage that has been fetched from a different domain. This is a protection mechanism against CSRF (Cross-Site Request Forgery) attacks. To explain what a CSRF attack is, it is necessary to explain how authentication and user sessions have been handled traditionally by most web applications:
Knowing how authentication works, a malicious webpage could try to force a user to execute unwanted actions on a web application in which they're currently authenticated using some social engineering. This could be an attack example:
www.mybank.com) receives an email saying that they won a brand new car. To get the prize, they only have to visit www.malicious-site.com/winner and fill out a form with some personal data.www.mybank.com/transfer which is the URL used for doing money transfers on the online bank.mybank.com, the authentication cookie is sent automatically.To avoid this kind of scenario, web browsers apply the same-origin policy.
Under the same-origin policy, a web browser allows scripts contained on a web page to access data on another server, but only if both share the same origin. An origin is defined as a combination of URI scheme, host name, and port number. Thanks to same-origin policy, attacks like the one explained previously are prevented by the browser itself because www.mybank.com and www.malicious-site.com don't share the same origin.
However, the way web applications are developed has evolved, and nowadays it’s very frequent to separate the frontend from the backend, deploying them independently. For instance, the frontend could be a Javascript single page application deployed on a CDN (myfrontend.com) and the backend, a microservices cluster deployed somewhere else (mybackend.com).
As the Javascript application needs to send requests to the API exposed by the backend and they are hosted on different domains, the web browser will prevent any communication between them due to the same-origin policy. This is where CORS comes into play.
CORS is a mechanism that allows bypassing the same-origin policy for trusted sources. This is the way it works in its simplest form:
Origin request header with the web page's origin as the value.Origin header and if the origin is allowed, it sets the Access-Control-Allow-Origin header to the Origin value.Access-Control-Allow-Origin header matches the origin of the tab the request originated from. If it doesn't match, it throws an error.The following are the criteria that define a simple request:
If the content of the request doesn't meet the criteria above, the browser first checks whether the actual request should be sent. This is done by sending a special request (called preflight request) to the server in advance. A preflight request first sends an HTTP request to the resource using the OPTIONS method with the following headers:
Origin: Specifies the domain that would like access to the resource. This is inserted by the browser in a cross-origin request.Access-Control-Request-Method: The HTTP method to be used in the actual request from the browser.Access-Control-Request-Headers: The custom headers to be sent in the actual cross-origin request.In response to a preflight request, the server sends a response with the following headers:
Access-Control-Allow-Origin: Specifies the domains allowed to access the resource.Access-Control-Allow-Credentials: Indicates whether browser credentials can be used to make the actual request (cookies for instance).Access-Control-Allow-Private-Network: Part of the PNA specification Pre-check requests will carry this header.Access-Control-Expose-Headers: Allows headers to be exposed to the browser.Access-Control-Max-Age: Specifies how long preflight request results can be cached.Access-Control-Allow-Methods: Indicates which methods are allowed when making an actual request.Access-Control-Allow-Headers: Indicates which headers can be used in the actual request.When the browser gets the preflight response, it checks if the origin is allowed and if the HTTP method and headers of the main request are in the list returned by the server. If so, it sends the main request, which will be a regular cross-origin request, it will include the Origin header and the response will contain Access-Control-Allow-Origin once again.
This proposal introduces a way to set all the CORS related configuration in Contour, letting Envoy do all the heavy work.
Envoy supports CORS via a filter and it can be configured using the API. The changes proposed in this document will allow the configuration of CORS policies in Contour.
At a high level the proposed changes will imply:
apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
name: google apis
namespace: prod
spec:
virtualhost:
fqdn: www.googleapis.com
# The CORS policy described here will apply to all the routes of the virtual host.
corsPolicy:
# Which domains can access the matched resources in a cross-site manner.
allowOrigin:
- "*"
# Which HTTP methods are allowed for cross-origin requests (case-sensitive).
allowMethods:
- GET
- POST
# The headers the server is going to accept (case-insensitive).
allowHeaders:
- cache-control
- content-type
- custom-header
# The non simple headers the client will be able to access (case-insensitive).
exposeHeaders:
- Content-Length
- Content-Range
# Whether the server allows sending credentials (cookies for instance) in cross-origin requests.
allowCredentials: true
allowPrivateNetwork: true
# the amount of time the preflight response will be cached. It's expressed in the Go duration format. If not supplied, browser default values will apply.
maxAge: 10m
routes:
- conditions:
- prefix: /analytics
services:
- name: analytics-api
port: 9999
The names and types of the proposed fields are inspired by the HTTP headers. The maxAge is going to be parsed into a time.Duration as this is going to be the type used in the Dag. If no value is provided, browser defaults will apply.
The virtual host struct will be updated to contain the CORS related information:
// contour/apis/contour/v1beta1/projectcontour/v1/httpproxy.go
// VirtualHost appears at most once. If it is present, the object is considered
// to be a "root".
type VirtualHost struct {
[... other members ...]
// Specifies the CORS policy to apply for the matched path.
// +optional
CorsPolicy *CorsPolicy `json:"corsPolicy,omitempty"`
}
// CorsPolicy allows setting de CORS policy
type CorsPolicy struct {
// Specifies whether the resource allows credentials.
AllowCredentials bool `json:"allowCredentials"`
// AllowOrigin specifies the origins that will be allowed to do CORS requests.
AllowOrigin []string `json:"allowOrigin"`
// AllowMethods specifies the content for the *access-control-allow-methods* header.
AllowMethods []string `json:"allowMethods"`
// AllowHeaders specifies the content for the *access-control-allow-headers* header.
AllowHeaders []string `json:"allowHeaders"`
// ExposeHeaders Specifies the content for the *access-control-expose-headers* header.
ExposeHeaders []string `json:"exposeHeaders"`
// MaxAgeSeconds specifies the content for the *access-control-max-age* header.
MaxAge string `json:"maxAge"`
// AllowPrivateNetwork specifies the content for the *access-control-allow-private-network* header.
AllowPrivateNetwork bool `json:"allowPrivateNetwork,omitempty"`
}
The common DAG struct will be updated accordingly:
// contour/internal/dag/dag.go
// A VirtualHost represents a named L4/L7 service.
type VirtualHost struct {
[... other members ...]
CorsPolicy *CorsPolicy
}
In order to enable the CORS filter in Envoy we will update contour/internal/envoy/route.go and map the values from DAG's route to protobuf.
All the CORS related tasks could be handled in each application using an application level middleware or similar. However, it isn't something people usually want to do at this level for the following reasons:
It would be great if we could just enable a flag and apply some sane defaults. Unfortunately, it's not easy to find some defaults that would work for most of the proposed fields:
X-Requested-With header while for grpc-web services, you'll want to allow headers like x-grpc-web or grpc-timeout.This approach wouldn't be valid for managing all the CORS related logic. Some of the headers like Access-Control-Allow-Headers are only set in response to preflight requests (method=OPTIONS). In addition to this, we have to take into account that CORS is not just about headers, some server side logic is needed as well. For example, the server should return the Access-Control-Allow-Origin header with the Origin value sent by the browser only if that Origin is allowed.