Handle CORS headers and OPTIONS method for HTTP API #272

Closed
opened 2025-12-29 01:25:37 +01:00 by adam · 24 comments
Owner

Originally created by @routerino on GitHub (Jun 9, 2022).

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 *
	}
}
Originally created by @routerino on GitHub (Jun 9, 2022). <!-- Headscale is a multinational community across the globe. Our common language is English. Please consider raising the bug report in this language. --> **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: ``` javascript 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](https://user-images.githubusercontent.com/45954722/172862437-f64c47d2-8ae1-46b1-95ab-87123487c74e.png) 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](https://user-images.githubusercontent.com/45954722/172862213-d8c060cf-eb58-4a4b-b486-e8dcf5942364.png) 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 * } } ```
adam added the stalebug labels 2025-12-29 01:25:37 +01:00
adam closed this issue 2025-12-29 01:25:37 +01:00
Author
Owner

@Mikle-Bond commented on GitHub (Jan 21, 2023):

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"'
@Mikle-Bond commented on GitHub (Jan 21, 2023): If you use [caddy-docker-proxy](https://github.com/lucaslorentz/caddy-docker-proxy), here's the same (mostly) config, done in labels: ```yaml 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"' ```
Author
Owner

@deimjons commented on GitHub (Aug 30, 2023):

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)

@deimjons commented on GitHub (Aug 30, 2023): 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](https://plugins.traefik.io/plugins/632b03d4eff06de33b092374/cors-preflight) 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)
Author
Owner

@Mikle-Bond commented on GitHub (Aug 31, 2023):

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

@Mikle-Bond commented on GitHub (Aug 31, 2023): @deimjons Maybe try this plugin instead? https://plugins.traefik.io/plugins/628c9f0f108ecc83915d7771/replace-status-code
Author
Owner

@deimjons commented on GitHub (Sep 1, 2023):

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

@deimjons commented on GitHub (Sep 1, 2023): @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.
Author
Owner

@mich2k commented on GitHub (Sep 20, 2023):

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/

@mich2k commented on GitHub (Sep 20, 2023): > 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](https://plugins.traefik.io/plugins/632b03d4eff06de33b092374/cors-preflight) 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](https://github.com/Medzoner/traefik-plugin-cors-preflight/issues/8)) did you manage? I also have to do this edit: https://doc.traefik.io/traefik/v2.4/middlewares/headers/
Author
Owner

@deimjons commented on GitHub (Oct 1, 2023):

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

@deimjons commented on GitHub (Oct 1, 2023): no, I use it via prefix /admin.. ((
Author
Owner

@masterwishx commented on GitHub (Jan 7, 2024):

How to add this to Nginx Proxy Manager ?

@masterwishx commented on GitHub (Jan 7, 2024): How to add this to Nginx Proxy Manager ?
Author
Owner

@sapstar commented on GitHub (Feb 11, 2024):

How to add this to Nginx Proxy Manager ?

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

@sapstar commented on GitHub (Feb 11, 2024): > How to add this to Nginx Proxy Manager ? Hi, did you ever figure this out? I am also unable to access api via NPM.
Author
Owner

@masterwishx commented on GitHub (Feb 11, 2024):

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)

@masterwishx commented on GitHub (Feb 11, 2024): > > 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)
Author
Owner

@sapstar commented on GitHub (Feb 12, 2024):

Thank you very much. That sorted it.

@sapstar commented on GitHub (Feb 12, 2024): Thank you very much. That sorted it.
Author
Owner

@fcwys commented on GitHub (Feb 29, 2024):

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

@fcwys commented on GitHub (Feb 29, 2024): 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
Author
Owner

@B08Z commented on GitHub (Feb 29, 2024):

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.

@B08Z commented on GitHub (Feb 29, 2024): > If you use [caddy-docker-proxy](https://github.com/lucaslorentz/caddy-docker-proxy), here's the same (mostly) config, done in labels: > > ```yaml > 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.
Author
Owner

@fcwys commented on GitHub (Feb 29, 2024):

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

@fcwys commented on GitHub (Feb 29, 2024): I don't use Caddy, and I don't actually have any plans to use it. I just want to run Headscale directly.
Author
Owner

@B08Z commented on GitHub (Mar 4, 2024):

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 commented on GitHub (Mar 4, 2024): > 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
Author
Owner

@GoodiesHQ commented on GitHub (Apr 26, 2024):

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

@GoodiesHQ commented on GitHub (Apr 26, 2024): @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.
Author
Owner

@github-actions[bot] commented on GitHub (Jul 26, 2024):

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

@github-actions[bot] commented on GitHub (Jul 26, 2024): This issue is stale because it has been open for 90 days with no activity.
Author
Owner

@github-actions[bot] commented on GitHub (Aug 2, 2024):

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

@github-actions[bot] commented on GitHub (Aug 2, 2024): This issue was closed because it has been inactive for 14 days since being marked as stale.
Author
Owner

@fjeddy commented on GitHub (Aug 22, 2024):

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.

@fjeddy commented on GitHub (Aug 22, 2024): 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.
Author
Owner

@Corbeno commented on GitHub (Aug 25, 2024):

Here's my solution for Nginx Proxy Manager. I'm no expert but it works :)

I have two different URLs, for example:
headscale.mydomain.com
headscale-admin.mydomain.com

I put this under the NPM config for headscale.mydomain.com

location / {
        # This part isn't necessary for the CORS fix, just also what I  have
	proxy_pass $forward_scheme://$server:$port;
	proxy_http_version 1.1;
	proxy_set_header Upgrade $http_upgrade;
	proxy_set_header Connection "upgrade";
	proxy_set_header Host $host;
	#proxy_redirect http:// https://; # don't work, not sure
	proxy_buffering off;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header X-Forwarded-Proto $scheme;
	add_header Strict-Transport-Security "max-age=15552000; includeSubDomains";

        # This part and below is for CORS to work with my other domain
        # Handle OPTIONS requests
         if ($request_method = OPTIONS) {
            add_header 'Access-Control-Allow-Origin' 'https://headscale-admin.mydomain.com;
            add_header 'Access-Control-Allow-Headers' '*';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
            return 204;
        }

        # CORS headers for other requests
        add_header 'Access-Control-Allow-Origin' 'https://headscale-admin.mydomain.com';
        add_header 'Access-Control-Allow-Headers' '*';
}
@Corbeno commented on GitHub (Aug 25, 2024): Here's my solution for Nginx Proxy Manager. I'm no expert but it works :) I have two different URLs, for example: headscale.mydomain.com headscale-admin.mydomain.com I put this under the NPM config for headscale.mydomain.com ``` location / { # This part isn't necessary for the CORS fix, just also what I have proxy_pass $forward_scheme://$server:$port; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; #proxy_redirect http:// https://; # don't work, not sure proxy_buffering off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; add_header Strict-Transport-Security "max-age=15552000; includeSubDomains"; # This part and below is for CORS to work with my other domain # Handle OPTIONS requests if ($request_method = OPTIONS) { add_header 'Access-Control-Allow-Origin' 'https://headscale-admin.mydomain.com; add_header 'Access-Control-Allow-Headers' '*'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; return 204; } # CORS headers for other requests add_header 'Access-Control-Allow-Origin' 'https://headscale-admin.mydomain.com'; add_header 'Access-Control-Allow-Headers' '*'; } ```
Author
Owner

