Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 43 additions & 12 deletions event-gateway/gateway-controller/cmd/controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import (
"github.com/wso2/api-platform/common/eventhub"
commonmodels "github.com/wso2/api-platform/common/models"
"github.com/wso2/api-platform/common/webhooksecret"
"github.com/wso2/api-platform/httpkit/httpclient"

"github.com/wso2/api-platform/gateway/gateway-controller/pkg/adminserver"
"github.com/wso2/api-platform/gateway/gateway-controller/pkg/api/handlers"
Expand Down Expand Up @@ -460,7 +461,12 @@ func main() {

serverOpts := []policyxds.ServerOption{policyxds.WithOnFirstConnect(policyEngineConnected)}
if cfg.Controller.PolicyServer.TLS.Enabled {
serverOpts = append(serverOpts, policyxds.WithTLS(cfg.Controller.PolicyServer.TLS.CertFile, cfg.Controller.PolicyServer.TLS.KeyFile))
policyXDSTLSConfig, err := coreconfig.BuildXDSServerTLSConfig(cfg.Controller.PolicyServer.TLS)
if err != nil {
log.Error("invalid policy_server.tls config, refusing to start policy xDS server in plaintext", slog.Any("error", err))
os.Exit(1)
}
serverOpts = append(serverOpts, policyxds.WithMTLS(policyXDSTLSConfig, cfg.Controller.PolicyServer.TLS.AllowedClientIdentities))
}
policyXDSServer := policyxds.NewServer(policySnapshotManager, apiKeySnapshotManager, lazyResourceSnapshotManager, subscriptionSnapshotManager, webhookSecretSnapshotManager, cfg.Controller.PolicyServer.Port, log, serverOpts...)
go func() {
Expand All @@ -481,15 +487,29 @@ func main() {
policyValidator := coreconfig.NewPolicyValidator(policyDefinitions)
validator.SetPolicyValidator(policyValidator)

apiSvc := utils.NewAPIDeploymentService(configStore, db, snapshotManager, validator, &cfg.Router, eventHubInstance, gatewayID, secretsService)
// Build the single shared outbound *http.Client used by every control-plane /
// platform-API call this process makes, built once here and injected into every
// constructor below instead of each one building (or caching) its own.
httpClientCfg, err := coreconfig.BuildHTTPClientConfig(cfg.Controller.HTTPClient, cfg.Controller.ControlPlane.InsecureSkipVerify)
if err != nil {
log.Error("Invalid controller.http_client configuration", slog.Any("error", err))
os.Exit(1)
}
httpClient, err := httpclient.New(httpClientCfg)
if err != nil {
log.Error("Failed to build HTTP client", slog.Any("error", err))
os.Exit(1)
}

apiSvc := utils.NewAPIDeploymentService(configStore, db, snapshotManager, validator, &cfg.Router, eventHubInstance, gatewayID, secretsService, httpClient)
mcpSvc := utils.NewMCPDeploymentService(configStore, db, snapshotManager, policyManager, policyValidator, eventHubInstance, gatewayID, secretsService)
llmSvc := utils.NewLLMDeploymentService(configStore, db, snapshotManager, lazyResourceXDSManager, templateDefinitions, apiSvc, &cfg.Router, policyVersionResolver, policyValidator)

cpClient := controlplane.NewClient(
cfg.Controller.ControlPlane, log, configStore, db, snapshotManager, validator, &cfg.Router,
apiKeyXDSManager, apiKeyStore, &cfg.APIKey, policyManager, cfg, policyDefinitions,
lazyResourceXDSManager, templateDefinitions, subscriptionSnapshotManager, eventHubInstance,
secretsService, webhookSecretStore, webhookSecretSnapshotManager,
secretsService, webhookSecretStore, webhookSecretSnapshotManager, httpClient,
)
cpClient.SetControlPlaneEventGatewayHooks(controlplanehooks.Hooks{})
if err := cpClient.Start(); err != nil {
Expand All @@ -499,8 +519,6 @@ func main() {
llmSvc.SetControlPlanePusher(cpClient, cfg.Controller.ControlPlane.DeploymentSyncEnabled)
mcpSvc.SetControlPlanePusher(cpClient, cfg.Controller.ControlPlane.DeploymentSyncEnabled)

httpClient := &http.Client{Timeout: 10 * time.Second}

restAPIService := restapi.NewRestAPIService(
configStore, db, snapshotManager, policyManager, apiSvc, apiKeyXDSManager,
cpClient, &cfg.Router, cfg, httpClient, coreconfig.NewParser(), validator, log,
Expand Down Expand Up @@ -544,11 +562,15 @@ func main() {
os.Exit(1)
}

apiServer := handlers.NewAPIServer(
apiServer, err := handlers.NewAPIServer(
configStore, db, snapshotManager, policyManager, lazyResourceXDSManager, log, cpClient,
policyDefinitions, templateDefinitions, validator, apiKeyXDSManager, cfg, eventHubInstance,
subscriptionSnapshotManager, secretsService, restAPIService,
subscriptionSnapshotManager, secretsService, restAPIService, httpClient,
)
if err != nil {
log.Error("Failed to create API server", slog.Any("error", err))
os.Exit(1)
}

eventGatewayHandler := handler.NewWebSubServer(handler.Deps{
Store: configStore,
Expand Down Expand Up @@ -627,7 +649,7 @@ func main() {
var controllerAdminServer *adminserver.Server
if cfg.Controller.AdminServer.Enabled {
adminAuthz := authenticators.AuthorizationMiddleware(
commonmodels.AuthConfig{ResourceRoles: adminResourceRoles()}, log)
commonmodels.AuthConfig{ResourceRoles: adminResourceRoles(), HTTPClient: httpClient}, log)
adminProtect := func(next http.Handler) http.Handler {
return authMiddleWare(adminAuthz(next))
}
Expand Down Expand Up @@ -810,9 +832,16 @@ func generateAuthConfig(cfg *coreconfig.Config) (commonmodels.AuthConfig, error)
}, nil
}

// adminResourceRoles builds the deny-by-default scope map for the controller
// admin/debug server (config_dump, xds_sync_status, pprof), gating every
// endpoint except the public health probe behind the "admin" role. Mirrors
// gateway-controller's core main.go adminResourceRoles implementation.
func adminResourceRoles() map[string][]string {
const adminRole = "admin"

// prefixed builds a resource key of the form "<METHOD> <AdminAPIBasePath><path>"
// matching the versioned routes registered via HandlerWithOptions(BaseURL=AdminAPIBasePath),
// mirroring the prefixed helper in generateAuthConfig.
prefixed := func(methodAndPath string) string {
idx := strings.Index(methodAndPath, " ")
if idx < 0 {
Expand All @@ -826,8 +855,10 @@ func adminResourceRoles() map[string][]string {
"GET /xds_sync_status": {adminRole},
}

// pprof endpoints are registered directly on the mux rather than under a
// BaseURL, so their pattern carries no method and no base-path prefix.
// pprof endpoints are registered directly on the mux (not via a BaseURL), so
// their r.Pattern carries no method and no base-path prefix — map them as-is.
// Only registered when admin_server.pprof is enabled, but mapping them
// unconditionally is harmless and keeps them admin-gated when it is.
pprofRoles := map[string][]string{
"/debug/pprof/": {adminRole},
"/debug/pprof/cmdline": {adminRole},
Expand All @@ -836,8 +867,8 @@ func adminResourceRoles() map[string][]string {
"/debug/pprof/trace": {adminRole},
}

// The admin API is served on both the versioned and the legacy unprefixed
// paths, so both keys are needed for the authz middleware to match.
// Populate both the versioned and legacy (unprefixed) keys for each relative
// route so the authz middleware matches either form, exactly as generateAuthConfig does.
resourceRoles := make(map[string][]string, len(relativeRoles)*2+len(pprofRoles))
for methodAndPath, roles := range relativeRoles {
resourceRoles[prefixed(methodAndPath)] = roles
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func registerConnectors(registry *connectors.Registry, cfg *config.Config) {
DeliveryConcurrency: cfg.WebSub.DeliveryConcurrency,
RuntimeID: cfg.RuntimeID,
ConsumerGroupPrefix: cfg.Kafka.ConsumerGroupPrefix,
HTTPClient: cfg.HTTPClient,
})
})

Expand Down
97 changes: 97 additions & 0 deletions event-gateway/gateway-runtime/configs/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,29 @@ websub_https_port = 8443
websub_tls_enabled = true
websub_tls_cert_file = "/etc/event-gateway/tls/default-listener.crt"
websub_tls_key_file = "/etc/event-gateway/tls/default-listener.key"
# Inbound TLS tuning for the WebSub-HTTPS listener. All four fields are optional —
# empty uses Go's crypto/tls defaults — but min/max version must both be set or both
# left empty. Same vocabulary as http_client.tls above.
websub_tls_min_version = "TLS1_2"
websub_tls_max_version = "TLS1_3"
# Hybrid PQC group listed first with classical fallbacks after — a peer that doesn't
# yet support X25519MLKEM768 still succeeds via the later entries.
websub_tls_curve_preferences = "X25519MLKEM768,X25519,P-256"
# Comma-separated Go crypto/tls cipher suite names; only affects TLS 1.2 and below.
# Empty uses Go's own default secure set.
websub_tls_cipher_suites = ""
websocket_port = 8081
# HTTPS port for WebSocket server (used when TLS is enabled for WebBrokerApi)
websocket_https_port = 8444
# Set to true to serve WebBrokerApi WebSocket connections over WSS (TLS).
websocket_tls_enabled = true
websocket_tls_cert_file = "/etc/event-gateway/tls/default-listener.crt"
websocket_tls_key_file = "/etc/event-gateway/tls/default-listener.key"
# Inbound TLS tuning for the WebSocket-HTTPS listener — same shape as websub_tls_* above.
websocket_tls_min_version = "TLS1_2"
websocket_tls_max_version = "TLS1_3"
websocket_tls_curve_preferences = "X25519MLKEM768,X25519,P-256"
websocket_tls_cipher_suites = ""
admin_port = 9002
metrics_port = 9003

Expand Down Expand Up @@ -59,6 +75,87 @@ subscriptions_topic_name = "__subscriptions"
# config_file = ""
# chains_file = ""

# Shared HTTP client configuration for WebSub's outbound calls to subscriber-supplied
# CallbackURLs: intent verification (subscribe/unsubscribe challenge — see
# internal/connectors/receiver/websub/verification.go) and message delivery (see
# .../delivery.go). Both clients share this same pooling/TLS/proxy/SSRF posture; each
# keeps its own existing timeout (websub.verification_timeout_seconds above for
# verification; a fixed 30s delivery timeout for delivery) which always overrides
# http_client.timeouts.overall below for that client.
#
# SSRF guarding is ALWAYS enabled for both clients and is not configurable off: every
# CallbackURL dialed here is tenant/subscriber-supplied, exactly the scenario
# ssrf-prevention.md targets. netguard.PermitPrivateBlockMetadata() is applied
# unconditionally in code (private/loopback backends stay reachable — in-cluster
# subscribers are the common case — while link-local/metadata/unspecified/multicast
# destinations are refused at dial time). Only the redirect/scheme knobs below
# (http_client.ssrf) are configurable.

[http_client.pooling]
max_idle_conns = 100
max_idle_conns_per_host = 10
max_conns_per_host = 100
idle_conn_timeout = "90s"
keep_alive = "30s"
disable_keep_alives = false
# HTTP/2 is off by default: this client always sets a custom TLS config, which makes Go's
# Transport conservatively disable HTTP/2 unless re-enabled here — only opt in after
# considering the connection-coalescing caveat in httpclient.PoolingConfig.EnableHTTP2's doc.
enable_http2 = false

[http_client.timeouts]
# Common default only: the Verifier and Deliverer each override this with their own
# existing per-call-site timeout — see this section's header comment above.
overall = "30s"
dial = "10s"
tls_handshake = "10s"
response_header = "10s"
expect_continue = "1s"
# 0 = package default (10MiB); a negative value disables the response-size bound entirely.
max_response_bytes = 0

[http_client.tls]
min_version = "TLS1_2"
max_version = "TLS1_3"
# Comma-separated TLS 1.3 key-exchange groups, most preferred first. Hybrid PQC group
# listed first with classical fallbacks after — a peer that doesn't yet support
# X25519MLKEM768 still succeeds via the later entries.
curve_preferences = "X25519MLKEM768,X25519,P-256"
# Comma-separated Go crypto/tls cipher suite names; only affects TLS 1.2 and below. Empty
# uses Go's own default secure set.
cipher_suites = ""
# PEM CA bundle / mTLS client cert+key for the outbound call to the subscriber callback.
# Empty uses the system root pool and no client certificate.
root_ca_file = ""
client_cert_file = ""
client_key_file = ""

[http_client.proxy]
# "none" (default) | "environment" (HTTP_PROXY/HTTPS_PROXY/NO_PROXY) | "url"
mode = "none"
url = ""
username = ""
password = ""
no_proxy = []
# Required ("delegated" or "manual_connect") whenever mode != "none" — SSRF guarding is
# always enabled for this client, see this section's header comment above.
egress = ""

[http_client.proxy.tls]
# Configures a SEPARATE TLS handshake to an https:// proxy itself, decoupled from
# http_client.tls above (which always governs the origin handshake).
root_ca_file = ""
client_cert_file = ""
client_key_file = ""
insecure_skip_verify = false

[http_client.ssrf]
# SSRF guarding itself is always enabled in code (not configurable off — see this
# section's header comment above); only the redirect/scheme knobs are exposed here.
# 0 uses netguard's own default (5 redirects); empty allowed_schemes defaults to {"https"}.
max_redirects = 0
allowed_schemes = []

[logging]
level = "info"
format = "text"
Expand Down
2 changes: 1 addition & 1 deletion event-gateway/gateway-runtime/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
github.com/wso2/api-platform/common v0.0.0
github.com/wso2/api-platform/gateway/gateway-runtime/policy-engine v0.0.0-00010101000000-000000000000
github.com/wso2/api-platform/sdk/core v0.4.0
github.com/wso2/api-platform/httpkit v0.0.0-local
google.golang.org/grpc v1.82.1
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
Expand Down Expand Up @@ -46,7 +47,6 @@ require (
github.com/prometheus/procfs v0.19.2 // indirect
github.com/stoewer/go-strcase v1.3.1 // indirect
github.com/twmb/franz-go/pkg/kmsg v1.13.1 // indirect
github.com/wso2/api-platform/httpkit v0.0.0-local // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
Expand Down
Loading
Loading