juanfont/headscale · error

tag should be lowercase

Error message

tag should be lowercase

What it means

Error from submitConfirmForm when the OIDC confirmation interstitial HTML does not contain an action=" attribute. The function scrapes the rendered form to POST the confirmation, so a missing action attribute means the page is not the expected form (wrong page, error page, or template change).

Source

Thrown at hscontrol/api/v1/tags.go:12

package apiv1

import (
	"errors"
	"strings"
)

// ACL tag validation, shared by the node and pre-auth-key resources. These
// reproduce the gRPC validateTag checks and messages.
var (
	errTagMissingPrefix = errors.New("tag must start with the string 'tag:'")
	errTagNotLowercase  = errors.New("tag should be lowercase")
	errTagHasSpaces     = errors.New("tags must not contain spaces")
)

// validateTag reports whether an ACL tag is well formed: it must start with
// "tag:", be lowercase, and contain no spaces.
func validateTag(tag string) error {
	switch {
	case !strings.HasPrefix(tag, "tag:"):
		return errTagMissingPrefix
	case strings.ToLower(tag) != tag:
		return errTagNotLowercase
	case len(strings.Fields(tag)) > 1:
		return errTagHasSpaces
	default:
		return nil
	}
}

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Log the passed htmlBody to identify which page was actually returned.
  2. Check headscale OIDC logs for why the interstitial was not rendered (failed state/nonce validation).
  3. If you modified templates, ensure the form keeps action="..." with a double-quoted attribute.
  4. Ensure the same HTTP client (with CSRF cookie) is used for the callback and the confirm POST.
Defensive patterns

Strategy: validation

Validate before calling

if !strings.Contains(htmlBody, `action="`) {
    log.Printf("unexpected interstitial page: %.500s", htmlBody)
    return fmt.Errorf("%s page is not the confirmation form", hostname)
}

Type guard

func isConfirmForm(htmlBody string) bool {
    return strings.Contains(htmlBody, `action="`) && strings.Contains(htmlBody, "csrf")
}

Prevention

When it happens

Trigger: submitConfirmForm receiving HTML that is not the confirmation interstitial — e.g. an error page or login page was rendered instead, or a headscale template change removed/renamed the action attribute.

Common situations: OIDC callback rendering an error instead of the confirmation form, headscale version drift changing the interstitial markup, or a session/cookie mismatch causing re-render of a different page.

Related errors


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