Files
cairnobs/ingest/cmd/ingest/main.go
T
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

147 lines
5.3 KiB
Go

// Command ingest is the Sentry ingest service. It has two halves that can
// run in one process or be split across deployments via --mode:
//
// - server: mTLS gRPC front end that agents push batches to; forwards
// them onto Redpanda unchanged.
// - consumer: reads back off Redpanda, normalizes, batch-writes to
// ClickHouse.
// - all (default): both, in one process — the Phase 0 / docker-compose
// shape. Splitting into separate deployments later is a k8s manifest
// change, not a code change.
package main
import (
"context"
"flag"
"fmt"
"log/slog"
"os"
"os/signal"
"syscall"
"github.com/jackc/pgx/v5/pgxpool"
"golang.org/x/sync/errgroup"
"github.com/sentry/sentry/ingest/clickhousewriter"
"github.com/sentry/sentry/ingest/consumer"
"github.com/sentry/sentry/ingest/internal/agentregistry"
"github.com/sentry/sentry/ingest/internal/config"
"github.com/sentry/sentry/ingest/internal/grpcserver"
"github.com/sentry/sentry/ingest/internal/producer"
"github.com/sentry/sentry/ingest/internal/tenantresolver"
logsv1 "github.com/sentry/sentry/proto/sentry/logs/v1"
)
// singleTenantWriter adapts *clickhousewriter.Writer -- which only
// knows how to write to the one ClickHouse database it was constructed
// with -- to consumer.chWriter's tenant-tagged signature, by simply
// ignoring the tag. This is this binary's single-tenant behavior,
// unchanged from before per-tenant ingest credentials existed: every
// record lands in the same shared database regardless of which tenant
// (if any) it was resolved to. enterprise/cmd/enterprise-ingest is
// where a tag-respecting writer (enterprise/internal/chwriter.Registry)
// actually routes per tenant instead.
type singleTenantWriter struct {
w *clickhousewriter.Writer
}
func (s singleTenantWriter) WriteBatch(ctx context.Context, records []consumer.Record) error {
plain := make([]*logsv1.LogRecord, len(records))
for i, r := range records {
plain[i] = r.Record
}
return s.w.WriteBatch(ctx, plain)
}
func main() {
mode := flag.String("mode", "all", "which half of ingest to run: server | consumer | all")
flag.Parse()
if *mode != "server" && *mode != "consumer" && *mode != "all" {
fmt.Fprintf(os.Stderr, "unknown --mode %q, must be server|consumer|all\n", *mode)
os.Exit(1)
}
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
cfg, err := config.Load()
if err != nil {
logger.Error("loading config", "error", err)
os.Exit(1)
}
for _, w := range cfg.DevCredentialWarnings() {
logger.Warn(w)
}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
g, ctx := errgroup.WithContext(ctx)
if *mode == "server" || *mode == "all" {
p := producer.New(cfg.Redpanda)
defer p.Close()
// resolver stays nil (every batch's tenant_id header is simply
// never set) unless ENTERPRISE_AUTH_URL is configured -- matches
// every other "off unless configured" optional dependency in
// this codebase.
var resolver grpcserver.TenantResolver
if cfg.EnterpriseAuthURL != "" {
resolver = tenantresolver.New(cfg.EnterpriseAuthURL)
logger.Info("ingest tenant resolution configured", "enterprise_auth_url", cfg.EnterpriseAuthURL)
} else {
logger.Info("ENTERPRISE_AUTH_URL not set -- ingest records carry no tenant_id, single-tenant behavior")
}
// agents stays nil (CheckIn always reports "no override," nothing
// recorded) unless AGENT_REGISTRY_POSTGRES_ADDR is configured --
// same "off unless configured" shape as resolver above. Uses its
// own pgxpool rather than sharing one across mode=server/consumer
// -- consumer's half of this binary has no Postgres dependency at
// all today and shouldn't gain one just because server's did.
var agents grpcserver.AgentRegistry
if cfg.AgentRegistry.Postgres.Addr != "" {
dsn := fmt.Sprintf("postgres://%s:%s@%s/%s",
cfg.AgentRegistry.Postgres.Username, cfg.AgentRegistry.Postgres.Password,
cfg.AgentRegistry.Postgres.Addr, cfg.AgentRegistry.Postgres.Database)
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
logger.Error("opening agent registry postgres pool", "error", err)
os.Exit(1)
}
defer pool.Close()
agents = agentregistry.New(pool)
logger.Info("agent registry configured", "postgres_addr", cfg.AgentRegistry.Postgres.Addr)
} else {
logger.Info("AGENT_REGISTRY_POSTGRES_ADDR not set -- agent check-ins are accepted but not recorded, no remote config")
}
srv := grpcserver.New(logger, cfg.GRPC, cfg.TLS, p, resolver, agents)
g.Go(func() error { return srv.Run(ctx) })
}
if *mode == "consumer" || *mode == "all" {
chw, err := clickhousewriter.New(ctx, clickhousewriter.Config{
Addr: cfg.ClickHouse.Addr, Database: cfg.ClickHouse.Database,
Username: cfg.ClickHouse.Username, Password: cfg.ClickHouse.Password,
})
if err != nil {
logger.Error("connecting to clickhouse", "error", err)
os.Exit(1)
}
defer chw.Close()
c := consumer.New(logger, consumer.Config{
Brokers: cfg.Redpanda.Brokers, Topic: cfg.Redpanda.Topic, ConsumerGroup: cfg.Redpanda.ConsumerGroup,
BatchMaxSize: cfg.Batch.MaxSize, FlushIntervalMS: cfg.Batch.FlushIntervalMS,
}, singleTenantWriter{w: chw})
g.Go(func() error { return c.Run(ctx) })
}
logger.Info("ingest started", "mode", *mode)
if err := g.Wait(); err != nil {
logger.Error("ingest exited with error", "error", err)
os.Exit(1)
}
}