Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ can do.
restores it when someone changes it locally, and reports the event.
- **Compliance reporting.** Every machine reports, per policy, whether it is
compliant, and the web interface shows the state of the whole fleet.
Bor Enterprise adds point-in-time compliance reports that freeze that
state as signed evidence for auditors.
- **Simple rollout.** Each server hosts signed agent package repositories
and a deploy wizard, so agents install and enroll with one script and
managed desktops need no internet access.
Expand Down
50 changes: 50 additions & 0 deletions doc/configuration/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,53 @@ sudo chmod 0600 /etc/bor/escrow.kek
up separately from the database backup. See [Backup and
restore](../operations/backup.md).

## reports

Compliance reports: point-in-time snapshots of the fleet's compliance
results and posture, with schedules, delivery, waivers and sign-off. See
[Compliance reports](../user-guide/reports.md). Compliance reports are
implemented by the Bor Enterprise server; the community server ignores this
section. Every key can also be set with its environment variable, which
wins over the file, so the section can be configured from the environment
alone. **Settings -> Compliance reports** shows the settings in force
(read-only) and sends a test email. Reports are stored in PostgreSQL (tables `ext_reports_*`) and are
part of the normal database backup.

| YAML key | Environment variable | Default | Description |
|----------|----------------------|---------|-------------|
| `reports.retention_days` | `BOR_REPORT_RETENTION_DAYS` | `1095` | Days a report is kept. An hourly job deletes older unpinned reports and records a `report.purge` audit event. `0` keeps reports forever. |
| `reports.stale_after` | `BOR_REPORT_STALE_AFTER` | `168h` | Age after which a compliance result counts as stale and a node that is not online counts as unreachable in a report. A Go duration such as `72h`. |
| `reports.max_results` | `BOR_REPORT_MAX_RESULTS` | `100000` | A report with more node-policy results than this is refused; narrow its scope. |
| `reports.generate_timeout` | `BOR_REPORT_GENERATE_TIMEOUT` | `120s` | Time budget of one generation, including a fresh-evidence wait. |
| `reports.daily_summary` | `BOR_REPORT_DAILY_SUMMARY` | `true` | Store one summary (counts only) per day for the fleet and for every node group; they feed the compliance trend. |
| `reports.refresh_wait` | `BOR_REPORT_REFRESH_WAIT` | `60s` | How long a fresh-evidence report waits for the agents to re-check. Bounded by `generate_timeout`. |
| `reports.refresh_rate` | `BOR_REPORT_REFRESH_RATE` | `50` | Agents per second a fresh-evidence report asks to re-check. |
| `reports.public_url` | `BOR_REPORT_PUBLIC_URL` | (none) | The web UI's address, `https://...`; deliveries link to reports under it. Without it, webhook payloads and emails carry no links. |
| `reports.timestamp_url` | `BOR_REPORT_TIMESTAMP_URL` | (none) | An RFC 3161 time-stamping authority that time-stamps the PDF signature. Leave it empty on air-gapped installations; a PDF that cannot be time-stamped is not produced. |
| `reports.smtp.host` | `BOR_REPORT_SMTP_HOST` | (none) | Mail relay for email delivery; empty disables email. |
| `reports.smtp.port` | `BOR_REPORT_SMTP_PORT` | `587`, or `465` with `tls: implicit` | Relay port. |
| `reports.smtp.tls` | `BOR_REPORT_SMTP_TLS` | `starttls` | `starttls` (the relay must offer it) or `implicit`. Mail is never sent in the clear. |
| `reports.smtp.username` | `BOR_REPORT_SMTP_USERNAME` | (none) | Login for the relay (SMTP AUTH PLAIN over TLS). |
| `reports.smtp.password_file` / `password_env` | `BOR_REPORT_SMTP_PASSWORD_FILE`, `BOR_REPORT_SMTP_PASSWORD` | (none) | The relay password: read from a file, or from the environment variable `password_env` names. `BOR_REPORT_SMTP_PASSWORD` gives the password itself (for example from a container secret) and wins over every other source. Never in `server.yaml`. |
| `reports.smtp.ca_file` | `BOR_REPORT_SMTP_CA_FILE` | (system CAs) | PEM file of the certificate authorities the relay's certificate is checked against, for a relay with a certificate from a private CA. |
| `reports.smtp.from` | `BOR_REPORT_SMTP_FROM` | (none) | Sender address, for example `Bor <bor@example.com>`. Required with `host`. |
| `reports.allow_http_webhooks` | `BOR_REPORT_ALLOW_HTTP_WEBHOOKS` | `false` | Allow `http://` webhook URLs. Keep it off outside test environments. |
| `reports.ocsf_findings` | `BOR_REPORT_OCSF_FINDINGS` | `true` | Forward compliance status changes as OCSF Compliance Findings when the audit log is forwarded in OCSF format. |
| `reports.required_approvals` | `BOR_REPORT_REQUIRED_APPROVALS` | `1` | Approvals (by people other than the generator) a report needs to count as approved; 1 to 5. |
| `reports.waiver_max_days` | `BOR_REPORT_WAIVER_MAX_DAYS` | `365` | Longest lifetime of a waiver. |
| `reports.catch_up_window` | `BOR_REPORT_CATCH_UP_WINDOW` | `24h` | A schedule run missed while the server was down runs at startup when it is younger than this; older runs are skipped. |

