diff --git a/README.md b/README.md index 9e3e2e2..df7ae07 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,10 @@ how many clusters were excluded for insufficient data. A score built from six clusters is a weaker claim than one built from sixty, and the report says which one you are looking at. +A score is also only as good as what the scan could see. A scan that could +not observe a cluster says so and is not graded, and a finding is never +produced from a read that failed. + Fleetsweeper is therefore not a replacement for policy engines, CIS benchmarks, or vulnerability scanning. Those check clusters against absolute rules, and they are the tools that catch the failure mode above. diff --git a/UPGRADING.md b/UPGRADING.md index ce9799e..871e6b7 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -14,6 +14,60 @@ stability contract and the GitHub release notes for the full feature list. 5. Verify `kubectl logs deploy/fleetsweeper | grep migrate` shows no errors and `/readyz` returns 200. +## v0.14.0 + +No schema migration. Reports, scores, and a few API shapes change on the +first scan after the upgrade, because a scan that could not see something no +longer reports it as healthy. + +- **Reports can be ungraded.** The Fleet Score carries a `graded` flag and a + `coverage` object (`expected_runs`, `observed_runs`, `blind_runs`, + `blind_clusters`, `fraction`). A score is graded only when every cluster has + an observed scanner run and at least 80 percent of expected runs are + observed. An ungraded score has an empty `grade`, and its dashboard and + brief lead with the coverage gap. + +- **Clusters can be `unknown`.** A cluster that looks healthy but had a + scanner run that errored or never ran reads `unknown` instead of `healthy`. + Its cluster score is charged like degraded. The Fleet Score does not charge + unknown clusters, the coverage gate covers them. + +- **Metrics averages are optional.** `avg_cpu_percent`, `avg_memory_percent`, + `max_cpu_percent`, and `max_memory_percent` are omitted when no node + resolved a usage percentage, instead of reading 0. Clients that read them + as numbers must handle absence. + +- **Ack bodies carry only context.** `POST /api/findings/{fingerprint}/ack` + and `POST /api/alerts/{fingerprint}/ack` accept `ack_by`, `reason`, and + `snooze_until`. A body that names `cluster`, `scanner`, or `title` is + rejected with 400 instead of being ignored. A fingerprint that no current + finding carries returns 404. A fleet or clusterless finding needs a key + with `*` scope, and so does an ack on an alert with no cluster. + +- **Group management is admin only.** `POST /api/groups` and + `DELETE /api/groups/{name}` refuse operators, including operators with `*` + scope. + +- **Non-root counts use the effective user.** The workload-security scanner + adds `baseline_containers` and `run_as_non_root_containers`, and renames + `unknown_root_containers` to `run_as_unknown_containers`. A container is + non-root when its effective `runAsUser` is above 0, or when `runAsNonRoot` + is true and no user is set. An effective `runAsUser` of 0 is root even under + `runAsNonRoot: true`, and `runAsNonRoot: false` alone is unknown. Scans + stored before the upgrade contribute 0 to the new baseline fractions. + +- **Privileged namespaces count as unenforced.** A namespace whose Pod + Security enforce label is `privileged` now counts in `unenforced_count`, not + `enforced_count`. The `security/psa-unenforced` finding charges only + namespaces with no enforce label. Namespaces set to `privileged` are listed + in the finding and not charged. + +- **Two new Prometheus series.** `fleetsweeper_fleet_score_graded` is 1 when + the scan is graded and 0 otherwise. `fleetsweeper_coverage_ratio` is the + share of expected scanner runs that observed the fleet. `fleetsweeper_fleet_score` + is not emitted while the scan is ungraded, so alert rules that read it + should also check the graded series. + ## v0.13.0 No schema migration. Findings, counts, and some outputs change on the first diff --git a/deploy/helm/fleetsweeper/Chart.yaml b/deploy/helm/fleetsweeper/Chart.yaml index eb47a0f..ccbf600 100644 --- a/deploy/helm/fleetsweeper/Chart.yaml +++ b/deploy/helm/fleetsweeper/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: fleetsweeper description: Multi-cluster Kubernetes fleet comparison and audit type: application -version: 0.9.0 -appVersion: "0.9.0" +version: 0.14.0 +appVersion: "0.14.0" home: https://github.com/dcadolph/fleetsweeper sources: - https://github.com/dcadolph/fleetsweeper diff --git a/docs/operator/rbac.md b/docs/operator/rbac.md index 47e7d9c..88cce18 100644 --- a/docs/operator/rbac.md +++ b/docs/operator/rbac.md @@ -88,7 +88,7 @@ curl -X POST -H "Authorization: Bearer $BOOTSTRAP" \ https://fleetsweeper.example.com/api/admin/keys ``` -The response contains the raw token. Save it now; the server stores only +The response contains the raw token. Save it now. The server stores only its SHA-256 hash and cannot reveal it again. ## Listing and revoking @@ -118,7 +118,7 @@ Filters: Audit entries include actor identity, method, path, status, duration, and a short error excerpt for failures. Read-only requests are not audited to -keep the volume manageable; they are still captured by the request log at +keep the volume manageable. They are still captured by the request log at info level. ## Whoami @@ -135,7 +135,7 @@ A fresh deployment looks like this: 2. Start fleetsweeper. The token grants admin access. 3. Mint a scoped operator key per pipeline: `fleetsweeper apikey create --name ci-prod --role operator --scope 'group:prod'` -4. Hand the scoped key to that pipeline; rotate the bootstrap token to +4. Hand the scoped key to that pipeline. Rotate the bootstrap token to long-term storage as the emergency credential. 5. Use `GET /api/admin/audit` to confirm the pipelines are using their own keys and the bootstrap token is no longer being used in normal traffic. diff --git a/docs/production-readiness.md b/docs/production-readiness.md index 25ad8d8..fb34666 100644 --- a/docs/production-readiness.md +++ b/docs/production-readiness.md @@ -6,7 +6,7 @@ What Fleetsweeper ships ready for the box. | ---------------------- | -------------- | | HA backend | SQLite default, PostgreSQL via `--db-driver=postgres` | | Multi-replica safe | Kubernetes Lease-based leader election (`coordination.k8s.io/v1`) | -| Multi-tenant RBAC | Scoped API keys (`admin`, `operator`, `viewer`) with cluster scope (`*`, names, or `group:`) | +| Multi-tenant RBAC | Scoped API keys (`admin`, `operator`, `viewer`) with cluster scope (`*`, names, or `group:`) on writes. Reads are not scoped | | Audit log | Every mutating request captured. Queryable at `GET /api/admin/audit`. Retention via `--audit-retention` | | Declarative ops | `ClusterScan` CRD with an in-process reconciler | | GitOps integrations | `FleetDriftReport` CR plus `PolicyReport` (wgpolicyk8s.io) | diff --git a/go.mod b/go.mod index 37a5fb9..00dd326 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/google/go-containerregistry v0.21.5 github.com/jackc/pgx/v5 v5.9.2 github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.10 go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 @@ -73,7 +74,6 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/sirupsen/logrus v1.9.4 // indirect - github.com/spf13/pflag v1.0.10 // indirect github.com/vbatts/tar-split v0.12.2 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect diff --git a/internal/explain/explain.go b/internal/explain/explain.go index 7dee292..270f3f7 100644 --- a/internal/explain/explain.go +++ b/internal/explain/explain.go @@ -186,8 +186,9 @@ func topics() []Topic { Remediation: []string{ "Drop privileged: true unless the workload genuinely needs full host access.", "Add seccompProfile: { type: RuntimeDefault } as a baseline.", - "Move legitimately-privileged workloads into a dedicated namespace with " + - "Pod Security enforce=privileged for clarity.", + "Move legitimately-privileged workloads into a dedicated namespace labeled " + + "enforce=privileged. That label enforces nothing, so the namespace is " + + "listed by security/psa-unenforced but not charged.", }, Related: []string{"security", "rbac-audit"}, },