@gregoryca commented on GitHub (Oct 7, 2024):

For @mich2k and anyone else looking for the traefik config:

   - "traefik.http.middlewares.corsHeader.headers.accesscontrolallowmethods=GET,OPTIONS,PUT"
   - "traefik.http.middlewares.corsHeader.headers.accesscontrolallowheaders=Authorization, *"
   - "traefik.http.middlewares.corsHeader.headers.accesscontrolalloworiginlist=https://headscale-ui.domain,https://headscale-ui.domain/"
   - "traefik.http.middlewares.corsHeader.headers.accesscontrolmaxage=100"
   - "traefik.http.middlewares.corsHeader.headers.addvaryheader=true"

This should work, and prevent the error @B08Z encountered.

@gregoryca commented on GitHub (Oct 7, 2024): For @mich2k and anyone else looking for the traefik config: - "traefik.http.middlewares.corsHeader.headers.accesscontrolallowmethods=GET,OPTIONS,PUT" - "traefik.http.middlewares.corsHeader.headers.accesscontrolallowheaders=Authorization, *" - "traefik.http.middlewares.corsHeader.headers.accesscontrolalloworiginlist=https://headscale-ui.domain,https://headscale-ui.domain/" - "traefik.http.middlewares.corsHeader.headers.accesscontrolmaxage=100" - "traefik.http.middlewares.corsHeader.headers.addvaryheader=true" This should work, and prevent the error @B08Z encountered.
Author
Owner

