Files
fleet/server/fleet/api_orbit.go
T
Victor Lyuboslavsky 98060b08a6 Add Windows managed local account server flow (#48721) (#49924)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #48721 

Part 2 of https://github.com/fleetdm/fleet/issues/43488

# Checklist for submitter

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.

## Testing

- [x] Added/updated automated tests

- [x] QA'd all new/changed functionality manually

## Database migrations

- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Windows devices can now create and securely escrow managed local
account passwords during enrollment.
* Added Windows managed local account status and password availability
to host details.
  * Device-reported setup errors are surfaced with helpful details.
* Account creation is automatically requested when supported by the
device, plan, and configuration.
* **Bug Fixes**
  * Windows accounts are excluded from password rotation workflows.
  * Re-enrollment correctly triggers account creation when needed.
  * Passwords remain available when settings change after enrollment.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-03 09:03:27 -05:00

418 lines
14 KiB
Go

package fleet
import (
"context"
"crypto/x509"
"encoding/json"
"errors"
"io"
"net/http"
"net/url"
"os"
)
// EnrollOrbitRequest is the request Orbit instances use to enroll to Fleet.
type EnrollOrbitRequest struct {
// EnrollSecret is the secret to authenticate the enroll request.
EnrollSecret string `json:"enroll_secret"`
// HardwareUUID is the device's hardware UUID.
HardwareUUID string `json:"hardware_uuid"`
// HardwareSerial is the device's serial number.
HardwareSerial string `json:"hardware_serial"`
// Hostname is the device's hostname.
Hostname string `json:"hostname"`
// Platform is the device's platform as defined by osquery.
Platform string `json:"platform"`
// PlatformLike is the device's platform_like as defined by osquery.
PlatformLike string `json:"platform_like"`
// OsqueryIdentifier holds the identifier used by osquery.
// If not set, then the hardware UUID is used to match orbit and osquery.
OsqueryIdentifier string `json:"osquery_identifier"`
// ComputerName is the device's friendly name (optional).
ComputerName string `json:"computer_name"`
// HardwareModel is the device's hardware model.
HardwareModel string `json:"hardware_model"`
// EUAToken is a Fleet-signed JWT containing the user's UPN and Windows MDM device ID.
EUAToken string `json:"eua_token,omitempty"`
}
// SetOrbitNodeKeyer is the interface implemented by orbit request types that
// carry an orbit node key for authentication.
type SetOrbitNodeKeyer interface {
SetOrbitNodeKey(nodeKey string)
}
/////////////////////////////////////////////////////////////////////////////////
// Orbit config
/////////////////////////////////////////////////////////////////////////////////
type OrbitGetConfigRequest struct {
OrbitNodeKey string `json:"orbit_node_key"`
}
func (r *OrbitGetConfigRequest) SetOrbitNodeKey(nodeKey string) {
r.OrbitNodeKey = nodeKey
}
func (r *OrbitGetConfigRequest) OrbitHostNodeKey() string {
return r.OrbitNodeKey
}
// DecodeBody implements the bodyDecoder interface for custom request body decoding.
// This endpoint is susceptible to client read timeouts (poll.DeadlineExceededError).
// By implementing DecodeBody, we classify those network errors as client errors.
func (r *OrbitGetConfigRequest) DecodeBody(_ context.Context, reader io.Reader, _ url.Values, _ []*x509.Certificate) error {
if err := json.NewDecoder(reader).Decode(r); err != nil {
if errors.Is(err, os.ErrDeadlineExceeded) {
return &BadRequestError{
Message: "request body read timeout",
InternalErr: err,
}
}
return err
}
return nil
}
type OrbitGetConfigResponse struct {
OrbitConfig
Err error `json:"error,omitempty"`
}
func (r OrbitGetConfigResponse) Error() error { return r.Err }
/////////////////////////////////////////////////////////////////////////////////
// Enroll Orbit
/////////////////////////////////////////////////////////////////////////////////
type EnrollOrbitResponse struct {
OrbitNodeKey string `json:"orbit_node_key,omitempty"`
Err error `json:"error,omitempty"`
}
func (r EnrollOrbitResponse) Error() error { return r.Err }
/////////////////////////////////////////////////////////////////////////////////
// Ping orbit endpoint
/////////////////////////////////////////////////////////////////////////////////
type OrbitPingRequest struct{}
type OrbitPingResponse struct{}
func (r OrbitPingResponse) Error() error { return nil }
/////////////////////////////////////////////////////////////////////////////////
// SetOrUpdateDeviceToken endpoint
/////////////////////////////////////////////////////////////////////////////////
type SetOrUpdateDeviceTokenRequest struct {
OrbitNodeKey string `json:"orbit_node_key"`
DeviceAuthToken string `json:"device_auth_token"`
}
func (r *SetOrUpdateDeviceTokenRequest) SetOrbitNodeKey(nodeKey string) {
r.OrbitNodeKey = nodeKey
}
func (r *SetOrUpdateDeviceTokenRequest) OrbitHostNodeKey() string {
return r.OrbitNodeKey
}
type SetOrUpdateDeviceTokenResponse struct {
Err error `json:"error,omitempty"`
}
func (r SetOrUpdateDeviceTokenResponse) Error() error { return r.Err }
/////////////////////////////////////////////////////////////////////////////////
// Get Orbit pending script execution request
/////////////////////////////////////////////////////////////////////////////////
type OrbitGetScriptRequest struct {
OrbitNodeKey string `json:"orbit_node_key"`
ExecutionID string `json:"execution_id"`
}
func (r *OrbitGetScriptRequest) SetOrbitNodeKey(nodeKey string) {
r.OrbitNodeKey = nodeKey
}
func (r *OrbitGetScriptRequest) OrbitHostNodeKey() string {
return r.OrbitNodeKey
}
type OrbitGetScriptResponse struct {
Err error `json:"error,omitempty"`
*HostScriptResult
}
func (r OrbitGetScriptResponse) Error() error { return r.Err }
/////////////////////////////////////////////////////////////////////////////////
// Post Orbit script execution result
/////////////////////////////////////////////////////////////////////////////////
type OrbitPostScriptResultRequest struct {
OrbitNodeKey string `json:"orbit_node_key"`
*HostScriptResultPayload
}
func (r *OrbitPostScriptResultRequest) SetOrbitNodeKey(nodeKey string) {
r.OrbitNodeKey = nodeKey
}
func (r *OrbitPostScriptResultRequest) OrbitHostNodeKey() string {
return r.OrbitNodeKey
}
type OrbitPostScriptResultResponse struct {
Err error `json:"error,omitempty"`
}
func (r OrbitPostScriptResultResponse) Error() error { return r.Err }
/////////////////////////////////////////////////////////////////////////////////
// Post Orbit device mapping (custom email)
/////////////////////////////////////////////////////////////////////////////////
type OrbitPutDeviceMappingRequest struct {
OrbitNodeKey string `json:"orbit_node_key"`
Email string `json:"email"`
}
func (r *OrbitPutDeviceMappingRequest) SetOrbitNodeKey(nodeKey string) {
r.OrbitNodeKey = nodeKey
}
func (r *OrbitPutDeviceMappingRequest) OrbitHostNodeKey() string {
return r.OrbitNodeKey
}
type OrbitPutDeviceMappingResponse struct {
Err error `json:"error,omitempty"`
}
func (r OrbitPutDeviceMappingResponse) Error() error { return r.Err }
/////////////////////////////////////////////////////////////////////////////////
// Post Orbit disk encryption key
/////////////////////////////////////////////////////////////////////////////////
type OrbitPostDiskEncryptionKeyRequest struct {
OrbitNodeKey string `json:"orbit_node_key"`
EncryptionKey []byte `json:"encryption_key"`
ClientError string `json:"client_error"`
}
func (r *OrbitPostDiskEncryptionKeyRequest) SetOrbitNodeKey(nodeKey string) {
r.OrbitNodeKey = nodeKey
}
func (r *OrbitPostDiskEncryptionKeyRequest) OrbitHostNodeKey() string {
return r.OrbitNodeKey
}
type OrbitPostDiskEncryptionKeyResponse struct {
Err error `json:"error,omitempty"`
}
func (r OrbitPostDiskEncryptionKeyResponse) Error() error { return r.Err }
func (r OrbitPostDiskEncryptionKeyResponse) Status() int { return http.StatusNoContent }
/////////////////////////////////////////////////////////////////////////////////
// Post Orbit LUKS (Linux disk encryption) data
/////////////////////////////////////////////////////////////////////////////////
// LUKS key type values reported by orbit in OrbitPostLUKSRequest.KeyType. An
// empty value is treated as LUKSKeyTypePassphrase for backward compatibility
// with older orbit agents that predate TPM-backed FDE support.
const (
// LUKSKeyTypePassphrase is a Fleet-generated passphrase added to a
// dedicated LUKS key slot (the legacy path). It carries a Salt and a
// numeric KeySlot.
LUKSKeyTypePassphrase = "passphrase"
// LUKSKeyTypeRecoveryKey is a snapd/secboot-managed recovery key escrowed
// from a host using TPM-backed full-disk encryption (e.g. Ubuntu 26). It
// has no Salt and no numeric KeySlot because snapd owns the key slots.
LUKSKeyTypeRecoveryKey = "recovery_key"
)
type OrbitPostLUKSRequest struct {
OrbitNodeKey string `json:"orbit_node_key"`
Passphrase string `json:"passphrase"`
Salt string `json:"salt"`
KeySlot *uint `json:"key_slot"`
ClientError string `json:"client_error"`
// KeyType identifies how the escrowed secret unlocks the volume. Empty or
// LUKSKeyTypePassphrase means the legacy passphrase-in-a-key-slot path;
// LUKSKeyTypeRecoveryKey means a TPM-backed FDE recovery key (no Salt/KeySlot).
KeyType string `json:"key_type"`
}
func (r *OrbitPostLUKSRequest) SetOrbitNodeKey(nodeKey string) {
r.OrbitNodeKey = nodeKey
}
func (r *OrbitPostLUKSRequest) OrbitHostNodeKey() string {
return r.OrbitNodeKey
}
type OrbitPostLUKSResponse struct {
Err error `json:"error,omitempty"`
}
func (r OrbitPostLUKSResponse) Error() error { return r.Err }
func (r OrbitPostLUKSResponse) Status() int { return http.StatusNoContent }
/////////////////////////////////////////////////////////////////////////////////
// Post Orbit Windows managed local account password
/////////////////////////////////////////////////////////////////////////////////
// OrbitPostManagedLocalAccountRequest carries the device-generated password that Windows fleetd escrows after creating
// the managed local admin account. ClientError, when set, reports a device-side failure so the server can log it without
// recording a password.
type OrbitPostManagedLocalAccountRequest struct {
OrbitNodeKey string `json:"orbit_node_key"`
Password string `json:"password"`
ClientError string `json:"client_error"`
}
func (r *OrbitPostManagedLocalAccountRequest) SetOrbitNodeKey(nodeKey string) {
r.OrbitNodeKey = nodeKey
}
func (r *OrbitPostManagedLocalAccountRequest) OrbitHostNodeKey() string {
return r.OrbitNodeKey
}
type OrbitPostManagedLocalAccountResponse struct {
Err error `json:"error,omitempty"`
}
func (r OrbitPostManagedLocalAccountResponse) Error() error { return r.Err }
func (r OrbitPostManagedLocalAccountResponse) Status() int { return http.StatusNoContent }
/////////////////////////////////////////////////////////////////////////////////
// Get Orbit software install details
/////////////////////////////////////////////////////////////////////////////////
type OrbitGetSoftwareInstallRequest struct {
OrbitNodeKey string `json:"orbit_node_key"`
OrbotNodeKey string `json:"orbot_node_key"` // legacy typo -- keep for backwards compatibility with orbit <= 1.38.0
InstallUUID string `json:"install_uuid"`
}
func (r *OrbitGetSoftwareInstallRequest) SetOrbitNodeKey(nodeKey string) {
r.OrbitNodeKey = nodeKey
r.OrbotNodeKey = nodeKey // legacy typo -- keep for backwards compatability with fleet server < 4.63.0
}
func (r *OrbitGetSoftwareInstallRequest) OrbitHostNodeKey() string {
if r.OrbitNodeKey != "" {
return r.OrbitNodeKey
}
return r.OrbotNodeKey
}
type OrbitGetSoftwareInstallResponse struct {
Err error `json:"error,omitempty"`
*SoftwareInstallDetails
}
func (r OrbitGetSoftwareInstallResponse) Error() error { return r.Err }
/////////////////////////////////////////////////////////////////////////////////
// Download Orbit software installer request
/////////////////////////////////////////////////////////////////////////////////
type OrbitDownloadSoftwareInstallerRequest struct {
Alt string `query:"alt"`
OrbitNodeKey string `json:"orbit_node_key"`
InstallerID uint `json:"installer_id"`
}
func (r *OrbitDownloadSoftwareInstallerRequest) SetOrbitNodeKey(nodeKey string) {
r.OrbitNodeKey = nodeKey
}
func (r *OrbitDownloadSoftwareInstallerRequest) OrbitHostNodeKey() string {
return r.OrbitNodeKey
}
/////////////////////////////////////////////////////////////////////////////////
// Post Orbit software install result
/////////////////////////////////////////////////////////////////////////////////
type OrbitPostSoftwareInstallResultRequest struct {
OrbitNodeKey string `json:"orbit_node_key"`
*HostSoftwareInstallResultPayload
}
func (r *OrbitPostSoftwareInstallResultRequest) SetOrbitNodeKey(nodeKey string) {
r.OrbitNodeKey = nodeKey
}
func (r *OrbitPostSoftwareInstallResultRequest) OrbitHostNodeKey() string {
return r.OrbitNodeKey
}
type OrbitPostSoftwareInstallResultResponse struct {
Err error `json:"error,omitempty"`
}
func (r OrbitPostSoftwareInstallResultResponse) Error() error { return r.Err }
func (r OrbitPostSoftwareInstallResultResponse) Status() int { return http.StatusNoContent }
/////////////////////////////////////////////////////////////////////////////////
// Get Orbit setup experience status
/////////////////////////////////////////////////////////////////////////////////
type GetOrbitSetupExperienceStatusRequest struct {
OrbitNodeKey string `json:"orbit_node_key"`
ForceRelease bool `json:"force_release"`
// Whether to re-enqueue canceled setup experience steps after a previous
// software install failure on MacOS.
ResetFailedSetupSteps bool `json:"reset_failed_setup_steps"`
}
func (r *GetOrbitSetupExperienceStatusRequest) SetOrbitNodeKey(nodeKey string) {
r.OrbitNodeKey = nodeKey
}
func (r *GetOrbitSetupExperienceStatusRequest) OrbitHostNodeKey() string {
return r.OrbitNodeKey
}
type GetOrbitSetupExperienceStatusResponse struct {
Results *SetupExperienceStatusPayload `json:"setup_experience_results,omitempty"`
Err error `json:"error,omitempty"`
}
func (r GetOrbitSetupExperienceStatusResponse) Error() error { return r.Err }
/////////////////////////////////////////////////////////////////////////////////
// Setup experience init
/////////////////////////////////////////////////////////////////////////////////
type OrbitSetupExperienceInitRequest struct {
OrbitNodeKey string `json:"orbit_node_key"`
}
func (r *OrbitSetupExperienceInitRequest) SetOrbitNodeKey(nodeKey string) {
r.OrbitNodeKey = nodeKey
}
func (r *OrbitSetupExperienceInitRequest) OrbitHostNodeKey() string {
return r.OrbitNodeKey
}
type OrbitSetupExperienceInitResponse struct {
Result SetupExperienceInitResult `json:"result"`
Err error `json:"error,omitempty"`
}
func (r OrbitSetupExperienceInitResponse) Error() error {
return r.Err
}