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
2 changes: 2 additions & 0 deletions api/v1alpha1/envoygateway_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,8 @@ type ExtensionAPISettings struct {
// DisableLua determines if Lua EnvoyExtensionPolicies should be disabled.
// If set to true, the Lua EnvoyExtensionPolicy feature will be disabled.
DisableLua bool `json:"disableLua"`
// EnableSDSSecretRef enables read SDS(Secret Discovery Service) settings from a secret(with type gateway.envoyproxy.io/sds).
EnableSDSSecretRef bool `json:"enableSDSSecretRef"`
Copy link
Copy Markdown
Member

@zhaohuabing zhaohuabing Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are gating the broader "fetching certificates from third-party SDS" feature, not just the specific "SDS secret type"?

It might makes sense to use a more general name like EnableThirdPartySDS here.

cc @arkodg

}

// EnvoyGatewayProvider defines the desired configuration of a provider.
Expand Down
3 changes: 3 additions & 0 deletions api/v1alpha1/shared_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import (
)

const (
// SDSSecretType is the type for secrets that reference SDS configuration
SDSSecretType = "gateway.envoyproxy.io/sds"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we make this opt in at EnvoyGateway or EnvoyProxy level ?
cc @guydc

// DefaultDeploymentReplicas is the default number of deployment replicas.
DefaultDeploymentReplicas = 1
// DefaultDeploymentCPUResourceRequests for deployment cpu resource
Expand Down
79 changes: 65 additions & 14 deletions internal/gatewayapi/backendtlspolicy.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,15 +262,14 @@ func (t *Translator) processServerValidationTLSSettings(

if !tlsConfig.InsecureSkipVerify {
tlsConfig.UseSystemTrustStore = ptr.Deref(backend.Spec.TLS.WellKnownCACertificates, "") == gwapiv1.WellKnownCACertificatesSystem

if tlsConfig.UseSystemTrustStore {
tlsConfig.CACertificate = &ir.TLSCACertificate{
Name: fmt.Sprintf("%s/%s-ca", backend.Name, backend.Namespace),
}
} else if len(backend.Spec.TLS.CACertificateRefs) > 0 {
caRefs := getObjectReferences(gwapiv1.Namespace(backend.Namespace), backend.Spec.TLS.CACertificateRefs)
// Backend doesn't allow cross-namespace reference, so pass nil resources here.
caCert, err := t.getCaCertsFromCARefs(nil, caRefs, resource.ResourceMetadata{
caCert, sds, err := t.getCaCertsFromCARefs(nil, caRefs, resource.ResourceMetadata{
Name: backend.Name,
Namespace: backend.Namespace,
Kind: resource.KindBackendTLSPolicy,
Expand All @@ -282,6 +281,7 @@ func (t *Translator) processServerValidationTLSSettings(
tlsConfig.CACertificate = &ir.TLSCACertificate{
Certificate: caCert,
Name: fmt.Sprintf("%s/%s-ca", backend.Name, backend.Namespace),
SDS: sds,
}
}
}
Expand Down Expand Up @@ -404,8 +404,27 @@ func (t *Translator) processClientTLSSettings(
)
return tlsConfig, err
}
tlsConfig.ClientCertificates = append(tlsConfig.ClientCertificates, getTLSCertificateFromSecret(secret))
// Check if this is an SDS reference secret
if secret.Type == egv1a1.SDSSecretType {
if !t.SDSSecretRefEnabled {
return tlsConfig, fmt.Errorf("SDS Secret reference is not enabled in EnvoyGateway configuration")
}
// For SDS reference secrets, extract the SDS secret name and URL from data
s, err := ir.NewSDSConfig(secret)
if err != nil {
return tlsConfig, fmt.Errorf("invalid SDS reference secret: %w", err)
}
tlsConfig.ClientCertificates = []ir.TLSCertificate{
{
SDS: s,
},
}
} else {
// Regular secret processing
tlsConfig.ClientCertificates = append(tlsConfig.ClientCertificates, getTLSCertificateFromSecret(secret))
}
}

return tlsConfig, nil
}

Expand Down Expand Up @@ -472,7 +491,7 @@ func (t *Translator) getBackendTLSBundle(backendTLSPolicy *gwapiv1.BackendTLSPol
caRefs := getObjectReferences(gwapiv1.Namespace(backendTLSPolicy.Namespace), backendTLSPolicy.Spec.Validation.CACertificateRefs)
// BackendTLSPolicy doesn't allow cross-namespace reference,
// so pass nil resources here
caCert, err := t.getCaCertsFromCARefs(nil, caRefs, resource.ResourceMetadata{
caCert, sds, err := t.getCaCertsFromCARefs(nil, caRefs, resource.ResourceMetadata{
Group: egv1a1.GroupName,
Name: backendTLSPolicy.Name,
Namespace: backendTLSPolicy.Namespace,
Expand All @@ -481,10 +500,13 @@ func (t *Translator) getBackendTLSBundle(backendTLSPolicy *gwapiv1.BackendTLSPol
if err != nil {
return nil, err
}

tlsBundle.CACertificate = &ir.TLSCACertificate{
Certificate: caCert,
Name: fmt.Sprintf("%s/%s-ca", backendTLSPolicy.Name, backendTLSPolicy.Namespace),
SDS: sds,
}

return tlsBundle, nil
}

Expand All @@ -503,9 +525,11 @@ func getObjectReferences(ns gwapiv1.Namespace, refs []gwapiv1.LocalObjectReferen

// getCaCertsFromCARefs retrieves CA certificates from the given CA refs. It supports ConfigMap, Secret, and ClusterTrustBundle kinds.
// TODO: move out of backendtlspolicy.go
func (t *Translator) getCaCertsFromCARefs(resources *resource.Resources, caCertificates []gwapiv1.ObjectReference, meta resource.ResourceMetadata) ([]byte, error) {
func (t *Translator) getCaCertsFromCARefs(resources *resource.Resources, caCertificates []gwapiv1.ObjectReference, meta resource.ResourceMetadata,
) (caCert []byte, sds *ir.SDSConfig, err error) {
ca := ""
foundSupportedRef := false
var foundSDSConfig *ir.SDSConfig
for _, caRef := range caCertificates {
kind := string(caRef.Kind)
var caRefNs string
Expand All @@ -531,7 +555,7 @@ func (t *Translator) getCaCertsFromCARefs(resources *resource.Resources, caCerti
},
resources.ReferenceGrants,
) {
return nil, fmt.Errorf("%w for caCertificateRef %s/%s (kind: %s, namespace: %s)", ErrRefNotPermitted, caRef.Group, caRef.Name, kind, caRefNs)
return nil, nil, fmt.Errorf("%w for caCertificateRef %s/%s (kind: %s, namespace: %s)", ErrRefNotPermitted, caRef.Group, caRef.Name, kind, caRefNs)
}
}

Expand All @@ -546,25 +570,41 @@ func (t *Translator) getCaCertsFromCARefs(resources *resource.Resources, caCerti
}
ca += crt
} else {
return nil, fmt.Errorf("no ca found in configmap %s", cm.Name)
return nil, nil, fmt.Errorf("no ca found in configmap %s", cm.Name)
}
} else {
return nil, fmt.Errorf("configmap %s not found in namespace %s", caRef.Name, caRefNs)
return nil, nil, fmt.Errorf("configmap %s not found in namespace %s", caRef.Name, caRefNs)
}
case resource.KindSecret:
foundSupportedRef = true
secret := t.GetSecret(caRefNs, string(caRef.Name))
if secret != nil {
// Check if this is an SDS reference secret
if secret.Type == egv1a1.SDSSecretType {
if !t.SDSSecretRefEnabled {
return nil, nil, fmt.Errorf("SDS Secret reference is not enabled in EnvoyGateway configuration")
}
if foundSDSConfig != nil {
return nil, nil, fmt.Errorf("multiple SDS reference secrets are not supported")
}
// For SDS reference secrets, extract the SDS secret name and URL from data
foundSDSConfig, err = ir.NewSDSConfig(secret)
if err != nil {
return nil, nil, fmt.Errorf("invalid SDS reference secret %s: %w", secret.Name, err)
}
continue
}
// Regular secret processing
if crt, dataOk := getOrFirstFromData(secret.Data, CACertKey); dataOk {
if ca != "" {
ca += "\n"
}
ca += string(crt)
} else {
return nil, fmt.Errorf("no ca found in secret %s", secret.Name)
return nil, nil, fmt.Errorf("no ca found in secret %s", secret.Name)
}
} else {
return nil, fmt.Errorf("secret %s not found in namespace %s", caRef.Name, caRefNs)
return nil, nil, fmt.Errorf("secret %s not found in namespace %s", caRef.Name, caRefNs)
}
case resource.KindClusterTrustBundle:
foundSupportedRef = true
Expand All @@ -575,18 +615,29 @@ func (t *Translator) getCaCertsFromCARefs(resources *resource.Resources, caCerti
}
ca += ctb.Spec.TrustBundle
} else {
return nil, fmt.Errorf("cluster trust bundle %s not found", caRef.Name)
return nil, nil, fmt.Errorf("cluster trust bundle %s not found", caRef.Name)
}
}
}

// Validate that SDS is not mixed with regular certificates
if foundSDSConfig != nil && ca != "" {
return nil, nil, fmt.Errorf("cannot mix SDS reference secrets with other CA certificate types")
}

// Return SDS config if found
if foundSDSConfig != nil {
return nil, foundSDSConfig, nil
}

// Return regular certificates if found
if ca == "" {
if !foundSupportedRef {
return nil, fmt.Errorf("%w in caCertificateRefs", ErrInvalidCACertificateKind)
return nil, nil, fmt.Errorf("%w in caCertificateRefs", ErrInvalidCACertificateKind)
}
return nil, ErrNoValidCACertificate
return nil, nil, ErrNoValidCACertificate
}
return []byte(ca), nil
return []byte(ca), nil, nil
}

func getAncestorRefs(policy *gwapiv1.BackendTLSPolicy) []*gwapiv1.ParentReference {
Expand Down
6 changes: 4 additions & 2 deletions internal/gatewayapi/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (t *Translator) ProcessGatewayTLS(gateways []*GatewayContext, resources *re
gtwDefaultFrontendTLSValidation := gtw.Spec.TLS.Frontend.Default
allowInsecureFallback := false
if gtwDefaultFrontendTLSValidation.Validation != nil {
caCert, err := t.getCaCertsFromCARefs(resources, gtwDefaultFrontendTLSValidation.Validation.CACertificateRefs, resource.ResourceMetadata{
caCert, sds, err := t.getCaCertsFromCARefs(resources, gtwDefaultFrontendTLSValidation.Validation.CACertificateRefs, resource.ResourceMetadata{
Group: gwapiv1.GroupVersion.Group,
Kind: resource.KindGateway,
Name: gtw.Name,
Expand All @@ -70,6 +70,7 @@ func (t *Translator) ProcessGatewayTLS(gateways []*GatewayContext, resources *re
TLSCACertificate: &ir.TLSCACertificate{
Name: irGatewayTLSCACertName(gtw.Gateway, "default"),
Certificate: caCert,
SDS: sds,
},
Mode: frontendValidationMode(gtwDefaultFrontendTLSValidation.Validation.Mode),
}
Expand All @@ -87,7 +88,7 @@ func (t *Translator) ProcessGatewayTLS(gateways []*GatewayContext, resources *re
gtwPerPortCaCertificate[portValidation.Port] = nil
continue
}
caCert, err := t.getCaCertsFromCARefs(resources, portValidation.TLS.Validation.CACertificateRefs, resource.ResourceMetadata{
caCert, sds, err := t.getCaCertsFromCARefs(resources, portValidation.TLS.Validation.CACertificateRefs, resource.ResourceMetadata{
Group: gwapiv1.GroupVersion.Group,
Kind: resource.KindGateway,
Name: gtw.Name,
Expand All @@ -106,6 +107,7 @@ func (t *Translator) ProcessGatewayTLS(gateways []*GatewayContext, resources *re
TLSCACertificate: &ir.TLSCACertificate{
Name: irGatewayTLSCACertName(gtw.Gateway, strconv.Itoa(int(portValidation.Port))),
Certificate: caCert,
SDS: sds,
},
Mode: frontendValidationMode(portValidation.TLS.Validation.Mode),
}
Expand Down
1 change: 1 addition & 0 deletions internal/gatewayapi/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ func (r *Runner) subscribeAndTranslate(sub <-chan watchable.Snapshot[string, *re
GlobalRateLimitEnabled: r.EnvoyGateway.RateLimit != nil,
EnvoyPatchPolicyEnabled: r.EnvoyGateway.ExtensionAPIs != nil && r.EnvoyGateway.ExtensionAPIs.EnableEnvoyPatchPolicy,
BackendEnabled: r.EnvoyGateway.ExtensionAPIs != nil && r.EnvoyGateway.ExtensionAPIs.EnableBackend,
SDSSecretRefEnabled: r.EnvoyGateway.ExtensionAPIs != nil && r.EnvoyGateway.ExtensionAPIs.EnableSDSSecretRef,
ControllerNamespace: r.ControllerNamespace,
GatewayNamespaceMode: r.EnvoyGateway.GatewayNamespaceMode(),
MergeGateways: gatewayapi.IsMergeGatewaysEnabled(resources),
Expand Down
95 changes: 95 additions & 0 deletions internal/gatewayapi/testdata/sds-disabled.in.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
envoyProxyForGatewayClass:
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
namespace: envoy-gateway-system
name: test
spec:
backendTLS:
clientCertificateRef:
kind: Secret
name: sds-client-cert-default
alpnProtocols:
# without this, need to enforce mTLS with PeerAuthentication
- istio
gateways:
- apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
namespace: envoy-gateway
name: gateway-1
spec:
gatewayClassName: envoy-gateway-class
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: All
backends:
- apiVersion: gateway.envoyproxy.io/v1alpha1
kind: Backend
metadata:
name: spire-uds
namespace: envoy-gateway-system
spec:
endpoints:
- unix:
path: /var/run/secrets/workload-spiffe-uds/socket
type: Endpoints
backendTLSPolicies:
- apiVersion: gateway.networking.k8s.io/v1alpha2
kind: BackendTLSPolicy
metadata:
name: policy-btls
namespace: default
spec:
targetRefs:
- kind: Service
name: service-1
validation:
caCertificateRefs:
- name: sds-client-cert-rootca
kind: Secret
hostname: example.com
httpRoutes:
- apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: httproute-1
namespace: default
spec:
parentRefs:
- namespace: envoy-gateway
name: gateway-1
sectionName: http
rules:
- matches:
- path:
type: Exact
value: "/exact"
backendRefs:
- name: service-1
port: 8080
secrets:
- apiVersion: v1
kind: Secret
metadata:
name: sds-client-cert-default
namespace: envoy-gateway-system
type: gateway.envoyproxy.io/sds
data:
# /var/run/secrets/workload-spiffe-uds/socket
url: L3Zhci9ydW4vc2VjcmV0cy93b3JrbG9hZC1zcGlmZmUtdWRzL3NvY2tldA==
name: ZGVmYXVsdA== # base64 for "default"
- apiVersion: v1
kind: Secret
metadata:
name: sds-client-cert-rootca
namespace: default
type: gateway.envoyproxy.io/sds
data:
# /var/run/secrets/workload-spiffe-uds/socket
url: L3Zhci9ydW4vc2VjcmV0cy93b3JrbG9hZC1zcGlmZmUtdWRzL3NvY2tldA==
secretName: Uk9PVENB # base64 for "ROOTCA"
Loading
Loading