Monitoring¶
This page explains how to watch a running Bor installation: the built-in Prometheus metrics endpoint and every metric it exports, a scrape configuration, alert rules built only on metrics Bor actually exposes, the checks you can use as health probes, and where the server and agent write their logs.
Overview¶
| What | Where | Notes |
|---|---|---|
| Prometheus metrics | http://127.0.0.1:9090/metrics (default) |
Separate listener, always started by bor-server |
| Server liveness | GET https://bor.example.com/api/v1/version |
No authentication, does not touch the database |
| Server logs | journalctl -u bor-server |
Syslog identifier bor-server |
| Agent logs | journalctl -u bor-agent on each endpoint |
Syslog identifier bor-agent |
| Audit trail | Audit Logs page, export, or syslog forwarding | See Audit log |
The metrics endpoint¶
bor-server always starts a dedicated HTTP listener for Prometheus, separate
from the HTTPS web port (server.enrollment_port, 443 recommended, default
8443) used by browsers and agents. It serves:
/metrics- the metrics in Prometheus / OpenMetrics text format./- redirects to/metrics.
By default the listener binds to 127.0.0.1:9090, so only processes on the
server itself can scrape it. There is no switch to turn the listener off; keep
it on the loopback address if you do not use it.
On startup the server logs which address it is using:
Port 9090 is also the Prometheus default
If Prometheus runs on the same host as bor-server, both will try to bind
port 9090. A failed metrics listener does not stop bor-server: it only
logs Metrics server error: listen tcp 127.0.0.1:9090: bind: address already in use
and keeps serving the UI and agents without metrics. Move one of the two to
another port, for example listen_addr: "127.0.0.1:9469".
Configuration¶
Set the options in the metrics section of /etc/bor/server.yaml, or with the
matching environment variables (for example in /etc/bor/server.env).
Environment variables take precedence over the YAML file.
| YAML key | Environment variable | Default | Description |
|---|---|---|---|
metrics.listen_addr |
BOR_METRICS_ADDR |
127.0.0.1:9090 |
host:port the listener binds to. Use a management interface address to allow remote scraping, or :9090 for all interfaces. |
metrics.bearer_token |
BOR_METRICS_TOKEN |
empty | When set, every request must carry Authorization: Bearer <token>; anything else gets 401 Unauthorized. |
metrics.tls_cert_file |
BOR_METRICS_TLS_CERT_FILE |
empty | PEM certificate. Together with the key, switches the listener to HTTPS. |
metrics.tls_key_file |
BOR_METRICS_TLS_KEY_FILE |
empty | PEM private key for the certificate above. |
The certificate and key must be set together. If only one of them is set the server refuses to start with:
Failed to load configuration: both BOR_METRICS_TLS_CERT_FILE and BOR_METRICS_TLS_KEY_FILE must be set, or neither
Example: expose the endpoint on a management network with a token and TLS.
metrics:
listen_addr: "192.0.2.10:9090"
tls_cert_file: "/etc/bor/metrics.crt"
tls_key_file: "/etc/bor/metrics.key"
Put the token in /etc/bor/server.env instead of the YAML file:
Generate a token with openssl rand -hex 32. The certificate and key files
must be readable by the bor user. Restart the server to apply changes:
Verify from the Prometheus host:
curl -s -H "Authorization: Bearer $TOKEN" --cacert metrics-ca.crt \
https://192.0.2.10:9090/metrics | grep '^bor_'
Note
The metrics listener is plain HTTP unless you configure a certificate. When you bind it to anything other than the loopback address, set a bearer token, enable TLS, and restrict the port with a firewall.
Metrics reference¶
Most Bor metrics are gauges computed from the database at scrape time:
each scrape runs a few COUNT ... GROUP BY queries against PostgreSQL. The
exceptions are the four counters marked below, which count events since the
server process started and reset to zero on restart.
The endpoint also exports the standard Go runtime (go_*) and process
(process_*) metric families.
Nodes¶
bor_nodes_total(gauge, labelstatus)- Number of enrolled nodes by their cached connection status. The series for
online,degraded,offlineandunknownare always present (zero when empty). A node becomesonlinewhen its agent completes the initial policy sync on port 8444,offlinewhen that stream closes, and isunknownuntil its agent has connected for the first time. bor_node_certificate_expiry_seconds(gauge, labelsnode,fqdn)- Seconds until the node's mTLS client certificate expires. Negative values
mean the certificate has already expired.
nodeis the node name in Bor (the certificate common name),fqdnis the host name the agent reported. Agent certificates are issued for 90 days. bor_node_last_seen_seconds(gauge, labelsnode,fqdn)- Unix timestamp of the last heartbeat. The agent sends a heartbeat each time
it (re)connects its policy stream and when an administrator requests a
metadata refresh, not periodically. A node with a healthy, long-lived
connection can therefore have an old value. Use
bor_nodes_totalfor liveness and this metric to see when a node last reconnected.
Policies and bindings¶
bor_policies_total(gauge, labelsstate,type)- Number of policies by lifecycle state (
draft,released,archived) and policy type (for exampleFirefox,Dconf,Kconfig,Polkit). Only combinations that exist are exported. Onlyreleasedpolicies are delivered to agents. bor_policy_bindings_total(gauge, labelstate)- Number of policy-to-node-group bindings,
enabledordisabled. Both series are always present.
Compliance¶
bor_compliance_results_total(gauge, labelstatus)- Number of compliance results, one per (node, policy) pair, by the status
the agent last reported. The series
compliant,non_compliant,errorandunknownare always present. Results with the statusinapplicable(the policy does not apply to that endpoint, for example the target software is not installed) are not exported.
Fleet compliance ratio:
bor_compliance_results_total{status="compliant"}
/ ignoring(status)
sum without(status) (bor_compliance_results_total)
Users¶
bor_users_total(gauge, labelssource,enabled)- Number of user accounts by authentication source (
local,ldap, and other sources if present) and whether the account is enabled (trueorfalse).
Audit¶
bor_audit_events_total(gauge, labelaction)- Number of rows in the audit log, per action (for example
create,update,delete,tamper_detected,auth.login,access_denied). This is a snapshot of the table, not a monotonic counter: it drops when the retention job purges old entries. An action with no rows has no series.
Agent package downloads¶
bor_agent_package_downloads_total(counter, labelsformat,arch)- Agent packages downloaded from this server's
/agent/repository.formatisdeb,rpm,apkorarch;archisamd64,arm64,ppc64leorunknown. Only successfulGETrequests for package files are counted, not index or key files.
Flatpak catalog¶
bor_flatpak_catalog_apps_total(gauge, labelrepo)- Applications indexed in the server-side Flatpak catalog (Settings -> Flatpak Repositories), per repository.
bor_flatpak_catalog_last_success_timestamp(gauge, labelrepo)- Unix timestamp of the last successful catalog refresh or upload per
repository,
0if the catalog was never refreshed. bor_flatpak_catalog_refresh_total(counter, labelsrepo,outcome)- Catalog refresh attempts.
outcomeisok,unchanged(the repository answered "not modified"),error, orupload(catalog uploaded by hand).
Disk encryption¶
These metrics carry aggregate labels only, never node names or key material.
bor_luks_volumes_total(gauge, labelescrowed)- LUKS volumes reported by agents, split by whether an active recovery key is
escrowed on the server (
true/false). bor_luks_recovery_keys_overdue_total(gauge, no labels)- Volumes with a pending recovery-key rotation request that the node has not completed yet.
bor_luks_escrow_operations_total(counter, labelsop,outcome)- Recovery-key escrow operations.
opisescrow,confirm,releaseordestroy;outcomeisokorerror. bor_luks_recovery_key_reveals_total(counter, no labels)- Recovery keys revealed to administrators. Each reveal is also recorded in the audit log.
Missing series mean a failed query
If a database query fails during a scrape, the server logs a line starting
with metrics: (for example metrics: CountByStatus: ...) and simply
omits that metric from the response. The scrape itself still succeeds.
Alerting on absent(bor_nodes_total) therefore catches a server that is up
but cannot query its database.
Prometheus scrape configuration¶
Scraping from the Bor server itself (default loopback listener):
Scraping a remote listener with TLS and a bearer token:
scrape_configs:
- job_name: bor
scheme: https
authorization:
type: Bearer
credentials_file: /etc/prometheus/bor-metrics.token
tls_config:
ca_file: /etc/prometheus/bor-metrics-ca.crt
static_configs:
- targets: ["bor.example.com:9090"]
Each scrape runs several database queries, so a scrape interval of 30 to 60 seconds is plenty.
Suggested alerts¶
The rules below only use metrics listed above. Adjust thresholds and for
durations to your fleet.
groups:
- name: bor
rules:
- alert: BorMetricsDown
expr: up{job="bor"} == 0
for: 5m
labels:
severity: critical
annotations:
summary: "Bor metrics endpoint is not reachable"
- alert: BorDatabaseQueriesFailing
expr: absent(bor_nodes_total{job="bor"})
for: 10m
labels:
severity: critical
annotations:
summary: "bor-server returns no node metrics (database queries failing or target down)"
- alert: BorNodeCertificateNotRenewed
expr: bor_node_certificate_expiry_seconds < 21 * 86400
for: 1h
labels:
severity: warning
annotations:
summary: "Certificate of node {{ $labels.node }} expires in less than 21 days"
description: >
The agent renews its certificate when the bor-agent service starts
and the certificate is within 30 days of expiry. Restart bor-agent
on {{ $labels.fqdn }} while the certificate is still valid.
- alert: BorNodeCertificateExpired
expr: bor_node_certificate_expiry_seconds < 0
for: 5m
labels:
severity: critical
annotations:
summary: "Certificate of node {{ $labels.node }} has expired"
description: >
The agent can no longer connect or renew. Re-enroll the node.
- alert: BorNodesOffline
expr: bor_nodes_total{status="offline"} > 0
for: 30m
labels:
severity: warning
annotations:
summary: "{{ $value }} Bor node(s) offline"
- alert: BorComplianceFailures
expr: bor_compliance_results_total{status=~"non_compliant|error"} > 0
for: 30m
labels:
severity: warning
annotations:
summary: "{{ $value }} compliance result(s) in state {{ $labels.status }}"
- alert: BorTamperDetected
expr: delta(bor_audit_events_total{action="tamper_detected"}[1h]) > 0
labels:
severity: warning
annotations:
summary: "A Bor-managed file was modified outside Bor"
description: "Check Audit Logs for tamper_detected events."
- alert: BorFlatpakCatalogStale
expr: time() - bor_flatpak_catalog_last_success_timestamp > 3 * 86400
for: 1h
labels:
severity: info
annotations:
summary: "Flatpak catalog {{ $labels.repo }} not refreshed for 3 days"
- alert: BorLuksRotationOverdue
expr: bor_luks_recovery_keys_overdue_total > 0
for: 1d
labels:
severity: warning
annotations:
summary: "{{ $value }} LUKS volume(s) have not completed a requested key rotation"
- alert: BorLuksVolumesWithoutEscrow
expr: bor_luks_volumes_total{escrowed="false"} > 0
for: 1d
labels:
severity: warning
annotations:
summary: "{{ $value }} LUKS volume(s) have no escrowed recovery key"
- alert: BorLuksEscrowErrors
expr: increase(bor_luks_escrow_operations_total{outcome="error"}[1h]) > 0
labels:
severity: warning
annotations:
summary: "Recovery-key escrow operation {{ $labels.op }} failed"
Notes on these rules:
BorNodeCertificateNotRenewed: the agent checks its certificate only when thebor-agentservice starts. An agent that runs for months without a restart (or reboot) is not renewed until it restarts, so an alert well inside the 30-day renewal window tells you which endpoints need a restart.BorTamperDetectedusesdeltabecausebor_audit_events_totalis a gauge. The very first tamper event creates the series and is not seen bydelta; check the Audit Logs page as well.BorFlatpakCatalogStaleonly makes sense when catalog refresh is enabled (flatpak_catalog.refresh_enabled, on by default). Each repository's refresh interval is configured per repository; adjust the threshold to match.- Do not alert on
bor_node_last_seen_secondsage: as explained above, it only changes when an agent reconnects.
Health checks¶
Bor has no dedicated health or readiness endpoint. Use these instead:
| Check | Command | What it proves |
|---|---|---|
| HTTPS listener (web port) | curl -fsS --cacert ca.crt https://bor.example.com/api/v1/version |
The UI/enrollment listener accepts TLS and HTTP. Returns {"version":"..."} without authentication and without touching the database. |
| Agent listener (8444) | openssl s_client -connect bor.example.com:8444 -CAfile ca.crt </dev/null |
The mTLS port is reachable and presents a certificate signed by the Bor CA. |
| Database | absent(bor_nodes_total) in Prometheus, or the metrics: lines in the server log |
The server can query PostgreSQL. |
| Service state | systemctl is-active bor-server |
systemd sees the process running. |
Download ca.crt once from https://bor.example.com/agent/ca.crt (when
the server ships the agent repository), or copy it from
/var/lib/bor/pki/ca/ca.crt on the server. If the UI port uses an ACME or
other publicly trusted certificate, omit --cacert for the web port check; port
8444 always uses the certificate signed by the internal CA.
Tip
Because /api/v1/version does not query the database, combine it with the
database check above in any load balancer or uptime monitor that should
detect a broken database connection.
Logs¶
Both services log to the systemd journal.
Server¶
sudo journalctl -u bor-server -f # follow
sudo journalctl -u bor-server -b # since last boot
sudo journalctl -u bor-server --since "1 hour ago"
sudo journalctl -u bor-server -p warning # warnings and errors only
Useful lines to look for:
| Log line | Meaning |
|---|---|
Bor Policy Management Server <version> (community edition) |
Startup |
HTTPS + enrollment gRPC listening on :<port> |
UI and enrollment port (server.enrollment_port) is up |
Agent policy gRPC (mTLS) listening on :8444 |
Agent port is up |
Applied migration: <version> |
A database schema migration ran (after upgrades) |
Client <node> entering watch mode at revision <n> |
An agent finished its initial sync and is streaming |
LDAP authentication failed for user <name>: <reason> |
Failed LDAP login, with the LDAP error |
Audit log retention: purged <n> records older than <d> days |
Retention job removed old audit entries |
WARNING: JWT_SECRET not set |
No JWT secret configured; sessions do not survive restarts |
Agent¶
On each endpoint:
| Log line | Meaning |
|---|---|
Server enrollment: <host>:<port> policy: <host>:8444 |
Addresses read from /etc/bor/config.yaml |
Agent is enrolled ... using mTLS credentials |
Certificates found in /var/lib/bor/agent |
Connecting to policy stream (last_known_revision=<n>)... |
Connecting to port 8444 |
Policy update: type=<type> id=<id> name=<name> version=<v> |
A policy was received |
Received empty snapshot (no policies assigned) |
Connected, but no released policy is bound to the node's groups |
Policy stream disconnected: <error> |
Connection lost; the agent retries with backoff up to 60 seconds |
Certificate expires within 30 days, renewing... |
Automatic renewal at service start |
Tamper protection: restoring <path> |
A managed file was changed locally and is being restored |
See Troubleshooting for what to do when these messages point to a problem.