factum2

module
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 31, 2026 License: AGPL-3.0

README

Factum

ci

Factum tracks network infrastructure (devices, customers, services) and syncs it with external systems of record — NetBox and Lime CRM sync data into factum's Postgres DB, while DNS, Icinga and LibreNMS are synced from factum. It's a Go monorepo producing several CLI binaries (cmd/*) plus a web GUI (factum-web) with a Vue 3 SPA frontend (web/frontend).

See DEV.md for full setup/build/run details (config file shape, Makefile targets, dev workflow, binaries list) and AGENTS.md for architecture notes. To add a capacity service type (roles, platform packs, CLI templates), see docs/cfgmgmt-service-design.md.

Prerequisites

  • PostgreSQL (app data, via GORM)
  • Python 3 (only for install.py on a production host)
  • Go 1.25+ and Node.js ^22.18.0 or >=24.12.0 (only to build/dev from source — see web/frontend/package.json)

Quickstart

Production (GitHub release)

No Go or Node needed. Tagged releases ship linux/amd64 and linux/arm64 binaries plus systemd units. The installer lives at /etc/factum2/install.py so you can re-run it later to pick another tag.

1. Create the PostgreSQL database
sudo -u postgres psql
create database factum2;
create user factum2_user with encrypted password '<changeme>';
grant all privileges on database factum2 to factum2_user;
alter database factum2 owner to factum2_user;

Schema migrations are a dedicated command (factum-web migrate / factum migrate) — they do not run when the GUI or a sync CLI starts, because rewriting tables while factum-web is serving is unsafe. install.py applies them during install (step 3); to run them by hand, stop the GUI first:

sudo /opt/factum2/factum-web migrate -f /etc/factum2/factum2.yaml
2. Config and installer
sudo mkdir -p /etc/factum2
sudo curl -fsSL -o /etc/factum2/factum2.yaml \
  https://raw.githubusercontent.com/abundo/factum2/main/examples/factum2.yaml
sudo curl -fsSL -o /etc/factum2/factum2-worker.yaml \
  https://raw.githubusercontent.com/abundo/factum2/main/examples/factum2-worker.yaml
sudo curl -fsSL -o /etc/factum2/install.py \
  https://raw.githubusercontent.com/abundo/factum2/main/install.py
sudo chmod +x /etc/factum2/install.py

If the repo is private, add -H "Authorization: Bearer $GITHUB_TOKEN" to the curl commands (or export GITHUB_TOKEN=... before running install.py).

Edit /etc/factum2/factum2.yaml: set db: credentials and web.jwtsecret (openssl rand -base64 48). Edit /etc/factum2/factum2-worker.yaml for the local worker (worker.listen, worker.token; factum.url/token are Stat/Dial fallback, omit on start-only hosts). Almost all other runtime settings (NetBox/Lime/DNS/Icinga/LibreNMS, ...) live in the database and are edited from the admin UI. See DEV.md § Configuration for the full YAML key reference.

3. Select a release
sudo /etc/factum2/install.py

On a TTY the installer lists GitHub releases; highlight one and press Enter. Non-interactive: sudo /etc/factum2/install.py --install latest --yes.

That copies binaries to /opt/factum2, stops factum-web if it is running, applies schema migrations (factum-web migrate), then installs systemd units:

  • this host (primary): factum2-web.service and factum2-worker.service
  • each enabled worker node: factum2-worker.service

A unit that is not on disk yet is installed and systemctl enable --now'd. If the file is already there and matches this release, it is left alone and the unit is restarted. If it has been modified, the installer prints a diff and asks before overwriting (--yes overwrites without asking).

4. Create the first admin user
sudo /opt/factum2/factum-web createadmin -f /etc/factum2/factum2.yaml

Then log in at the address in web.bind (the example config uses http://127.0.0.1:8091).

From source (development)
sudo mkdir -p /etc/factum2
sudo cp examples/factum2.yaml /etc/factum2/
make            # all CLI binaries into build/ (excludes factum-web-release)
make frontend   # builds web/frontend -> web/static/vue

Tagged releases (v*) are built with GoReleaser and published by GitHub Actions — see DEV.md § Release.

go run ./cmd/web migrate -f /etc/factum2/factum2.yaml
APP_ENV=development go run ./cmd/web start -f /etc/factum2/factum2.yaml -b 0.0.0.0:8090

APP_ENV=development allows starting without web.jwtsecret set (falls back to an insecure key) — don't use it against anything but a local/dev database.

go run ./cmd/web createadmin -f /etc/factum2/factum2.yaml

Then log in at http://localhost:8090.

For frontend hot-reload (cd web/frontend && npm install && npm run dev, proxies to the backend on :8090), ./install.py --source from this tree, and everything else, see DEV.md.

Installing a worker node

A worker node is a factum-worker instance running the start subcommand on a remote host (typically the DNS/Icinga/LibreNMS/Oxidized server, or any other host that needs to run one of the sync tools). The primary dials out to it, so the worker host only needs one inbound firewall rule scoped to the primary's IP (/hub on worker.listen) — see AGENTS.md § Worker / hub transport for why the dial direction is reversed.

Co-located CLIs (factum-dns, factum-icinga, factum-librenms, factum-oxidized, factum-device-sync, factum-driver, factum-icinga-notifications) reach the primary's REST handlers through that hub connection, via a localhost-only unix socket (/run/factum-worker/api.sock). Worker networks then do not need a route to the primary's HTTPS port. The primary still serves HTTPS to operators and to NetBox's POST /api/netbox-webhook.

  worker host                              management network
  ───────────                              ──────────────────
  factum-worker :8443 /hub  <── ws:// ───  factum-web (dials out)
  unix /run/factum-worker/api.sock         HTTPS :443  <── operators
  CLIs ── HTTP ────────────^               HTTPS :443  <── NetBox webhook
Path Direction Required?
Primary → worker.listen /hub (ws://) outbound from primary / inbound on worker, scoped to primary IP Yes
Worker network → primary HTTPS :443 worker → primary No, once this stack is live and mixed-UID CLIs can open the socket (group factum + icinga/nagios user)
Operator browser → primary HTTPS inbound on primary, management net Yes
NetBox → POST /api/netbox-webhook inbound on primary, from NetBox Yes

Closing worker-net → primary :443 is an operator firewall step after this stack is in production; the software does not unbind the port. Do not close it until Icinga notification commands can open the unix socket (see group factum below). factum-worker run is not tunneled (POST /api/worker/run NDJSON) and still needs HTTPS from whatever host you run it on — typically a management-net host, not the worker.

Follow-up: hub WSS — encrypt the hub WebSocket (ws://wss://) now that config secrets ride it. Until that follow-up, /hub is plaintext; keep it off the public internet and scoped to the primary's IP. Do not treat those secrets as TLS-protected on the hub.

  1. Build and copy the binary. make factum-worker (or make release for every binary) builds build/factum-worker; copy it to the target host, e.g. /opt/factum2/factum-worker. (/etc/factum2/install.py from a GitHub release, or ./install.py --source from this tree, automates this step — plus groupadd -r factum and the systemd unit in step 4 — over ssh for every node already registered and enabled in the worker_nodes table.)

  2. Create the config file, starting from examples/factum2-worker.yaml:

    sudo mkdir -p /etc/factum2
    sudo cp examples/factum2-worker.yaml /etc/factum2/factum2-worker.yaml
    

    Then edit it for this host:

    • factum.url / factum.token — Stat/Dial fallback only (socket missing, unreadable, or undialable). They are not a retry path for unix 502 / timeout after Dial succeeded. Start-only hosts may omit both. Keep them for factum-worker run if you use it from this host, for factum-icinga-notifications until the icinga/nagios user is in group factum, and for any CLI not co-located with a worker. Force HTTPS even if the socket exists with FACTUM_WORKER_API_SOCKET=none (or 0) in that CLI's environment (same as factum.socket: none).
    • worker.listen — bind address for the hub listener the primary dials into, e.g. :8443. Only /hub is served here.
    • worker.token — shared secret this worker expects from the primary on connect; set the same value on the matching WorkerNode.Token in step 3.
    • worker.commands — trim the map down to only the commands this host should handle, with cmd pointing at that tool's path on this host (e.g. /opt/factum2/factum-dns). Add --job to a command's args to get structured sync-job events instead of plain console output (see DEV.md § Sync job events).
    • Relocate the unix socket with FACTUM_WORKER_API_SOCKET on the worker unit and in CLI environments. Do not set only worker.api_socket or only factum.socket — they will drift.

    Generate both secrets with openssl rand -base64 32.

    netbox/lime/becs are the exception: unlike the others, they talk to Postgres directly instead of fetching config over the hub (see AGENTS.md's "Sync model" section), and default to reading /etc/factum2/factum2.yaml (the full config, with a db: section) rather than factum2-worker.yaml. Only put them in a worker's commands map on the primary host itself, where that full config already exists at the default path.

  3. Register the node with the primary: admin UI → Worker nodes → Add, with Address set to host:port matching this node's worker.listen and Token matching its worker.token. Takes effect within one RemoteManager reconcile pass (~10s) — no primary restart needed.

  4. Install and start the systemd unit. Prefer re-running /etc/factum2/install.py on the primary: it runs groupadd -r factum (idempotent) before copying factum2-worker.service (Group=factum) to each enabled worker, compares it with whatever is already in /etc/systemd/system, and asks before overwriting a modified file. systemd Group= without the group fails the unit (Failed to determine group credentials) and takes hub command dispatch down — do not copy the unit until groupadd has run.

    Manual fallback:

    getent group factum >/dev/null || sudo groupadd -r factum
    sudo cp examples/factum2-worker.service /etc/systemd/system/
    sudo systemctl daemon-reload
    sudo systemctl enable --now factum2-worker.service
    

    On Icinga hosts, add the notification UID to the group so factum-icinga-notifications can open the socket (until then, Stat EACCES falls back to HTTPS). Supplementary groups take effect at process start, so restart the Icinga daemon (and any long-lived notification helper) after usermod, then verify as that UID before closing :443:

    sudo usermod -aG factum icinga    # or nagios, matching the NotificationCommand user
    sudo systemctl restart icinga2    # or nagios
    sudo -u icinga stat /run/factum-worker/api.sock
    

    The socket dir is /run/factum-worker (root:factum 0750); the socket is 0660 (chmod'd by factum-worker start, independent of umask). Connecting to it is equivalent to possessing the service token.

  5. Verify: /sync/status in the web UI (or GET /api/worker/status) lists connected nodes and what they handle; journalctl -u factum2-worker -f on the worker host for logs. Confirm a co-located CLI (e.g. factum-librenms show-config) hits the socket. Then, if mixed-UID CLIs are in group factum and you are not using factum-worker run from this host, you may close worker-net → primary :443. Leave FACTUM_WORKER_API_SOCKET=none unset when you do — a unix 502 after Dial will not fail over to HTTPS.

NetBox webhook (partial sync)

POST /api/netbox-webhook lets NetBox push change events instead of waiting for factum-netbox sync's full polling sync. On a Device, Interface or IP Address create/update (or an Interface/IP delete) it resyncs just that one device (interfaces, addresses and tags included). On a Device deletion it removes the matching netbox-sourced factum row by the payload's id — NetBox has already deleted the object, so it cannot be re-fetched. Cable and site create/update re-fetch that one object and upsert the local Connection/Site row; their deletions remove the row by the payload's id the same way. Tenant events are ignored — customer→tenant sync is factum→NetBox.

The endpoint isn't a logged-in user or a factum.token service client, so it authenticates differently: it verifies NetBox's HMAC-SHA512 X-Hook-Signature header against a shared secret, and rejects every request if that secret isn't set. Configure it in two places:

  1. factum — admin UI → Settings → NetBox tab → "Webhook secret", set to a random string, then Save.

  2. NetBox (3.x/4.x split webhook config into a reusable "Webhook" endpoint definition plus one or more "Event Rules" that bind it to specific object types/events):

    • Operations → Webhooks → Add:
      • Name: e.g. factum-sync
      • URL: https://<factum-host>/api/netbox-webhook
      • HTTP method: POST, HTTP content type: application/json (default)
      • Secret: the same string entered in factum's admin UI above
      • Leave the body template blank — factum expects NetBox's default payload shape (event/model/data/...).
      • Enable SSL verification unless <factum-host> is on a self-signed cert reachable only internally.
    • Operations → Event Rules → Add:
      • Object types: DCIM → Device, DCIM → Interface, DCIM → Cable, DCIM → Site, IPAM → IP Address
      • Events: enable Creations, Updates and Deletions. Device/cable/site deletions remove the matching factum row; interface/IP deletions still resync the parent device.
      • Action type: Webhook, Action: the webhook created above.
    • NetBox's webhook edit page has a "Test" action that sends a real signed sample payload — useful for confirming the secret matches without waiting for a real change.

factum-netbox check reads the live NetBox extras API and verifies that setup: a webhook whose URL is {PublicBaseURL}/api/netbox-webhook, enabled event rules covering Device / Interface / IP Address / Cable / Site create+update+delete, and reports the custom fields factum needs. Pass --update to create missing fields and patch drifted label/description/group/object types (never required, and never a type change: NetBox forbids that). Selection fields without a prescribed choice list (role) are reported if missing; NetBox will not accept a select field with no choices. Missing alarm_destination / alarm_timeperiod are created with seed choices (example addresses and SLA windows) and those lists are never updated if the field already exists. connection_method gets a ssh/telnet choice set. Integration fields (becs_oid, librenms_id, optical_role) are only created when that source/destination is enabled. The webhook secret is write-only in NetBox, so the check only confirms factum has one configured. Exits non-zero if anything required cannot be fixed.

License

AGPL-3.0-or-later. Copyright (c) 2026 Anders Löwinger.

Directories

Path Synopsis
cmd
becs command
device-sync command
dns command
driver command
factum command
icinga command
icinga-notifications command
factum-icinga-notifications is the Icinga2 NotificationCommand invoked directly by icinga2 whenever a host/service alarm fires.
factum-icinga-notifications is the Icinga2 NotificationCommand invoked directly by icinga2 whenever a host/service alarm fires.
librenms command
lime command
netbox command
oxidized command
web command
Web backend for factumn2
Web backend for factumn2
worker command
internal
device-sync
syncs interfaces, addresses, VRF-aware address dedup, LLDP-discovered cable connections, and on-device ELINEs (as NetBox L2VPNs of type EVPL) from live network devices into NetBox.
syncs interfaces, addresses, VRF-aware address dedup, LLDP-discovered cable connections, and on-device ELINEs (as NetBox L2VPNs of type EVPL) from live network devices into NetBox.
dns
jobevent
Package jobevent lets a sync tool (internal/dns, internal/icinga, internal/librenms, internal/netbox, internal/lime) report structured info/warning/error progress instead of ad hoc fmt.Println/slog calls.
Package jobevent lets a sync tool (internal/dns, internal/icinga, internal/librenms, internal/netbox, internal/lime) report structured info/warning/error progress instead of ad hoc fmt.Println/slog calls.
jobscheduler
Package jobscheduler runs user-defined JobSchedule rows: each due schedule triggers the same StartJob path as the Job overview page (one sync target, or a sequenced "sync all").
Package jobscheduler runs user-defined JobSchedule rows: each due schedule triggers the same StartJob path as the Job overview page (one sync target, or a sequenced "sync all").
ldapauth
Package ldapauth implements the search-and-bind flow used to authenticate a user against an LDAP/Active Directory server and to read the attributes (email, display name, group membership) needed for local auto-provisioning and role sync.
Package ldapauth implements the search-and-bind flow used to authenticate a user against an LDAP/Active Directory server and to read the attributes (email, display name, group membership) needed for local auto-provisioning and role sync.
mail
Package mail sends outbound email over the shared SMTP relay settings (Settings.Smtp*/EmailSender, edited on the admin UI's "Email" destination tab, projected into util.CommonConfig by util.NewCommonConfig).
Package mail sends outbound email over the shared SMTP relay settings (Settings.Smtp*/EmailSender, edited on the admin UI's "Email" destination tab, projected into util.CommonConfig by util.NewCommonConfig).
worker
Package worker runs predefined shell commands dispatched by the primary over the hub transport (see hub.go/hub_agent.go).
Package worker runs predefined shell commands dispatched by the primary over the hub transport (see hub.go/hub_agent.go).
Dev build: the static Vue frontend is read straight from disk (relative to the process's working directory), so rerunning `npm run build` takes effect without a Go rebuild.
Dev build: the static Vue frontend is read straight from disk (relative to the process's working directory), so rerunning `npm run build` takes effect without a Go rebuild.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL