Files
cairnobs/deploy
jcoffey-dev 4b5dae5879 Add local login, agent extra log paths, IPv4/IPv6 metrics; remediate security audit findings
This is a large squashed commit covering two batches of prior uncommitted
work plus a full security-audit remediation pass, kept together because
go.mod/go.sum and several shared files (main.go, handler.go) were touched
by both and splitting risked non-building intermediate commits.

Features (built earlier, previously uncommitted):
- Local username/password login for single-tenant deployments with no
  SSO configured (api/localauth, alerting/internal/sessioncheck,
  sentryctl users, web/src/routes/login, metadata migrations 0040/0041).
- Remotely-editable additional log file paths for agents, on top of
  their existing primary source (api/agents, agent/sentry-agent
  extra-file-path diffing, web agent config UI).
- IPv4/IPv6 addresses reported alongside other host system metrics.

Security audit remediation (this pass, all live-verified in production):
- Critical: block ClickHouse SSRF table functions (url/remote/file/s3/...)
  in the raw-SQL query escape hatch.
- High: deny sensitive paths and require Admin to add agent
  extra_file_paths (Editor could previously point an agent at /etc/shadow
  or an SSH key); alerting webhook targets now validate against
  internal/metadata/loopback addresses, both at creation and send time;
  alerting's session middleware now enforces an Editor+ floor on
  mutating requests instead of "any authenticated session"; bumped
  goxmldsig to close a SAML signature-verification bypass (GO-2026-4753).
- Medium: per-IP login rate limiting; security response headers
  (HSTS/CSP/nosniff/X-Frame-Options/Referrer-Policy/Permissions-Policy)
  on web/nginx.conf; a DevCredentialWarnings check in every Go service's
  config loader, logging loudly at startup if a deployment is still on
  docker-compose.yml's literal dev-only credentials; dependency bumps
  (golang.org/x/text, grpc, x/net, quick-xml, h2) across every affected
  Go module and both Rust crates, including a previously-uncovered x/net
  vulnerability in deploy/operator; a new security-scan.yml CI workflow
  running cargo-deny/govulncheck/npm-audit, mirroring the existing
  license-compliance.yml matrix shape.
- Low: removed sentryctl's plaintext --password flag (shell
  history/`ps` exposure) in favor of stdin and a --password-stdin flag
  for reset-password's optional specific-password path; a dummy bcrypt
  comparison closes a login response-time username-enumeration
  side-channel.
2026-08-18 23:53:20 -07:00
..

deploy

