juanfont / headscale

An open source, self-hosted implementation of the Tailscale control server
BSD 3-Clause "New" or "Revised" License
21.03k stars 1.16k forks source link

Handle CORS headers and OPTIONS method for HTTP API #623

Open routerino opened 2 years ago

routerino commented 2 years ago

Bug description

When trying to use a browser to generate API requests (like, hypothetically, if you're building a web frontend for headscale), the browser expects to to use CORS to determine if it can talk to the external server. The browser does this by the following:

For this to work, we need two things:

To Reproduce Generate a fetch request from a browser in a separate domain. Such as:

let apiKey = '<my api key>';
let url = 'https://<my headscale domain>/api/v1/machine';

window.fetch(url, {
    method: 'GET',
    headers: {
        Authorization: `Bearer ${apiKey}`
    }}).then((resp) => resp.json()).then(function (data) {console.log(data);}).catch(function (error) {console.log(error);});});

If no CORS headers are specified, you get this nice error in the browser console: image

If you have the right headers (if you, for example, inject them with a reverse proxy) but the OPTIONS request is blocked by authorization, you get this nice error instead: image

Because the OPTIONS request is returning a 401 unauthorized when it shouldn't.

Both are not ideal. You can fix both with a reverse proxy, but you certainly shouldn't have to. The web server (gin?) should return OPTIONS with a 204 and be setting the CORS headers on all requests (and the CORS headers should be configurable).

Context info

These problems were fixed externally by routing through a Caddy reverse proxy using these matching settings:

@hs-options {
    host hs.<my-domain>
    method OPTIONS
}
@hs-other {
    host hs.<my-domain>
}
handle @hs-options {
    header {
        Access-Control-Allow-Origin https://<my-frontend-subdomain>
        Access-Control-Allow-Headers *
    }
    respond 204
}
handle @hs-other {
    reverse_proxy http://headscale:8080 {
        header_down Access-Control-Allow-Origin https://<my-frontend-subdomain>
        header_down Access-Control-Allow-Headers *
    }
}
Mikle-Bond commented 1 year ago

If you use caddy-docker-proxy, here's the same (mostly) config, done in labels:

    labels:
      caddy: "headscale.${BASE_DOMAIN}"
      caddy.@hs-other.host: "headscale.${BASE_DOMAIN}"
      caddy.@hs-options.host: "headscale.${BASE_DOMAIN}"
      caddy.@hs-options.method: OPTIONS

      caddy.0_import: tlsdns

      caddy.1_handle: "@hs-options"
      caddy.1_handle.header.Access-Control-Allow-Origin: "https://ui.headscale.${BASE_DOMAIN}"
      caddy.1_handle.header.Access-Control-Allow-Headers: "*"
      caddy.1_handle.header.Access-Control-Allow-Methods: '"POST, GET, OPTIONS, DELETE"'
      caddy.1_handle.respond: "204"

      caddy.8_handle: /metrics
      caddy.8_handle.import: auth
      caddy.8_handle.reverse_proxy: "{{upstreams 9090}}"

      caddy.9_handle: "@hs-other"
      caddy.9_handle.reverse_proxy: "{{upstreams 80}}"
      caddy.9_handle.reverse_proxy.header_down_1: "Access-Control-Allow-Origin https://ui.headscale.${BASE_DOMAIN}"
      caddy.9_handle.reverse_proxy.header_down_2: "Access-Control-Allow-Headers *"
      caddy.9_handle.reverse_proxy.header_down_3: 'Access-Control-Allow-Methods "POST, GET, OPTIONS, DELETE"'
deimjons commented 10 months ago

Someone know how to configure it for Traefik? I tryed to add next labels:

traefik.http.routers.headscale-public-https.middlewares: headscale-cors
traefik.http.middlewares.headscale-cors.headers.customResponseHeaders.Access-Control-Allow-Origin: https://web.headscale.yourdomain.example
traefik.http.middlewares.headscale-cors.headers.customResponseHeaders.Access-Control-Allow-Methods: GET, POST, PUT, DELETE
traefik.http.middlewares.headscale-cors.headers.customResponseHeaders.Access-Control-Allow-Headers: Content-Type

Looks like the first part of the problem is solved, but I have a problem with 204 status code in the answer..

I think maybe this plugin should help but I can't configure it properly, it shows me error "status code is smallest than minimum value: 100" (Issue with details opened here: https://github.com/Medzoner/traefik-plugin-cors-preflight/issues/8)

Mikle-Bond commented 10 months ago

@deimjons Maybe try this plugin instead? https://plugins.traefik.io/plugins/628c9f0f108ecc83915d7771/replace-status-code

deimjons commented 10 months ago

@Mikle-Bond Thank you for your attention. I tried this plugin but it didn't help me. I don't know: I doing something wrong or the plugin just not working.

I have added additional routes in labels:

      traefik.http.routers.headscale-options.rule: Host(`headscale.yourdomain.example/api/v1/apikey`) && Method(`OPTIONS`)
      traefik.http.routers.headscale-options.entrypoints: websecure
      traefik.http.routers.headscale-options.tls: true
      traefik.http.routers.headscale-options.tls.certresolver: prod
      traefik.http.routers.headscale-options.middlewares: replace-response-code@file  

