Docs menu ▾
Genos docs / updated 2026-10-28

How Genos works

Short version: at install, Genos takes a picture of what a server looks like. From then on, the enforcer blocks changes that don't fit that picture and reports what it saw. The fleet server collects those reports, checks they're genuine, and shows you which servers are healthy and which have gone quiet.

The life of a server

sequenceDiagram
  autonumber
  participant S as Scanner
  participant D as Daemon
  participant F as Fleet server
  participant Y as You
  Note over S: Install day
  S->>S: Scan host
  S->>F: Baseline (once)
  S->>D: Start daemon
  D->>F: Enroll (token)
  F-->>D: agent_id
  loop every 30 s
    D->>F: Signed heartbeat
  end
  loop on a protected change
    D->>F: Signed decisions
  end
  F->>Y: Dashboard
  1. Scan once. The scanner records the host's attack surface: the baseline. It never rescans the same host. After install, the baseline changes only through an authorized process, never because the host changed.
  2. Enforce. The daemon loads small programs into the kernel that block unauthorized changes, and it logs every decision.
  3. Report. The daemon sends a heartbeat every 30 seconds and forwards its decisions. Every message is signed with a key that never leaves the host.
  4. Watch. The fleet server verifies each message and shows the result. A server that stops reporting turns red.

The baseline

The scanner reads only metadata, such as owners, permissions, inodes and which packages are installed. It never reads file contents. It works through the catalog: 78 Linux threat vectors and 191 sub-vectors (persistence, privilege escalation, credential access and so on), each with the places on a system where it could show up.

For every place that exists on the host, the scanner records an entity: a path, its kind, how often it's expected to change, and which vectors it relates to. A typical server has tens of thousands. Two hashes summarize the result:

Hash Covers
entities_digest Every entity, exactly as recorded
canonical_hash The baseline's stable identity, used to tie the daemon to the scan

The scanner keeps a small record of the scan on the host (/var/lib/genos-inventory/baseline.json). It uploads the full report to the fleet server once.

Enforcement

The enforcer uses BPF LSM: small, kernel-verified programs attached to Linux security hooks. When a process tries to create, rename or link a file inside a protected directory, the hook asks two questions:

Is this directory protected?          no  → allow
Is there a live, approved grant?      yes → allow
                                      no  → deny (EPERM)

Every answer goes into a ring buffer, and the daemon drains it and reports it.

Today the protected set is one directory, /etc/sudoers.d, where a single new file can give an attacker full root. Approved changes go through grants: time-limited permissions that pass a policy (two different people, a path inside the directory, at most 15 minutes). The policy runs in a WebAssembly sandbox with no access to anything but the request. A grant request must be signed with the approver's key (/etc/genos/approver-ed25519.pub). Unsigned requests work only in an explicitly insecure demo mode.

For the full mechanism, see the enforcer README.

Reporting

The daemon reaches out to the fleet server over HTTPS. The server never connects to the daemon.

Message When Carries
Enroll First start One-time token, the host's public key, hostname
Heartbeat Every 30 s Posture: enforcing yes/no, attached hooks, policy and scanner hashes, baseline hashes, uptime, kernel, live grants
Events After each decision batch ALLOW/DENY, action, uid, directory inode, time
Lifecycle Daemon start daemon_start

Every message after enrollment is signed and numbered, so the server can tell it's genuine, unaltered and not a replay. For the exact format, see the API reference.

How the dashboard knows a baseline is real

The scanner uploads the baseline before the daemon has an identity, so the upload itself isn't signed. The fleet server closes that gap by comparing two things it receives separately:

flowchart LR
  up["Scanner upload<br/>canonical_hash A"] --> cmp{"Same hashes?"}
  hb["Signed heartbeat<br/>canonical_hash B"] --> cmp
  cmp -- "A = B" --> ok["Verified"]
  cmp -- "A ≠ B" --> bad["Mismatch + alert"]

The daemon reads its hashes from the scan record on the host, and signs every heartbeat. If they match the upload, the baseline is Verified. If someone uploaded a different baseline for that host, the dashboard shows Mismatch and raises an alert.

What the fleet server can't do

By design, it has no way to send anything to a host: no commands, no config, no policy. Changes to a host go through your own channels and are checked on the host. That means compromising the fleet server doesn't hand an attacker your servers. See the Security model.

View this page on GitHub ↗