@taizen01 commented on GitHub (Feb 7, 2025):

Here's my solution for Nginx Proxy Manager. I'm no expert but it works :)

I have two different URLs, for example: headscale.mydomain.com headscale-admin.mydomain.com

I put this under the NPM config for headscale.mydomain.com

location / {
        # This part isn't necessary for the CORS fix, just also what I  have
	proxy_pass $forward_scheme://$server:$port;
	proxy_http_version 1.1;
	proxy_set_header Upgrade $http_upgrade;
	proxy_set_header Connection "upgrade";
	proxy_set_header Host $host;
	#proxy_redirect http:// https://; # don't work, not sure
	proxy_buffering off;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header X-Forwarded-Proto $scheme;
	add_header Strict-Transport-Security "max-age=15552000; includeSubDomains";

        # This part and below is for CORS to work with my other domain
        # Handle OPTIONS requests
         if ($request_method = OPTIONS) {
            add_header 'Access-Control-Allow-Origin' 'https://headscale-admin.mydomain.com;
            add_header 'Access-Control-Allow-Headers' '*';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
            return 204;
        }

        # CORS headers for other requests
        add_header 'Access-Control-Allow-Origin' 'https://headscale-admin.mydomain.com';
        add_header 'Access-Control-Allow-Headers' '*';
}

I had to adjust some parts of your config, then it worked for me! Thank you.

location / {
    # Proxy-Einstellungen
    proxy_pass $forward_scheme://$server:$port;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_buffering off;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    add_header Strict-Transport-Security "max-age=15552000; includeSubDomains";

    # CORS-Handling für OPTIONS-Preflight-Requests
    if ($request_method = OPTIONS) {
        add_header 'Access-Control-Allow-Origin' 'https://your-domain' always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, OPTIONS' always;
        add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, User-Agent' always;
        add_header 'Access-Control-Expose-Headers' 'Content-Length, Content-Range' always;
        add_header 'Content-Length' 0 always;
        add_header 'Content-Type' 'text/plain; charset=utf-8' always;
        return 204;
    }

    # CORS-Header für alle anderen Requests
    add_header 'Access-Control-Allow-Origin' 'https://your-domain' always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, OPTIONS' always;
    add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, User-Agent' always;
    add_header 'Access-Control-Expose-Headers' 'Content-Length, Content-Range' always;
}
@taizen01 commented on GitHub (Feb 7, 2025): > Here's my solution for Nginx Proxy Manager. I'm no expert but it works :) > > I have two different URLs, for example: headscale.mydomain.com headscale-admin.mydomain.com > > I put this under the NPM config for headscale.mydomain.com > > ``` > location / { > # This part isn't necessary for the CORS fix, just also what I have > proxy_pass $forward_scheme://$server:$port; > proxy_http_version 1.1; > proxy_set_header Upgrade $http_upgrade; > proxy_set_header Connection "upgrade"; > proxy_set_header Host $host; > #proxy_redirect http:// https://; # don't work, not sure > proxy_buffering off; > proxy_set_header X-Real-IP $remote_addr; > proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; > proxy_set_header X-Forwarded-Proto $scheme; > add_header Strict-Transport-Security "max-age=15552000; includeSubDomains"; > > # This part and below is for CORS to work with my other domain > # Handle OPTIONS requests > if ($request_method = OPTIONS) { > add_header 'Access-Control-Allow-Origin' 'https://headscale-admin.mydomain.com; > add_header 'Access-Control-Allow-Headers' '*'; > add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; > return 204; > } > > # CORS headers for other requests > add_header 'Access-Control-Allow-Origin' 'https://headscale-admin.mydomain.com'; > add_header 'Access-Control-Allow-Headers' '*'; > } > ``` I had to adjust some parts of your config, then it worked for me! Thank you. ``` location / { # Proxy-Einstellungen proxy_pass $forward_scheme://$server:$port; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_buffering off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; add_header Strict-Transport-Security "max-age=15552000; includeSubDomains"; # CORS-Handling für OPTIONS-Preflight-Requests if ($request_method = OPTIONS) { add_header 'Access-Control-Allow-Origin' 'https://your-domain' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, User-Agent' always; add_header 'Access-Control-Expose-Headers' 'Content-Length, Content-Range' always; add_header 'Content-Length' 0 always; add_header 'Content-Type' 'text/plain; charset=utf-8' always; return 204; } # CORS-Header für alle anderen Requests add_header 'Access-Control-Allow-Origin' 'https://your-domain' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, User-Agent' always; add_header 'Access-Control-Expose-Headers' 'Content-Length, Content-Range' always; } ```
Author
Owner

