Fleet MDM
Device management for Tractorbeam using Fleet.
Overview
Fleet provides:
- Device inventory: Hardware specs, installed software, OS versions
- MDM (Mobile Device Management): Configuration profiles, remote wipe, app deployment
- Osquery: Real-time device queries for security and compliance
- Self-service: Users can install approved software via Fleet Desktop
┌─────────────────────────────────────────────────────────────────────────────┐
│ fleet.tractorbeam.ai │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ ECS Task │ │ Aurora MySQL │ │ ElastiCache │ │
│ │ (Fleet Go) │◄──►│ (fleetdb) │ │ (Valkey) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ ▲ │
└─────────│────────────────────────────────────────────────────────────────────┘
│ HTTPS (443)
│
┌─────┴─────┐
│ ALB │
└───────────┘
▲
│
┌──────┴──────┐
│ Devices │
│ (macOS/iOS) │
└─────────────┘Infrastructure: Terraform in aws/modules/fleet/
Accessing Fleet
| URL | Purpose |
|---|---|
| https://fleet.tractorbeam.ai | Admin UI and API |
| https://fleet.tractorbeam.ai/healthz | Health check |
Authentication: Login with your Tractorbeam email. Fleet uses local auth (no SSO yet).
MDM Setup
Fleet MDM requires several Apple certificates and integrations.
Prerequisites
Before MDM can issue enrollment profiles, configure these in Fleet:
| Component | Purpose | Location in Fleet |
|---|---|---|
| APNs Certificate | Push notifications to devices | Settings → Integrations → MDM → Apple Push |
| ABM Token | Automatic device enrollment | Settings → Integrations → MDM → Apple Business Manager |
| SCEP Certificate | Device identity certificates | Auto-generated by Fleet |
Apple Business Manager (ABM):
- Log in to business.apple.com
- Settings → Device Management Settings
- Add Fleet as an MDM server
- Set "Default Server Assignment" for personal devices to Fleet
BYOD Enrollment
Fleet supports two BYOD enrollment methods depending on the platform.
iOS/iPadOS: Account-driven User Enrollment
Platform: iOS and iPadOS only. macOS is not supported by Fleet for account-driven enrollment, even though Apple supports it on macOS 14+. This is a Fleet limitation - they only implemented iOS/iPadOS support in Fleet 4.72.0.
For personal iPhones and iPads, users can enroll via their Managed Apple Account:
- Open Settings → General → VPN & Device Management
- Tap Sign In to Work or School Account
- Enter Tractorbeam email (Okta credentials via federated auth)
- Device automatically enrolls in Fleet
Service Discovery: Apple discovers the MDM server via a well-known file:
- File location:
marketingrepo atpublic/.well-known/com.apple.remotemanagement - For iOS 18.2+, Fleet can manage service discovery automatically via ABM
- The well-known file is a fallback for older devices
See: Fleet BYOD Guide
macOS: Manual Enrollment Profile
Platform: macOS only. This is the only BYOD method for personal Macs not in ABM.
Why not account-driven? Apple supports Account-driven User Enrollment on macOS 14+, but Fleet hasn't implemented macOS support yet. If you try "Sign In to Work or School Account" on a Mac, you'll get "Did not receive an enrollment profile from your MDM server."
For personal Macs, users must manually install an enrollment profile:
- In Fleet UI: Hosts → Add hosts → macOS tab
- Copy the enrollment URL (team-specific, contains enroll secret)
- Share the URL with users (email, Slack, etc.)
- User opens URL, downloads enrollment profile
- User installs profile: System Settings → Privacy & Security → Profiles
This gives IT management capabilities while the Mac remains a personal device.
Company-owned Device Enrollment
Devices purchased through Apple Business Manager enroll automatically:
- Device powers on and connects to network
- ABM directs device to Fleet
- Fleet sends enrollment profile
- Device enrolls and applies configurations
GitOps Configuration
Fleet configuration is managed via GitOps in fleet-config/.
fleet-config/
├── default.yml # Global settings (all teams)
├── teams/ # Team-specific configs
│ ├── workstations.yml
│ ├── byod.yml
│ └── personal-mobile.yml
├── lib/ # Shared policies and queries
│ ├── all/
│ └── macos/
└── gitops.sh # Apply scriptApplying Changes
Changes deploy automatically via GitHub Actions (.github/workflows/fleet-gitops.yml).
Manual apply:
cd fleet-config
export FLEET_URL="https://fleet.tractorbeam.ai"
export FLEET_API_TOKEN="..." # From Secrets Manager
./gitops.shConfiguration Reference
See Fleet GitOps docs.
CLI Tools
fleetctl
# Install
brew install fleetdm/tap/fleetctl
# Configure
fleetctl config set --address https://fleet.tractorbeam.ai
fleetctl login
# Common commands
fleetctl get hosts # List enrolled devices
fleetctl get host <hostname> # Device details
fleetctl query --query "SELECT * FROM users" --hosts <hostname>Secrets
| Secret | Location | Purpose |
|---|---|---|
| Database password | tractorbeam-fleet/database-password | Aurora MySQL |
| License key | tractorbeam-fleet/license-key | Fleet Premium |
| Private key | fleet-server-private-key | Session encryption |
| GitOps token | tractorbeam/github-actions/fleet | CI/CD |
Note: The
tractorbeam-fleet/prefix predates the naming convention (tractorbeam/fleet/). See the migration plan indocs/standards/naming-conventions.md.
Operations
AWS Authentication
All commands require AWS credentials for the Shared Services account:
# SSO login (if configured)
aws sso login
# Or assume role from management account
export AWS_PROFILE=shared-servicesViewing Logs
Fleet logs are in CloudWatch Logs in the Shared Services account (707264479446).
# Assume role to Shared Services (from management account)
CREDS=$(AWS_PROFILE=root aws sts assume-role \
--role-arn "arn:aws:iam::707264479446:role/OrganizationAccountAccessRole" \
--role-session-name "fleet-logs" --output json)
export AWS_ACCESS_KEY_ID=$(echo "$CREDS" | jq -r '.Credentials.AccessKeyId')
export AWS_SECRET_ACCESS_KEY=$(echo "$CREDS" | jq -r '.Credentials.SecretAccessKey')
export AWS_SESSION_TOKEN=$(echo "$CREDS" | jq -r '.Credentials.SessionToken')
# Find Fleet log group (name is auto-generated by Terraform)
aws logs describe-log-groups --query 'logGroups[*].logGroupName' --output table
# Stream live logs (last 5 minutes, follow)
# Replace LOG_GROUP with actual name from above (e.g., terraform-2026...)
aws logs tail $LOG_GROUP --since 5m --follow
# Search for MDM enrollment attempts
aws logs filter-log-events \
--log-group-name $LOG_GROUP \
--start-time $(($(date +%s) - 3600))000 \
--filter-pattern "account_driven_enroll" \
--query 'events[*].message' --output text
# Search for errors
aws logs filter-log-events \
--log-group-name $LOG_GROUP \
--start-time $(($(date +%s) - 3600))000 \
--filter-pattern "level=error" \
--query 'events[*].message' --output text | tr '\t' '\n' | head -50ECS Service Status
# Check service status (requires Shared Services role - see above)
aws ecs describe-services \
--cluster tractorbeam-fleet \
--services fleet \
--query 'services[0].{status:status,running:runningCount,desired:desiredCount,events:events[0:3]}'
# List running tasks
aws ecs list-tasks --cluster tractorbeam-fleet --service-name fleet
# Get task details
TASK_ID=$(aws ecs list-tasks --cluster tractorbeam-fleet --service-name fleet \
--query 'taskArns[0]' --output text | cut -d'/' -f3)
aws ecs describe-tasks \
--cluster tractorbeam-fleet \
--tasks $TASK_ID \
--query 'tasks[0].{status:lastStatus,startedAt:startedAt,taskDef:taskDefinitionArn}'
# Get log configuration from task definition
aws ecs describe-task-definition \
--task-definition fleet:3 \
--query 'taskDefinition.containerDefinitions[0].logConfiguration'
# Force new deployment (rolling restart)
aws ecs update-service \
--cluster tractorbeam-fleet \
--service fleet \
--force-new-deploymentDatabase Connectivity
# Check RDS cluster status
aws rds describe-db-clusters \
--db-cluster-identifier tractorbeam-fleet \
--query 'DBClusters[0].{status:Status,endpoint:Endpoint,reader:ReaderEndpoint}'Health Checks
# Fleet health endpoint
curl -s https://fleet.tractorbeam.ai/healthz && echo "OK"
# Service discovery endpoint (after marketing PR merges)
curl -s https://tractorbeam.ai/.well-known/com.apple.remotemanagement | jq .
# Check MDM enrollment endpoint responds
curl -s -o /dev/null -w "%{http_code}" \
https://fleet.tractorbeam.ai/api/mdm/apple/account_driven_enrollALB Metrics
# Get ALB ARN
ALB_ARN=$(aws elbv2 describe-load-balancers \
--names tractorbeam-fleet \
--query 'LoadBalancers[0].LoadBalancerArn' --output text)
# Check target health
aws elbv2 describe-target-health \
--target-group-arn $(aws elbv2 describe-target-groups \
--load-balancer-arn $ALB_ARN \
--query 'TargetGroups[0].TargetGroupArn' --output text)Troubleshooting
"Did not receive an enrollment profile"
This error during BYOD enrollment means Fleet isn't returning a valid profile.
Step 0: Check platform support (most common cause for macOS)
Account-driven User Enrollment (Settings → Sign In to Work or School Account) only works on iOS and iPadOS in Fleet. If you see this error on macOS, that's expected - Fleet hasn't implemented macOS support for account-driven enrollment yet (as of Feb 2026), even though Apple supports it on macOS 14+. Use manual enrollment instead - see macOS: Manual Enrollment Profile.
Step 1: Verify service discovery is working (iOS/iPadOS only)
# Should return JSON with BaseURL pointing to Fleet
curl -s https://tractorbeam.ai/.well-known/com.apple.remotemanagement | jq .
# Check Content-Type header is application/json
curl -sI https://tractorbeam.ai/.well-known/com.apple.remotemanagement | grep -i content-typeStep 2: Check Fleet MDM configuration
- MDM enabled? (Settings → Integrations → MDM)
- APNs certificate uploaded and valid (not expired)?
- ABM token connected and not expired?
Step 3: Check Fleet logs for enrollment errors
# Assume role to Shared Services first (see Operations section)
# Find log group name
LOG_GROUP=$(aws logs describe-log-groups --query 'logGroups[?starts_with(logGroupName, `terraform-`)].logGroupName' --output text)
# Search for MDM enrollment attempts
aws logs filter-log-events \
--log-group-name $LOG_GROUP \
--start-time $(($(date +%s) - 3600))000 \
--filter-pattern "account_driven_enroll" \
--query 'events[*].message' --output text
# Look for: "level=error ... user=unauthenticated" = auth failure
# Look for: "level=info ... serving MDM service discovery" = service discovery workingStep 4: Check APNs and DEP sync status
# Verify APNs pusher is running
aws logs filter-log-events \
--log-group-name $LOG_GROUP \
--start-time $(($(date +%s) - 3600))000 \
--filter-pattern "apple_mdm_apns_pusher status=completed" \
--query 'length(events)'
# Verify DEP sync with ABM
aws logs filter-log-events \
--log-group-name $LOG_GROUP \
--start-time $(($(date +%s) - 3600))000 \
--filter-pattern "nanodep-syncer" \
--query 'events[*].message' --output text | tr '\t' '\n' | tail -5Common causes:
- APNs certificate not uploaded or expired
- ABM token expired (renew annually)
- Domain not verified in ABM
- IdP (Okta) not federated with ABM
Device not appearing in Fleet
- Check device has network connectivity
- Verify enrollment completed (Settings → VPN & Device Management)
- Wait a few minutes for initial check-in
- Check Fleet activity log for errors
MDM commands not executing
- Verify APNs certificate is valid (not expired)
- Check device has internet access
- Try manually triggering a check-in from device settings
Service discovery returns 404
The well-known file is missing or misconfigured. Check:
- File exists in
marketingrepo:public/.well-known/com.apple.remotemanagement - Marketing site is deployed
- No redirect/rewrite interfering (Vercel doesn't allow rewrites on
.well-known)
Related Documentation
- Fleet Package Deployment - Deploying software to Fleet
- Apple Signing - Code signing for macOS packages
- Identity Integration - How Okta integrates with ABM