Writing · security
A new osquery lane into Samma, and the osquery client in Docker
By Mattias Hemmingsson · · 4 min read
Samma has a new input. Next to scanners and Wazuh, hosts can now send osquery results straight into the pipeline on their own lane, samma.logs.osquery.
osquery lets you ask a machine questions in SQL: which users exist, who is in the sudo group, which SSH keys can log in, what is listening on which port. Run those queries on a schedule, send only what changed, and you have a very good picture of what is happening on a host.
How it fits
host: osquery-client (Docker)
│ HTTPS POST, mutual TLS, client certificate per host
▼
cluster: osquery-gateway (Vector) ── one record per result ──► NATS samma.logs.osquery
├──► siem-storage ──► TimescaleDB
└──► detection-engine (Sigma) ──► alerts
- The client (samma-io/osquery-client) is osqueryd 5.24.1 with its TLS logger, in a container.
- The gateway is Vector with an
http_serversource and TLS. It only accepts clients whose certificate our CA signed. It splits each batch into one record per result, addssource,feed,event_time,search_textandgateway_peer, and publishes to NATS. - After NATS it is the same pipeline as every other lane: one consumer stores to TimescaleDB, another runs Sigma rules.
Nothing gets lost on the way
The gateway uses end-to-end acknowledgements. It only answers the client's POST once NATS JetStream has stored the batch. If the gateway or NATS is down, osquery keeps the results in its on-disk buffer and retries. That buffer is a Docker volume, so it survives a container restart too.
What the client collects
Scheduled queries every 10 minutes to 1 hour:
- local users, members of
root/sudo/wheel/adm/docker - SSH
authorized_keys,sudoers, system and user crontabs - listening ports with the owning process
- logged-in users and login history
- kernel modules, packages, setuid binaries, OS version
Real-time events, sent once a minute:
- process events (eBPF): every program started, with parent, uid and path
- socket events (eBPF): outbound connects and inbound accepts
- file events (inotify): changes to account, sudo, SSH and cron files and the binary folders
Scheduled queries are differential. After the first run, osquery only sends rows that were added or removed. The first run on a new host sends everything with counter: 0, and the rules skip it, so a new host doesn't fire a wall of alerts.
Some things are left out on purpose. Command lines often carry passwords and tokens. Compilers, container runtimes and polling commands like ps are filtered from process events, and DNS lookups from socket events. On a busy host they are most of the volume and none of the signal. A developer machine running Docker and Kubernetes tools ends up at about 10 to 20 MiB per hour.
Connect a host
1. Once: the CA and the gateway certificate
pki/make-ca.sh
pki/issue-server.sh # SAN DNS:osquery-gateway
The gateway's Kubernetes Secret is made from the server cert, key and ca.crt. In our cluster the gateway listens on NodePort 30516.
2. Per host: a client certificate
pki/issue-client.sh web-01
# -> pki/out/clients/web-01/{ca.crt,client.crt,client.key}
Copy that folder to the host as ./certs, next to compose.yaml. Nothing in pki/out/ is committed to git.
3. On the host: start the container
OSQUERY_CLIENT_TAG=<tag> \
OSQUERY_GATEWAY_IP=10.0.0.33 \
OSQUERY_HOST_IDENTIFIER=web-01 \
docker compose up -d
<tag> is a published image tag (a short commit sha from main) of ghcr.io/samma-io/osquery-client. OSQUERY_HOST_IDENTIFIER should match the name on the certificate. The compose file maps the name osquery-gateway to the node IP, so TLS checks the name in the server certificate.
Within five minutes the samma_heartbeat query sends its first row, and you can see the host in TimescaleDB:
SELECT time, record->>'name' AS query, record->>'action' AS action
FROM siem_events
WHERE lane = 'osquery' AND record->>'hostIdentifier' = 'web-01'
ORDER BY time DESC LIMIT 20;
The compose file, and what it costs
services:
osquery-client:
image: ghcr.io/samma-io/osquery-client:${OSQUERY_CLIENT_TAG}
restart: unless-stopped
pid: host
network_mode: host
extra_hosts:
- "osquery-gateway:${OSQUERY_GATEWAY_IP}"
environment:
OSQUERY_GATEWAY: osquery-gateway:30516
OSQUERY_HOST_IDENTIFIER: ${OSQUERY_HOST_IDENTIFIER}
OSQUERY_EVENTS: "true"
cap_add: [SYS_ADMIN, BPF, PERFMON, SYS_PTRACE]
security_opt: [apparmor=unconfined, seccomp=unconfined, label=disable]
ulimits:
memlock: -1
volumes:
- ./certs:/etc/osquery/certs:ro
- osquery-client-db:/var/osquery
- /sys/kernel/debug:/sys/kernel/debug:ro
- /etc/passwd:/etc/passwd:ro
- /etc/group:/etc/group:ro
# ... sudoers, crontab, utmp/wtmp, dpkg, /home, /root
- /:/host:ro
volumes:
osquery-client-db:
Be clear about what this is. To see every process start and file change, the container traces the host kernel with eBPF and reads the whole host file system. Treat it like a root agent on the host.
If that is too much for a machine, drop the capabilities, the security options, memlock, /sys/kernel/debug and the / mount, and set OSQUERY_EVENTS=false. The scheduled queries still work. You lose the real-time events.
Detection: Sigma rules for osquery
The osquery lane ships with 11 Sigma rules in the detection-engine:
| Rule | Fires when |
|---|---|
| user added | a new local account appears |
| admin group member added | someone joins root/sudo/wheel/adm/docker |
| authorized key added | a new SSH key can log in |
| sudoers changed / crontab changed | the file gets a new row |
| SUID binary added | a new setuid program shows up in /usr |
| kernel module loaded | a new module appears |
| new listening port | a new TCP listener (not Docker's DNS) |
| exec from /tmp | a program runs from /tmp, /var/tmp or /dev/shm |
| shell outbound connection | a shell process opens a connection out |
| sensitive file changed | passwd, shadow, group, sudoers, /etc/ssh/ or any .ssh folder |
They match on osquery's own fields: name (the query), action and columns.*. For example "a new SSH key, but not on the first run":
detection:
selection:
name: samma_authorized_keys
action: added
filter_baseline:
counter: 0
condition: selection and not filter_baseline
Revoking a host
There is no revocation list yet. Client certificates are valid for one year. To cut a host off sooner, replace the CA, re-issue the gateway certificate and re-issue the certificates for the hosts you keep. It is blunt, but with a handful of hosts it takes a few minutes.
Wazuh or osquery?
Both, and they cover different things. The Wazuh agent is good at reading logs and decoding them into known events. osquery is good at answering "what is the state of this machine right now, and what changed". In Samma they land on two lanes of the same NATS pipeline, in the same TimescaleDB table, checked by the same Sigma engine.
Filed underosquery · samma · siem · nats · docker · security · ebpf