juanfont/headscale · error · ErrNodeMarkedTaggedButHasNoTags

node marked as tagged but has no tags

Error message

node marked as tagged but has no tags

What it means

ErrNodeMarkedTaggedButHasNoTags is a sentinel error in hscontrol/state/tags.go:13 returned by validateNodeOwnership when a node's IsTagged() is true but its Tags slice is empty. It guards the tags-XOR-user-ownership invariant: a tagged node must carry at least one tag.

Source

Thrown at hscontrol/state/tags.go:13

package state

import (
	"errors"
	"fmt"

	"github.com/juanfont/headscale/hscontrol/types"
	"github.com/rs/zerolog/log"
)

var (
	// ErrNodeMarkedTaggedButHasNoTags is returned when a node is marked as tagged but has no tags.
	ErrNodeMarkedTaggedButHasNoTags = errors.New("node marked as tagged but has no tags")

	// ErrNodeHasNeitherUserNorTags is returned when a node has neither a user nor tags.
	ErrNodeHasNeitherUserNorTags = errors.New("node has neither user nor tags - must be owned by user or tagged")

	// ErrRequestedTagsInvalidOrNotPermitted is returned when requested tags are invalid or not permitted.
	// This message format matches Tailscale SaaS: "requested tags [tag:xxx] are invalid or not permitted".
	ErrRequestedTagsInvalidOrNotPermitted = errors.New("requested tags")
)

// ErrTaggedNodeHasUser is returned when a tagged node has a [types.Node.UserID] set.
var ErrTaggedNodeHasUser = errors.New("tagged node must not have user_id set")

// validateNodeOwnership ensures proper node ownership model.
// A node must be either user-owned or tagged, and these are mutually exclusive:
// tagged nodes must not have a [types.Node.UserID], and user-owned nodes must
// not have tags.
func validateNodeOwnership(node *types.Node) error {
	if node.IsTagged() {

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Ensure the node gets at least one valid tag before it is marked tagged (use state.SetTags, not direct field writes)
  2. If the intent is a user-owned node, clear the tagged marker instead of emptying Tags
  3. Fix test fixtures to include a tag when exercising tagged-node paths
  4. Audit direct DB writes / import scripts that bypass ownership validation

Example fix

// before
node := types.Node{MachineKey: mk, NodeKey: nk} // tagged marker set, Tags empty

// after
// go through state.SetTags(node, []string{"tag:web"}) so ownership stays consistent
Defensive patterns

Strategy: validation

Validate before calling

func hasValidOwnership(n types.NodeView) error {
    if n.IsTagged() && len(n.Tags()) == 0 {
        return errors.New("tagged node missing tags")
    }
    return nil
}

Type guard

null

Try / catch

if errors.Is(err, state.ErrNodeMarkedTaggedButHasNoTags) { /* set tags or clear the tagged marker */ }

Prevention

When it happens

Trigger: Constructing or persisting a types.Node with a tagged owner marker but an empty Tags list; SetTags clearing tags without clearing the tagged marker; tests building node fixtures that set the tagged flag without tags.

Common situations: Manual construction of Node structs in code or tests bypassing SetTags validation; partial DB updates that empty the tags column but leave the ownership marker; import scripts writing nodes directly.

Related errors


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