tailscale/tailscale · error · os.ErrInvalid

%w: cannot logon as domain user without being joined to a do

Error message

%w: cannot logon as domain user without being joined to a domain

What it means

logonAs refuses S4U logon for domain-qualified accounts (DOMAIN\user where the prefix is neither '.' nor the local computer name) when winenv.IsDomainJoined() reports the machine is not domain-joined. Kerberos S4U requires domain membership; the error wraps os.ErrInvalid so callers can errors.Is it.

Source

Thrown at util/winutil/s4u/lsa_windows.go:271

// access token for the user if successful. srcName must be non-empty, ASCII,
// and no more than 8 characters long. If srcName does not meet this criteria,
// LogonAs will return ErrBadSrcName wrapped with additional information; use
// errors.Is to check for it. When capLevel == CapCreateProcess, the logon
// enforces the user's logon hours policy (when present).
func (ls *lsaSession) logonAs(srcName string, u *user.User, capLevel CapabilityLevel) (token windows.Token, err error) {
	if ln := len(srcName); ln == 0 || ln > _TOKEN_SOURCE_LENGTH {
		return 0, fmt.Errorf("%w, actual length is %d", ErrBadSrcName, ln)
	}
	if err := checkASCII(srcName); err != nil {
		return 0, fmt.Errorf("%w: %v", ErrBadSrcName, err)
	}

	sanitizedUserName, isDomainUser, err := checkDomainAccount(u.Username)
	if err != nil {
		return 0, err
	}
	if isDomainUser && !winenv.IsDomainJoined() {
		return 0, fmt.Errorf("%w: cannot logon as domain user without being joined to a domain", os.ErrInvalid)
	}

	var pkgID uint32
	var authInfo unsafe.Pointer
	var authInfoLen uint32
	enforceLogonHours := capLevel == CapCreateProcess
	if isDomainUser {
		pkgID, err = authPkgIDKerberos.GetErr(func() (uint32, error) {
			return ls.getAuthPkgID(_MICROSOFT_KERBEROS_NAME)
		})
		if err != nil {
			return 0, err
		}

		upn16, err := samToUPN16(sanitizedUserName)
		if err != nil {
			return 0, fmt.Errorf("samToUPN16: %w", err)
		}

View on GitHub (pinned to 6e0912f979)

Solutions

  1. Check domain-join state (dsregedit /status, nltest /dsgetdc:) when this error appears
  2. Rejoin the domain, or target the local account by stripping the domain prefix
  3. Branch on errors.Is(err, os.ErrInvalid) and route the user to a configuration fix rather than retrying

Example fix

// before
u := &user.User{Username: "CORP\\alice"}
sess, err := s4u.Login(logf, srcName, u, s4u.CapCreateProcess)

// after
if strings.Contains(u.Username, "\\") && !winenv.IsDomainJoined() {
    return errors.New("domain account on non-domain machine; rejoin domain or use a local account")
}
sess, err := s4u.Login(logf, srcName, u, s4u.CapCreateProcess)
Defensive patterns

Strategy: validation

Validate before calling

// gate before calling s4u.Login with domain-style names
if _, _, ok := strings.Cut(u.Username, "\\"); ok && !winenv.IsDomainJoined() {
    return errors.New("domain account on non-domain machine")
}

Try / catch

sess, err := s4u.Login(logf, srcName, u, capLevel)
if err != nil {
    if errors.Is(err, os.ErrInvalid) && strings.Contains(err.Error(), "joined to a domain") {
        return errors.New("machine is not domain-joined; rejoin or use a local account")
    }
    return err
}

Prevention

When it happens

Trigger: u.Username like "CORP\alice" on a workgroup machine; the computer was un-joined from the domain after configuration; a local user whose name happens to carry a foreign domain-style prefix.

Common situations: Environment drift: test VMs cloned without domain join; machines removed from the domain but still holding domain-format usernames; usernames captured on one host reused on another.

Related errors


AI-assisted analysis of tailscale/tailscale@6e0912f979 (2026-08-18). Data as JSON: /api/errors/d78a9a6239e5ed1c. Report an issue: GitHub.