Phase 4: SSO scaffolding, RBAC enforcement, tenant-scoped dashboards, audit logging, K8s deployment

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).
This commit is contained in:
2026-08-13 22:16:59 -07:00
parent 9435115ab7
commit 3eb0f4c589
116 changed files with 8589 additions and 126 deletions
+91
View File
@@ -0,0 +1,91 @@
// Package config loads enterprise-auth's configuration from environment
// variables, same convention as every other Go service in this repo.
package config
import (
"fmt"
"os"
)
type Config struct {
HTTPListenAddr string
Postgres PostgresConfig
OIDC OIDCConfig
SAML SAMLConfig
SessionSigningKey []byte
}
type PostgresConfig struct {
Addr string
Database string
Username string
Password string
}
// OIDCConfig is optional -- a deployment might configure OIDC, SAML,
// both, or (during early rollout) neither yet. Load() doesn't fail if
// these are unset; internal/oidc.New is only called once IssuerURL is
// actually present.
type OIDCConfig struct {
IssuerURL string
ClientID string
ClientSecret string
RedirectURL string
}
// SAMLConfig is likewise optional. Note this only records *presence* --
// enough for /auth/features (internal/authhandler) to report
// saml_enabled -- it does not itself fetch/parse IDPMetadataURL into the
// *saml.EntityDescriptor internal/saml.New requires; that fetch (and the
// login/ACS HTTP handlers that would use it) is deferred, same as OIDC's
// login/callback handlers -- see cmd/enterprise-auth/main.go's doc
// comment.
type SAMLConfig struct {
EntityID string
ACSURL string
IDPMetadataURL string
}
func Load() (Config, error) {
cfg := Config{
HTTPListenAddr: getenv("HTTP_LISTEN_ADDR", ":8082"),
Postgres: PostgresConfig{
Addr: getenv("POSTGRES_ADDR", "localhost:5432"),
Database: getenv("POSTGRES_DATABASE", "sentry_metadata"),
Username: getenv("POSTGRES_USERNAME", "sentry"),
Password: getenv("POSTGRES_PASSWORD", ""),
},
OIDC: OIDCConfig{
IssuerURL: getenv("OIDC_ISSUER_URL", ""),
ClientID: getenv("OIDC_CLIENT_ID", ""),
ClientSecret: getenv("OIDC_CLIENT_SECRET", ""),
RedirectURL: getenv("OIDC_REDIRECT_URL", ""),
},
SAML: SAMLConfig{
EntityID: getenv("SAML_ENTITY_ID", ""),
ACSURL: getenv("SAML_ACS_URL", ""),
IDPMetadataURL: getenv("SAML_IDP_METADATA_URL", ""),
},
}
// Required, unlike OIDC/SAML above: every enterprise-auth deployment
// issues and validates session/service tokens (internal/session),
// even one that hasn't configured any IdP yet. 32 bytes matches
// internal/session.MinSigningKeyBytes -- not imported here to avoid
// a config->session dependency for one constant, but the two values
// must be kept in sync.
signingKey := getenv("ENTERPRISE_SESSION_SIGNING_KEY", "")
if len(signingKey) < 32 {
return Config{}, fmt.Errorf("ENTERPRISE_SESSION_SIGNING_KEY must be set to at least 32 bytes (got %d)", len(signingKey))
}
cfg.SessionSigningKey = []byte(signingKey)
return cfg, nil
}
func getenv(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}