juanfont/headscale

Handle CORS headers and OPTIONS method for HTTP API

routerino opened this issue · 18 comments

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:

  • Sending a pre-flight OPTIONS request, expecting back a 204 response with the CORS headers attached
  • Once the response is accepted, sending the real API request with all the data

For this to work, we need two things:

  • The server (headscale) to generate CORS headers (and have it be configurable to set the domains appropriately)
  • The server to accept OPTIONS requests without authorization.

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 *
	}
}

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"'

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)

@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

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/

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

How to add this to Nginx Proxy Manager ?

How to add this to Nginx Proxy Manager ?

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

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)

Thank you very much. That sorted it.

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

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.

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

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

@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.

This issue is stale because it has been open for 90 days with no activity.

This issue was closed because it has been inactive for 14 days since being marked as stale.

fjeddy commented

This issue is preventing and stopping developers from creating any serious web-ui for headscale and should not be closed, without this, all we're gonna have is the five minute UI's that currently exist.