@javito1081 commented on GitHub (Apr 19, 2025):

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)

What do u mean by this? im trying to set it up with nginx proxy manager as well but its not working, it fails to test the connection :-(

@javito1081 commented on GitHub (Apr 19, 2025): > > > 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) What do u mean by this? im trying to set it up with nginx proxy manager as well but its not working, it fails to test the connection :-(
Author
Owner

@javito1081 commented on GitHub (Apr 19, 2025):

Here's my solution for Nginx Proxy Manager. I'm no expert but it works :)

I have two different URLs, for example: headscale.mydomain.com headscale-admin.mydomain.com

I put this under the NPM config for headscale.mydomain.com

location / {
        # This part isn't necessary for the CORS fix, just also what I  have
	proxy_pass $forward_scheme://$server:$port;
	proxy_http_version 1.1;
	proxy_set_header Upgrade $http_upgrade;
	proxy_set_header Connection "upgrade";
	proxy_set_header Host $host;
	#proxy_redirect http:// https://; # don't work, not sure
	proxy_buffering off;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header X-Forwarded-Proto $scheme;
	add_header Strict-Transport-Security "max-age=15552000; includeSubDomains";

        # This part and below is for CORS to work with my other domain
        # Handle OPTIONS requests
         if ($request_method = OPTIONS) {
            add_header 'Access-Control-Allow-Origin' 'https://headscale-admin.mydomain.com;
            add_header 'Access-Control-Allow-Headers' '*';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
            return 204;
        }

        # CORS headers for other requests
        add_header 'Access-Control-Allow-Origin' 'https://headscale-admin.mydomain.com';
        add_header 'Access-Control-Allow-Headers' '*';
}

u mean the advance tab in the NPM forwarder?

@javito1081 commented on GitHub (Apr 19, 2025): > Here's my solution for Nginx Proxy Manager. I'm no expert but it works :) > > I have two different URLs, for example: headscale.mydomain.com headscale-admin.mydomain.com > > I put this under the NPM config for headscale.mydomain.com > > ``` > location / { > # This part isn't necessary for the CORS fix, just also what I have > proxy_pass $forward_scheme://$server:$port; > proxy_http_version 1.1; > proxy_set_header Upgrade $http_upgrade; > proxy_set_header Connection "upgrade"; > proxy_set_header Host $host; > #proxy_redirect http:// https://; # don't work, not sure > proxy_buffering off; > proxy_set_header X-Real-IP $remote_addr; > proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; > proxy_set_header X-Forwarded-Proto $scheme; > add_header Strict-Transport-Security "max-age=15552000; includeSubDomains"; > > # This part and below is for CORS to work with my other domain > # Handle OPTIONS requests > if ($request_method = OPTIONS) { > add_header 'Access-Control-Allow-Origin' 'https://headscale-admin.mydomain.com; > add_header 'Access-Control-Allow-Headers' '*'; > add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; > return 204; > } > > # CORS headers for other requests > add_header 'Access-Control-Allow-Origin' 'https://headscale-admin.mydomain.com'; > add_header 'Access-Control-Allow-Headers' '*'; > } > ``` u mean the advance tab in the NPM forwarder?
Author
Owner

@javito1081 commented on GitHub (Apr 19, 2025):

Here's my solution for Nginx Proxy Manager. I'm no expert but it works :)
I have two different URLs, for example: headscale.mydomain.com headscale-admin.mydomain.com
I put this under the NPM config for headscale.mydomain.com

location / {
        # This part isn't necessary for the CORS fix, just also what I  have
	proxy_pass $forward_scheme://$server:$port;
	proxy_http_version 1.1;
	proxy_set_header Upgrade $http_upgrade;
	proxy_set_header Connection "upgrade";
	proxy_set_header Host $host;
	#proxy_redirect http:// https://; # don't work, not sure
	proxy_buffering off;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header X-Forwarded-Proto $scheme;
	add_header Strict-Transport-Security "max-age=15552000; includeSubDomains";

        # This part and below is for CORS to work with my other domain
        # Handle OPTIONS requests
         if ($request_method = OPTIONS) {
            add_header 'Access-Control-Allow-Origin' 'https://headscale-admin.mydomain.com;
            add_header 'Access-Control-Allow-Headers' '*';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
            return 204;
        }

        # CORS headers for other requests
        add_header 'Access-Control-Allow-Origin' 'https://headscale-admin.mydomain.com';
        add_header 'Access-Control-Allow-Headers' '*';
}

