tailscale/tailscale · error

LsaLogonUser(%q): %w, SubStatus: %v

Error message

LsaLogonUser(%q): %w, SubStatus: %v

What it means

The core LsaLogonUser S4U (service-for-user) call failed. The message carries both the primary NTSTATUS and SubStatus: the primary covers STATUS_LOGON_FAILURE / STATUS_ACCOUNT_RESTRICTION style failures, while SubStatus often pins the account-level reason (STATUS_ACCOUNT_DISABLED, STATUS_PASSWORD_EXPIRED, STATUS_INVALID_LOGON_HOURS, Kerberos KDC errors). LsaLogonUser itself requires the caller to hold SeTcbPrivilege.

Source

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

	var srcContext _TOKEN_SOURCE
	copy(srcContext.SourceName[:], []byte(srcName))
	if err := allocateLocallyUniqueId(&srcContext.SourceIdentifier); err != nil {
		return 0, err
	}

	originName, err := windows.NewNTString(srcName)
	if err != nil {
		return 0, err
	}

	var profileBuf uintptr
	var profileBufLen uint32
	var logonID windows.LUID
	var quotas _QUOTA_LIMITS
	var subNTStatus windows.NTStatus
	ntStatus := lsaLogonUser(ls.handle, originName, _Network, pkgID, authInfo, authInfoLen, nil, &srcContext, &profileBuf, &profileBufLen, &logonID, &token, &quotas, &subNTStatus)
	if e := wingoes.ErrorFromNTStatus(ntStatus); e.Failed() {
		return 0, fmt.Errorf("LsaLogonUser(%q): %w, SubStatus: %v", u.Username, e, subNTStatus)
	}
	if profileBuf != 0 {
		lsaFreeReturnBuffer(profileBuf)
	}
	return token, nil
}

// samToUPN16 converts SAM-style account name samName to a UPN account name,
// returned as a UTF-16 slice.
func samToUPN16(samName string) (upn16 []uint16, err error) {
	_, samAccount, hasSep := strings.Cut(samName, `\`)
	if !hasSep {
		return nil, fmt.Errorf("%w: expected samName to contain a backslash", os.ErrInvalid)
	}

	// This is essentially the same algorithm used by Win32-OpenSSH:
	// First, try obtaining a UPN directly...
	upn16, err = translateName(samName, windows.NameSamCompatible, windows.NameUserPrincipal)

View on GitHub (pinned to 6e0912f979)

Solutions

  1. Read SubStatus in the message first: it distinguishes account state from infrastructure failure
  2. Verify the account state (enabled, not locked, not expired) in AD or the SAM
  3. Check KDC/DC reachability and time skew (Kerberos is time-sensitive)
  4. Confirm the calling process runs as SYSTEM with SeTcbPrivilege

Example fix

// before
sess, err := s4u.Login(logf, srcName, u, s4u.CapCreateProcess)
if err != nil { return err } // opaque

// after
sess, err := s4u.Login(logf, srcName, u, s4u.CapCreateProcess)
if err != nil {
    if strings.Contains(err.Error(), "STATUS_ACCOUNT_DISABLED") {
        return errors.New("account disabled; contact IT")
    }
    if strings.Contains(err.Error(), "STATUS_INVALID_LOGON_HOURS") {
        return errors.New("outside allowed logon hours")
    }
    return err
}
Defensive patterns

Strategy: try-catch

Try / catch

sess, err := s4u.Login(logf, srcName, u, capLevel)
if err != nil {
    msg := err.Error()
    switch {
    case strings.Contains(msg, "STATUS_ACCOUNT_DISABLED"):
        return errors.New("account disabled")
    case strings.Contains(msg, "STATUS_PASSWORD_EXPIRED"):
        return errors.New("account password expired")
    case strings.Contains(msg, "STATUS_INVALID_LOGON_HOURS"):
        return errors.New("outside permitted logon hours")
    }
    return err // includes primary NTSTATUS and SubStatus for diagnosis
}

Prevention

When it happens

Trigger: Target account disabled, locked out, or expired; logon-hours policy violated when capLevel == CapCreateProcess; Kerberos S4U refused because the KDC is unreachable or the computer account is broken; caller thread missing SeTcbPrivilege.

Common situations: SSH pre-auth group lookups (ListGroupIDsForSSHPreAuthOnly) and launching processes as arbitrary users; service-account policy changes by IT; DC outages; using a Session from a non-SYSTEM context.

Related errors


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