CLI
obserae-cli is the admin client. It talks to the daemon over a
Unix socket — no network port, no authentication beyond filesystem
permissions on the socket itself.
obserae-cli [--socket PATH] <command> [flags]
Connecting to the daemon
By default, obserae-cli looks for /var/run/obserae.sock. For
local development or non-standard paths, pass --socket:
obserae-cli --socket ./data/obserae.sock status
The easiest way to avoid repeating the flag is a shell alias:
alias obserae-cli='obserae-cli --socket /var/lib/obserae/run/obserae.sock'
Getting help
Every command and verb is self-documenting, and help is printed locally — no running daemon required:
obserae-cli # list every command
obserae-cli --help # same (also: -h, help)
obserae-cli rule --help # a group's verbs (also: rule -h, rule help)
obserae-cli rule add --help # a verb's flags
A mistyped command or verb prints the relevant usage and exits non-zero, so a typo never fails silently.
Command map
| Command | Purpose |
|---|---|
status | Full daemon status snapshot (same payload as GET /api/status) |
config | Bulk import / export of the WHOLE configuration (one YAML bundle) |
masterkey | Export / rotate the at-rest master key (base64) |
network | CRUD on networks |
host | CRUD on hosts |
interface | CRUD on host interfaces (always --host-scoped) |
service | CRUD on host services (always --host-scoped) |
group | CRUD on groups (members are hosts and/or other groups) |
rule | CRUD on individual Flow Matrix rules |
matches | Read-only view of detection matches |
alert | Triage fired detection alerts: ls/show/ack/close/rm |
output | Manage alert delivery targets (webhook/Gotify): CRUD, test, deliveries |
exporter | Label discovered NetFlow exporters and trigger a rescan |
enrichment | IP enrichment: master toggle, per-source feeds, refresh, data purge |
user | Manage GUI users, groups, API tokens; reset the admin password |
ldap | Configure LDAP / Active Directory sign-in: show / set / test |
oidc | Configure OpenID Connect (OIDC / SSO) sign-in: show / set / test |
query | Run an NFQL pipeline against the daemon |
flows | Export raw flow records, or replay a capture back through ingestion |
ps | Live database activity (in-flight ops + writer queue) |
retention | Show the data-retention policy + last sweep, or run one now |
backup | Snapshots + offsite: status/run/list/restore, destinations, ship, restore-from-archive |
Most read commands accept --json for machine output. Most rm
commands accept --yes (skip prompt) and --dry-run (preview only).
status
obserae-cli status [--json]
The full daemon status snapshot. This prints exactly the payload the
authenticated GET /api/status endpoint returns — one shared snapshot for both
the CLI and the API. Useful as a smoke test or a monitoring datapoint. Add
--json for the raw machine-readable form.
version: v1.2.0
commit: fe2bc84
data version: 12
started: 2026-04-29 08:12:33 UTC
uptime: 1h2m22s
networks: 5
hosts: 11
services: 23
groups: 6
rules: 10
expansions: 1462
alert rules active: 4
coverage 1h: 98.7%
violations closed 1h: 12
flows: 1284091
flows/sec: 42.0
templates received: 2
packets awaiting template: 0
sessions active (RAM): 124
sessions half-open (RAM): 3
sessions closed: 8945
sessions open (live): 12345 / 500000 (2.5%) [ok]
sessions evicted: 0
sessions dropped (closes/emits): 0 / 0
enrich LRU: 412345 / 1000000 (41.2%)
alerts total: 37
alerts 1h / 24h: 2 / 15
alerts by status: ack=5, closed=20, new=12
alerts by severity: high=8, info=20, medium=9
files written: 9021
records ingested: 1284091
last flush: 2026-04-29T09:14:51Z
flush ms (avg/max): 12.4 / 88
db size: 3.1 GB
disk free / total: 812.4 GB / 1.0 TB
buffer dir: 4.2 MB (3 files)
backup dir: 1.9 GB (14 files)
heap in-use / sys: 210.5 MB / 512.0 MB
goroutines: 148
udp drops: 0
matcher cursor lag: 2s
audit integrity: ok (0 breaks)
templates received (templates_received) is the number of NetFlow v9
templates known by obserae. They are restored at startup, so a restart should
not interrupt flow decoding while waiting for the exporter’s next template
refresh. packets awaiting template (packets_awaiting_template) is the
number of packets that could not be decoded yet. A non-zero value with
flows: 0 usually means a brand-new exporter has not sent its templates; force
the exporter to refresh them, or wait for its next refresh cycle.
The session/enrich rows are live fill gauges: entry counts against their
configured limits, not bytes. sessions evicted counts sessions force-closed
under capacity pressure, and enrich LRU shows the enrichment lookup cache
against its capacity.
When the web GUI is disabled (web.enabled: false), live health sampling is not
running, so rows such as flows/sec, coverage, flush timing, runtime metrics
and audit integrity read zero. Counts, session/enrich gauges, storage figures
and the alert roll-up stay accurate.
config
Bulk import / export / validate of the whole configuration as one
YAML bundle — the CLI pendant of the Backup/Restore pages (same file).
One top-level key per domain: cartography, flow_matrix, alerting,
outputs, devices, enrichment, exporters, users, llm (the AI
provider endpoints), assistant (the per-user spend ceilings), nat (declared
rules, detection switches, suppressions and dismissed proposals),
backup, backup_destinations, backup_processes, retention,
rule_sets (local vocabulary plus installed-pack state), and login_security (rate limiting, source-IP allowlist and MFA
policy). Local-user MFA state is exported under users, with TOTP secrets in
master-key-derived enc:v1: envelopes and recovery codes kept as hashes.
The daemon’s own start-up file (configs/obserae.yaml) is not part of
the bundle: it is infrastructure deployed with the daemon, while the bundle
carries what you configured through the GUI or this CLI.
obserae-cli config import FILE # apply the bundle
obserae-cli config export [--output FILE]
Use - for FILE to read stdin / write stdout. A section present in the
file replaces that domain; an absent section is left untouched. The bundle
is validated fully before any write, then applied in dependency order
(cartography first). import prints what was applied / kept / warned.
Every reversible credential, including output tokens and per-user TOTP secrets, rides as an encrypted
enc:v1:envelope derived from this instance’s master key. Back up that key with the bundle. Generated YAML contains data only, without explanatory comments. The old per-domain bulk commands (cartography,rulesimport/export) were folded into this one; per-entity commands below remain.
masterkey
Export or rotate the single at-rest master key (masterkey.bin). One key
protects every stored secret (alert credentials, device API secrets, session
keys) and the audit-seal authenticity — everything is HKDF-derived from it.
obserae-cli masterkey export [--output FILE] # print the key as base64
obserae-cli masterkey import FILE # rotate to a base64 key (- for stdin)
Export it to your secret manager. import rotates the key: every secret is
re-encrypted and every audit seal re-signed under the new key, live — no
restart. (GUI equivalent: Settings → Master key.)
Treat the exported key like a root credential. To restore onto a rebuilt instance instead, place the backed-up
masterkey.bin(mode0400) in the data directory before the first boot.
Per-entity CRUD: network, host, interface, service, group
All five entities share the same five verbs (add, ls, show,
update, rm).
Networks
obserae-cli network add prod-vlan20 --cidr 10.20.0.0/16 [--vlan 20] [--description S] [--nat-gateway HOST]
obserae-cli network ls [--json]
obserae-cli network show NAME [--json]
obserae-cli network update NAME [--name N] [--cidr X] [--vlan V] [--description S] [--nat-gateway HOST]
obserae-cli network update NAME --nat-gateway "" # clear the declared NAT gateway
obserae-cli network rm NAME [--yes] [--dry-run]
Hosts
obserae-cli host add NAME
obserae-cli host update NAME --name NEW
obserae-cli host rm NAME [--yes] [--dry-run]
Interfaces (scoped under a host)
obserae-cli interface add eth0 --host srv-db --network prod-vlan20 --ip 10.20.0.5
obserae-cli interface ls --host srv-db
obserae-cli interface update NAME --host H [--name N] [--network NET] [--ip IP]
obserae-cli interface rm NAME --host H [--yes] [--dry-run]
Services (scoped under a host)
obserae-cli service add postgres --host srv-db \
--protocol TCP --port 5432 --interfaces eth0,eth1 [--description S]
obserae-cli service ls --host srv-db
obserae-cli service update NAME --host H \
[--name N] [--protocol P] [--port N] [--interfaces a,b] [--description S]
obserae-cli service rm NAME --host H [--yes] [--dry-run]
Groups
obserae-cli group add backend [--members a,b,c]
obserae-cli group ls [--json]
obserae-cli group show backend
obserae-cli group update backend [--name N] [--members a,b] # replace member list
obserae-cli group update backend [--add-member X] [--rm-member Y] # incremental
obserae-cli group rm backend [--yes] [--dry-run]
Delete-with-preview
Every rm first asks the daemon what would cascade, prints a summary,
then prompts:
$ obserae-cli network rm admin
Deleting network admin will:
- delete 11 entities
- modify 0 entities
Proceed? [N/y/I]
- N (default) aborts.
- y commits.
- I lists every affected entity, then re-prompts.
--yesskips the prompt entirely (scripts).--dry-runprints the impact and never writes.
rule
Per-entity CRUD on Flow Matrix rules. Bulk import/export of the whole rule
set is part of config (the flow_matrix: section). See
Flow Matrix rules for the YAML schema.
obserae-cli rule add NAME \
--src REF --dst REF \
[--src-service S] [--dst-service S] \
[--src-iface IF] [--dst-iface IF] \
[--protocol P] [--description S] \
[--enabled=BOOL]
obserae-cli rule ls [--json]
obserae-cli rule show NAME [--json]
obserae-cli rule update NAME \
[--name NEW] [--src REF] [--dst REF] \
[--src-service S] [--dst-service S] \
[--src-iface IF] [--dst-iface IF] \
[--protocol P] [--description S] \
[--enabled=BOOL]
obserae-cli rule rm NAME [--yes] [--dry-run]
obserae-cli rule enable NAME [--json]
obserae-cli rule disable NAME [--json]
obserae-cli rule bulk [--enable a,b,c] [--disable d,e]
enable / disable are shorthands for update --enabled=BOOL: a disabled
rule is kept but stops matching traffic. bulk toggles many rules at once
(“disable these twelve false-positives”); a name in both lists resolves to
disable, and per-rule failures don’t abort the rest.
The protocol is normally carried by --src-service /
--dst-service (*/TCP, 53/UDP, or a catalogued service name).
--protocol P is an optional legacy override that is reconciled
with the service tokens — it must not contradict a protocol pinned
by either side. See Flow Matrix rules.
The reference syntax for --src / --dst is described in
Cartography.
matches
Read-only view of detection matches.
obserae-cli matches ls \
[--rule NAME] # filter by rule
[--since DURATION|RFC3339] # only matches newer than this
[--limit N] # cap (default 50)
[--json]
Examples:
obserae-cli matches ls --since 1h
obserae-cli matches ls --rule public-https --limit 200
obserae-cli matches ls --since 2026-05-01T12:00:00Z
obserae-cli matches ls --json | jq '.[] | select(.protocol == 6)'
Default text output:
<matched_at> <rule> <ip_a:port_a> <-> <ip_b:port_b> proto=<n> server=<ip:port> (<method>/<conf>) session=<uuid>
The <-> reflects the non-orientation of a session — the endpoint pair
shown is the session’s internal canonical IP ordering that this dedicated
matches endpoint denormalises (not the client/server surface NFQL
exposes); the inferred server=… recovers the operationally-meaningful
client/server direction.
alert
Triage fired detection alerts — the CLI image of the Detection page. Alerts
are addressed by their UUID (ls prints it). Status: new → ack →
closed.
obserae-cli alert ls [--severity S] [--status new|ack|closed] [--rule N] \
[--since 1h|RFC3339] [--limit N] [--json]
obserae-cli alert show ID [--json]
obserae-cli alert ack ID
obserae-cli alert close ID
obserae-cli alert rm ID [ID...] [--yes]
--since accepts a duration (1h) or an RFC3339 timestamp. rm accepts
several ids and reports how many were deleted (unknown ids are skipped).
output
Manage alert delivery targets (webhook / Gotify) — the CLI image of the Outputs page. Outputs are addressed by name.
obserae-cli output ls [--json]
obserae-cli output show NAME [--json]
# webhook (secret via stdin keeps it out of shell history)
printf '%s' "$SECRET" | obserae-cli output add alerts-hook \
--type webhook --url https://hooks.example/obserae --secret - \
[--header "X-Env: prod"]... [--min-severity high] [--rules a,b]
# gotify
obserae-cli output add pager --type gotify \
--base-url https://gotify.example --secret APP_TOKEN
obserae-cli output update NAME [--url …] [--min-severity …] [--secret -]
obserae-cli output enable|disable NAME
obserae-cli output rm NAME [--yes]
obserae-cli output test NAME # send a test delivery now
obserae-cli output deliveries NAME [--limit N] [--json]
Secrets are write-only — ls / show only show a has_secret flag, never
the value. Use --secret - to read the secret from stdin. On update, a
config flag (--url, --header, …) merges onto the stored config; the
secret changes only when --secret is passed. output test sends a
synthetic alert right away and exits non-zero on failure. The full output
set is also part of the outputs: config-bundle section.
Community may operate webhook, Discord, Telegram, SMTP email and Gotify
destinations; this CLI currently authors webhook and Gotify directly. Every
other destination type is included with Business, Business+ and Enterprise.
After a license passes its 30-day grace period, ls and show retain those
destinations and report license_locked: true, while update, enable/disable,
test, delivery-history and removal commands fail with
professional license required. No destination or pending delivery is deleted,
and renewal makes the stored configuration usable again.
exporter
Label the NetFlow exporters the daemon has observed, and trigger a rescan.
obserae-cli exporter ls [--json]
obserae-cli exporter show IP [--json]
obserae-cli exporter set IP [--name N] [--type T] [--details D]
obserae-cli exporter rescan
Exporters are discovered, not created — a row appears the first time a
device sends flows (keyed by sampler_address), so there is no add/rm.
set labels an existing exporter; only name, equipment_type and
details are editable (the seen/flow-count columns are daemon-owned). Labels
are also carried in the exporters: config-bundle section.
enrichment
Drive IP enrichment — the master toggle and the per-source feeds (cloud CIDR ranges, threat-intel lists, GeoIP, ASN) — the CLI image of the Connectors enrichment pages (Cloud Attribution, Threat Intelligence, GeoIP, ASN).
obserae-cli enrichment status [--json] # master toggle + last change
obserae-cli enrichment enable | disable # master switch
obserae-cli enrichment sources [--json] # every feed + its state
obserae-cli enrichment source NAME --enabled=BOOL # toggle one feed
obserae-cli enrichment refresh NAME # force a fetch now (async)
obserae-cli enrichment purge NAME [--yes] # delete a feed's downloaded ranges
obserae-cli enrichment add-url NAME --display LABEL --url URL [options]
obserae-cli enrichment import NAME FILE --display LABEL [options]
obserae-cli enrichment replace NAME FILE
obserae-cli enrichment delete NAME [--yes]
sources shows each feed’s enabled flag, fetch status, range count and last
fetch. refresh enqueues an immediate fetch (poll sources to watch
fetching → idle). The enabled flags are also in the enrichment:
config-bundle section; refresh and purge are the operational actions with no
YAML form.
Disabling a feed — with source NAME --enabled=false, or everything at once with
disable — stops it matching immediately and frees its memory, but keeps the
ranges on disk so re-enabling needs no download. refresh on a feed that is off
is refused for that reason: it would put traffic on the wire and start using the
feed again. Turn it back on first.
purge deletes those downloaded ranges and frees the disk. The feed stays listed
and refreshable; the enrichment already recorded on past traffic is untouched and
ages out with retention. It prompts unless you pass --yes, and is refused while
that feed is fetching.
add-url creates a periodically refreshed custom threat feed. --format
accepts auto, plain, csv, tsv, json, ndjson, stix or misp;
--indicator-field and --detail-field disambiguate generic CSV/JSON feeds.
Use --interval 15m through --interval 720h. Authentication options are
--auth-type basic|bearer|header, --auth-username, --auth-secret and
--auth-header; note that a literal --auth-secret can be visible in the
process list and shell history, so prefer the GUI or YAML import on shared
administration hosts.
import creates an upload-backed source, prints the parser report and digest,
and accepts the same format/field options. replace atomically replaces its
content after validation. delete removes only custom sources (and asks for
confirmation unless --yes); built-in feeds remain protected.
query
Run an NFQL pipeline. See NFQL for the full language.
obserae-cli query [--json] [--arg VALUE]... NFQL
| Flag | Effect |
|---|---|
--json | Emit a list of objects (one per row) instead of a table. |
--arg V | Bind one ? placeholder. Repeatable; left-to-right order. |
--arg V coerces:
s:foo→ string"foo"(escape hatch for values that look like numbers).- decimal → integer.
- anything else → string.
Examples:
# Plain table
obserae-cli query 'FROM flows | LIMIT 5'
# Bound parameter
obserae-cli query --arg 443 'FROM flows | WHERE dst_port == ? | LIMIT 5'
# Cartography reference as a string
obserae-cli query --arg "host:proxy:eth1" \
'FROM flows | WHERE src_addr == ? | LIMIT 20'
# Relative time bound (negative = N seconds before now)
obserae-cli query --arg -3600 'FROM flows | BETWEEN ? AND *'
# JSON output for downstream tooling
obserae-cli query --json 'FROM flows | KEEP src_addr, bytes | SORT bytes DESC | LIMIT 10' | jq
# Pivot cascade: signal in one table, drill in another
obserae-cli query \
'FROM session_matches | LAST 3600
> FROM sessions | PIVOT session_id == session_id
| KEEP client_ip, server_ip, role_method'
The default table output decodes a few well-known integer columns
into their canonical names (protocol → TCP, tcp_flags →
SYN,ACK). --json keeps raw numeric values so downstream filters
keep working.
flows
Move raw flow records in and out of a daemon. export writes what the store
holds; import replays a capture back through the ingestion pipeline, so the
sessions, conversations and detections it produces are the ones live traffic
would have produced.
Use it to take an incident off one probe and study it on another, to build a reproducible fixture from real traffic, or to seed a demo with a day of plausible activity in a few seconds.
flows export
obserae-cli flows export --since 24h > incident.jsonl
obserae-cli flows export --since 7d --gzip > week.jsonl.gz
obserae-cli flows export --since 2026-08-01T00:00:00Z --output august.jsonl
| Flag | Meaning |
|---|---|
--since | Required. A window (90m, 24h, 7d) or an absolute RFC3339 instant. |
--gzip | Compress the output. |
--output FILE | Write to a file instead of stdout. |
Output is NDJSON: one flow per line, oldest first, with timestamps as absolute RFC3339 strings. It goes to stdout so it can be redirected or piped; the record count goes to stderr, keeping the redirected file clean.
The export reads one hour of the store at a time. That keeps memory flat whatever the window, and produces an ordered file — which the import needs, since the sessionizer only tolerates so much reordering.
flows import
obserae-cli flows import --file incident.jsonl --time relative
obserae-cli flows import --file week.jsonl.gz --time shift --by -7d
obserae-cli flows import --file fixture.jsonl --time absolute --json
| Flag | Meaning |
|---|---|
--file | Required. The capture to replay; - reads stdin. A gzipped file is detected automatically. |
--time | absolute (default), relative, or shift. See below. |
--by | The offset for --time shift, e.g. -7d, -90m, 2h30m. |
--json | Print the summary as JSON instead of a table. |
When the command returns, the data is there. Flows, sessions and conversations are written and queryable — no delay to wait out, nothing to poll. A script can import and query on the next line.
Progress goes to stderr, the summary to stdout:
replaying... 128394 flows at 2026-08-04 18:42:10 sessions 4210
imported 294118 flows in 47.2s
sessions closed 9903 conversations 1204
window 2026-08-04 00:00:01 → 2026-08-05 00:00:00
late flows 12
Choosing --time
| Mode | What it does | When |
|---|---|---|
absolute | Keeps every timestamp as written. | Replaying a capture into the period it came from. |
relative | Shifts everything so the newest flow becomes now, keeping the spacing between flows. | Making an old capture look like it just happened — the mode that makes rules and alerts fire. |
shift --by | Applies the offset you give. | Placing a capture at a chosen point in the past. |
relative reads the file twice (once to find its newest flow), so it needs a
real file — with - (stdin), use absolute or shift.
Writing a capture by hand
A fixture does not have to come from an export. Each line is a flow object, and timestamps take two forms:
- a string is an absolute instant:
"2026-08-05T10:00:00Z" - a number is an offset in seconds from now:
0is now,-60a minute ago,-86400yesterday
{"time_received": -120, "src_addr": "10.0.0.5", "dst_addr": "10.0.0.1", "src_port": 51000, "dst_port": 443, "protocol": 6, "bytes": 1200, "packets": 8, "tcp_flags": 2}
{"time_received": -119, "src_addr": "10.0.0.1", "dst_addr": "10.0.0.5", "src_port": 443, "dst_port": 51000, "protocol": 6, "bytes": 8000, "packets": 6, "tcp_flags": 18}
The relative form keeps a fixture from going stale: it always describes traffic “two minutes ago”, whenever it is run.
Only time_received is required. A malformed line is counted and skipped, not
fatal — one bad record does not cost you the rest of the capture.
What to expect
- One import at a time. A second is refused rather than queued.
- Live traffic keeps flowing. The replay runs its own pipeline; ingestion is never paused.
- Rules and alerts follow the wall clock. A capture replayed further back
than a couple of hours is ingested and queryable, but the matcher will not
revisit it and alert rules covering “the last 5 minutes” will not fire on it.
Use
--time relativewhen you want detections. - Retention still applies. Importing with
--by -7dwhileretention.flows_max_ageis shorter means the next sweep deletes what you just wrote. The summary warns you when that is the case.
ps
A top-like view of current read/write activity. Use it when ingestion,
queries, matching or enrichment feel slow and you want to see which operation is
busy right now. activity is an accepted alias for ps.
obserae-cli ps # one snapshot
obserae-cli ps --watch 1s # refresh every second (Ctrl-C to stop)
obserae-cli ps --json # machine-readable
writer pool: in_use=1 open=1 waits since start (cumulative): count=12 total=4.231s
IN-FLIGHT (2)
POOL ELAPSED STATE OP
writer 3.120s RUNNING sessions.correlator.assign_batch
reader 12ms running query.nfql
RECENT (8, slowest first)
POOL ELAPSED ERR OP
writer 890ms flows.ingest_batch
writer 120ms enrichment.insert_batch
- IN-FLIGHT lists operations executing now, longest first. The first
writerrow is flaggedRUNNING; if itsELAPSEDvalue keeps climbing, it is the operation currently delaying writes.readerrows are read-only queries such as NFQL or Cockpit views. - The
waits since startcounters are cumulative since the daemon started. They only ever grow, so a large total is not an alarm by itself. Judge contention from theRUNNINGoperation’s elapsed time. The Cockpit DB activity pane adds a per-intervalwait deltathat falls back to zero when pressure clears. - RECENT is a ring of the last completed operations, slowest first,
so a spike that already finished is still visible.
✗marks a failure.
The same data drives the DB activity pane on the Cockpit page.
retention
Read the live data-retention policy and last-sweep result, or trigger a sweep now.
obserae-cli retention status [--json] # live policy + last-sweep counters
obserae-cli retention run # trigger a sweep now (async)
Policy changes are declarative — edit the retention: section and
obserae-cli config import, or use the Lifecycle page.
There is no retention set.
backup
Inspect the snapshot policy and files, trigger a snapshot, or run a point-in-time restore.
obserae-cli backup status [--json] # policy + last snapshot + next action
obserae-cli backup run [--full] # snapshot now (async)
obserae-cli backup list [--json]
obserae-cli backup plan --point-in-time RFC3339 [--json]
obserae-cli backup restore --point-in-time RFC3339 [--dry-run] --confirm [--json]
# Offsite backup: destinations, on-demand ship, portable-archive restore.
obserae-cli backup destinations list [--json]
obserae-cli backup destinations add --name NAME --kind local|s3|sftp \
--config JSON [--secret S] [--ship-on-full] [--keep N] [--disabled]
obserae-cli backup destinations remove ID
obserae-cli backup ship --destination ID [--json]
obserae-cli backup restore-from-archive --file ARCHIVE.tar.zst \
--master-key-file KEY --confirm
backup run lets the runner decide full-vs-delta (like the GUI “Backup
now”); --full forces a full. The backup policy is changed via the
backup: config section or the Lifecycle page, not a set verb.
listshows every snapshot directory on disk — daily fulls and the chain of deltas that descend from them — with kind, timestamp, size and parent.planpreviews the restore chain that lands closest to--point-in-time(the full chosen as base, the ordered deltas, the cumulative size) without touching anything.restoreis destructive and refuses to run without--confirm. With--dry-runevery step runs except the final swap, so you can validate a chain safely. On a real restore the daemon swaps the live database and exits — your supervisor (systemd, Docker, …) restarts it on the restored file.
# Typical workflow: list, preview, rehearse, apply.
obserae-cli backup list
obserae-cli backup plan --point-in-time 2026-05-28T13:00:00Z
obserae-cli backup restore --point-in-time 2026-05-28T13:00:00Z --dry-run --confirm
obserae-cli backup restore --point-in-time 2026-05-28T13:00:00Z --confirm
Offsite backup
The offsite subcommands mirror the Lifecycle page’s backup-destinations panel:
destinations list / add / removemanages where archives are shipped.--configis a JSON object whose shape depends on--kind:localneeds{"dir":"…"},s3needs endpoint/region/bucket/prefix plus a--secretaccess key,sftpneeds host/port/path plus a--secretpassword or PEM. Secrets are write-only —listnever shows them.--ship-on-fullships each fresh full automatically;--keep Nbounds the archives retained remotely.ship --destination IDpacks the latest full and uploads it to that destination now, printing the remote ref.restore-from-archiverestores from a portable.tar.zstarchive (produced by an offsite ship or the GUI’s “Download backup”). It is destructive and, likerestore, refuses without--confirm. The daemon validates the archive — format, DuckDB version and the master-key canary — before any destructive I/O, so a wrong--master-key-fileis rejected up front with the live database untouched. The master key is the base64 value fromobserae-cli masterkey export.
# Offsite workflow.
obserae-cli backup destinations add --name nas --kind local \
--config '{"dir":"/mnt/nas/obserae"}' --ship-on-full --keep 14
obserae-cli backup ship --destination <id>
obserae-cli masterkey export --output master.key
obserae-cli backup restore-from-archive \
--file obserae-backup-20260716T120000.tar.zst \
--master-key-file master.key --confirm
Backup scheduling and retention are configured on the Lifecycle page; the full restore workflow and its exit-after-swap rationale are documented there too.
User & access management
The user group administers GUI accounts, groups, API tokens and the admin
password — the same model as the GUI Users page,
usable headless (e.g. to recover access).
Additional users and groups are included with Business, Business+, and Enterprise licenses. In Community, only the built-in local
admincan sign in; the single-admin recovery commands below remain available. Stored users and groups are preserved across license changes.
# Recover access: generate a fresh admin password (printed once).
obserae-cli user reset-admin-password
# Or set a chosen one (also accepts the password on stdin).
obserae-cli user set-admin-password --password 's3cret'
# Users.
obserae-cli user ls
obserae-cli user add --username alice --password 's3cret' --display "Alice" --groups analyst
obserae-cli user rm alice
# Groups (built-in admin/analyst/auditor/monitoring cannot be changed).
obserae-cli user group-ls
obserae-cli user group-add --name soc --perms cartography:read,sessions:read,alerts:read,alerts:ack
obserae-cli user group-rm soc # only if it has no members
# API tokens (the secret is printed once; use it as a Bearer credential).
obserae-cli user token-add --name ci --user alice
obserae-cli user token-ls
obserae-cli user token-rm <TOKEN_ID> # revoke
A token authenticates REST calls against the web API:
curl -H "Authorization: Bearer obs_…" http://127.0.0.1:8080/api/query …
The full permission catalogue (cartography:read, rules:write,
nfql:execute, users:manage, …) is described on the
Users page.
LDAP / Active Directory (ldap)
Configure directory sign-in headless — the same settings as the
Identity & Access → Authentication page. Full guide: Authentication.
LDAP is included with Business, Business+ and Enterprise. Without a usable
license (after grace), every ldap command is refused before any directory
connection is attempted; the stored provider settings are preserved.
obserae-cli ldap show # current config (bind password hidden)
obserae-cli ldap test # dial the directory and bind the service account
obserae-cli ldap set \
--enabled --url ldaps://dc.corp:636 \
--bind-dn 'CN=svc-obserae,OU=Service,DC=corp,DC=local' --bind-password 'REDACTED' \
--user-base-dn 'OU=Users,DC=corp,DC=local' --user-filter '(sAMAccountName=%s)' \
--map 'CN=SOC-Admins,OU=Groups,DC=corp,DC=local=>admin' \
--map 'CN=Analysts,OU=Groups,DC=corp,DC=local=>analyst'
set changes only the flags you pass; --map 'AD_GROUP=>ROLE' is repeatable and
replaces the whole mapping list. The local admin stays a break-glass login even
when LDAP is on.
OpenID Connect (oidc)
Configure SSO sign-in headless — the same settings as the Identity & Access →
OIDC / SSO page. Full guide: Authentication.
OIDC / SSO is included with Business, Business+ and Enterprise. Without a
usable license (after grace), every oidc command and browser SSO flow is
refused while the stored provider settings are preserved.
obserae-cli oidc show # current config (client secret hidden)
obserae-cli oidc test # run provider discovery against the issuer URL
obserae-cli oidc set \
--enabled --issuer https://idp.corp/realms/obserae \
--client-id obserae --client-secret 'REDACTED' \
--map 'soc-admins=>admin' --map 'analysts=>analyst'
set changes only the flags you pass; --map 'GROUP=>ROLE' is repeatable and
replaces the whole mapping list. An identity mapping to no role is refused; the
local admin stays a break-glass login.
Common errors
| Symptom | Cause | Fix |
|---|---|---|
dial unix /var/run/obserae.sock: connect: no such file or directory | Daemon not running, or wrong path | Start the daemon, or pass --socket |
daemon: rule "X": not found | Typo or rule deleted | obserae-cli rule ls for the current set |
daemon: name "X" is used by both host and group | Cartography name collision | Pick distinct names (one global namespace) |
sem: 1:18: unknown column "prtocol" | NFQL typo | Look at line:column for the offending token |
sem: 1:32: host "srv-typoo": not found | Cartography reference doesn’t resolve | Fix the name in the query or the cartography |
professional license required | The command targets a licensed feature after grace | Install or renew a Business, Business+ or Enterprise license |
For NFQL-specific errors, see NFQL.