```yaml
reports:
retention_days: 1095
public_url: "https://bor.example.com"
smtp:
host: "smtp.example.com"
username: "bor"
password_file: "/etc/bor/smtp-password"
from: "Bor <bor@example.com>"
```

## Systemd options that are not configuration keys

Some behavior is controlled through the service environment rather than
Expand Down Expand Up @@ -458,4 +505,7 @@ audit:

disk_encryption:
escrow_kek_file: "/etc/bor/escrow.kek"
reports:
retention_days: 1095
stale_after: 168h
```
46 changes: 43 additions & 3 deletions doc/development/editions.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ expires) without a restart.
### Features

`Feature` is a string type. The known features are listed in
`edition.AllFeatures`; there is currently one:
`edition.AllFeatures`; there are currently two:

`NodeGroupScopedRBAC` (`"node_group_scoped_rbac"`)
: Role bindings scoped to a single node group (delegated
Expand All @@ -103,6 +103,14 @@ expires) without a restart.
nodeGroupScopes := func() bool { return ed.Enabled(edition.NodeGroupScopedRBAC) }
```

`ComplianceReports` (`"reports"`)
: Point-in-time compliance reports. They are implemented entirely by the
enterprise `reports` extension module (storage, generation, API under
`/api/v1/ext/reports/`, signing, documents, schedules); the community
server contains only the web UI pages, which show the **Reports** page
while the edition enables the feature (the feature list of
`/api/v1/auth/me` drives it) and a Bor Enterprise notice otherwise.

To add a feature, declare a new `Feature` constant, append it to
`AllFeatures`, and gate the behavior on `ed.Enabled(...)` at request time.

Expand Down Expand Up @@ -227,8 +235,40 @@ Audit and secrets
`RequestUser(r)` returns the signed-in user inside a `HandleAdmin`
route.

The community edition registers no modules, so `/api/v1/ext-panels`
returns an empty list and the login page shows no extra buttons.
Permissions for user routes
: `HandleUser(pattern, h)` mounts a handler for any signed-in user; the
handler checks each operation with `Authorize(w, r, resource, action,
scoped)`, which returns where the user holds the permission (globally,
or the node groups of a scoped role binding) and answers 403 with an
audited denial otherwise. `RegisterScopableResource(resource)` lets
node-group-scoped role bindings grant the module's permissions; the
module must then confine every operation to the returned groups
(`DenyOutOfScope` refuses and audits an object outside them).

Agents, compliance results and certificates
: `Agents().Resync(nodeName)` asks a connected agent to re-apply its
policies and report compliance again.
`RegisterComplianceListener(l)` tells the module about every
compliance result an agent reports, with the previous status, the
items and the node's operating system; the listener runs on the
agent's request and must return quickly. `SIEM()` forwards a
pre-formatted message with the audit log's syslog forwarding (and
reports its format, `ocsf` or `cef`, or none). `PKI()` gives access to
the server's certificate authority: `CACertificatePEM()` and
`IssueCertificate(ctx, ext.CertificateRequest{...})`, which today
issues document-signing certificates (extended key usage
id-kp-documentSigning, no TLS usages) for a public key the module
generated and keeps itself.

Metrics
: `RegisterMetrics(collector)`, called from `Init`, adds a Prometheus
collector to the server's `/metrics` endpoint. Metric names start with
`bor_`; a collector the registry rejects (an invalid or clashing
metric) is logged and left out without stopping the server.

The community edition registers no modules and enables no features, so
`/api/v1/ext-panels` returns an empty list, the login page shows no extra
buttons and no compliance report routes exist.

## Building a distribution

Expand Down
77 changes: 77 additions & 0 deletions doc/operations/monitoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,40 @@ These metrics carry aggregate labels only, never node names or key material.
: Recovery keys revealed to administrators. Each reveal is also recorded in
the audit log.

### Compliance reports (Bor Enterprise)

Served by servers whose license includes compliance reports. Like the
metrics above they are read from the database on every scrape.

`bor_compliance_reports_total` (gauge, label `kind`)
: Stored reports: `full` reports and daily `summary` snapshots.

`bor_compliance_report_last_generated_timestamp_seconds` (gauge, label `trigger`)
: Unix time of the newest stored report per trigger: `manual`,
`scheduled`, `daily_summary` or `api`. A trigger that never produced a
report has no series.

`bor_compliance_score_pct` (gauge, label `scope`)
: Fleet compliance score in percent (`scope="fleet"`) from the newest
daily summary that covers all nodes.

`bor_compliance_report_schedules` (gauge, label `state`)
: Report schedules by the result of their last run (`ok`, `error`,
`skipped`, `never_run`), with switched-off schedules as `disabled`.

