OpsLens is a small, centralized Linux security observability system written in Go. It records signed SSH, process and outbound-network events from up to ten Ubuntu/Debian servers, evaluates behavioral detection rules, and presents the result in an embedded live dashboard.
The project is observe-only: it does not kill processes, block addresses, inspect TLS, or replace an antivirus/EDR.
opslens-server: HTTPS collector, SQLite storage, detection engine, SSE stream and embedded dashboard.opslens-agent: Linux collector with eBPF process/network hooks,/procreconciliation, SSH journal import, DNS metadata capture and a durable 1 GiB spool.opslensctl: creates one-time enrollment tokens and verifies retained event hash chains.
Every event is assigned a monotonic sequence, chained to the previous event, hashed and signed with the agent's Ed25519 identity. Agent transport uses a separate mTLS certificate. A compromised monitored host cannot use its ingest-only certificate to query or delete central records. SQLite is tamper-evident through these signatures, but it is not WORM storage: compromise of the collector can still delete the database or its retained prefix.
- Collector: Linux or macOS, reachable only over your VPN.
- Agent: Ubuntu/Debian x86-64, Linux 5.15+, cgroup v2 and
/sys/kernel/btf/vmlinux. - Agent privileges:
CAP_BPF,CAP_PERFMON,CAP_NET_ADMIN,CAP_NET_RAW,CAP_DAC_READ_SEARCH, andCAP_SYS_RESOURCE. The supplied systemd unit grants these capabilities. - Go 1.25+ for source builds.
If eBPF cannot load, SSH and polling collectors continue and the host reports a degraded health event. Standard DNS over UDP/53 is decoded only far enough to retain the query name; packet payloads are not stored. DoH, DoT, HTTP bodies, TLS contents and environment variables are never collected.
make test
make linuxThe Linux binaries are written to bin/. The agent does not require clang or kernel headers at runtime because its small eBPF programs are assembled by cilium/ebpf.
Create a dedicated account and state directory, install the binaries, and bind to the collector's VPN address:
sudo useradd --system --home /var/lib/opslens --shell /usr/sbin/nologin opslens
sudo install -d -o opslens -g opslens -m 0700 /var/lib/opslens
sudo install -m 0755 bin/opslens-server-linux-amd64 /usr/local/bin/opslens-server
sudo install -m 0755 bin/opslensctl-linux-amd64 /usr/local/bin/opslensctl
sudo install -m 0644 deploy/opslens-server.service /etc/systemd/system/
sudo install -d -m 0750 /etc/opslens
sudo sh -c 'printf "OPSLENS_LISTEN=10.8.0.1:8443\n" > /etc/opslens/server.env'
sudo systemctl daemon-reload
sudo systemctl enable --now opslens-serverOn first start OpsLens creates its internal CA and server certificate in /var/lib/opslens. The certificate includes interface addresses present at that moment. If the VPN address changes, stop the service, remove only server.crt and server.key, then restart to issue a new server certificate. Do not remove ca.crt or ca.key after agents have enrolled.
The dashboard has deliberately no application login. Keep port 8443 restricted to the VPN/firewall; the default binary bind is 127.0.0.1:8443.
Create a token on the collector and copy the CA certificate plus agent binary to the monitored host:
sudo -u opslens opslensctl token --db /var/lib/opslens/opslens.db --label web-01 --ttl 15m
scp /var/lib/opslens/ca.crt root@web-01:/etc/opslens/ca.crtOn the monitored server, install the unit and create /etc/opslens/agent.json from configs/agent.example.json. Paste the token, set the collector VPN URL, then start the service:
sudo install -m 0755 opslens-agent-linux-amd64 /usr/local/bin/opslens-agent
sudo install -m 0644 deploy/opslens-agent.service /etc/systemd/system/
sudo install -d -m 0700 /var/lib/opslens /etc/opslens
sudo chmod 0600 /etc/opslens/agent.json
sudo systemctl daemon-reload
sudo systemctl enable --now opslens-agentThe token is consumed once and removed from the agent configuration after enrollment. Private signing and TLS keys remain mode 0600 under /var/lib/opslens.
Open https://<collector-vpn-ip>:8443/. The UI includes Overview, Hosts, SSH Activity, Outbound Network, Processes, Detections and IP Identity settings. Tables are server-filtered and update through SSE.
Verify all retained chains offline or during normal operation:
sudo -u opslens opslensctl verify --db /var/lib/opslens/opslens.dbEvents are purged after 30 days. Detection evidence, IP identities, host records and settings remain. Verification begins at the signed anchor of the retained suffix, so the tool validates alteration and internal gaps inside the retention window; it cannot prove that the complete SQLite file or its oldest retained prefix was not deleted by an attacker controlling the collector.
Built-in declarative rules live in internal/rules/builtin.yaml. They cover SSH brute force/root login, execution from temporary directories, deleted executables, reverse-shell/tunnel tools, persistence commands, shells spawned by network daemons, rapid enumeration, security-service disabling, unusual outbound ports and miner-like commands. Each alert has severity, confidence, suppression and MITRE ATT&CK identifiers. The format is Sigma-inspired but is not advertised as fully Sigma-compatible. Pass --rules=/path/rules.yaml to load a reviewed custom pack instead of the embedded pack.
Notifications use the internal/notifier.Notifier interface and a persistent SQLite outbox. The current implementations log or discard notifications; a future Slack notifier can consume the same detection envelope without modifying collectors or rules.
- Flow byte totals and duration are present in the event schema but may be zero when the kernel hook cannot provide a reliable lifecycle value; connection creation remains visible.
- DNS names are available only for observable UDP/53 queries. Encrypted DNS and direct IP connections show no domain.
- SSH history can only be imported from journal/auth files still retained when the agent is first installed.
- Root on an agent can stop future telemetry or steal its keys. Previously accepted central events remain outside that host's control, and sequence gaps are reported.
- Root on the collector can delete SQLite. Use external immutable backups if protection from collector compromise becomes a requirement.