I had to adjust some parts of your config, then it worked for me! Thank you.

location / {
    # Proxy-Einstellungen
    proxy_pass $forward_scheme://$server:$port;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_buffering off;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    add_header Strict-Transport-Security "max-age=15552000; includeSubDomains";

    # CORS-Handling für OPTIONS-Preflight-Requests
    if ($request_method = OPTIONS) {
        add_header 'Access-Control-Allow-Origin' 'https://your-domain' always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, OPTIONS' always;
        add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, User-Agent' always;
        add_header 'Access-Control-Expose-Headers' 'Content-Length, Content-Range' always;
        add_header 'Content-Length' 0 always;
        add_header 'Content-Type' 'text/plain; charset=utf-8' always;
        return 204;
    }

    # CORS-Header für alle anderen Requests
    add_header 'Access-Control-Allow-Origin' 'https://your-domain' always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, OPTIONS' always;
    add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, User-Agent' always;
    add_header 'Access-Control-Expose-Headers' 'Content-Length, Content-Range' always;
}

This one worked for me, this goes into the Advance tab of the domain on NPM, i just changed https://your-domain for my ui domain and worked like a charm :-)

@javito1081 commented on GitHub (Apr 19, 2025): > > Here's my solution for Nginx Proxy Manager. I'm no expert but it works :) > > I have two different URLs, for example: headscale.mydomain.com headscale-admin.mydomain.com > > I put this under the NPM config for headscale.mydomain.com > > ``` > > location / { > > # This part isn't necessary for the CORS fix, just also what I have > > proxy_pass $forward_scheme://$server:$port; > > proxy_http_version 1.1; > > proxy_set_header Upgrade $http_upgrade; > > proxy_set_header Connection "upgrade"; > > proxy_set_header Host $host; > > #proxy_redirect http:// https://; # don't work, not sure > > proxy_buffering off; > > proxy_set_header X-Real-IP $remote_addr; > > proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; > > proxy_set_header X-Forwarded-Proto $scheme; > > add_header Strict-Transport-Security "max-age=15552000; includeSubDomains"; > > > > # This part and below is for CORS to work with my other domain > > # Handle OPTIONS requests > > if ($request_method = OPTIONS) { > > add_header 'Access-Control-Allow-Origin' 'https://headscale-admin.mydomain.com; > > add_header 'Access-Control-Allow-Headers' '*'; > > add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; > > return 204; > > } > > > > # CORS headers for other requests > > add_header 'Access-Control-Allow-Origin' 'https://headscale-admin.mydomain.com'; > > add_header 'Access-Control-Allow-Headers' '*'; > > } > > ``` > > I had to adjust some parts of your config, then it worked for me! Thank you. > > ``` > location / { > # Proxy-Einstellungen > proxy_pass $forward_scheme://$server:$port; > proxy_http_version 1.1; > proxy_set_header Upgrade $http_upgrade; > proxy_set_header Connection "upgrade"; > proxy_set_header Host $host; > proxy_buffering off; > proxy_set_header X-Real-IP $remote_addr; > proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; > proxy_set_header X-Forwarded-Proto $scheme; > add_header Strict-Transport-Security "max-age=15552000; includeSubDomains"; > > # CORS-Handling für OPTIONS-Preflight-Requests > if ($request_method = OPTIONS) { > add_header 'Access-Control-Allow-Origin' 'https://your-domain' always; > add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, OPTIONS' always; > add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, User-Agent' always; > add_header 'Access-Control-Expose-Headers' 'Content-Length, Content-Range' always; > add_header 'Content-Length' 0 always; > add_header 'Content-Type' 'text/plain; charset=utf-8' always; > return 204; > } > > # CORS-Header für alle anderen Requests > add_header 'Access-Control-Allow-Origin' 'https://your-domain' always; > add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, OPTIONS' always; > add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, User-Agent' always; > add_header 'Access-Control-Expose-Headers' 'Content-Length, Content-Range' always; > } > ``` This one worked for me, this goes into the Advance tab of the domain on NPM, i just changed https://your-domain for my ui domain and worked like a charm :-)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/headscale#272