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.
sentryctl agents list|get, config get|set|clear, restart -- same
list/get shape as dashboards/alerts, plus a config sub-subcommand
mirroring dashboards' permissions since an override has its own
lifecycle distinct from the agent resource itself.
config set is the one command with real logic: since PUT
/agents/{host}/config replaces the whole stored override rather than
patching individual fields, it fetches the agent's current effective
config first and merges only the flags actually passed on top of it,
mirroring the web UI's edit form logic in Go. restart requires
explicit confirmation (interactive y/N or --yes) and refuses on
non-interactive stdin without --yes, the same posture cmd_query.go's
--nl/--execute already established for anything that changes what's
running.
Live-verified the merge logic specifically, since it's the part most
likely to hide a real bug: setting one field on a clean agent
correctly carried forward its other reported values, and a second
config set call correctly carried forward the first call's override
rather than resetting it to the reported baseline. config clear and
restart --yes both round-tripped against a live agent, with the
restart picked up and acted on within one check-in cycle.
This closes out the agent-management punch list (restart, fleet-wide
alerting, this CLI surface) -- see /docs/agent-management-design.md.
PUT/DELETE /dashboards/{id}/permissions/{userId} (per-resource dashboard
grants, built earlier this phase) had no caller but Go tests and curl --
named as a real, disclosed gap in docs/phase-4-runbook.md. Adds a CLI
surface: sentryctl dashboards permissions list/grant/revoke, following
the existing dashboards subcommand pattern.
grant/revoke needed a new httpclient.go helper (httpMutateNoBody) since
both endpoints respond 204 No Content -- the existing helpers all expect
a JSON body to pretty-print. grant validates the role client-side
(viewer/editor only, mirroring api/dashboards.validGrantRole) before
making a request, since Admin/Owner already have tenant-wide dashboard
access and a resource-level grant can never raise someone past Editor.
Verified with real httptest.Server round trips (method, path, request
body, and error-body parsing on a 501 from a deployment with no
enterprise permission service wired in) -- the same pattern every other
sentryctl subcommand's tests already use, no fake/mock client needed
since sentryctl itself is just an HTTP client with no store of its own.
RBAC (api/internal/authz) is live on /query and /dashboards, backed by a
new enterprise/ module (session issuance, audit logging, RBAC storage,
OIDC/SAML protocol wiring) that core never imports -- only calls over
HTTP. Found and fixed a real cross-tenant vulnerability in dashboards
(no tenant_id filtering at all) while writing the threat model doc.
Two things are explicitly NOT done, documented rather than hidden:
tenant isolation for log data itself (/query still shares one ClickHouse
connection and Tantivy index across every tenant -- RBAC controls who
can query, not what a query can see), and human SSO login (protocol
wiring exists, no HTTP handler calls it yet). See
docs/security/threat-model.md and docs/phase-4-runbook.md.
Also adds deploy/ (Go Operator + Helm chart, validated offline only --
no cluster was reachable in this environment).
Saved, shareable multi-panel dashboards (table/line/bar/single-stat
panels via gridstack + uPlot, global + per-panel time range, JSON
export/import) and threshold/absence alert rules with an
ok/pending/firing evaluator and webhook/Slack/PagerDuty delivery.
- New /metadata component: Postgres control-plane store for dashboards,
panels, notification targets, alert rules/state, and delivery log --
see docs/phase-3-dashboard-design.md for why ClickHouse's MergeTree
family isn't a fit for this access pattern (needs real row-level
locking and read-your-writes consistency).
- api/internal/dashboards: dashboard/panel CRUD, pure -- panel query
execution stays client-side, reusing the existing /query endpoint.
- New /alerting service: rule/target CRUD, a ticker-driven evaluator
(claim-then-evaluate concurrency control, transactional-outbox
delivery, query errors and threshold zero-rows never coerced into a
false transition) and webhook/Slack/PagerDuty delivery with
retry/backoff. See docs/phase-3-alerting-design.md for the full
state-machine design and the four correctness properties it
implements.
- web: /dashboards and /alerts UIs; cli: sentryctl dashboards/alerts
list/get/apply, seeding a future Terraform provider's JSON contract.
- hack/alert-load-test: 500 rules against real ClickHouse data, real
measured results in docs/phase-3-runbook.md.
Five real bugs found by actually running this against a live stack
(documented in the runbook, not just fixed silently): a latent Phase 2
bug where ClickHouse rejected the timestamp format used for
earliest=/latest= queries; a "now" literal token injected into query
text; a GridStack/uPlot layout-timing race; JS's Date.parse being too
lenient to use as a timestamp-detection heuristic; a rule's "enabled"
field silently defaulting to false when omitted; and the evaluator's
claim-batch-size and worker-pool-concurrency defaulting to the same
value, causing 500 concurrently-due rules to take 125s to cycle through
instead of the configured 60s.