also, I added a plugin and middleware (like they show in the documentation example) in the configuration file of traefik: traefik.yaml

experimental:
  plugins:
    traefik-replace-response-code:
      moduleName: "github.com/pierre-verhaeghe/traefik-replace-response-code"
      version: "v0.2.0"

http:
  middlewares:
    replace-response-code:
      plugin:
        traefik-replace-response-code:
          inputCode: 401
          outputCode: 200
          removeBody: "true"

As a result, I have the same error:

Access to fetch at 'https://headscale.yourdomain.example/api/v1/apikey' from origin 'https://admin.headscale.yourdomain.example' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.

mich2k commented 10 months ago

Someone know how to configure it for Traefik? I tryed to add next labels:

traefik.http.routers.headscale-public-https.middlewares: headscale-cors
traefik.http.middlewares.headscale-cors.headers.customResponseHeaders.Access-Control-Allow-Origin: https://web.headscale.yourdomain.example
traefik.http.middlewares.headscale-cors.headers.customResponseHeaders.Access-Control-Allow-Methods: GET, POST, PUT, DELETE
traefik.http.middlewares.headscale-cors.headers.customResponseHeaders.Access-Control-Allow-Headers: Content-Type

Looks like the first part of the problem is solved, but I have a problem with 204 status code in the answer..

I think maybe this plugin should help but I can't configure it properly, it shows me error "status code is smallest than minimum value: 100" (Issue with details opened here: Medzoner/traefik-plugin-cors-preflight#8)

did you manage? I also have to do this

edit: https://doc.traefik.io/traefik/v2.4/middlewares/headers/

deimjons commented 9 months ago

no, I use it via prefix /admin.. ((

masterwishx commented 6 months ago

How to add this to Nginx Proxy Manager ?

sapstar commented 5 months ago

How to add this to Nginx Proxy Manager ?

Hi, did you ever figure this out? I am also unable to access api via NPM.

masterwishx commented 5 months ago

How to add this to Nginx Proxy Manager ?

Hi, did you ever figure this out? I am also unable to access api via NPM.

Yes, all working fine, if you using cloudflare disable the proxy (orange cloud)

sapstar commented 5 months ago

Thank you very much. That sorted it.

fcwys commented 4 months ago

I hope to support CORS, and I would like to use healscale directly instead of using Nginx and other programs for proxy, which is very inconvenient

B08Z commented 4 months ago

If you use caddy-docker-proxy, here's the same (mostly) config, done in labels:

    labels:
      caddy: "headscale.${BASE_DOMAIN}"
      caddy.@hs-other.host: "headscale.${BASE_DOMAIN}"
      caddy.@hs-options.host: "headscale.${BASE_DOMAIN}"
      caddy.@hs-options.method: OPTIONS

      caddy.0_import: tlsdns

      caddy.1_handle: "@hs-options"
      caddy.1_handle.header.Access-Control-Allow-Origin: "https://ui.headscale.${BASE_DOMAIN}"
      caddy.1_handle.header.Access-Control-Allow-Headers: "*"
      caddy.1_handle.header.Access-Control-Allow-Methods: '"POST, GET, OPTIONS, DELETE"'
      caddy.1_handle.respond: "204"

      caddy.8_handle: /metrics
      caddy.8_handle.import: auth
      caddy.8_handle.reverse_proxy: "{{upstreams 9090}}"

      caddy.9_handle: "@hs-other"
      caddy.9_handle.reverse_proxy: "{{upstreams 80}}"
      caddy.9_handle.reverse_proxy.header_down_1: "Access-Control-Allow-Origin https://ui.headscale.${BASE_DOMAIN}"
      caddy.9_handle.reverse_proxy.header_down_2: "Access-Control-Allow-Headers *"
      caddy.9_handle.reverse_proxy.header_down_3: 'Access-Control-Allow-Methods "POST, GET, OPTIONS, DELETE"'

Has anyone else made this work I can't figure it out.

fcwys commented 4 months ago

I don't use Caddy, and I don't actually have any plans to use it. I just want to run Headscale directly.

B08Z commented 4 months ago

Cross-Origin Request Warning: The Same Origin Policy will disallow reading the remote resource at https://headscale.domain.com/api/v1/node soon. (Reason: When the Access-Control-Allow-Headers is *, the Authorization header is not covered. To include the Authorization header, it must be explicitly listed in CORS header Access-Control-Allow-Headers).

I am getting this error with the above implementation using Headscale-admin

GoodiesHQ commented 2 months ago

@B08Z This might be super old and you may have opened an issue on headscale-admin referencing this, but you should be able to use this value to allow CORS from anywhere:

"Access-Control-Allow-Headers Authorization, *" or perhaps 'Access-Control-Allow-Headers "Authorization, *"' is the right syntax?

It needs to be explicit for whatever reason.