SQL for OS State
    FIM & Auditing

    Deploy osquery on a VPS

    Install and operate osquery on a RamNode VPS — interactive SQL queries against processes, ports, packages, and file integrity events, with the osqueryd scheduler and logging.

    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

    ItemRequirement
    RamNode VPSAny plan. osqueryd idles under 30 MB RSS with a modest schedule
    OSUbuntu 24.04, Debian 12/13, Rocky/AlmaLinux 9/10, or openSUSE
    AccessRoot or sudo
    Version targetosquery 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

    shell
    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 osquery

    RHEL family

    shell
    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 osquery

    Verify the binaries landed:

    shell
    osqueryi --version
    osqueryd --version

    Step 2: Explore with osqueryi

    osqueryi is a standalone shell. It does not need the daemon running and does not touch the daemon's database.

    shell
    sudo osqueryi

    Useful starting points:

    shell
    -- 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:

    shell
    .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 table

    Table 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:

    shell
    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
    EOF

    The 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:

    shell
    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;"
        ]
      }
    }
    EOF

    Two 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": true when you care about rows disappearing as well as appearing.
    • Snapshot logs the full result set every time. Set "snapshot": true only for small, slow-moving queries. A snapshot of processes every 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

    shell
    sudo systemctl enable --now osqueryd
    sudo systemctl status osqueryd --no-pager

    Results land in /var/log/osquery/osqueryd.results.log, one JSON object per line. Watch it:

    shell
    sudo tail -f /var/log/osquery/osqueryd.results.log | jq .

    Rotate the logs. osquery will not do it for you:

    shell
    sudo tee /etc/logrotate.d/osquery > /dev/null <<'EOF'
    /var/log/osquery/*.log {
      daily
      rotate 7
      compress
      delaycompress
      missingok
      notifempty
      copytruncate
    }
    EOF

    Use 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:

    shell
    --disable_events=false
    --enable_file_events=true
    --audit_allow_config=true
    --audit_allow_process_events=true
    --events_expiry=3600
    --events_max=50000

    Then define the paths and the query that drains the event buffer:

    shell
    {
      "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:

    1. % matches one directory level. %% recurses. /etc/% will not see /etc/nginx/nginx.conf.
    2. The file_events table 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:

    shell
    sudo systemctl restart osqueryd
    sudo touch /etc/osquery-fim-test
    sleep 310
    sudo grep -c file_events /var/log/osquery/osqueryd.results.log

    Step 6: Tune for a small VPS

    Check what your own schedule costs:

    shell
    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:

    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=20 so a fleet of identical VPS instances does not run the same query at the same second.
    • Avoid SELECT * on processes, file, or anything under /proc. Name your columns.
    • Do not schedule hash or file against a whole filesystem. Scope to specific directories.
    • Set --events_max low 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:

    shell
    {
      "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:

    shell
    --extensions_socket=/var/osquery/osquery.em
    --extensions_autoload=/etc/osquery/extensions.load
    --extensions_timeout=10

    Extensions 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=syslog and 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:

    shell
    sudo osqueryd --flagfile=/etc/osquery/osquery.flags --verbose

    Usually a JSON syntax error in osquery.conf. Validate first:

    shell
    python3 -m json.tool /etc/osquery/osquery.conf > /dev/null && echo OK

    Query 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:

    shell
    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:

    shell
    sudo grep -i "stopping" /var/log/osquery/osqueryd.INFO

    Denylisted query. Confirm with:

    shell
    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.