Overview
Most homelab monitoring answers "is the host up and how loaded is it" — Prometheus, Zabbix, and friends already cover that ground on this site. osquery answers a different question: "what is actually happening inside this endpoint right now," exposed as SQL tables over processes, listening sockets, loaded kernel modules, scheduled tasks, installed packages, USB devices, and hundreds of other operating-system facts. Fleet is the open-source control plane that turns a pile of osquery agents into a fleet you can query, schedule against, and alert on from one place — MIT-licensed, no feature gate between self-hosted and cloud, and explicitly supported for homelab use.
This project stands up Fleet in Docker, enrolls a Linux endpoint (the pattern is identical for macOS and Windows), runs ad-hoc live queries against it, builds a scheduled query that streams data continuously, and defines a policy that flags hosts drifting out of compliance — the same mechanics a fleet of 5 or 5,000 endpoints uses. Where Wazuh gives you host-based IDS and Zabbix gives you infra health, Fleet gives you point-in-time and continuous endpoint telemetry you can pivot on with plain SQL — a strong complement if you already run the Wazuh XDR/SIEM homelab.
What you'll build:
- Fleet server + MySQL 8 + Redis in Docker Compose, fronted by TLS
- One or more osquery agents enrolled via generated installer/node key
- Ad-hoc live queries run from the Fleet UI and REST API
- A scheduled query streaming process-listening-port data
- A policy that flags hosts with disk encryption disabled or unexpected local admin accounts
Architecture
┌─────────────────────────────────────────────────┐
│ Docker Host (Fleet) │
│ │
│ ┌───────────┐ ┌────────────┐ ┌────────────┐ │
│ │ mysql │◄─►│ fleet │◄─►│ redis │ │
│ │ :3306 │ │ server │ │ :6379 │ │
│ └───────────┘ │ :8080/443 │ └────────────┘ │
│ └──────┬─────┘ │
└──────────────────────────┼───────────────────────┘
│ HTTPS (osquery TLS plugin)
┌─────────────┼─────────────┐
▼ ▼ ▼
┌───────────────┐ ┌──────────┐ ┌───────────────┐
│ Linux endpoint │ │ macOS │ │ Windows │
│ osqueryd │ │ osqueryd │ │ osqueryd │
└───────────────┘ └──────────┘ └───────────────┘
│
▼
┌────────────────┐
│ Log forwarder │
│ (webhook/syslog)│
│ → SIEM / Wazuh │
└────────────────┘
Each osqueryd agent authenticates to Fleet over HTTPS using a node key issued at enrollment, then polls for its configuration — which scheduled queries to run, on what interval, and which packs/policies apply. Query results and status logs stream back to Fleet, which can forward them to a webhook, syslog, Kafka, or Kinesis destination for ingestion into a SIEM.
Prerequisites
- Docker Engine 24+ and Docker Compose v2
- 2 vCPU / 4 GB RAM minimum for the Fleet stack (MySQL is the heavier consumer)
- At least one endpoint to enroll — a Linux VM, macOS laptop, or Windows box on the same network
- A domain name or
/etc/hostsentry for the Fleet server if you want valid TLS instead of a self-signed cert - Basic comfort reading osquery's schema tables — the UI autocompletes them, but knowing
processes,listening_ports, andusersup front helps
Step 1 — Docker Compose Stack
mkdir fleet-stack && cd fleet-stack
cat > .env <<'EOF'
MYSQL_ROOT_PASSWORD=CHANGE_ME_ROOT
MYSQL_DATABASE=fleet
MYSQL_USER=fleet
MYSQL_PASSWORD=CHANGE_ME_STRONG_PASSWORD
FLEET_MYSQL_ADDRESS=mysql:3306
FLEET_REDIS_ADDRESS=redis:6379
FLEET_SERVER_ADDRESS=0.0.0.0:8080
FLEET_SERVER_TLS=false
FLEET_LOGGING_JSON=true
EOFdocker-compose.yml:
services:
mysql:
image: mysql:8.0
container_name: fleet-mysql
restart: unless-stopped
env_file: .env
command: --default-authentication-plugin=mysql_native_password
volumes:
- fleet-mysql-data:/var/lib/mysql
redis:
image: redis:7-alpine
container_name: fleet-redis
restart: unless-stopped
fleet-prepare:
image: fleetdm/fleet:v4.66.0
container_name: fleet-prepare
env_file: .env
command: fleet prepare db --no-prompt
depends_on:
- mysql
- redis
restart: on-failure
fleet:
image: fleetdm/fleet:v4.66.0
container_name: fleet-server
restart: unless-stopped
env_file: .env
ports:
- "8080:8080"
depends_on:
- fleet-prepare
command: fleet serve
volumes:
fleet-mysql-data:Pin the Fleet image tag to whatever is current on the releases page — v4.66.0 above is a placeholder for "latest stable at build time." Bring the stack up in two passes since fleet prepare db must finish schema migration before fleet serve starts:
docker compose up -d mysql redis
docker compose run --rm fleet-prepare
docker compose up -d fleet
docker compose logs -f fleetWait for Successfully started Fleet in the logs.
Step 2 — Initial Admin Setup
Open http://YOUR_HOST:8080 (self-signed/plaintext for lab use — put this behind Traefik/Caddy with real TLS before enrolling anything outside a fully trusted LAN, since node keys and query results transit this connection). The first-run wizard creates your admin account and org name.
Once logged in, grab a Fleet API token for scripting: your avatar → My account → Get API token. Save it as an environment variable for later:
export FLEET_TOKEN="paste_your_token_here"
export FLEET_URL="http://YOUR_HOST:8080"Step 3 — Generate an Enrollment Package and Enroll an Endpoint
Fleet builds a per-platform installer (.deb, .rpm, .pkg, .msi) that bundles the enrollment secret and osquery config, so you never hand-edit osquery.conf on the endpoint. From the UI: Hosts → Add hosts, pick the target platform, and download the generated installer, or use fleetctl:
# Install fleetctl locally
curl -fsSL https://raw.githubusercontent.com/fleetdm/fleet/main/tools/install-fleetctl.sh | sh
fleetctl config set --address $FLEET_URL --token $FLEET_TOKEN
# Generate a Linux .deb enroll package
fleetctl package --type deb \
--fleet-url=$FLEET_URL \
--enroll-secret=$(fleetctl get enroll-secret --json | jq -r '.secrets[0].secret')On the target Linux endpoint:
sudo dpkg -i fleet-osquery_*.deb
sudo systemctl status orbit # osquery runs under Fleet's "Orbit" runtime wrapperBack in the Fleet UI under Hosts, the new endpoint appears within a minute once it completes its first check-in. Click into it to see the auto-collected host details — OS version, installed software, hardware, MDM enrollment (if applicable) — all sourced from osquery tables under the hood.
Step 4 — Run a Live Query
Live queries execute SQL against enrolled hosts on demand and return results as agents check in (typically within seconds). Queries → New query, or run it ad hoc from the host detail page. Start with something that proves the pipeline works end to end:
SELECT pid, name, path, cmdline
FROM processes
WHERE name NOT LIKE 'kworker%'
ORDER BY start_time DESC
LIMIT 20;Target your enrolled host and hit Run. You should see live process data stream back within a few seconds. Try a second query that's actually useful for a homelab: everything listening on a network socket, joined against the owning process:
SELECT lp.port, lp.protocol, p.name, p.path
FROM listening_ports lp
JOIN processes p ON lp.pid = p.pid
WHERE lp.address != '127.0.0.1'
ORDER BY lp.port;This is the single most useful osquery query for a new host — "what's actually exposed on this box" answered from ground truth, not from a service definition file that might be stale.
Step 5 — Schedule a Continuous Query
Ad-hoc queries are great for hunting; scheduled queries build a continuous stream of telemetry. Queries → New query, write the SQL, then set Schedule with an interval and target team/host label instead of running it once:
SELECT lp.port, lp.protocol, p.name, p.path, p.cmdline
FROM listening_ports lp
JOIN processes p ON lp.pid = p.pid
WHERE lp.address NOT IN ('127.0.0.1', '::1');Set the interval to 3600 seconds and enable Automations → send data to log destination for this query so results flow continuously into whatever you configure in Step 6, rather than only being visible when you manually click into the query.
Step 6 — Forward Results to a Log Destination
Fleet supports several log destinations for scheduled query results and status/result logs (FLEET_OSQUERY_RESULT_LOG_PLUGIN / FLEET_OSQUERY_STATUS_LOG_PLUGIN). For a homelab wired into an existing SIEM, a webhook is the fastest path — add to your .env and restart the fleet service:
FLEET_OSQUERY_RESULT_LOG_PLUGIN=filesystem
FLEET_FILESYSTEM_STATUS_LOG_FILE=/logs/osqueryd.status.log
FLEET_FILESYSTEM_RESULT_LOG_FILE=/logs/osqueryd.results.log
FLEET_FILESYSTEM_ENABLE_LOG_ROTATION=trueMount a volume at /logs in the fleet service and point Promtail or your Wazuh log collector at the resulting files, the same pattern used in the log management and Wazuh builds — Fleet becomes one more structured JSON source feeding the existing pipeline instead of a second console you have to babysit separately.
Step 7 — Define a Compliance Policy
Policies are just osquery SQL that returns zero rows for pass, one or more rows for fail, evaluated on a schedule against every enrolled host. Policies → Add policy:
-- Fails (returns a row) if full-disk encryption is NOT enabled
SELECT 1 FROM disk_encryption WHERE encrypted = 0 LIMIT 1;-- Fails if a non-standard user is in the local admin/sudo group
SELECT 1 FROM groups g
JOIN user_groups ug ON g.gid = ug.gid
JOIN users u ON ug.uid = u.uid
WHERE g.groupname IN ('sudo', 'wheel', 'admin')
AND u.username NOT IN ('root', 'YOUR_KNOWN_ADMIN');Enable Automations → policy failures and point it at a webhook (Discord, n8n, or your SOAR of choice) so newly-failing hosts generate an alert instead of requiring a manual dashboard check.
Testing Your Setup
Confirm the whole loop — enrollment, live query, scheduled query, policy — works before calling it done:
# 1. Confirm the host is checking in
fleetctl get hosts
# 2. Run a live query via API and confirm results return
fleetctl query --hosts YOUR_HOSTNAME \
--query "SELECT version FROM os_version;"
# 3. Force a policy failure to test alerting: temporarily add a throwaway
# user to the sudo group on the test endpoint, then re-run the policy
sudo useradd -m testuser && sudo usermod -aG sudo testuserWithin one policy evaluation cycle, the host should flip to "failing" for the admin-group policy in the Fleet UI, and your configured webhook should fire. Clean up immediately after confirming:
sudo deluser --remove-home testuserAlso verify the scheduled query is actually landing in your log destination:
docker exec fleet-server tail -n 5 /logs/osqueryd.results.log | jq .Deployment and Hardening
- Terminate TLS in front of Fleet (Traefik/Caddy) with a real certificate — the enrollment secret and node keys are bearer credentials; don't ship them over plaintext HTTP beyond a fully trusted LAN
- Rotate the enroll secret periodically (
fleetctl get enroll-secret, then generate a new one and reissue installers) — a leaked secret lets an attacker enroll a rogue host that then receives whatever queries you schedule - Scope API tokens to service accounts, not personal admin logins, for anything automated (CI, SOAR playbooks)
- Back up the MySQL volume — host inventory, saved queries, and policies all live there:
docker exec fleet-mysql mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" fleet | gzip > fleet-backup-$(date +%F).sql.gz - Review scheduled query intervals before scaling past a handful of hosts — a 60-second interval against
processeson 200 endpoints generates real load on both the endpoints and the Fleet/MySQL backend; minutes-to-hours is realistic for most compliance-style queries
Extensions and Next Steps
Detection-as-code: Fleet's query packs map cleanly onto public rule sets — the Fleet library ships pre-built queries for CIS benchmarks and common threat-hunting patterns you can import wholesale instead of writing SQL from scratch.
SIEM correlation: Feed Fleet's result logs into the same Loki/Wazuh pipeline already ingesting other host telemetry, then correlate osquery's processes/listening_ports snapshots against Wazuh's real-time file-integrity and rule alerts for a fuller picture during an investigation.
MDM and vulnerability data: Fleet's premium features add native MDM (profile push, remote lock/wipe) on top of the same agent — out of scope for a pure visibility build, but worth knowing the same osquery agent underpins Fleet's device-management story if you outgrow "just visibility."
Extension tables: osquery supports custom extensions written in Go or Python for data the built-in schema doesn't cover — useful for pulling application-specific state (e.g. a custom service's health endpoint) into the same SQL interface as the OS-level tables.