Kubernetes deployment for Sentry, added in Phase 4 (/deploy was deliberately stubbed through Phase 3 -- see /CLAUDE.md's Phase 3 non-goals). Two pieces:

  • operator/ -- a small Go controller-runtime Operator managing one CRD (Tenant). See operator/README.md.
  • helm/sentry/ -- a Helm chart covering every docker-compose.yml service, plus the operator and Tenant CRs when enterprise.enabled=true. See helm/sentry/README.md.

What "multi-tenant-aware" means here, precisely

Per /docs/phase-4-isolation-design.md, tenant isolation itself lives at the application layer inside enterprise/ (enterprise-api holds a map of per-tenant ClickHouse connection pools via internal/chrunner; search holds a map of per-tenant Tantivy indices via src/registry.rs) -- not at the Kubernetes layer. This directory is not "one Deployment per tenant" or a general multi-cluster system; that's an explicit Phase 4 non-goal (see /CLAUDE.md). What it does add:

  • A Tenant CRD + controller (operator/internal/controller) that reflects real provisioning state onto status.phase/a Ready condition, derived from whether enterprise-api -provision-tenant has reported real ClickHouse provisioning.
  • A Helm chart that can install zero-or-more Tenant CRs (values.tenants) alongside the rest of the stack, and swaps api's Deployment for enterprise-api's whenever enterprise.enabled is true, so which query binary actually serves traffic is no longer a separately-forgettable decision (see helm/sentry/README.md's "api vs enterprise-api" section).

Now unified, in a deliberately lightweight way: enterprise-api -provision-tenant=<id> stays the sole real actor — it's the only thing that calls ClickHouse (CREATE DATABASE/CREATE USER/GRANT, via enterprise/internal/tenantprovision) and writes rbacstore. What changed: once it succeeds, it also syncs the result into the Tenant CRD (enterprise/internal/tenantcrd) — creating the Secret with real credentials (the controller no longer generates a placeholder one that authenticated against nothing) and setting the status fields the controller reads to compute Phase/Ready. The controller itself gained no new credentials and still never touches ClickHouse/Postgres -- it's a pure function of spec.suspended and whatever -provision-tenant has reported, never an independent second guess at "is this tenant really provisioned." A Tenant reaching status.phase: Active now means the same thing rbacstore.tenants. status='active' does, not two different claims — see enterprise/internal/tenantcrd's and operator/internal/controller/ tenant_controller.go's doc comments for the full split, and enterprise-api -provision-tenant's TENANT_CRD_NAMESPACE env var (set automatically by the Helm chart when tenantOperator.enabled) to turn this on. Deliberately not built: the operator's reconcile loop itself calling ClickHouse/rbacstore directly (a "full unification" option considered and set aside — it would give the operator two new credential sets and require real reconcile-loop idempotency design for an inherently one-shot external side effect, a bigger and riskier change than this repo's provisioning story needed to close the actual gap, which was two disconnected sources of truth, not two actors).

Verification status -- read before trusting this against a real cluster

Now verified against a real live Kubernetes cluster. kind/ kubectl/helm were installed without root (static binaries into ~/.local/bin), a real local cluster was created, every image this chart references was built and loaded into it, and the full two-tenant walkthrough (helm/sentry/README.md) was run end to end -- both tenants reached Tenant.status.phase: Active with real generated ClickHouse credentials in their Secrets. See /docs/phase-4-runbook.md §7 for the exact commands and the two real chart bugs this run found and fixed (enterprise-auth missing its Postgres connection env vars entirely, ClickHouse missing the env var that grants CREATE USER privilege) -- neither helm lint, helm template, nor the kubeconform schema check below could have caught either, since both only manifest once real pods actually try to start and connect to each other.

What was verified offline, before real cluster access existed (still true, kept as additional evidence, not superseded by the above):

  • deploy/operator: go build/go vet/go test ./... all pass, including reconciler tests against controller-runtime's fake client (internal/controller/tenant_controller_test.go) -- real reconcile logic exercised, but not against a real apiserver (no envtest binaries available; see that test file's doc comment).
  • enterprise/internal/tenantcrd (the "lightweight unification" half -provision-tenant runs): go test passes against k8s.io/client-go's fake dynamic and typed clientsets -- real client library, fake transport, same shape as enterprise/internal/ searchclient's in-process gRPC tests. What this doesn't prove: that sentry.io/v1alpha1.Tenant's real CRD schema (a real apiserver's OpenAPI validation) accepts exactly what this package writes -- the helm template/kubeconform check below covers the schema shape, not a live write against it.
  • deploy/operator/config/crd/sentry.io_tenants.yaml: parsed with sigs.k8s.io/yaml + strict-unmarshaled into the real k8s.io/apiextensions-apiserver CustomResourceDefinition Go type -- catches YAML syntax errors and structural mistakes, not a live-cluster admission check.
  • deploy/helm/sentry: helm lint passes; helm template renders cleanly under both default values and a enterprise.enabled: true + two-tenant override; the rendered output was checked with kubeconform -strict against the real Kubernetes 1.31 OpenAPI schema for every built-in resource kind (22-31 resources depending on values, 0 invalid) -- this catches schema mistakes (wrong field names, wrong types) but not whether the resources actually reconcile correctly together on a live cluster (Job/StatefulSet startup ordering, PVC provisioning, actual pod scheduling). Specifically confirmed by parsing the rendered YAML (not just eyeballing it): exactly one Deployment/Service named sentry-api renders in each mode, with the enterprise.enabled: true render using the enterprise-api image and the default render using plain api's. Also confirmed for the tenantOperator.enabled: true case: enterprise-api gets its own ServiceAccount/Role/RoleBinding (exactly tenants/tenants/status/ secrets, no more), tenant-operator's own ClusterRole no longer grants secrets at all, and TENANT_CRD_NAMESPACE is set on enterprise-api's container only when tenantOperator.enabled is true.
  • Docker image builds (operator/Dockerfile and every other Dockerfile this chart references) are now confirmed working too -- all twelve images this chart needs were built and loaded into the test kind cluster above.

Before relying on this in production: ingest needs a real cert-manager (or equivalent) issued Secret, not hack/dev-certs's throwaway dev certs; ClickHouse/Postgres data isn't backed by anything durable beyond the cluster's own PVC provisioner in this chart; and only Auth0 has been tried as a real external IdP so far (see /docs/phase-4-runbook.md §3a/§3b).