osquery exposes an operating system as a relational database. Processes, listening ports, installed packages, kernel modules, cron jobs, and file integrity events all become SQL tables you can query with standard SELECT statements. On a VPS fleet this replaces a pile of one-off shell scripts with a single query interface that returns structured data.
This guide covers a standalone osquery install on a RamNode VPS: interactive querying, the scheduling daemon, logging, performance limits, and file integrity monitoring. It does not cover centralized management. For that, see the companion guide on deploying Fleet.
What you need
| Item | Requirement |
|---|---|
| RamNode VPS | Any plan. osqueryd idles under 30 MB RSS with a modest schedule |
| OS | Ubuntu 24.04, Debian 12/13, Rocky/AlmaLinux 9/10, or openSUSE |
| Access | Root or sudo |
| Version target | osquery 5.23.x (5.23.1 was published June 2026 as a bug and security fix release) |
Check the release page before you pin a version. The project ships minor releases roughly every two months, and a build can sit as a GitHub prerelease for a couple of weeks before it is marked stable. Do not let an automation job track "latest" blindly across a fleet.
Step 1: Install the package
Debian and Ubuntu
export OSQUERY_KEY=1484120AC4E9F8A1A577AEEE97A80C63C9D8B80B
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://pkg.osquery.io/deb/pubkey.gpg | sudo gpg --dearmor -o /etc/apt/keyrings/osquery.gpg
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/osquery.gpg] https://pkg.osquery.io/deb deb main" \
| sudo tee /etc/apt/sources.list.d/osquery.list
sudo apt update
sudo apt install -y osqueryRHEL family
curl -L https://pkg.osquery.io/rpm/GPG | sudo tee /etc/pki/rpm-gpg/RPM-GPG-KEY-osquery
sudo dnf config-manager --add-repo https://pkg.osquery.io/rpm/osquery-s3-rpm.repo
sudo dnf install -y osqueryVerify the binaries landed:
osqueryi --version
osqueryd --versionStep 2: Explore with osqueryi
osqueryi is a standalone shell. It does not need the daemon running and does not touch the daemon's database.
sudo osqueryiUseful starting points:
-- What is listening, and what binary owns it?
SELECT DISTINCT p.name, p.path, l.port, l.protocol, l.address
FROM listening_ports l
JOIN processes p ON l.pid = p.pid
WHERE l.port != 0;
-- Processes running from a deleted binary on disk. Classic red flag.
SELECT pid, name, path, cmdline FROM processes WHERE on_disk = 0;
-- Users with a login shell and no password expiry
SELECT u.username, u.shell, u.directory FROM users u WHERE u.shell NOT LIKE '%nologin%';
-- Kernel and OS
SELECT * FROM os_version;
SELECT version FROM kernel_info;
-- Packages installed in the last day (Debian family)
SELECT name, version, source FROM deb_packages ORDER BY name;Shell helpers worth knowing:
.tables list every table on this platform
.schema processes show columns for one table
.mode line readable output for wide rows
.all users SELECT * from a tableTable availability is platform specific. deb_packages will not exist on Rocky, rpm_packages will not exist on Ubuntu. Write your packs with that in mind.
Step 3: Configure the daemon
osqueryd reads a flags file and a config file. Start with the flags:
sudo tee /etc/osquery/osquery.flags > /dev/null <<'EOF'
--config_path=/etc/osquery/osquery.conf
--database_path=/var/osquery/osquery.db
--pidfile=/var/osquery/osquery.pidfile
--logger_path=/var/log/osquery
--logger_plugin=filesystem
--logger_min_status=1
--disable_events=false
--enable_syslog=true
--host_identifier=hostname
--schedule_splay_percent=20
--worker_threads=2
--watchdog_level=0
--watchdog_memory_limit=250
--watchdog_utilization_limit=30
--utc
EOFThe watchdog settings matter on a small VPS. osqueryd runs a watcher process that kills and restarts the worker if it exceeds the memory or CPU ceiling. A 250 MB memory limit and 30 percent utilization ceiling keeps a runaway query from crowding out whatever the box actually does for a living.
Now the config:
sudo tee /etc/osquery/osquery.conf > /dev/null <<'EOF'
{
"options": {
"schedule_default_interval": 3600
},
"schedule": {
"system_info": {
"query": "SELECT hostname, cpu_brand, physical_memory FROM system_info;",
"interval": 86400,
"snapshot": true,
"description": "Baseline hardware facts"
},
"listening_ports": {
"query": "SELECT DISTINCT p.name, p.path, l.port, l.protocol, l.address FROM listening_ports l JOIN processes p ON l.pid = p.pid;",
"interval": 600,
"description": "Track what opens a socket"
},
"deb_packages_delta": {
"query": "SELECT name, version, arch FROM deb_packages;",
"interval": 3600,
"removed": true,
"description": "Package add and remove events"
},
"crontab": {
"query": "SELECT event, minute, hour, day_of_month, month, day_of_week, command, path FROM crontab;",
"interval": 3600,
"description": "Cron changes"
},
"authorized_keys": {
"query": "SELECT k.uid, u.username, k.key_file, k.algorithm, k.key FROM users u CROSS JOIN authorized_keys k USING (uid);",
"interval": 1800,
"description": "SSH key drift"
},
"startup_items": {
"query": "SELECT name, path, status, source FROM startup_items;",
"interval": 3600
}
},
"decorators": {
"load": [
"SELECT uuid AS host_uuid FROM system_info;",
"SELECT hostname AS hostname FROM system_info;"
]
}
}
EOFTwo logging modes drive everything here:
- Differential (the default) logs only what changed since the last run. Cheap, and the right choice for anything you want to alert on. Set
"removed": truewhen you care about rows disappearing as well as appearing. - Snapshot logs the full result set every time. Set
"snapshot": trueonly for small, slow-moving queries. A snapshot ofprocessesevery minute will bury your disk.
Decorators attach columns to every log line. They are how you correlate results after shipping logs off the host.
Step 4: Start it
sudo systemctl enable --now osqueryd
sudo systemctl status osqueryd --no-pagerResults land in /var/log/osquery/osqueryd.results.log, one JSON object per line. Watch it:
sudo tail -f /var/log/osquery/osqueryd.results.log | jq .Rotate the logs. osquery will not do it for you:
sudo tee /etc/logrotate.d/osquery > /dev/null <<'EOF'
/var/log/osquery/*.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
copytruncate
}
EOFUse copytruncate rather than create. osqueryd holds the file handle open and will keep writing to the rotated inode otherwise.
Step 5: Add file integrity monitoring
FIM needs the events subsystem, which on Linux means either inotify or the audit subsystem. Add these flags:
--disable_events=false
--enable_file_events=true
--audit_allow_config=true
--audit_allow_process_events=true
--events_expiry=3600
--events_max=50000Then define the paths and the query that drains the event buffer:
{
"file_paths": {
"binaries": [
"/usr/bin/%%",
"/usr/sbin/%%",
"/bin/%%",
"/sbin/%%"
],
"configuration": [
"/etc/%%",
"/root/.ssh/%%"
],
"webroot": [
"/var/www/%%"
]
},
"exclude_paths": {
"configuration": [
"/etc/mtab",
"/etc/resolv.conf",
"/etc/adjtime"
]
},
"schedule": {
"file_events": {
"query": "SELECT target_path, category, action, sha256, time FROM file_events;",
"interval": 300,
"removed": false,
"description": "Drain the FIM buffer"
}
}
}Two gotchas:
%matches one directory level.%%recurses./etc/%will not see/etc/nginx/nginx.conf.- The
file_eventstable is an event table. It only returns rows that arrived since the last query and were not yet expired. If you never schedule the query, the buffer fills and events are dropped. If you exclude noisy paths carelessly, you get silence and think everything is fine.
Restart and confirm events are flowing:
sudo systemctl restart osqueryd
sudo touch /etc/osquery-fim-test
sleep 310
sudo grep -c file_events /var/log/osquery/osqueryd.results.logStep 6: Tune for a small VPS
Check what your own schedule costs:
sudo osqueryi --line "SELECT name, average_memory, wall_time, executions, output_size FROM osquery_schedule ORDER BY wall_time DESC;"That table is populated by the daemon, so query the daemon's database rather than a fresh shell:
sudo osqueryi --line --database_path=/var/osquery/osquery.db --connect \
"SELECT name, average_memory, wall_time_ms, executions FROM osquery_schedule ORDER BY wall_time_ms DESC LIMIT 10;"If a query dominates wall_time_ms, either widen its interval or narrow its WHERE clause. Watchdog kills show up in /var/log/osquery/osqueryd.INFO as a stopping message with the offending query name. A query that repeatedly trips the watchdog gets denylisted for 24 hours, and you will silently lose that data until you fix it or clear the denylist.
Other things that pay off on a 1 GB or 2 GB plan:
- Set
--schedule_splay_percent=20so a fleet of identical VPS instances does not run the same query at the same second. - Avoid
SELECT *onprocesses,file, or anything under/proc. Name your columns. - Do not schedule
hashorfileagainst a whole filesystem. Scope to specific directories. - Set
--events_maxlow enough that a runaway event source cannot eat RAM.
Step 7: Extend the schema
Two mechanisms are worth knowing.
ATC (Automatic Table Construction) turns any SQLite file on the host into an osquery table. No code required:
{
"auto_table_construction": {
"app_users": {
"query": "SELECT id, email, created_at FROM users",
"path": "/var/lib/myapp/app.db",
"columns": ["id", "email", "created_at"],
"platform": "linux"
}
}
}Extensions are separate processes that register new tables over a Thrift socket. Run them under systemd, not as osqueryd children, and add:
--extensions_socket=/var/osquery/osquery.em
--extensions_autoload=/etc/osquery/extensions.load
--extensions_timeout=10Extensions run with osqueryd's privileges. Treat one like any other root daemon you install.
Step 8: Ship the logs somewhere
A results log nobody reads is not monitoring. Options in rough order of effort:
- Filesystem plugin plus your existing agent. Point Vector, Fluent Bit, or Promtail at
/var/log/osquery/osqueryd.results.log. Simplest path if you already run a log pipeline. - Syslog. Set
--logger_plugin=syslogand let rsyslog forward. - TLS remote. osqueryd fetches config and posts results to an HTTPS endpoint. This is what Fleet implements, and it also gives you live queries and distributed reads. See the Fleet guide.
Troubleshooting
Daemon exits immediately. Run it in the foreground and read the error:
sudo osqueryd --flagfile=/etc/osquery/osquery.flags --verboseUsually a JSON syntax error in osquery.conf. Validate first:
python3 -m json.tool /etc/osquery/osquery.conf > /dev/null && echo OKQuery returns nothing in the daemon but works in osqueryi. Differential logging is doing its job. The first run establishes a baseline and logs nothing. Only changes after that produce rows. Set "snapshot": true if you genuinely want the full set each time.
Table exists but is empty. Check osquery_events for the event publisher state:
SELECT publisher, subscription, events, active FROM osquery_events;If active is 0, the events subsystem is off or the publisher is unsupported in your kernel or container.
Watchdog restarting the worker. Grep the INFO log:
sudo grep -i "stopping" /var/log/osquery/osqueryd.INFODenylisted query. Confirm with:
SELECT name, denylisted FROM osquery_schedule WHERE denylisted = 1;Clear it by stopping the daemon, removing the database, and restarting. You lose differential state, so the next run re-baselines.
Where to go next
A single host running osqueryd with filesystem logging is useful but does not scale past a handful of servers. The moment you have more than a few VPS instances, you want centralized config, live queries, and a host inventory. The next guide covers deploying Fleet on a RamNode VPS to manage exactly this agent.