`bor_compliance_report_schedules_overdue` (gauge, no labels)
: Enabled schedules whose next run is more than 15 minutes late: the
scheduler is not running (for example the license has expired) or a
run hangs.

`bor_compliance_report_deliveries` (gauge, label `status`)
: Webhook calls and emails in the delivery queue: `pending`, `sending`,
`sent` and `failed`. Finished deliveries are kept for 90 days.

`bor_compliance_report_signing_certificate_expiry_timestamp_seconds` (gauge, no labels)
: Expiry of the active report-signing certificate. The server renews it
30 days before; a value closer than that means renewal fails.

!!! note "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
Expand Down Expand Up @@ -351,6 +385,45 @@ groups:
severity: warning
annotations:
summary: "Recovery-key escrow operation {{ $labels.op }} failed"

# Bor Enterprise compliance reports
- alert: BorReportScheduleFailing
expr: bor_compliance_report_schedules{state="error"} > 0
labels:
severity: warning
annotations:
summary: "{{ $value }} compliance report schedule(s) failed their last run"

- alert: BorReportScheduleOverdue
expr: bor_compliance_report_schedules_overdue > 0
for: 15m
labels:
severity: warning
annotations:
summary: "Compliance report schedules are not running"

- alert: BorReportDeliveryFailed
expr: delta(bor_compliance_report_deliveries{status="failed"}[1h]) > 0
labels:
severity: warning
annotations:
summary: "A compliance report delivery failed after all retries"

- alert: BorReportSigningCertificateNotRenewed
expr: bor_compliance_report_signing_certificate_expiry_timestamp_seconds - time() < 20 * 86400
for: 1h
labels:
severity: critical
annotations:
summary: "The compliance report signing certificate was not renewed"

- alert: BorFleetComplianceLow
expr: bor_compliance_score_pct{scope="fleet"} < 90
for: 1d
labels:
severity: info
annotations:
summary: "Fleet compliance score is {{ $value }}%"
```

Notes on these rules:
Expand All @@ -365,6 +438,10 @@ Notes on these rules:
- `BorFlatpakCatalogStale` only 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.
- `BorReportDeliveryFailed` uses `delta` because the delivery counts are
gauges; the **Settings -> Compliance reports** page lists the failed
deliveries and retries them. Set the `BorFleetComplianceLow` threshold
to your own target.
- Do not alert on `bor_node_last_seen_seconds` age: as explained above, it only
changes when an agent reconnects.

Expand Down
18 changes: 18 additions & 0 deletions doc/reference/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,18 @@ hold yourself.
|---|---|
| `updates:operate` | Run a Software updates action on one node from the **Software updates** section of the node details: refresh the package metadata, run the update engine once with the policy's mode, or reboot the node (forced reboots warn logged-in users first). The action runs on the node at once and is audit-logged (`updates.action`) with the outcome. Can be delegated per node group. |

### Compliance reports

These permissions exist on Bor Enterprise servers, which create them
together with the report tables; a community server has neither.

| Permission | Allows |
|---|---|
| `report:view` | List, open and download compliance reports (the **Reports** page). Downloads are audit-logged (`report.download`). Can be delegated per node group: a scoped holder sees only reports whose scope lies inside their groups. |
| `report:create` | Generate a compliance report. Audit-logged (`report.generate`) with the report's hash. Can be delegated per node group: a scoped holder generates reports over their groups only. |
| `report:manage` | Delete compliance reports (`report.delete`), pin and unpin them (legal hold, `report.pin`, `report.unpin`), and create, change, run and delete report schedules. Global only. |
| `report:attest` | Sign a compliance report off (approve or reject it, `report.attest`) when someone else generated it, and accept and revoke waivers (`report_waiver.create`, `report_waiver.revoke`). Global only. |

## Built-in roles

`Y` means the role holds the permission on a new installation.
Expand All @@ -130,6 +142,10 @@ hold yourself.
| `binding:create` | Y | Y | | | | |
| `binding:toggle` | Y | Y | | | | |
| `compliance:view` | Y | Y | | | Y | Y |
| `report:view` | Y | Y | | | Y | Y |
| `report:create` | Y | Y | | | | Y |
| `report:manage` | Y | | | | | |
| `report:attest` | Y | | | | | Y |
| `node:view` | Y | Y | | | | Y |
| `node:create` | Y | Y | | | | |
| `node:edit` | Y | Y | | | | |
Expand Down Expand Up @@ -181,6 +197,8 @@ checked, the **Policy Editor** role can release policies (it holds
## Navigation and permissions

The sidebar hides **Disk Encryption** without `disk_encryption:view`,
**Reports** without `report:view` or when the edition does not enable
compliance reports,
**Audit Logs** without `audit_log:view`, and **Settings** unless you hold
at least one of `user:view`, `role:view`, `user_group:view`,
`settings:manage` or `flatpak_repo:view`. Other pages are always listed
Expand Down
Loading
Loading