Skip to content

Headscale: Added an option to set an Access-Control-Allow-Origin resp… #2302

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions config-example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ grpc_listen_addr: 127.0.0.1:50443
# are doing.
grpc_allow_insecure: false

# The allow_origins list will allow you to set the Access-Control-Allow-Origin header to the origin in the list.
# This will allow you to enable cors and set headscale without a reverse proxy.
# Multiple origins can be set in the allow_origins list.
# Options:
# - "*" is disabled (due to security risks).
# - "https://example.com" to only allow access from a specific origin.
# - "https://example.com:1234" to allow access from a specific origin with a port.
cors:
allow_origins: []

# The Noise section includes specific configuration for the
# TS2021 Noise protocol
noise:
Expand Down
56 changes: 56 additions & 0 deletions hscontrol/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -455,10 +455,66 @@ func (h *Headscale) ensureUnixSocketIsAbsent() error {
return os.Remove(h.cfg.UnixSocket)
}

// corsHeaderMiddleware will add an "Access-Control-Allow-Origin" to enable CORS.
func (h *Headscale) corsHeadersMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// skip disabled CORS endpoints
if !h.enabledCorsRoutes(r.URL.Path) {
next.ServeHTTP(w, r)
return
}

origin := r.Header.Get("Origin")
// we compare origin from the allowed Origins list. Then add the header with origin
for _, allowedOrigin := range h.cfg.AllowedOrigins.Origins {
if allowedOrigin == origin {
w.Header().Set("Vary", "Origin")
w.Header().Set("Access-Control-Allow-Origin", allowedOrigin)
break
}
}
next.ServeHTTP(w, r)
})
}

func (h *Headscale) enabledCorsRoutes(routerPath string) bool {
// enable all api endpoints
if strings.HasPrefix(routerPath, "/api/") {
return true
}

// A list of enabled CORS endpoints
enabledRoutes := []string{
"/health",
"/key",
"/register/{registration_id}",
"/oidc/callback",
"/verify",
"/derp",
"/derp/probe",
"/derp/latency-check",
"/bootstrap-dns",
"/machine/register",
"/machine/map",
}

for _, routes := range enabledRoutes {
if routes == routerPath {
return true
}
}

return false
}

func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *mux.Router {
router := mux.NewRouter()
router.Use(prometheusMiddleware)

if len(h.cfg.AllowedOrigins.Origins) != 0 {
router.Use(h.corsHeadersMiddleware)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think this should be added for all endpoints, it should only be needed for the API?

Copy link
Author

@Jisse-Meruma Jisse-Meruma Jan 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @kradalby,

Thank you for your feedback. I understand your concern about applying this to all endpoints.

I agree that not all endpoints require CORS headers. However, the Tailscale Client does use several non-API routes — specifically /register, /key, /machine, /derp, /verify and /health — to communicate with the Headscale server via HTTP requests. For these endpoints, implementing CORS headers is necessary to allow proper functionality when CORS is enabled. Additionally, the Tailscale SSH web client operates for example in a similar manner.

At the same time, I understand that certain endpoints such as /apple, /windows, and /swagger do not need CORS headers.

I will give you a code draft of our idea on how to implement this in a simple matter.

If you have any questions feel free to ask me for any clarification if needed.

}

router.HandleFunc(ts2021UpgradePath, h.NoiseUpgradeHandler).Methods(http.MethodPost, http.MethodGet)

router.HandleFunc("/health", h.HealthHandler).Methods(http.MethodGet)
Expand Down
18 changes: 18 additions & 0 deletions hscontrol/types/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ type Config struct {
Log LogConfig
DisableUpdateCheck bool

AllowedOrigins CorsConfig

Database DatabaseConfig

DERP DERPConfig
Expand Down Expand Up @@ -208,6 +210,10 @@ type LogTailConfig struct {
Enabled bool
}

type CorsConfig struct {
Origins []string
}

type CLIConfig struct {
Address string
APIKey string
Expand Down Expand Up @@ -332,6 +338,8 @@ func LoadConfig(path string, isFile bool) error {
viper.SetDefault("tuning.batch_change_delay", "800ms")
viper.SetDefault("tuning.node_mapsession_buffered_chan_size", 30)

viper.SetDefault("access_control_allow_origin", "")

viper.SetDefault("prefixes.allocation", string(IPAllocationStrategySequential))

if err := viper.ReadInConfig(); err != nil {
Expand Down Expand Up @@ -530,6 +538,14 @@ func logtailConfig() LogTailConfig {
}
}

func corsConfig() CorsConfig {
allowedOrigins := viper.GetStringSlice("cors.allowed_origins")

return CorsConfig{
Origins: allowedOrigins,
}
}

func policyConfig() PolicyConfig {
policyPath := viper.GetString("policy.path")
policyMode := viper.GetString("policy.mode")
Expand Down Expand Up @@ -903,6 +919,8 @@ func LoadServerConfig() (*Config, error) {
GRPCAllowInsecure: viper.GetBool("grpc_allow_insecure"),
DisableUpdateCheck: false,

AllowedOrigins: corsConfig(),

PrefixV4: prefix4,
PrefixV6: prefix6,
IPAllocation: IPAllocationStrategy(alloc),
Expand Down