juanfont/headscale · error

MOCKOIDC_CLIENT_SECRET not defined

Error message

MOCKOIDC_CLIENT_SECRET not defined

What it means

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.

Source

Thrown at cmd/headscale/cli/mockoidc.go:22

	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net"
	"net/http"
	"os"
	"strconv"
	"time"

	"github.com/juanfont/headscale/hscontrol/util/zlog/zf"
	"github.com/oauth2-proxy/mockoidc"
	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
)

var (
	errMockOidcClientIDNotDefined     = errors.New("MOCKOIDC_CLIENT_ID not defined")
	errMockOidcClientSecretNotDefined = errors.New("MOCKOIDC_CLIENT_SECRET not defined")
	errMockOidcPortNotDefined         = errors.New("MOCKOIDC_PORT not defined")
	errMockOidcUsersNotDefined        = errors.New("MOCKOIDC_USERS not defined")
)

const refreshTTL = 60 * time.Minute

var accessTTL = 2 * time.Minute

func init() {
	rootCmd.AddCommand(mockOidcCmd)
}

var mockOidcCmd = &cobra.Command{
	Use:   "mockoidc",
	Short: "Runs a mock OIDC server for testing",
	Long:  "This internal command runs a OpenID Connect for testing purposes",
	RunE: func(cmd *cobra.Command, args []string) error {
		err := mockOIDC()

View on GitHub (pinned to 565fd254d0)

Solutions

  1. 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.
  2. Check the concurrent warning log 'no upgrade header in TS2021 request...' to confirm this exact path.
  3. 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.
  4. Ensure no intermediary (CDN, ALB, mTLS terminator) downgrades or strips hop-by-hop headers.

Example fix

# before (nginx strips upgrade -> 500 Internal error on /ts2021)
location / {
    proxy_pass http://headscale;
}

# after
location / {
    proxy_pass http://headscale;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
}
Defensive patterns

Strategy: validation

Validate before calling

// Smoke-test that the upgrade headers survive your proxy chain before pointing tailscaled at it.
req, _ := http.NewRequest(http.MethodGet, serverURL+"/ts2021", nil)
req.Header.Set("Upgrade", "websocket")
req.Header.Set("Connection", "Upgrade")
resp, err := client.Do(req)
if err != nil || (resp != nil && resp.StatusCode != http.StatusSwitchingProtocols && resp.StatusCode != http.StatusBadRequest) {
    // a 500 here means Upgrade was stripped — fix the proxy before registering nodes
    log.Printf("upgrade header not passed through: status=%v err=%v", resp, err)
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/a2664d5597fd9fdb. Report an issue: GitHub.