Skip to content

fix(security): secure defaults for the chart, the stores and tenants - #424

Merged
luiacuaniello merged 1 commit into
mainfrom
fix/secure-defaults
Nov 14, 2026
Merged

luiacuaniello merged 1 commit into
mainfrom
fix/secure-defaults

Conversation

@luiacuaniello

Copy link
Copy Markdown
Owner

Three changes from the security list of 10 November, each with its own section under 1.39.4 in UPGRADING.

A default Helm install now runs the production profile with its NetworkPolicies on. backend.env defaulted to demo, so a release nobody configured served the API and ingest unauthenticated to the whole cluster. The backend refuses to start in production without an API credential and an ingest secret, so the chart now refuses to render until it knows their source - secrets.generate, secrets.existingSecret, or auth.* with ingest.hmacSecret - or until backend.env=demo is asked for by name, as the SSO demo values now do. CI lints both profiles and checks that a default render stops and that every way out renders. The kind installs run the new default with generated credentials, and check that the API and ingest refuse a caller without them and accept the generated ones (the probes sign v2), that NATS refuses a pod the NetworkPolicy admits, and that no other pod reaches it.

With STORE_ENCRYPTION_KEY set, the governance stores and the audit log read a record the key had not sealed, so that stores written before the key stayed readable. That let anyone who could write to the volume add a suppression, or rewrite the audit chain, whose hashes carry no key. Such a record is now refused (cryptostore.ErrPlaintext), while a blob cut inside the sealed prefix is still a torn write. What predates the key is sealed once by perspectivegraph seal-stores (-dry-run lists it) or by one start with STORE_SEAL_EXISTING=true (Helm: crypto.sealExisting); an audit chain is verified before it is sealed.

A tenant now has one spelling past authentication. Tokens, OIDC claims and ingest signatures normalize it (internal/tenant), the governance stores normalize what they are handed, and records written under a token's spelling are re-keyed: on load in the file stores, at startup in Postgres (pgmigrate.NormalizeTenants). For a tenant such as Acme-Corp the verdicts were split: the analyzer's calibration trend and the KEV holdout used the normalized name, the API the token's.

Measured with Go 1.27.4: build, vet and test pass, the Postgres suites pass against a database and library coverage is 83.4%; govulncheck, gosec and staticcheck are clean; scripts/chart-install.sh passes on kind v1.36.1 with the bundled database, CloudNativePG and the Kubernetes connector.

BEHAVIOUR CHANGE: a helm install or upgrade with backend.env at its default runs the production profile and does not render without a credential source; networkPolicy.enabled defaults to true. With STORE_ENCRYPTION_KEY set the engine refuses a record the key did not seal, until seal-stores seals what predates it. Tenant names are normalized past authentication, in /auth/me and new audit records too.

What & why

What does this change, and why? Link any issue (Closes #123).

Checklist

  • PR title follows Conventional Commits (feat:, fix:, docs:, …)
  • Backend: gofmt, go vet, go test ./..., and gosec are clean
    (CI runs them; make test locally)
  • Frontend (if touched): tsc, npm run build, and vitest pass
  • Docs + Postman updated for any user-facing change - the page in docs/manual/
    (the README only if the first screen changes), .env.example and
    docs/perspectivegraph.postman_collection.json
  • New behavior has tests (and a fuzz test for any new parser)
  • No secrets committed (the gitleaks gate enforces it)
  • Didn't weaken the tool's own controls (auth/RBAC, ingest HMAC, audit log,
    at-rest encryption, export signing)

How was it verified?

Tests added/updated, manual steps, or a make seed/make demo run - whatever
proves it works.

Three changes from the security list of 10 November, each with its
own section under 1.39.4 in UPGRADING.

A default Helm install now runs the production profile with its
NetworkPolicies on. backend.env defaulted to demo, so a release nobody
configured served the API and ingest unauthenticated to the whole
cluster. The backend refuses to start in production without an API
credential and an ingest secret, so the chart now refuses to render
until it knows their source - secrets.generate, secrets.existingSecret,
or auth.* with ingest.hmacSecret - or until backend.env=demo is asked
for by name, as the SSO demo values now do. CI lints both profiles and
checks that a default render stops and that every way out renders. The
kind installs run the new default with generated credentials, and check
that the API and ingest refuse a caller without them and accept the
generated ones (the probes sign v2), that NATS refuses a pod the
NetworkPolicy admits, and that no other pod reaches it.

With STORE_ENCRYPTION_KEY set, the governance stores and the audit log
read a record the key had not sealed, so that stores written before
the key stayed readable. That let anyone who could write to the volume
add a suppression, or rewrite the audit chain, whose hashes carry no
key. Such a record is now refused (cryptostore.ErrPlaintext), while a
blob cut inside the sealed prefix is still a torn write. What predates
the key is sealed once by `perspectivegraph seal-stores` (-dry-run
lists it) or by one start with STORE_SEAL_EXISTING=true (Helm:
crypto.sealExisting); an audit chain is verified before it is sealed.

A tenant now has one spelling past authentication. Tokens, OIDC claims
and ingest signatures normalize it (internal/tenant), the governance
stores normalize what they are handed, and records written under a
token's spelling are re-keyed: on load in the file stores, at startup
in Postgres (pgmigrate.NormalizeTenants). For a tenant such as
Acme-Corp the verdicts were split: the analyzer's calibration trend and
the KEV holdout used the normalized name, the API the token's.

Measured with Go 1.27.4: build, vet and test pass, the Postgres suites
pass against a database and library coverage is 83.4%; govulncheck,
gosec and staticcheck are clean; scripts/chart-install.sh passes on
kind v1.36.1 with the bundled database, CloudNativePG and the
Kubernetes connector.

BEHAVIOUR CHANGE: a helm install or upgrade with backend.env at its
default runs the production profile and does not render without a
credential source; networkPolicy.enabled defaults to true. With
STORE_ENCRYPTION_KEY set the engine refuses a record the key did not
seal, until seal-stores seals what predates it. Tenant names are
normalized past authentication, in /auth/me and new audit records too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Luigi Iacuaniello <luigi@a3thinker.it>
@luiacuaniello
luiacuaniello merged commit 6840ba7 into main Nov 14, 2026
24 checks passed
@luiacuaniello
luiacuaniello deleted the fix/secure-defaults branch November 14, 2026 18:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant