{"record":{"id":"a2664d5597fd9fdb","repo":"juanfont/headscale","slug":"mockoidc-client-secret-not-defined","errorCode":null,"errorMessage":"MOCKOIDC_CLIENT_SECRET not defined","messagePattern":"MOCKOIDC_CLIENT_SECRET not defined","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/headscale/cli/mockoidc.go","lineNumber":22,"sourceCode":"\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/juanfont/headscale/hscontrol/util/zlog/zf\"\n\t\"github.com/oauth2-proxy/mockoidc\"\n\t\"github.com/rs/zerolog/log\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar (\n\terrMockOidcClientIDNotDefined     = errors.New(\"MOCKOIDC_CLIENT_ID not defined\")\n\terrMockOidcClientSecretNotDefined = errors.New(\"MOCKOIDC_CLIENT_SECRET not defined\")\n\terrMockOidcPortNotDefined         = errors.New(\"MOCKOIDC_PORT not defined\")\n\terrMockOidcUsersNotDefined        = errors.New(\"MOCKOIDC_USERS not defined\")\n)\n\nconst refreshTTL = 60 * time.Minute\n\nvar accessTTL = 2 * time.Minute\n\nfunc init() {\n\trootCmd.AddCommand(mockOidcCmd)\n}\n\nvar mockOidcCmd = &cobra.Command{\n\tUse:   \"mockoidc\",\n\tShort: \"Runs a mock OIDC server for testing\",\n\tLong:  \"This internal command runs a OpenID Connect for testing purposes\",\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\terr := mockOIDC()","sourceCodeStart":4,"sourceCodeEnd":40,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/cmd/headscale/cli/mockoidc.go#L4-L40","documentation":"Returned as HTTP 500 by the TS2021 noise upgrade handler when the initial /ts2021 request carries no 'Upgrade' header. The noise protocol rides on a WebSocket-style HTTP upgrade, so a missing Upgrade header means something in front of headscale terminated or plain-proxied the connection instead of passing the upgrade through. The server logs a specific warning about reverse proxy misconfiguration at the same moment.","triggerScenarios":"Pointing tailscaled at a URL served by a reverse proxy that does not forward Upgrade/Connection headers (nginx without proxy_set_header Upgrade $http_upgrade; Connection $connection_upgrade); hitting the noise endpoint with a plain HTTP client (curl without upgrade headers); an HTTP/1.0-ish intermediary stripping hop-by-hop headers.","commonSituations":"New nginx/caddy/traefik/ingress-nginx config in front of headscale where WebSocket pass-through was not enabled; CDN or load balancer (e.g. plain ALB, Cloudflare free tier without WS) stripping upgrade headers; tailscaled --login-server pointed at a proxy hostname instead of headscale directly.","solutions":["Fix the reverse proxy to pass WebSockets: nginx needs 'proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection \"upgrade\";' and HTTP/1.1 ('proxy_http_version 1.1;'); caddy and traefik pass upgrades by default when not terminating them.","Check the concurrent warning log 'no upgrade header in TS2021 request...' to confirm this exact path.","Bypass the proxy for /ts2021 (or expose headscale directly on its own port) to verify tailscaled registers fine, then re-enable proxying with correct headers.","Ensure no intermediary (CDN, ALB, mTLS terminator) downgrades or strips hop-by-hop headers."],"exampleFix":"# before (nginx strips upgrade -> 500 Internal error on /ts2021)\nlocation / {\n    proxy_pass http://headscale;\n}\n\n# after\nlocation / {\n    proxy_pass http://headscale;\n    proxy_http_version 1.1;\n    proxy_set_header Upgrade $http_upgrade;\n    proxy_set_header Connection \"upgrade\";\n    proxy_set_header Host $host;\n}","handlingStrategy":"validation","validationCode":"// Smoke-test that the upgrade headers survive your proxy chain before pointing tailscaled at it.\nreq, _ := http.NewRequest(http.MethodGet, serverURL+\"/ts2021\", nil)\nreq.Header.Set(\"Upgrade\", \"websocket\")\nreq.Header.Set(\"Connection\", \"Upgrade\")\nresp, err := client.Do(req)\nif err != nil || (resp != nil && resp.StatusCode != http.StatusSwitchingProtocols && resp.StatusCode != http.StatusBadRequest) {\n    // a 500 here means Upgrade was stripped — fix the proxy before registering nodes\n    log.Printf(\"upgrade header not passed through: status=%v err=%v\", resp, err)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Configure WebSocket pass-through (Upgrade/Connection headers, HTTP/1.1) on any proxy in front of headscale.","Test the /ts2021 endpoint with an Upgrade header right after proxy changes.","Watch for the 'no upgrade header in TS2021 request' warning in headscale logs as an early detector."],"tags":["network","noise","websocket","reverse-proxy","registration"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}