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
21 changes: 16 additions & 5 deletions gateway/gateway-controller/cmd/controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,16 @@ func main() {
policyEngineConnected := make(chan struct{})

// Start xDS gRPC server with SDS support
xdsServer := xds.NewServer(snapshotManager, sdsSecretManager, cfg.Controller.Server.XDSPort, log, routerConnected)
var xdsServerOpts []xds.ServerOption
if cfg.Controller.Server.XDSTLS.Enabled {
xdsTLSConfig, err := config.BuildXDSServerTLSConfig(cfg.Controller.Server.XDSTLS)
if err != nil {
log.Error("invalid server.xds_tls config, refusing to start main xDS server in plaintext", slog.Any("error", err))
os.Exit(1)
}
xdsServerOpts = append(xdsServerOpts, xds.WithMTLS(xdsTLSConfig, cfg.Controller.Server.XDSTLS.AllowedClientIdentities))
}
xdsServer := xds.NewServer(snapshotManager, sdsSecretManager, cfg.Controller.Server.XDSPort, log, routerConnected, xdsServerOpts...)
go func() {
if err := xdsServer.Start(); err != nil {
log.Error("xDS server failed", slog.Any("error", err))
Expand Down Expand Up @@ -490,10 +499,12 @@ func main() {
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 := config.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, nil, cfg.Controller.PolicyServer.Port, log, serverOpts...)
go func() {
Expand Down
53 changes: 38 additions & 15 deletions gateway/gateway-controller/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,11 @@ type ServerConfig struct {
ShutdownTimeout time.Duration `koanf:"shutdown_timeout"`
GatewayID string `koanf:"gateway_id"`
SkipInvalidDeploymentsOnStartup bool `koanf:"skip_invalid_deployments_on_startup"`

// XDSTLS switches the main xDS gRPC server (serving Envoy, on XDSPort)
// from plaintext to mutual TLS. Off by default; see XDSServerTLSConfig
// for why xDS has no server-only mode.
XDSTLS XDSServerTLSConfig `koanf:"xds_tls"`
}

// AdminServerConfig holds controller admin HTTP server configuration.
Expand Down Expand Up @@ -337,15 +342,8 @@ type PprofConfig struct {

// PolicyServerConfig holds policy xDS server-related configuration
type PolicyServerConfig struct {
Port int `koanf:"port"`
TLS PolicyServerTLS `koanf:"tls"`
}

// PolicyServerTLS holds TLS configuration for the policy xDS server
type PolicyServerTLS struct {
Enabled bool `koanf:"enabled"`
CertFile string `koanf:"cert_file"`
KeyFile string `koanf:"key_file"`
Port int `koanf:"port"`
TLS XDSServerTLSConfig `koanf:"tls"`
}

// PoliciesConfig holds policy-related configuration
Expand Down Expand Up @@ -568,8 +566,8 @@ type UpstreamTLS struct {
// hybrid post-quantum group ("X25519MLKEM768", FIPS 203 ML-KEM-768 + X25519) followed by
// classical curves, so key exchange degrades gracefully to classical for peers that don't yet
// support the hybrid group.
EcdhCurves string `koanf:"ecdh_curves"`
TrustedCertPath string `koanf:"trusted_cert_path"`
EcdhCurves string `koanf:"ecdh_curves"`
TrustedCertPath string `koanf:"trusted_cert_path"`
CustomCertsPath string `koanf:"custom_certs_path"` // Directory containing custom trusted certificates
VerifyHostName bool `koanf:"verify_host_name"`
DisableSslVerification bool `koanf:"disable_ssl_verification"`
Expand Down Expand Up @@ -842,6 +840,16 @@ func defaultConfig() *Config {
ShutdownTimeout: 15 * time.Second,
GatewayID: constants.PlatformGatewayId,
SkipInvalidDeploymentsOnStartup: false,
XDSTLS: XDSServerTLSConfig{
Enabled: false,
CertFile: "./xds-certs/server.crt",
KeyFile: "./xds-certs/server.key",
ClientCAFile: "./xds-certs/ca.crt",
MinimumProtocolVersion: "TLS1_2",
MaximumProtocolVersion: "TLS1_3",
Ciphers: "",
EcdhCurves: "X25519,P-256",
},
},
AdminServer: AdminServerConfig{
Enabled: true,
Expand All @@ -858,10 +866,15 @@ func defaultConfig() *Config {
},
PolicyServer: PolicyServerConfig{
Port: 18001,
TLS: PolicyServerTLS{
Enabled: false,
CertFile: "./certs/server.crt",
KeyFile: "./certs/server.key",
TLS: XDSServerTLSConfig{
Enabled: false,
CertFile: "./xds-certs/server.crt",
KeyFile: "./xds-certs/server.key",
ClientCAFile: "./xds-certs/ca.crt",
MinimumProtocolVersion: "TLS1_2",
MaximumProtocolVersion: "TLS1_3",
Ciphers: "",
EcdhCurves: "X25519,P-256",
},
},
Policies: PoliciesConfig{
Expand Down Expand Up @@ -1367,6 +1380,16 @@ func (c *Config) Validate() error {
return fmt.Errorf("server.gateway_id is required and cannot be empty")
}

// Validate main xDS server mTLS config (serves Envoy on server.xds_port)
if err := ValidateXDSServerTLS("server.xds_tls", c.Controller.Server.XDSTLS); err != nil {
return err
}

// Validate policy xDS server mTLS config (serves the policy-engine on policy_server.port)
if err := ValidateXDSServerTLS("policy_server.tls", c.Controller.PolicyServer.TLS); err != nil {
return err
}

if c.Controller.AdminServer.Enabled {
if c.Controller.AdminServer.Port < 1 || c.Controller.AdminServer.Port > 65535 {
return fmt.Errorf("admin_server.port must be between 1 and 65535, got: %d", c.Controller.AdminServer.Port)
Expand Down
95 changes: 95 additions & 0 deletions gateway/gateway-controller/pkg/config/server_tls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package config

import (
"crypto/tls"
"fmt"
"strings"

"github.com/wso2/api-platform/httpkit/tlsconfig"
)

// ParseServerEcdhCurves parses a comma-separated EcdhCurves preference list
// (e.g. "X25519MLKEM768,X25519,P-256") into the tls.CurveID slice consumed by
// tls.Config.CurvePreferences. Used both to fail config validation closed on
// an unrecognized curve name and to build the REST API TLS listener's config.
//
// The name-to-tls.CurveID vocabulary is sourced from httpkit/tlsconfig (the
// shared, direction-neutral implementation); this function keeps its own
// wrapper for the "empty string is an error" behavior, which differs from
// tlsconfig.ParseCurvePreferences's "empty means use Go's defaults" stance —
// an explicit, always-on TLS listener config has no notion of "unset".
func ParseServerEcdhCurves(raw string) ([]tls.CurveID, error) {
parts := strings.Split(raw, ",")
curves := make([]tls.CurveID, 0, len(parts))
for _, part := range parts {
name := strings.TrimSpace(part)
if name == "" {
continue
}
curve, ok := tlsconfig.CurvesByName[name]
if !ok {
return nil, fmt.Errorf("unsupported ecdh curve %q (supported: X25519, P-256, P-384, P-521, X25519MLKEM768)", name)
}
curves = append(curves, curve)
}
if len(curves) == 0 {
return nil, fmt.Errorf("must specify at least one ecdh curve")
}
return curves, nil
}

// ValidateServerTLSVersions checks that min and max are both recognized
// version names and that min does not come after max. Unlike
// tlsconfig.ValidateVersionRange (which treats both-empty as "use Go's
// defaults"), this listener's config always requires both fields to name a
// real version — there is no "unset" state for an always-on TLS listener.
// tls.VersionTLSxx constants are monotonically increasing, so comparing the
// parsed values directly replaces a separate ordering table.
func ValidateServerTLSVersions(minVersion, maxVersion string) error {
minV, ok := tlsconfig.ParseVersion(minVersion)
if !ok {
return fmt.Errorf("minimum_protocol_version must be one of TLS1_0, TLS1_1, TLS1_2, TLS1_3, got: %q", minVersion)
}
maxV, ok := tlsconfig.ParseVersion(maxVersion)
if !ok {
return fmt.Errorf("maximum_protocol_version must be one of TLS1_0, TLS1_1, TLS1_2, TLS1_3, got: %q", maxVersion)
}
if minV > maxV {
return fmt.Errorf("minimum_protocol_version (%s) cannot be greater than maximum_protocol_version (%s)", minVersion, maxVersion)
}
return nil
}

// ParseServerTLSVersion converts a validated version name to its crypto/tls
// identifier. Callers should run ValidateServerTLSVersions first; an
// unrecognized name here returns ok=false rather than panicking.
func ParseServerTLSVersion(name string) (version uint16, ok bool) {
return tlsconfig.ParseVersion(name)
}

// ParseServerCiphers parses a comma-separated list of Go crypto/tls cipher
// suite names (e.g. "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256") into the
// []uint16 consumed by tls.Config.CipherSuites. An empty string is valid and
// returns (nil, nil) — Go's own default suite set/order applies. Only
// affects TLS 1.2 (and below) connections; TLS 1.3 ignores this field.
func ParseServerCiphers(raw string) ([]uint16, error) {
return tlsconfig.ParseCipherSuites(raw)
}
Loading
Loading