Ingress-nginx: Feature Request: CORs to allow specific domain

Created on 17 Aug 2017  Â·  18Comments  Â·  Source: kubernetes/ingress-nginx

Currently setting:

ingress.kubernetes.io/enable-cors: "true"

adds the following header:

add_header 'Access-Control-Allow-Origin' "*";

It would be great if we can have an annotation like:

ingress.kubernetes.io/cors-allow-origin: "sitea.com,siteb.com"

nginx

Most helpful comment

I used this post to inject a snippet, which does the job really well. The good thing with this approach is that it doesn't broadcast the list of allowed sites.

annotations:
   kubernetes.io/ingress.class: "nginx"
   nginx.ingress.kubernetes.io/configuration-snippet: |
      if ($http_origin ~* (^https?://([^/]+\.)*(localhost:4200|admin.example.com))) {
          set $cors "true";
      }
      # Nginx doesn't support nested If statements. This is where things get slightly nasty.
      # Determine the HTTP request method used
      if ($request_method = 'OPTIONS') {
          set $cors "${cors}options";
      }
      if ($request_method = 'GET') {
          set $cors "${cors}get";
      }
      if ($request_method = 'POST') {
          set $cors "${cors}post";
      }

      if ($cors = "true") {
          # Catch all incase there's a request method we're not dealing with properly
          add_header 'Access-Control-Allow-Origin' "$http_origin";
      }

      if ($cors = "trueget") {
          add_header 'Access-Control-Allow-Origin' "$http_origin";
          add_header 'Access-Control-Allow-Credentials' 'true';
          add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
          add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
      }

      if ($cors = "trueoptions") {
          add_header 'Access-Control-Allow-Origin' "$http_origin";

          #
          # Om nom nom cookies
          #
          add_header 'Access-Control-Allow-Credentials' 'true';
          add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';

          #
          # Custom headers and headers various browsers *should* be OK with but aren't
          #
          add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';

          #
          # Tell client that this pre-flight info is valid for 20 days
          #
          add_header 'Access-Control-Max-Age' 1728000;
          add_header 'Content-Type' 'text/plain charset=UTF-8';
          add_header 'Content-Length' 0;
          return 204;
      }

      if ($cors = "truepost") {
          add_header 'Access-Control-Allow-Origin' "$http_origin";
          add_header 'Access-Control-Allow-Credentials' 'true';
          add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
          add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
      }

All 18 comments

May also want to set headers, methods, content type as well. For example:

https://enable-cors.org/server_nginx.html

I'll take a look at this later.

By the way, are you guys facing problems with CORS in Firefox only? Enabling CORS seems to solve us some problems in Chrome and Opera (when doing requests to other domains) but it's breaking in Firefox.

The detail here is that the requests API/domain is using Client Certificate Auth, so when the user calls somewhere in the page that triggers the API, the Client Cert Auth opens for him correctly, but Firefox blocks the call.

@rikatz It's not really mentioned in this issue but looking at the PR, unless I'm mistaken what was implemented was only to allow defining a single origin address. Is that the case? That's kind of simplistic approach.

This basically prevents using Access-Control-Allow-Credentials: true with multiple domains. The problem is that that's not valid in connection with Access-Control-Allow-Origin: *. This combination causes CORS requests to fail at least in Chrome with error

Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. Origin '<addressHere>' is therefore not allowed access. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

I think the best fix for this would be to support multiple values for nginx.ingress.kubernetes.io/cors-allow-origin field. For our case, wildcard matches would also work.

In case anyone else has the same issue, I fixed it for now by setting CORS config directly to nginx.ingress.kubernetes.io/server-snippet. That's not ideal but should work.

Awesome @htuomola – Thanks for sharing the workaround. Would you mind posting an example with multiple allows origins in place? Would be super helpful. Thanks in advance :)

@akoenig well that's just a general nginx configuration issue, nothing really specific to Kubernetes. I implemented something similar to this. One thing that is missing from that sample is that you might want to configure those headers with add_header .... always so they get added to failed requests too. Without that when the backend returns e.g. 403 status code, the headers are omitted and that might not be desirable [docs].

I've also since my comment found the better-suited nginx.ingress.kubernetes.io/configuration-snippet which allows for ingress (nginx location) specific configurations. That makes the whole solution fine. Server-snippet was ugly as it's a local setup and should only be specified once per host.

@htuomola Thanks for your response. Yeah, I know that it is possible to inject ordinary nginx configurations. My question was not as precise as it should be. Sorry for that :) I asked for an example, because I tried the following and it didn't work out:

    nginx.ingress.kubernetes.io/server-snippet: |
      "add_header Access-Control-Allow-Origin 'domain0.tld,domain1.tld';"

The result was still an access-control-allow-origin: * :( – I guess, I will try the configuration-snippet then.

@akoenig you need the whole config, starting line 18 for it to work (until line 79). The same sample starts by comparing the $http_origin to list of allowed domains.
Also, with attribute: | in yaml, no need to use quotation marks. It's helpful to check the resulting config by logging inspecting /etc/nginx/nginx.conf in the ingress controller pod after deployment. Btw. I'd not mix this approach with any of the nginx.ingress.kubernetes.io/*cors* annotations as that might have problems.

Ah, that was super helpful. Thanks @htuomola 🎉

@akoenig @htuomola I've been pretty busy here, but will try to help on this.

What's the limiting one domain is this regex: https://github.com/kubernetes/ingress-nginx/blob/313fdd2d1ae4521e8741b38d03cae6dff6c95fa3/internal/ingress/annotations/cors/main.go#L39

If is there a better regex that allows multiple domains (I know there is :smile: ) you can make a PR that this should be solved (and also win a 'Ingress Contributor badge) ;)

Otherwise, I can take a look in this next week.

@rikatz Thanks. Unfortunately I think it's not quite that simple. The thing is that as far as I've understood the attribute value is just inserted to the header here (after regex validation):
https://github.com/kubernetes/ingress-nginx/blob/3c67976969b695f5b2968ed8804c2463809c32ac/rootfs/etc/nginx/template/nginx.tmpl#L564

However, the Access-Control-Allow-Origin header may only contain a single origin or *. That's why matching for multiple origins requires some nginx script to work. Basically what the above script does is it matches the request origin against a known list of origins and if yes, adds the request origin to the response. Client won't know that multiple origins are in fact allowed.

So the annotation could still be a regex in itself or maybe a comma-separated list (still string) or a Yaml sequence. Sorry but I'm busy too and maybe this isn't the best occasion to start learning Go. 😄

I used this post to inject a snippet, which does the job really well. The good thing with this approach is that it doesn't broadcast the list of allowed sites.

annotations:
   kubernetes.io/ingress.class: "nginx"
   nginx.ingress.kubernetes.io/configuration-snippet: |
      if ($http_origin ~* (^https?://([^/]+\.)*(localhost:4200|admin.example.com))) {
          set $cors "true";
      }
      # Nginx doesn't support nested If statements. This is where things get slightly nasty.
      # Determine the HTTP request method used
      if ($request_method = 'OPTIONS') {
          set $cors "${cors}options";
      }
      if ($request_method = 'GET') {
          set $cors "${cors}get";
      }
      if ($request_method = 'POST') {
          set $cors "${cors}post";
      }

      if ($cors = "true") {
          # Catch all incase there's a request method we're not dealing with properly
          add_header 'Access-Control-Allow-Origin' "$http_origin";
      }

      if ($cors = "trueget") {
          add_header 'Access-Control-Allow-Origin' "$http_origin";
          add_header 'Access-Control-Allow-Credentials' 'true';
          add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
          add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
      }

      if ($cors = "trueoptions") {
          add_header 'Access-Control-Allow-Origin' "$http_origin";

          #
          # Om nom nom cookies
          #
          add_header 'Access-Control-Allow-Credentials' 'true';
          add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';

          #
          # Custom headers and headers various browsers *should* be OK with but aren't
          #
          add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';

          #
          # Tell client that this pre-flight info is valid for 20 days
          #
          add_header 'Access-Control-Max-Age' 1728000;
          add_header 'Content-Type' 'text/plain charset=UTF-8';
          add_header 'Content-Length' 0;
          return 204;
      }

      if ($cors = "truepost") {
          add_header 'Access-Control-Allow-Origin' "$http_origin";
          add_header 'Access-Control-Allow-Credentials' 'true';
          add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
          add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
      }

Just tried your configuration-snippet above @claudiuchis. And can't get it to work, here are the nginx pod logs:

invalid condition "$http_origin" in /tmp/nginx-cfg434348179:412 nginx: [emerg] invalid condition "$http_origin" in /tmp/nginx-cfg434348179:412

We use this snippet to allow only our domains

annotations:
    ...
    ingress.kubernetes.io/configuration-snippet: |
      if ($http_origin ~ '^https:\/\/(.*\.)?example\.(com|net)$') {
        set $allow_origin $http_origin;
      }

      # Cors Preflight methods needs additional options and different Return Code
      if ($request_method = 'OPTIONS') {
        more_set_headers 'Access-Control-Allow-Origin: $allow_origin';
        more_set_headers 'Access-Control-Allow-Credentials: true';
        more_set_headers 'Access-Control-Allow-Methods: GET, PUT, POST, DELETE, PATCH, OPTIONS';
        more_set_headers 'Access-Control-Allow-Headers: DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,X-Client-Identifier';
        more_set_headers 'Access-Control-Max-Age: 1728000';
        more_set_headers 'Content-Type: text/plain charset=UTF-8';
        more_set_headers 'Content-Length: 0';
        return 204;
      }

      more_set_headers 'Access-Control-Allow-Origin: $allow_origin';
      more_set_headers 'Access-Control-Allow-Credentials: true';
      more_set_headers 'Access-Control-Allow-Methods: GET, PUT, POST, DELETE, PATCH, OPTIONS';
      more_set_headers 'Access-Control-Allow-Headers: DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,X-Client-Identifier';

I tried the workaround from @claudiuchis, but get the following error:

Access to XMLHttpRequest at 'https://someserver.com/...' from origin 'https://myserver.com/' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header contains multiple values '*, https://myserver.com/', but only one is allowed.

Note that the second value of Access-Control-Allow-Origin ("https://myserver.com/") displays the correct URL so it seems like the $http_origin is correctly parsed and replaced.
I have no clue where the wildcard entry comes from.
Any ideas?

I tried the workaround from @claudiuchis, but get the following error:

Access to XMLHttpRequest at 'https://someserver.com/...' from origin 'https://myserver.com/' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header contains multiple values '*, https://myserver.com/', but only one is allowed.

Note that the second value of Access-Control-Allow-Origin ("https://myserver.com/") displays the correct URL so it seems like the $http_origin is correctly parsed and replaced.
I have no clue where the wildcard entry comes from.
Any ideas?

Is this still in your annotations?
ingress.kubernetes.io/enable-cors: "true"

I think it was some sort of typo or something like that. I tried it over and over again. At some time it just worked and I cannot reproduce the problem.

I used this post to inject a snippet, which does the job really well. The good thing with this approach is that it doesn't broadcast the list of allowed sites.

annotations:
   kubernetes.io/ingress.class: "nginx"
   nginx.ingress.kubernetes.io/configuration-snippet: |
      if ($http_origin ~* (^https?://([^/]+\.)*(localhost:4200|admin.example.com))) {
          set $cors "true";
      }
      # Nginx doesn't support nested If statements. This is where things get slightly nasty.
      # Determine the HTTP request method used
      if ($request_method = 'OPTIONS') {
          set $cors "${cors}options";
      }
      if ($request_method = 'GET') {
          set $cors "${cors}get";
      }
      if ($request_method = 'POST') {
          set $cors "${cors}post";
      }

      if ($cors = "true") {
          # Catch all incase there's a request method we're not dealing with properly
          add_header 'Access-Control-Allow-Origin' "$http_origin";
      }

      if ($cors = "trueget") {
          add_header 'Access-Control-Allow-Origin' "$http_origin";
          add_header 'Access-Control-Allow-Credentials' 'true';
          add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
          add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
      }

      if ($cors = "trueoptions") {
          add_header 'Access-Control-Allow-Origin' "$http_origin";

          #
          # Om nom nom cookies
          #
          add_header 'Access-Control-Allow-Credentials' 'true';
          add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';

          #
          # Custom headers and headers various browsers *should* be OK with but aren't
          #
          add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';

          #
          # Tell client that this pre-flight info is valid for 20 days
          #
          add_header 'Access-Control-Max-Age' 1728000;
          add_header 'Content-Type' 'text/plain charset=UTF-8';
          add_header 'Content-Length' 0;
          return 204;
      }

      if ($cors = "truepost") {
          add_header 'Access-Control-Allow-Origin' "$http_origin";
          add_header 'Access-Control-Allow-Credentials' 'true';
          add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
          add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
      }

Hello @claudiuchis, I tried the above mentioned configuration but getting the attached error.
image
My Http_config variable is
image
I am not getting where I am doing wrong. Please help..!!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

natemurthy picture natemurthy  Â·  3Comments

kfox1111 picture kfox1111  Â·  3Comments

vdavidoff picture vdavidoff  Â·  3Comments

cabrinoob picture cabrinoob  Â·  3Comments

smeruelo picture smeruelo  Â·  3Comments