From 686328d336337a50a0afe941b5d72a060f2f0410 Mon Sep 17 00:00:00 2001 From: zirain Date: Tue, 14 Apr 2026 16:25:47 +0800 Subject: [PATCH 1/7] feat: add support for certificate fetching and rotation via SDS ref secret Signed-off-by: zirain --- internal/gatewayapi/backendtlspolicy.go | 82 +++++- internal/gatewayapi/listener.go | 6 +- .../testdata/envoyproxy-with-sds.in.yaml | 95 +++++++ .../testdata/envoyproxy-with-sds.out.yaml | 255 ++++++++++++++++++ internal/ir/xds.go | 16 ++ internal/ir/zz_generated.deepcopy.go | 25 ++ internal/xds/translator/listener.go | 10 + internal/xds/translator/sds.go | 186 +++++++++++++ .../translator/testdata/in/xds-ir/sds.yaml | 58 ++++ .../testdata/out/xds-ir/sds.clusters.yaml | 89 ++++++ .../testdata/out/xds-ir/sds.endpoints.yaml | 24 ++ .../testdata/out/xds-ir/sds.listeners.yaml | 35 +++ .../testdata/out/xds-ir/sds.routes.yaml | 29 ++ internal/xds/translator/translator.go | 78 ++++-- 14 files changed, 949 insertions(+), 39 deletions(-) create mode 100644 internal/gatewayapi/testdata/envoyproxy-with-sds.in.yaml create mode 100644 internal/gatewayapi/testdata/envoyproxy-with-sds.out.yaml create mode 100644 internal/xds/translator/sds.go create mode 100644 internal/xds/translator/testdata/in/xds-ir/sds.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/sds.clusters.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/sds.endpoints.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/sds.listeners.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/sds.routes.yaml diff --git a/internal/gatewayapi/backendtlspolicy.go b/internal/gatewayapi/backendtlspolicy.go index f1f863ea2e..73cbbd1a5d 100644 --- a/internal/gatewayapi/backendtlspolicy.go +++ b/internal/gatewayapi/backendtlspolicy.go @@ -22,6 +22,11 @@ import ( "github.com/envoyproxy/gateway/internal/utils" ) +const ( + // SDSSecretType is the type for secrets that reference SDS configuration + SDSSecretType = "gateway.envoyproxy.io/sds-ref" +) + var ( ErrRefNotPermitted = fmt.Errorf("cross-namespace reference is not permitted by any ReferenceGrant") ErrInvalidCACertificateKind = fmt.Errorf("Unsupported reference kind, supported kinds are ConfigMap, Secret, and ClusterTrustBundle") @@ -262,7 +267,6 @@ 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), @@ -270,7 +274,7 @@ func (t *Translator) processServerValidationTLSSettings( } 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, @@ -282,6 +286,7 @@ func (t *Translator) processServerValidationTLSSettings( tlsConfig.CACertificate = &ir.TLSCACertificate{ Certificate: caCert, Name: fmt.Sprintf("%s/%s-ca", backend.Name, backend.Namespace), + SDS: sds, } } } @@ -404,8 +409,35 @@ 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 == SDSSecretType { + // For SDS reference secrets, extract the SDS secret name and URL from data + sdsSecretName, hasSecretName := secret.Data["secretName"] + sdsURLBytes, hasURL := secret.Data["url"] + if hasSecretName && len(sdsSecretName) > 0 && hasURL && len(sdsURLBytes) > 0 { + tlsConfig.ClientCertificates = []ir.TLSCertificate{ + { + Name: string(sdsSecretName), + SDS: &ir.SDSConfig{ + SecretName: string(sdsSecretName), + URL: string(sdsURLBytes), + }, + }, + } + } else { + if !hasSecretName || len(sdsSecretName) == 0 { + err = fmt.Errorf("no secretName found in SDS reference secret %s", secret.Name) + } else { + err = fmt.Errorf("no url found in SDS reference secret %s", secret.Name) + } + return tlsConfig, err + } + } else { + // Regular secret processing + tlsConfig.ClientCertificates = append(tlsConfig.ClientCertificates, getTLSCertificateFromSecret(secret)) + } } + return tlsConfig, nil } @@ -472,7 +504,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, @@ -481,10 +513,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 } @@ -503,7 +538,8 @@ 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 for _, caRef := range caCertificates { @@ -531,7 +567,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) } } @@ -546,25 +582,43 @@ 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 == SDSSecretType { + // For SDS reference secrets, extract the SDS secret name and URL from data + sdsSecretName, hasSecretName := secret.Data["secretName"] + sdsURLBytes, hasURL := secret.Data["url"] + // TODO: support more sds options if needed. + if !hasSecretName || len(sdsSecretName) == 0 { + return nil, nil, fmt.Errorf("no secretName found in SDS reference secret %s", secret.Name) + } + if !hasURL || len(sdsURLBytes) == 0 { + return nil, nil, fmt.Errorf("no url found in SDS reference secret %s", secret.Name) + } + return nil, &ir.SDSConfig{ + SecretName: string(sdsSecretName), + URL: string(sdsURLBytes), + }, nil + } + // 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 @@ -575,18 +629,18 @@ 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) } } } 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 { diff --git a/internal/gatewayapi/listener.go b/internal/gatewayapi/listener.go index a9f01a7e48..c46d6d559c 100644 --- a/internal/gatewayapi/listener.go +++ b/internal/gatewayapi/listener.go @@ -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, @@ -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), } @@ -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, @@ -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), } diff --git a/internal/gatewayapi/testdata/envoyproxy-with-sds.in.yaml b/internal/gatewayapi/testdata/envoyproxy-with-sds.in.yaml new file mode 100644 index 0000000000..79d90c6573 --- /dev/null +++ b/internal/gatewayapi/testdata/envoyproxy-with-sds.in.yaml @@ -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-ref + data: + # /var/run/secrets/workload-spiffe-uds/socket + url: L3Zhci9ydW4vc2VjcmV0cy93b3JrbG9hZC1zcGlmZmUtdWRzL3NvY2tldA== + secretName: ZGVmYXVsdA== # base64 for "default" + - apiVersion: v1 + kind: Secret + metadata: + name: sds-client-cert-rootca + namespace: default + type: gateway.envoyproxy.io/sds-ref + data: + # /var/run/secrets/workload-spiffe-uds/socket + url: L3Zhci9ydW4vc2VjcmV0cy93b3JrbG9hZC1zcGlmZmUtdWRzL3NvY2tldA== + secretName: Uk9PVENB # base64 for "ROOTCA" diff --git a/internal/gatewayapi/testdata/envoyproxy-with-sds.out.yaml b/internal/gatewayapi/testdata/envoyproxy-with-sds.out.yaml new file mode 100644 index 0000000000..0017e2c5a3 --- /dev/null +++ b/internal/gatewayapi/testdata/envoyproxy-with-sds.out.yaml @@ -0,0 +1,255 @@ +backendTLSPolicies: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: BackendTLSPolicy + metadata: + name: policy-btls + namespace: default + spec: + targetRefs: + - group: "" + kind: Service + name: service-1 + validation: + caCertificateRefs: + - group: "" + kind: Secret + name: sds-client-cert-rootca + hostname: example.com + status: + ancestors: + - ancestorRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: Resolved all the Object references. + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +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 + status: + conditions: + - lastTransitionTime: null + message: The Backend was accepted + reason: Accepted + status: "True" + type: Accepted +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-1 + namespace: default + spec: + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + type: Exact + value: /exact + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + config: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + name: test + namespace: envoy-gateway-system + spec: + backendTLS: + alpnProtocols: + - istio + clientCertificateRef: + kind: Secret + name: sds-client-cert-default + logging: {} + status: {} + listeners: + - name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-1 + namespace: envoy-gateway-system +xdsIR: + envoy-gateway/gateway-1: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + protocol: TCP + http: + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - '*' + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + kind: Service + name: service-1 + namespace: default + sectionName: "8080" + name: httproute/default/httproute-1/rule/0/backend/0 + protocol: HTTP + tls: + alpnProtocols: + - istio + caCertificate: + name: policy-btls/default-ca + sds: + secretName: ROOTCA + url: /var/run/secrets/workload-spiffe-uds/socket + clientCertificates: + - name: default + sds: + secretName: default + url: /var/run/secrets/workload-spiffe-uds/socket + sni: example.com + weight: 1 + hostname: '*' + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/* + pathMatch: + distinct: false + exact: /exact + name: "" + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 diff --git a/internal/ir/xds.go b/internal/ir/xds.go index f45d3c0cb0..3dbb631630 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -471,6 +471,8 @@ type TLSConfig struct { type TLSCertificate struct { // Name of the Secret object. Name string `json:"name" yaml:"name"` + // SDS holds the configuration for a Secret Discovery Service (SDS) server. + SDS *SDSConfig `json:"sds,omitempty" yaml:"sds,omitempty"` // Certificate can be either a client or server certificate. Certificate []byte `json:"certificate,omitempty" yaml:"certificate,omitempty"` // PrivateKey for the server. @@ -479,6 +481,18 @@ type TLSCertificate struct { OCSPStaple []byte `json:"ocspStaple,omitempty" yaml:"ocspStaple,omitempty"` } +// SDSConfig holds the configuration for a Secret Discovery Service (SDS) server. +// +k8s:deepcopy-gen=true +type SDSConfig struct { + // SecretName is an identifier for the SDS configuration. + SecretName string `json:"secretName" yaml:"secretName"` + // URL is the URL of the SDS server + URL string `json:"url" yaml:"url"` + + // TODO: support additional SDS configuration options + // such as TLS settings for the SDS server, or authentication credentials if needed. +} + // TLSCrl holds a single CRL's details // +k8s:deepcopy-gen=true type TLSCrl struct { @@ -497,6 +511,8 @@ type TLSCACertificate struct { Name string `json:"name,omitempty" yaml:"name,omitempty"` // Certificate content. Certificate []byte `json:"certificate,omitempty" yaml:"certificate,omitempty"` + // SDS holds the configuration for a Secret Discovery Service (SDS) server. + SDS *SDSConfig `json:"sds,omitempty" yaml:"sds,omitempty"` } // SubjectAltName holds the subject alternative name for the certificate diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index 5beedef87e..7a7ac34fd2 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -4042,6 +4042,21 @@ func (in *RouteDestination) DeepCopy() *RouteDestination { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SDSConfig) DeepCopyInto(out *SDSConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SDSConfig. +func (in *SDSConfig) DeepCopy() *SDSConfig { + if in == nil { + return nil + } + out := new(SDSConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SecurityFeatures) DeepCopyInto(out *SecurityFeatures) { *out = *in @@ -4492,6 +4507,11 @@ func (in *TLSCACertificate) DeepCopyInto(out *TLSCACertificate) { *out = make([]byte, len(*in)) copy(*out, *in) } + if in.SDS != nil { + in, out := &in.SDS, &out.SDS + *out = new(SDSConfig) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSCACertificate. @@ -4507,6 +4527,11 @@ func (in *TLSCACertificate) DeepCopy() *TLSCACertificate { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSCertificate) DeepCopyInto(out *TLSCertificate) { *out = *in + if in.SDS != nil { + in, out := &in.SDS, &out.SDS + *out = new(SDSConfig) + **out = **in + } if in.Certificate != nil { in, out := &in.Certificate, &out.Certificate *out = make([]byte, len(*in)) diff --git a/internal/xds/translator/listener.go b/internal/xds/translator/listener.go index 4eb683fe36..854b9dd3f2 100644 --- a/internal/xds/translator/listener.go +++ b/internal/xds/translator/listener.go @@ -1049,6 +1049,11 @@ func buildXdsTLSCertSecret(tlsConfig *ir.TLSCertificate) *tlsv3.Secret { } } + // For SDS based CA certificate, the secret will be generated by SDS server, so we can skip adding the secret here + if tlsConfig.SDS != nil { + return nil + } + return &tlsv3.Secret{ Name: tlsConfig.Name, Type: &tlsv3.Secret_TlsCertificate{ @@ -1069,6 +1074,11 @@ func buildXdsTLSCaCertSecret(caCertificate *ir.TLSCACertificate, crl *ir.TLSCrl) } validationContext.OnlyVerifyLeafCertCrl = crl.OnlyVerifyLeafCertificate } + // For SDS based CA certificate, the secret will be generated by SDS server, so we can skip adding the secret here + if caCertificate.SDS != nil { + return nil + } + return &tlsv3.Secret{ Name: caCertificate.Name, Type: &tlsv3.Secret_ValidationContext{ diff --git a/internal/xds/translator/sds.go b/internal/xds/translator/sds.go new file mode 100644 index 0000000000..468b729269 --- /dev/null +++ b/internal/xds/translator/sds.go @@ -0,0 +1,186 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package translator + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "strings" + "time" + + cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" + corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + endpoint "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" + tlsv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" + resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/envoyproxy/gateway/internal/ir" + "github.com/envoyproxy/gateway/internal/xds/types" +) + +const defaultConnectionTimeout = 10 * time.Second + +func sdsSecretConfig(secretName, clusterName string) *tlsv3.SdsSecretConfig { + return &tlsv3.SdsSecretConfig{ + Name: secretName, + SdsConfig: &corev3.ConfigSource{ + ConfigSourceSpecifier: &corev3.ConfigSource_ApiConfigSource{ + ApiConfigSource: &corev3.ApiConfigSource{ + ApiType: corev3.ApiConfigSource_GRPC, + GrpcServices: []*corev3.GrpcService{ + { + TargetSpecifier: &corev3.GrpcService_EnvoyGrpc_{ + EnvoyGrpc: &corev3.GrpcService_EnvoyGrpc{ + ClusterName: clusterName, + }, + }, + }, + }, + }, + }, + }, + } +} + +// sdsClusterNameFromURL generates a unique cluster name from an SDS URL +func sdsClusterNameFromURL(url string) string { + // Sanitize the URL to create a valid cluster name + // For Unix domain sockets like "/var/run/sds", create a meaningful name + if strings.HasPrefix(url, "/") { + // Unix domain socket path + sanitized := strings.ReplaceAll(url, "/", "_") + sanitized = strings.Trim(sanitized, "_") + return fmt.Sprintf("sds_%s", sanitized) + } + // For other URLs, use a hash + hash := sha256.Sum256([]byte(url)) + return fmt.Sprintf("sds_%s", hex.EncodeToString(hash[:8])) +} + +// createSDSCluster creates an SDS cluster for the given URL +func createSDSCluster(tCtx *types.ResourceVersionTable, sdsURL string) error { + clusterName := sdsClusterNameFromURL(sdsURL) + + // Check if cluster already exists + if tCtx.XdsResources[resourcev3.ClusterType] != nil { + for _, resource := range tCtx.XdsResources[resourcev3.ClusterType] { + if c, ok := resource.(*cluster.Cluster); ok && c.Name == clusterName { + // Cluster already exists + return nil + } + } + } + + // Create the cluster based on the URL type + var c *cluster.Cluster + if strings.HasPrefix(sdsURL, "/") { + // Unix domain socket + c = &cluster.Cluster{ + Name: clusterName, + ClusterDiscoveryType: &cluster.Cluster_Type{ + Type: cluster.Cluster_STATIC, + }, + LoadAssignment: &endpoint.ClusterLoadAssignment{ + ClusterName: clusterName, + Endpoints: []*endpoint.LocalityLbEndpoints{ + { + LbEndpoints: []*endpoint.LbEndpoint{ + { + HostIdentifier: &endpoint.LbEndpoint_Endpoint{ + Endpoint: &endpoint.Endpoint{ + Address: &corev3.Address{ + Address: &corev3.Address_Pipe{ + Pipe: &corev3.Pipe{ + Path: sdsURL, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + ConnectTimeout: durationpb.New(defaultConnectionTimeout), + Http2ProtocolOptions: &corev3.Http2ProtocolOptions{}, + } + } else { + // TCP/IP address + return fmt.Errorf("TCP/IP SDS URLs are not yet supported: %s", sdsURL) + } + + if err := tCtx.AddXdsResource(resourcev3.ClusterType, c); err != nil { + return err + } + return nil +} + +// processSDSClusters scans the IR for SDS URLs and creates clusters for them +func processSDSClusters(tCtx *types.ResourceVersionTable, xdsIR *ir.Xds) error { + sdsURLs := make(map[string]bool) + + // Collect SDS URLs from HTTP listeners + for _, httpListener := range xdsIR.HTTP { + for _, route := range httpListener.Routes { + if route.Destination == nil { + continue + } + for _, dest := range route.Destination.Settings { + if dest.TLS != nil { + // Check CA certificate + if caCert := dest.TLS.CACertificate; caCert != nil { + if caCert.SDS != nil && caCert.SDS.URL != "" { + sdsURLs[caCert.SDS.URL] = true + } + } + // Check client certificates + for _, cert := range dest.TLS.ClientCertificates { + if cert.SDS != nil && cert.SDS.URL != "" { + sdsURLs[cert.SDS.URL] = true + } + } + } + } + } + } + + // Collect SDS URLs from TCP listeners + for _, tcpListener := range xdsIR.TCP { + for _, route := range tcpListener.Routes { + if route.Destination == nil { + continue + } + for _, dest := range route.Destination.Settings { + if dest.TLS != nil { + // Check CA certificate + if caCert := dest.TLS.CACertificate; caCert != nil { + if caCert.SDS != nil && caCert.SDS.URL != "" { + sdsURLs[caCert.SDS.URL] = true + } + } + // Check client certificates + for _, cert := range dest.TLS.ClientCertificates { + if cert.SDS != nil && cert.SDS.URL != "" { + sdsURLs[cert.SDS.URL] = true + } + } + } + } + } + } + + // Create clusters for each unique SDS URL + for url := range sdsURLs { + if err := createSDSCluster(tCtx, url); err != nil { + return err + } + } + + return nil +} diff --git a/internal/xds/translator/testdata/in/xds-ir/sds.yaml b/internal/xds/translator/testdata/in/xds-ir/sds.yaml new file mode 100644 index 0000000000..8e05a6b992 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/sds.yaml @@ -0,0 +1,58 @@ +http: + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - "*" + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + kind: Service + name: service-1 + namespace: default + sectionName: "8080" + name: httproute/default/httproute-1/rule/0/backend/0 + protocol: HTTP + tls: + alpnProtocols: + - istio + caCertificate: + sds: + secretName: ROOTCA + url: /var/run/secrets/workload-spiffe-uds/socket + clientCertificates: + - sds: + secretName: default + url: /var/run/secrets/workload-spiffe-uds/socket + sni: example.com + weight: 1 + hostname: "*" + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/* + pathMatch: + distinct: false + exact: /exact + name: "" diff --git a/internal/xds/translator/testdata/out/xds-ir/sds.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/sds.clusters.yaml new file mode 100644 index 0000000000..97e9bec4c6 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/sds.clusters.yaml @@ -0,0 +1,89 @@ +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: httproute/default/httproute-1/rule/0 + ignoreHealthOnHostRemoval: true + loadBalancingPolicy: + policies: + - typedExtensionConfig: + name: envoy.load_balancing_policies.least_request + typedConfig: + '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest + localityLbConfig: + localityWeightedLbConfig: {} + metadata: + filterMetadata: + envoy-gateway: + resources: + - kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0 + perConnectionBufferLimitBytes: 32768 + transportSocket: + name: dummy.transport_socket + typedConfig: + '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + commonTlsContext: {} + transportSocketMatches: + - match: + name: httproute/default/httproute-1/rule/0/tls/0 + name: httproute/default/httproute-1/rule/0/tls/0 + transportSocket: + name: envoy.transport_sockets.tls + typedConfig: + '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + commonTlsContext: + alpnProtocols: + - istio + combinedValidationContext: + defaultValidationContext: + matchTypedSubjectAltNames: + - matcher: + exact: example.com + sanType: DNS + validationContextSdsSecretConfig: + name: ROOTCA + sdsConfig: + apiConfigSource: + apiType: GRPC + grpcServices: + - envoyGrpc: + clusterName: sds_var_run_secrets_workload-spiffe-uds_socket + tlsCertificateSdsSecretConfigs: + - name: default + sdsConfig: + apiConfigSource: + apiType: GRPC + grpcServices: + - envoyGrpc: + clusterName: sds_var_run_secrets_workload-spiffe-uds_socket + sni: example.com + type: EDS + typedExtensionProtocolOptions: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + autoConfig: + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + httpProtocolOptions: {} +- connectTimeout: 10s + http2ProtocolOptions: {} + loadAssignment: + clusterName: sds_var_run_secrets_workload-spiffe-uds_socket + endpoints: + - lbEndpoints: + - endpoint: + address: + pipe: + path: /var/run/secrets/workload-spiffe-uds/socket + name: sds_var_run_secrets_workload-spiffe-uds_socket + type: STATIC diff --git a/internal/xds/translator/testdata/out/xds-ir/sds.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/sds.endpoints.yaml new file mode 100644 index 0000000000..153dbbf6c5 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/sds.endpoints.yaml @@ -0,0 +1,24 @@ +- clusterName: httproute/default/httproute-1/rule/0 + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 7.7.7.7 + portValue: 8080 + loadBalancingWeight: 1 + metadata: + filterMetadata: + envoy.transport_socket_match: + name: httproute/default/httproute-1/rule/0/tls/0 + loadBalancingWeight: 1 + locality: + region: httproute/default/httproute-1/rule/0/backend/0 + metadata: + filterMetadata: + envoy-gateway: + resources: + - kind: Service + name: service-1 + namespace: default + sectionName: "8080" diff --git a/internal/xds/translator/testdata/out/xds-ir/sds.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/sds.listeners.yaml new file mode 100644 index 0000000000..4d0fe90c54 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/sds.listeners.yaml @@ -0,0 +1,35 @@ +- address: + socketAddress: + address: 0.0.0.0 + portValue: 10080 + defaultFilterChain: + filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + suppressEnvoyHeaders: true + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: envoy-gateway/gateway-1/http + serverHeaderTransformation: PASS_THROUGH + statPrefix: http-10080 + useRemoteAddress: true + name: envoy-gateway/gateway-1/http + maxConnectionsToAcceptPerSocketEvent: 1 + name: envoy-gateway/gateway-1/http + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/sds.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/sds.routes.yaml new file mode 100644 index 0000000000..9b0bc0a534 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/sds.routes.yaml @@ -0,0 +1,29 @@ +- ignorePortInHostMatching: true + name: envoy-gateway/gateway-1/http + virtualHosts: + - domains: + - '*' + metadata: + filterMetadata: + envoy-gateway: + resources: + - kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http/* + routes: + - match: + path: /exact + metadata: + filterMetadata: + envoy-gateway: + resources: + - kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/* + route: + cluster: httproute/default/httproute-1/rule/0 + upgradeConfigs: + - upgradeType: websocket diff --git a/internal/xds/translator/translator.go b/internal/xds/translator/translator.go index 43703124b6..76c2b0ffa8 100644 --- a/internal/xds/translator/translator.go +++ b/internal/xds/translator/translator.go @@ -137,6 +137,10 @@ func (t *Translator) Translate(xdsIR *ir.Xds) (*types.ResourceVersionTable, erro errs = errors.Join(errs, err) } + if err := processSDSClusters(tCtx, xdsIR); err != nil { + errs = errors.Join(errs, err) + } + if err := processClusterForTracing(tCtx, xdsIR.Tracing, xdsIR.Metrics); err != nil { errs = errors.Join(errs, err) } @@ -409,18 +413,21 @@ func (t *Translator) processHTTPListenerXdsTranslation( if httpListener.TLS != nil { for c := range httpListener.TLS.Certificates { secret := buildXdsTLSCertSecret(&httpListener.TLS.Certificates[c]) - if err = tCtx.AddXdsResource(resourcev3.SecretType, secret); err != nil { - errs = errors.Join(errs, err) + if secret != nil { + if err = tCtx.AddXdsResource(resourcev3.SecretType, secret); err != nil { + errs = errors.Join(errs, err) + } } } if httpListener.TLS.CACertificate != nil { caSecret := buildXdsTLSCaCertSecret(httpListener.TLS.CACertificate, httpListener.TLS.Crl) - if err = tCtx.AddXdsResource(resourcev3.SecretType, caSecret); err != nil { - errs = errors.Join(errs, err) + if caSecret != nil { + if err = tCtx.AddXdsResource(resourcev3.SecretType, caSecret); err != nil { + errs = errors.Join(errs, err) + } } } - } // add http route client certs @@ -799,21 +806,27 @@ func (t *Translator) processTCPListenerXdsTranslation( // add tls route client certs for _, cert := range route.TLS.Terminate.ClientCertificates { secret := buildXdsTLSCertSecret(&cert) - if err := tCtx.AddXdsResource(resourcev3.SecretType, secret); err != nil { - errs = errors.Join(errs, err) + if secret != nil { + if err := tCtx.AddXdsResource(resourcev3.SecretType, secret); err != nil { + errs = errors.Join(errs, err) + } } } for _, s := range route.TLS.Terminate.Certificates { secret := buildXdsTLSCertSecret(&s) - if err := tCtx.AddXdsResource(resourcev3.SecretType, secret); err != nil { - errs = errors.Join(errs, err) + if secret != nil { + if err := tCtx.AddXdsResource(resourcev3.SecretType, secret); err != nil { + errs = errors.Join(errs, err) + } } } if route.TLS.Terminate.CACertificate != nil { caSecret := buildXdsTLSCaCertSecret(route.TLS.Terminate.CACertificate, route.TLS.Terminate.Crl) - if err := tCtx.AddXdsResource(resourcev3.SecretType, caSecret); err != nil { - errs = errors.Join(errs, err) + if caSecret != nil { + if err := tCtx.AddXdsResource(resourcev3.SecretType, caSecret); err != nil { + errs = errors.Join(errs, err) + } } } } else if route.Destination != nil { @@ -1109,8 +1122,10 @@ func addXdsCluster(tCtx *types.ResourceVersionTable, args *xdsClusterArgs) error if shouldValidateTLS { // Create an SDS secret for the CA certificate - either with inline bytes or with a filesystem ref secret := buildXdsUpstreamTLSCASecret(ds.TLS) - if err := tCtx.AddXdsResource(resourcev3.SecretType, secret); err != nil { - return err + if secret != nil { + if err := tCtx.AddXdsResource(resourcev3.SecretType, secret); err != nil { + return err + } } } } @@ -1179,6 +1194,12 @@ func buildXdsUpstreamTLSCASecret(tlsConfig *ir.TLSUpstreamConfig) *tlsv3.Secret }, } } + + // For SDS based CA certificate, the secret will be generated by SDS server, so we can skip adding the secret here + if tlsConfig.CACertificate.SDS != nil { + return nil + } + return &tlsv3.Secret{ Name: tlsConfig.CACertificate.Name, Type: &tlsv3.Secret_ValidationContext{ @@ -1199,6 +1220,12 @@ func buildValidationContext(tlsConfig *ir.TLSUpstreamConfig) (*tlsv3.CommonTlsCo }, DefaultValidationContext: &tlsv3.CertificateValidationContext{}, } + if tlsConfig.CACertificate.SDS != nil { + // get secret from SDS + sds := tlsConfig.CACertificate.SDS + clusterName := sdsClusterNameFromURL(sds.URL) + validationContext.ValidationContextSdsSecretConfig = sdsSecretConfig(sds.SecretName, clusterName) + } hasSANValidations := false // 3. If SubjectAltNames are specified, Hostname can be used for certificate selection @@ -1285,15 +1312,18 @@ func buildXdsUpstreamTLSSocketWthCert(tlsConfig *ir.TLSUpstreamConfig, requiresA tlsCtx.CommonTlsContext.AlpnProtocols = tlsConfig.ALPNProtocols } - if len(tlsConfig.ClientCertificates) > 0 { - for _, cert := range tlsConfig.ClientCertificates { - tlsCtx.CommonTlsContext.TlsCertificateSdsSecretConfigs = append( - tlsCtx.CommonTlsContext.TlsCertificateSdsSecretConfigs, - &tlsv3.SdsSecretConfig{ - Name: cert.Name, - SdsConfig: makeConfigSource(), - }) + for _, clientCert := range tlsConfig.ClientCertificates { + if sds := clientCert.SDS; sds != nil { + clusterName := sdsClusterNameFromURL(sds.URL) + sds := sdsSecretConfig(sds.SecretName, clusterName) + tlsCtx.CommonTlsContext.TlsCertificateSdsSecretConfigs = append(tlsCtx.CommonTlsContext.TlsCertificateSdsSecretConfigs, sds) + continue } + tlsCtx.CommonTlsContext.TlsCertificateSdsSecretConfigs = append(tlsCtx.CommonTlsContext.TlsCertificateSdsSecretConfigs, + &tlsv3.SdsSecretConfig{ + Name: clientCert.Name, + SdsConfig: makeConfigSource(), + }) } tlsCtxAny, err := proto.ToAnyWithValidation(tlsCtx) @@ -1315,8 +1345,10 @@ func processClientCertificates(tCtx *types.ResourceVersionTable, settings []*ir. if st.TLS != nil { for _, c := range st.TLS.ClientCertificates { secret := buildXdsTLSCertSecret(&c) - if err := tCtx.AddXdsResource(resourcev3.SecretType, secret); err != nil { - errs = errors.Join(errs, err) + if secret != nil { + if err := tCtx.AddXdsResource(resourcev3.SecretType, secret); err != nil { + errs = errors.Join(errs, err) + } } } } From 5c67cdadacf8a91cd062ae0c2671d5ae41ff7906 Mon Sep 17 00:00:00 2001 From: zirain Date: Tue, 14 Apr 2026 17:02:42 +0800 Subject: [PATCH 2/7] fix Signed-off-by: zirain --- internal/gatewayapi/backendtlspolicy.go | 20 +- .../gatewayapi/testdata/sds-invalid.in.yaml | 256 ++++++++++++++ .../gatewayapi/testdata/sds-invalid.out.yaml | 329 ++++++++++++++++++ ...nvoyproxy-with-sds.in.yaml => sds.in.yaml} | 4 +- ...oyproxy-with-sds.out.yaml => sds.out.yaml} | 0 internal/xds/translator/listener.go | 44 ++- 6 files changed, 636 insertions(+), 17 deletions(-) create mode 100644 internal/gatewayapi/testdata/sds-invalid.in.yaml create mode 100644 internal/gatewayapi/testdata/sds-invalid.out.yaml rename internal/gatewayapi/testdata/{envoyproxy-with-sds.in.yaml => sds.in.yaml} (96%) rename internal/gatewayapi/testdata/{envoyproxy-with-sds.out.yaml => sds.out.yaml} (100%) diff --git a/internal/gatewayapi/backendtlspolicy.go b/internal/gatewayapi/backendtlspolicy.go index 73cbbd1a5d..60052e41e4 100644 --- a/internal/gatewayapi/backendtlspolicy.go +++ b/internal/gatewayapi/backendtlspolicy.go @@ -542,6 +542,7 @@ func (t *Translator) getCaCertsFromCARefs(resources *resource.Resources, caCerti ) (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 @@ -603,10 +604,14 @@ func (t *Translator) getCaCertsFromCARefs(resources *resource.Resources, caCerti if !hasURL || len(sdsURLBytes) == 0 { return nil, nil, fmt.Errorf("no url found in SDS reference secret %s", secret.Name) } - return nil, &ir.SDSConfig{ + if foundSDSConfig != nil { + return nil, nil, fmt.Errorf("multiple SDS reference secrets are not supported") + } + foundSDSConfig = &ir.SDSConfig{ SecretName: string(sdsSecretName), URL: string(sdsURLBytes), - }, nil + } + continue } // Regular secret processing if crt, dataOk := getOrFirstFromData(secret.Data, CACertKey); dataOk { @@ -634,6 +639,17 @@ func (t *Translator) getCaCertsFromCARefs(resources *resource.Resources, caCerti } } + // 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, nil, fmt.Errorf("%w in caCertificateRefs", ErrInvalidCACertificateKind) diff --git a/internal/gatewayapi/testdata/sds-invalid.in.yaml b/internal/gatewayapi/testdata/sds-invalid.in.yaml new file mode 100644 index 0000000000..6ba06f9f7c --- /dev/null +++ b/internal/gatewayapi/testdata/sds-invalid.in.yaml @@ -0,0 +1,256 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-1 + namespace: envoy-gateway + 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: + # Test case 1: Multiple SDS reference secrets (should be rejected) + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: BackendTLSPolicy + metadata: + name: policy-multiple-sds + namespace: default + spec: + targetRefs: + - kind: Service + name: service-1 + validation: + caCertificateRefs: + - name: sds-ref-1 + kind: Secret + - name: sds-ref-2 + kind: Secret + hostname: example.com + # Test case 2: Mixing SDS with ConfigMap (should be rejected) + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: BackendTLSPolicy + metadata: + name: policy-sds-with-configmap + namespace: default + spec: + targetRefs: + - kind: Service + name: service-2 + validation: + caCertificateRefs: + - name: sds-ref-1 + kind: Secret + - name: ca-configmap + kind: ConfigMap + hostname: example.com + # Test case 3: Mixing SDS with regular Secret (should be rejected) + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: BackendTLSPolicy + metadata: + name: policy-sds-with-secret + namespace: default + spec: + targetRefs: + - kind: Service + name: service-3 + validation: + caCertificateRefs: + - name: sds-ref-1 + kind: Secret + - name: regular-ca-secret + 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: "/service1" + backendRefs: + - name: service-1 + port: 8080 + - matches: + - path: + type: Exact + value: "/service2" + backendRefs: + - name: service-2 + port: 8080 + - matches: + - path: + type: Exact + value: "/service3" + backendRefs: + - name: service-3 + port: 8080 +services: + - apiVersion: v1 + kind: Service + metadata: + name: service-1 + namespace: default + spec: + clusterIP: 10.11.12.13 + ports: + - port: 8080 + name: http + protocol: TCP + targetPort: 8080 + - apiVersion: v1 + kind: Service + metadata: + name: service-2 + namespace: default + spec: + clusterIP: 10.11.12.14 + ports: + - port: 8080 + name: http + protocol: TCP + targetPort: 8080 + - apiVersion: v1 + kind: Service + metadata: + name: service-3 + namespace: default + spec: + clusterIP: 10.11.12.15 + ports: + - port: 8080 + name: http + protocol: TCP + targetPort: 8080 +endpointSlices: + - apiVersion: discovery.k8s.io/v1 + kind: EndpointSlice + metadata: + name: endpointslice-service-1 + namespace: default + labels: + kubernetes.io/service-name: service-1 + addressType: IPv4 + ports: + - name: http + protocol: TCP + port: 8080 + endpoints: + - addresses: + - "10.244.0.11" + conditions: + ready: true + - apiVersion: discovery.k8s.io/v1 + kind: EndpointSlice + metadata: + name: endpointslice-service-2 + namespace: default + labels: + kubernetes.io/service-name: service-2 + addressType: IPv4 + ports: + - name: http + protocol: TCP + port: 8080 + endpoints: + - addresses: + - "10.244.0.12" + conditions: + ready: true + - apiVersion: discovery.k8s.io/v1 + kind: EndpointSlice + metadata: + name: endpointslice-service-3 + namespace: default + labels: + kubernetes.io/service-name: service-3 + addressType: IPv4 + ports: + - name: http + protocol: TCP + port: 8080 + endpoints: + - addresses: + - "10.244.0.13" + conditions: + ready: true +secrets: + # SDS reference secrets + - apiVersion: v1 + kind: Secret + metadata: + name: sds-ref-1 + namespace: default + type: gateway.envoyproxy.io/sds-ref + data: + url: L3Zhci9ydW4vc2VjcmV0cy93b3JrbG9hZC1zcGlmZmUtdWRzL3NvY2tldA== # /var/run/secrets/workload-spiffe-uds/socket + secretName: Uk9PVENB # ROOTCA + - apiVersion: v1 + kind: Secret + metadata: + name: sds-ref-2 + namespace: default + type: gateway.envoyproxy.io/sds-ref + data: + url: L3Zhci9ydW4vc2VjcmV0cy93b3JrbG9hZC1zcGlmZmUtdWRzL3NvY2tldA== # /var/run/secrets/workload-spiffe-uds/socket + secretName: Uk9PVENBMg== # ROOTCA2 + # Regular secret with CA certificate + - apiVersion: v1 + kind: Secret + metadata: + name: regular-ca-secret + namespace: default + type: Opaque + data: + ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURKekNDQWcrZ0F3SUJBZ0lVQWw2UWJFSGFLaWxVSzhUT3ppM2xJS2kxMDFRd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0l6RU5NQXNHQTFVRUF3d0VkR1Z6ZERFVE1CRUdBMVVFQ2d3S1pYaGhiWEJzWlMxSmJtTXdIaGNOTWpRdwpOekU1TVRRd09UUXhXaGNOTWprd056RTRNVFF3T1RReFdqQWpNUTB3Q3dZRFZRUUREQVIwWlhOME1STXdFUVlEClZRUUtEQXBsZUdGdGNHeGxMVWx1WXpDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUIKQU1OdWRiamhkQXo4Ky9zSjJvaFA2T0ZNQ3Z0WWpQMXJPQUtTZFh1RGtVOXNNd09uakUrZk5FZGl5VTBpcDBPeQpwQWtNR0RqbFFXZDQvRGxIN0hvV3o3UVJVSGt5emo0eWhONG5jSTlOMXZzQXdoaGQvNFM2WngwTE1YeTRvL0k4ClB2YnpjNFVjbUhBMlFGS2VpWGtoWEd1YkFVYWtEUjR4TGVSR3J4ZlhqaStPUmZwemY2V1pRNS9QWjRnMFdJZ3QKblhjZDFINVdGeTlOSUtCaGxJekJSZk1JMjMwT1BuNnNibkpiR1paZy8rQ29Ia2VFNDJzQ3VRQjFKZjVpcFlsNgoyNWJJSXVhcE9CaldMaDJYNWhsT1BvbWowOGxTUSsyd2VHNTdvRmVNOEgxbitxTFRuYU9jRmtoL3NLdGlvOEd1ClQ4REt1T1pINlVGN3pLL3ZGb1VDQXdFQUFhTlRNRkV3SFFZRFZSME9CQllFRkZxMWp3eHZPN1o5cHZBY1ZybGgKN01OZFdONGxNQjhHQTFVZEl3UVlNQmFBRkZxMWp3eHZPN1o5cHZBY1ZybGg3TU5kV040bE1BOEdBMVVkRXdFQgovd1FGTUFNQkFmOHdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBRjNERVVmcnc1a0YzbnZXRzkyMHNEQW01cC9PCmNsdCtCbXpnclF2UmtKOTdjWWl3akZKU2s3dmFXNk1UZk1pS2pRSE1mN2J3L1R5QlBPb3ROZy82OHRHZ2s1S3YKcmJ0RGxMWHJ5eXZNZ3VWVDJBOUVNd1dTQzBCR3JNRzRldlhqNG56U3dNTFh1aFhtRE5LbndHcmc1QnpwbHhONwpqMnp4NkFreXp4T3FOY2tKa0l2dW1oOHFkZXRGaU9rQ09pcFJ1SDF0RjdPbWk4WnBuY2E5a2czZzJhUlBOVTJqCkNybTdpRjVTQjJBM3dVLzQ2citaaHZRdFlCQWp6OVB1UmlGcUZQcmduNWF6VU5qcFRlS2RqZGxNcWRKMGlLRGcKY0Jaa3U5eEp3aDJQL3RWbGlaelorNWo0MExhMzY2QW1KUGdCSkFRRGdpWHNySUl6MC9qZHdNRT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= +configMaps: + - apiVersion: v1 + kind: ConfigMap + metadata: + name: ca-configmap + namespace: default + data: + ca.crt: | + -----BEGIN CERTIFICATE----- + MIIDJzCCAg+gAwIBAgIUAl6QbEHaKilUK8TOzi3lIKi101QwDQYJKoZIhvcNAQEL + BQAwIzENMAsGA1UEAwwEdGVzdDETMBEGA1UECgwKZXhhbXBsZS1JbmMwHhcNMjQw + NzE5MTQwOTQxWhcNMjkwNzE4MTQwOTQxWjAjMQ0wCwYDVQQDDAR0ZXN0MRMwEQYD + VQQKDApFeGFtcGxlLUluYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB + AMNudbjhdAz8+/sJ2ohP6OFMCvtYjP1rOAKSdXuDkU9sMwOnjE+fNEdiyU0ip0Oy + pAkMGDjlQWd4/DlH7HoWz7QRUHkyzj4yhN4ncI9N1vsAwhHd/4S6Zx0LMXy4o/I8 + Pvbzc4UcmHA2QFKeiXkhXGubAUakDR4xLeRGrxfXji+ORfpzf6WZQ5/PZ4g0WIgt + nXcd1H5WFy9NIKBhlIzBRfMI230OPn6sbnJbGZZg/+CoHkeE42sCuQB1Jf5ipYl6 + 25bIIuapOBjWLh2X5hlOPomj08lSQ+2weG57oFeM8H1n+qLTnaOcFkh/sKtio8Gu + T8DKuOZH6UF7zK/vFoUCAwEAAaNTMFEwHQYDVR0OBBYEFFq1jwxvO7Z9pvAcVrlh + 7MNdWN4lMB8GA1UdIwQYMBaAFFq1jwxvO7Z9pvAcVrlh7MNdWN4lMA8GA1UdEwEB + /wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAF3DEUfrw5kF3nvWG920sDAm5p/O + clt+BmzgrQvRkJ97cYiwjFJSk7vaW6MTfMiKjQHMf7bw/TyBPOotNg/68tGgk5Kv + rbtDlLXryyvMguVT2A9EMwWSC0BGrMG4evXj4nzSwMLXuhXmDNKnwGrg5BzplxN7 + j2zx6AkyxxOqNckJkIvumh8qdetFiOkCOipRuH1tF7Omi8Zpnca9kg3g2aRPNU2j + Crm7iF5SB2A3wU/46r+ZhvQtYBAjz9PuRiFqFPrgn5azUNjpTeKdjdlMqdJ0iKDg + cBZku9xJwh2P/tVliZyZ+5j40La366AmJPgBJAQDgiXsrIIz0/jdwME= + -----END CERTIFICATE----- diff --git a/internal/gatewayapi/testdata/sds-invalid.out.yaml b/internal/gatewayapi/testdata/sds-invalid.out.yaml new file mode 100644 index 0000000000..eef14eadff --- /dev/null +++ b/internal/gatewayapi/testdata/sds-invalid.out.yaml @@ -0,0 +1,329 @@ +backendTLSPolicies: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: BackendTLSPolicy + metadata: + name: policy-multiple-sds + namespace: default + spec: + targetRefs: + - group: "" + kind: Service + name: service-1 + validation: + caCertificateRefs: + - group: "" + kind: Secret + name: sds-ref-1 + - group: "" + kind: Secret + name: sds-ref-2 + hostname: example.com + status: + ancestors: + - ancestorRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: Multiple SDS reference secrets are not supported. + reason: NoValidCACertificate + status: "False" + type: Accepted + - lastTransitionTime: null + message: Multiple SDS reference secrets are not supported. + reason: InvalidCACertificateRef + status: "False" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: BackendTLSPolicy + metadata: + name: policy-sds-with-configmap + namespace: default + spec: + targetRefs: + - group: "" + kind: Service + name: service-2 + validation: + caCertificateRefs: + - group: "" + kind: Secret + name: sds-ref-1 + - group: "" + kind: ConfigMap + name: ca-configmap + hostname: example.com + status: + ancestors: + - ancestorRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: Cannot mix SDS reference secrets with other CA certificate types. + reason: NoValidCACertificate + status: "False" + type: Accepted + - lastTransitionTime: null + message: Cannot mix SDS reference secrets with other CA certificate types. + reason: InvalidCACertificateRef + status: "False" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: BackendTLSPolicy + metadata: + name: policy-sds-with-secret + namespace: default + spec: + targetRefs: + - group: "" + kind: Service + name: service-3 + validation: + caCertificateRefs: + - group: "" + kind: Secret + name: sds-ref-1 + - group: "" + kind: Secret + name: regular-ca-secret + hostname: example.com + status: + ancestors: + - ancestorRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: Cannot mix SDS reference secrets with other CA certificate types. + reason: NoValidCACertificate + status: "False" + type: Accepted + - lastTransitionTime: null + message: Cannot mix SDS reference secrets with other CA certificate types. + reason: InvalidCACertificateRef + status: "False" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller +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 + status: + conditions: + - lastTransitionTime: null + message: The Backend was accepted + reason: Accepted + status: "True" + type: Accepted +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-1 + namespace: default + spec: + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + type: Exact + value: /service1 + - backendRefs: + - name: service-2 + port: 8080 + matches: + - path: + type: Exact + value: /service2 + - backendRefs: + - name: service-3 + port: 8080 + matches: + - path: + type: Exact + value: /service3 + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: |- + Failed to process route rule 0 backendRef 0: multiple SDS reference secrets are not supported. + Failed to process route rule 1 backendRef 0: cannot mix SDS reference secrets with other CA certificate types. + Failed to process route rule 2 backendRef 0: cannot mix SDS reference secrets with other CA certificate types. + reason: InvalidBackendTLS + status: "False" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-1 + namespace: envoy-gateway-system +xdsIR: + envoy-gateway/gateway-1: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + protocol: TCP + http: + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - '*' + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - directResponse: + statusCode: 500 + hostname: '*' + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/* + pathMatch: + distinct: false + exact: /service1 + name: "" + - directResponse: + statusCode: 500 + hostname: '*' + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/1/match/0/* + pathMatch: + distinct: false + exact: /service2 + name: "" + - directResponse: + statusCode: 500 + hostname: '*' + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/2/match/0/* + pathMatch: + distinct: false + exact: /service3 + name: "" + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 diff --git a/internal/gatewayapi/testdata/envoyproxy-with-sds.in.yaml b/internal/gatewayapi/testdata/sds.in.yaml similarity index 96% rename from internal/gatewayapi/testdata/envoyproxy-with-sds.in.yaml rename to internal/gatewayapi/testdata/sds.in.yaml index 79d90c6573..4672450d2b 100644 --- a/internal/gatewayapi/testdata/envoyproxy-with-sds.in.yaml +++ b/internal/gatewayapi/testdata/sds.in.yaml @@ -78,7 +78,7 @@ secrets: metadata: name: sds-client-cert-default namespace: envoy-gateway-system - type: gateway.envoyproxy.io/sds-ref + type: gateway.envoyproxy.io/sds-ref data: # /var/run/secrets/workload-spiffe-uds/socket url: L3Zhci9ydW4vc2VjcmV0cy93b3JrbG9hZC1zcGlmZmUtdWRzL3NvY2tldA== @@ -92,4 +92,4 @@ secrets: data: # /var/run/secrets/workload-spiffe-uds/socket url: L3Zhci9ydW4vc2VjcmV0cy93b3JrbG9hZC1zcGlmZmUtdWRzL3NvY2tldA== - secretName: Uk9PVENB # base64 for "ROOTCA" + secretName: Uk9PVENB # base64 for "ROOTCA" diff --git a/internal/gatewayapi/testdata/envoyproxy-with-sds.out.yaml b/internal/gatewayapi/testdata/sds.out.yaml similarity index 100% rename from internal/gatewayapi/testdata/envoyproxy-with-sds.out.yaml rename to internal/gatewayapi/testdata/sds.out.yaml diff --git a/internal/xds/translator/listener.go b/internal/xds/translator/listener.go index 854b9dd3f2..42c894cbff 100644 --- a/internal/xds/translator/listener.go +++ b/internal/xds/translator/listener.go @@ -840,13 +840,19 @@ func buildDownstreamQUICTransportSocket(tlsConfig *ir.TLSConfig) (*corev3.Transp }, } - for _, tlsConfig := range tlsConfig.Certificates { + for _, cert := range tlsConfig.Certificates { + sdsConfig := &tlsv3.SdsSecretConfig{ + Name: cert.Name, + SdsConfig: makeConfigSource(), + } + if cert.SDS != nil { + // Use external SDS server instead of ADS + clusterName := sdsClusterNameFromURL(cert.SDS.URL) + sdsConfig = sdsSecretConfig(cert.SDS.SecretName, clusterName) + } tlsCtx.DownstreamTlsContext.CommonTlsContext.TlsCertificateSdsSecretConfigs = append( tlsCtx.DownstreamTlsContext.CommonTlsContext.TlsCertificateSdsSecretConfigs, - &tlsv3.SdsSecretConfig{ - Name: tlsConfig.Name, - SdsConfig: makeConfigSource(), - }) + sdsConfig) } if tlsConfig.CACertificate != nil || tlsConfig.ClientValidationEnabled { @@ -877,13 +883,19 @@ func buildXdsDownstreamTLSSocket(tlsConfig *ir.TLSConfig) (*corev3.TransportSock }, } - for _, tlsConfig := range tlsConfig.Certificates { + for _, cert := range tlsConfig.Certificates { + sdsConfig := &tlsv3.SdsSecretConfig{ + Name: cert.Name, + SdsConfig: makeConfigSource(), + } + if cert.SDS != nil { + // Use external SDS server instead of ADS + clusterName := sdsClusterNameFromURL(cert.SDS.URL) + sdsConfig = sdsSecretConfig(cert.SDS.SecretName, clusterName) + } tlsCtx.CommonTlsContext.TlsCertificateSdsSecretConfigs = append( tlsCtx.CommonTlsContext.TlsCertificateSdsSecretConfigs, - &tlsv3.SdsSecretConfig{ - Name: tlsConfig.Name, - SdsConfig: makeConfigSource(), - }) + sdsConfig) } if tlsConfig.CACertificate != nil || tlsConfig.ClientValidationEnabled { @@ -951,21 +963,27 @@ func setTLSValidationContext(tlsConfig *ir.TLSConfig, tlsCtx *tlsv3.CommonTlsCon return } - sdsSecretConfig := &tlsv3.SdsSecretConfig{ + sdsConfig := &tlsv3.SdsSecretConfig{ Name: tlsConfig.CACertificate.Name, SdsConfig: makeConfigSource(), } + if tlsConfig.CACertificate.SDS != nil { + // Use external SDS server instead of ADS + clusterName := sdsClusterNameFromURL(tlsConfig.CACertificate.SDS.URL) + sdsConfig = sdsSecretConfig(tlsConfig.CACertificate.SDS.SecretName, clusterName) + } + if needsDefaultValidationContext { tlsCtx.ValidationContextType = &tlsv3.CommonTlsContext_CombinedValidationContext{ CombinedValidationContext: &tlsv3.CommonTlsContext_CombinedCertificateValidationContext{ DefaultValidationContext: validationContext, - ValidationContextSdsSecretConfig: sdsSecretConfig, + ValidationContextSdsSecretConfig: sdsConfig, }, } } else { tlsCtx.ValidationContextType = &tlsv3.CommonTlsContext_ValidationContextSdsSecretConfig{ - ValidationContextSdsSecretConfig: sdsSecretConfig, + ValidationContextSdsSecretConfig: sdsConfig, } } } From c1faf4d4841a17cdda7072c1f2e710cf5fa8cee3 Mon Sep 17 00:00:00 2001 From: zirain Date: Sat, 18 Apr 2026 19:22:41 +0800 Subject: [PATCH 3/7] fix review comments Signed-off-by: zirain --- api/v1alpha1/shared_types.go | 3 ++ internal/gatewayapi/backendtlspolicy.go | 53 ++++++----------------- internal/gatewayapi/testdata/sds.out.yaml | 2 +- internal/ir/xds.go | 18 ++++++++ internal/xds/translator/sds.go | 47 +++++++++----------- 5 files changed, 56 insertions(+), 67 deletions(-) diff --git a/api/v1alpha1/shared_types.go b/api/v1alpha1/shared_types.go index 27178d2545..a70a748bcc 100644 --- a/api/v1alpha1/shared_types.go +++ b/api/v1alpha1/shared_types.go @@ -16,6 +16,9 @@ import ( ) const ( + // SDSSecretType is the type for secrets that reference SDS configuration + SDSSecretType = "gateway.envoyproxy.io/sds-ref" + // DefaultDeploymentReplicas is the default number of deployment replicas. DefaultDeploymentReplicas = 1 // DefaultDeploymentCPUResourceRequests for deployment cpu resource diff --git a/internal/gatewayapi/backendtlspolicy.go b/internal/gatewayapi/backendtlspolicy.go index 60052e41e4..cf385cc75d 100644 --- a/internal/gatewayapi/backendtlspolicy.go +++ b/internal/gatewayapi/backendtlspolicy.go @@ -22,11 +22,6 @@ import ( "github.com/envoyproxy/gateway/internal/utils" ) -const ( - // SDSSecretType is the type for secrets that reference SDS configuration - SDSSecretType = "gateway.envoyproxy.io/sds-ref" -) - var ( ErrRefNotPermitted = fmt.Errorf("cross-namespace reference is not permitted by any ReferenceGrant") ErrInvalidCACertificateKind = fmt.Errorf("Unsupported reference kind, supported kinds are ConfigMap, Secret, and ClusterTrustBundle") @@ -410,27 +405,16 @@ func (t *Translator) processClientTLSSettings( return tlsConfig, err } // Check if this is an SDS reference secret - if secret.Type == SDSSecretType { + if secret.Type == egv1a1.SDSSecretType { // For SDS reference secrets, extract the SDS secret name and URL from data - sdsSecretName, hasSecretName := secret.Data["secretName"] - sdsURLBytes, hasURL := secret.Data["url"] - if hasSecretName && len(sdsSecretName) > 0 && hasURL && len(sdsURLBytes) > 0 { - tlsConfig.ClientCertificates = []ir.TLSCertificate{ - { - Name: string(sdsSecretName), - SDS: &ir.SDSConfig{ - SecretName: string(sdsSecretName), - URL: string(sdsURLBytes), - }, - }, - } - } else { - if !hasSecretName || len(sdsSecretName) == 0 { - err = fmt.Errorf("no secretName found in SDS reference secret %s", secret.Name) - } else { - err = fmt.Errorf("no url found in SDS reference secret %s", secret.Name) - } - return tlsConfig, err + s, err := ir.NewSDSConfig(secret) + if err != nil { + return tlsConfig, fmt.Errorf("invalid SDS reference secret: %v", err) + } + tlsConfig.ClientCertificates = []ir.TLSCertificate{ + { + SDS: s, + }, } } else { // Regular secret processing @@ -593,23 +577,14 @@ func (t *Translator) getCaCertsFromCARefs(resources *resource.Resources, caCerti secret := t.GetSecret(caRefNs, string(caRef.Name)) if secret != nil { // Check if this is an SDS reference secret - if secret.Type == SDSSecretType { - // For SDS reference secrets, extract the SDS secret name and URL from data - sdsSecretName, hasSecretName := secret.Data["secretName"] - sdsURLBytes, hasURL := secret.Data["url"] - // TODO: support more sds options if needed. - if !hasSecretName || len(sdsSecretName) == 0 { - return nil, nil, fmt.Errorf("no secretName found in SDS reference secret %s", secret.Name) - } - if !hasURL || len(sdsURLBytes) == 0 { - return nil, nil, fmt.Errorf("no url found in SDS reference secret %s", secret.Name) - } + if secret.Type == egv1a1.SDSSecretType { if foundSDSConfig != nil { return nil, nil, fmt.Errorf("multiple SDS reference secrets are not supported") } - foundSDSConfig = &ir.SDSConfig{ - SecretName: string(sdsSecretName), - URL: string(sdsURLBytes), + // 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: %v", secret.Name, err) } continue } diff --git a/internal/gatewayapi/testdata/sds.out.yaml b/internal/gatewayapi/testdata/sds.out.yaml index 0017e2c5a3..3c5c2fd868 100644 --- a/internal/gatewayapi/testdata/sds.out.yaml +++ b/internal/gatewayapi/testdata/sds.out.yaml @@ -231,7 +231,7 @@ xdsIR: secretName: ROOTCA url: /var/run/secrets/workload-spiffe-uds/socket clientCertificates: - - name: default + - name: "" sds: secretName: default url: /var/run/secrets/workload-spiffe-uds/socket diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 3dbb631630..9919f6eef0 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -16,6 +16,7 @@ import ( "net/netip" "time" + corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -493,6 +494,23 @@ type SDSConfig struct { // such as TLS settings for the SDS server, or authentication credentials if needed. } +func NewSDSConfig(s *corev1.Secret) (*SDSConfig, error) { + sdsSecretName, hasSecretName := s.Data["secretName"] + sdsURLBytes, hasURL := s.Data["url"] + // TODO: support more sds options if needed. + if !hasSecretName || len(sdsSecretName) == 0 { + return nil, fmt.Errorf("no secretName found in SDS reference secret %s/%s", s.Namespace, s.Name) + } + if !hasURL || len(sdsURLBytes) == 0 { + return nil, fmt.Errorf("no url found in SDS reference secret %s/%s", s.Namespace, s.Name) + } + + return &SDSConfig{ + SecretName: string(sdsSecretName), + URL: string(sdsURLBytes), + }, nil +} + // TLSCrl holds a single CRL's details // +k8s:deepcopy-gen=true type TLSCrl struct { diff --git a/internal/xds/translator/sds.go b/internal/xds/translator/sds.go index 468b729269..71d3279c92 100644 --- a/internal/xds/translator/sds.go +++ b/internal/xds/translator/sds.go @@ -77,27 +77,23 @@ func createSDSCluster(tCtx *types.ResourceVersionTable, sdsURL string) error { } // Create the cluster based on the URL type - var c *cluster.Cluster - if strings.HasPrefix(sdsURL, "/") { - // Unix domain socket - c = &cluster.Cluster{ - Name: clusterName, - ClusterDiscoveryType: &cluster.Cluster_Type{ - Type: cluster.Cluster_STATIC, - }, - LoadAssignment: &endpoint.ClusterLoadAssignment{ - ClusterName: clusterName, - Endpoints: []*endpoint.LocalityLbEndpoints{ - { - LbEndpoints: []*endpoint.LbEndpoint{ - { - HostIdentifier: &endpoint.LbEndpoint_Endpoint{ - Endpoint: &endpoint.Endpoint{ - Address: &corev3.Address{ - Address: &corev3.Address_Pipe{ - Pipe: &corev3.Pipe{ - Path: sdsURL, - }, + c := &cluster.Cluster{ + Name: clusterName, + ClusterDiscoveryType: &cluster.Cluster_Type{ + Type: cluster.Cluster_STATIC, + }, + LoadAssignment: &endpoint.ClusterLoadAssignment{ + ClusterName: clusterName, + Endpoints: []*endpoint.LocalityLbEndpoints{ + { + LbEndpoints: []*endpoint.LbEndpoint{ + { + HostIdentifier: &endpoint.LbEndpoint_Endpoint{ + Endpoint: &endpoint.Endpoint{ + Address: &corev3.Address{ + Address: &corev3.Address_Pipe{ + Pipe: &corev3.Pipe{ + Path: sdsURL, }, }, }, @@ -107,12 +103,9 @@ func createSDSCluster(tCtx *types.ResourceVersionTable, sdsURL string) error { }, }, }, - ConnectTimeout: durationpb.New(defaultConnectionTimeout), - Http2ProtocolOptions: &corev3.Http2ProtocolOptions{}, - } - } else { - // TCP/IP address - return fmt.Errorf("TCP/IP SDS URLs are not yet supported: %s", sdsURL) + }, + ConnectTimeout: durationpb.New(defaultConnectionTimeout), + Http2ProtocolOptions: &corev3.Http2ProtocolOptions{}, } if err := tCtx.AddXdsResource(resourcev3.ClusterType, c); err != nil { From 01f79ab9824245ecfa406f9ee7c74a306eb5f1e0 Mon Sep 17 00:00:00 2001 From: zirain Date: Sat, 18 Apr 2026 20:10:40 +0800 Subject: [PATCH 4/7] fix lint Signed-off-by: zirain --- internal/gatewayapi/backendtlspolicy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/gatewayapi/backendtlspolicy.go b/internal/gatewayapi/backendtlspolicy.go index cf385cc75d..6b5e44d557 100644 --- a/internal/gatewayapi/backendtlspolicy.go +++ b/internal/gatewayapi/backendtlspolicy.go @@ -409,7 +409,7 @@ func (t *Translator) processClientTLSSettings( // 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: %v", err) + return tlsConfig, fmt.Errorf("invalid SDS reference secret: %w", err) } tlsConfig.ClientCertificates = []ir.TLSCertificate{ { @@ -584,7 +584,7 @@ func (t *Translator) getCaCertsFromCARefs(resources *resource.Resources, caCerti // 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: %v", secret.Name, err) + return nil, nil, fmt.Errorf("invalid SDS reference secret %s: %w", secret.Name, err) } continue } From d4d8430433e06291abc0f47dcd23d3efd640d14b Mon Sep 17 00:00:00 2001 From: zirain Date: Mon, 20 Apr 2026 09:33:32 +0800 Subject: [PATCH 5/7] add EnableSDSSecretRef in EnvoyGateway Signed-off-by: zirain --- api/v1alpha1/envoygateway_types.go | 2 + internal/gatewayapi/backendtlspolicy.go | 6 + internal/gatewayapi/runner/runner.go | 1 + .../gatewayapi/testdata/sds-disabled.in.yaml | 95 ++++++++ .../gatewayapi/testdata/sds-disabled.out.yaml | 225 ++++++++++++++++++ internal/gatewayapi/translator.go | 4 + internal/gatewayapi/translator_test.go | 14 ++ site/content/en/latest/api/extension_types.md | 1 + 8 files changed, 348 insertions(+) create mode 100644 internal/gatewayapi/testdata/sds-disabled.in.yaml create mode 100644 internal/gatewayapi/testdata/sds-disabled.out.yaml diff --git a/api/v1alpha1/envoygateway_types.go b/api/v1alpha1/envoygateway_types.go index 9026878ee4..eac51d8c34 100644 --- a/api/v1alpha1/envoygateway_types.go +++ b/api/v1alpha1/envoygateway_types.go @@ -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-ref). + EnableSDSSecretRef bool `json:"enableSDSSecretRef"` } // EnvoyGatewayProvider defines the desired configuration of a provider. diff --git a/internal/gatewayapi/backendtlspolicy.go b/internal/gatewayapi/backendtlspolicy.go index 6b5e44d557..a5f00f24e9 100644 --- a/internal/gatewayapi/backendtlspolicy.go +++ b/internal/gatewayapi/backendtlspolicy.go @@ -406,6 +406,9 @@ func (t *Translator) processClientTLSSettings( } // 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 { @@ -578,6 +581,9 @@ func (t *Translator) getCaCertsFromCARefs(resources *resource.Resources, caCerti 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") } diff --git a/internal/gatewayapi/runner/runner.go b/internal/gatewayapi/runner/runner.go index a0f09c48a9..84260374d3 100644 --- a/internal/gatewayapi/runner/runner.go +++ b/internal/gatewayapi/runner/runner.go @@ -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), diff --git a/internal/gatewayapi/testdata/sds-disabled.in.yaml b/internal/gatewayapi/testdata/sds-disabled.in.yaml new file mode 100644 index 0000000000..f12b212c2f --- /dev/null +++ b/internal/gatewayapi/testdata/sds-disabled.in.yaml @@ -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-ref + 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-ref + data: + # /var/run/secrets/workload-spiffe-uds/socket + url: L3Zhci9ydW4vc2VjcmV0cy93b3JrbG9hZC1zcGlmZmUtdWRzL3NvY2tldA== + secretName: Uk9PVENB # base64 for "ROOTCA" diff --git a/internal/gatewayapi/testdata/sds-disabled.out.yaml b/internal/gatewayapi/testdata/sds-disabled.out.yaml new file mode 100644 index 0000000000..18dcbce870 --- /dev/null +++ b/internal/gatewayapi/testdata/sds-disabled.out.yaml @@ -0,0 +1,225 @@ +backendTLSPolicies: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: BackendTLSPolicy + metadata: + name: policy-btls + namespace: default + spec: + targetRefs: + - group: "" + kind: Service + name: service-1 + validation: + caCertificateRefs: + - group: "" + kind: Secret + name: sds-client-cert-rootca + hostname: example.com + status: + ancestors: + - ancestorRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: SDS Secret reference is not enabled in EnvoyGateway configuration. + reason: NoValidCACertificate + status: "False" + type: Accepted + - lastTransitionTime: null + message: SDS Secret reference is not enabled in EnvoyGateway configuration. + reason: InvalidCACertificateRef + status: "False" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller +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 + status: + conditions: + - lastTransitionTime: null + message: The Backend was accepted + reason: Accepted + status: "True" + type: Accepted +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-1 + namespace: default + spec: + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + type: Exact + value: /exact + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: 'Failed to process route rule 0 backendRef 0: SDS Secret reference + is not enabled in EnvoyGateway configuration.' + reason: InvalidBackendTLS + status: "False" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + config: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + name: test + namespace: envoy-gateway-system + spec: + backendTLS: + alpnProtocols: + - istio + clientCertificateRef: + kind: Secret + name: sds-client-cert-default + logging: {} + status: {} + listeners: + - name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-1 + namespace: envoy-gateway-system +xdsIR: + envoy-gateway/gateway-1: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + protocol: TCP + http: + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - '*' + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - directResponse: + statusCode: 500 + hostname: '*' + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/* + pathMatch: + distinct: false + exact: /exact + name: "" + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 diff --git a/internal/gatewayapi/translator.go b/internal/gatewayapi/translator.go index 5a2a614046..5e63c5ebe7 100644 --- a/internal/gatewayapi/translator.go +++ b/internal/gatewayapi/translator.go @@ -101,6 +101,10 @@ type Translator struct { // BackendEnabled when the Backend feature is enabled. BackendEnabled bool + // SDSSecretRefEnabled is true if EnvoyProxy can reference SDS secrets using the sds-ref type Secret. + // This could be enabled by Envoy Gateway configuration. + SDSSecretRefEnabled bool + // ExtensionGroupKinds stores the group/kind for all resources // introduced by an Extension so that the translator can // store referenced resources in the IR for later use. diff --git a/internal/gatewayapi/translator_test.go b/internal/gatewayapi/translator_test.go index 2945992c37..5b87a19ac5 100644 --- a/internal/gatewayapi/translator_test.go +++ b/internal/gatewayapi/translator_test.go @@ -55,6 +55,7 @@ func TestTranslate(t *testing.T) { GatewayNamespaceMode bool RunningOnHost bool LuaEnvoyExtensionPolicyDisabled bool + SDSEnabled bool }{ { name: "envoypatchpolicy-invalid-feature-disabled", @@ -82,6 +83,16 @@ func TestTranslate(t *testing.T) { name: "envoyextensionpolicy-lua-feature-disabled", LuaEnvoyExtensionPolicyDisabled: true, }, + { + name: "sds", + BackendEnabled: true, + SDSEnabled: true, + }, + { + name: "sds-invalid", + BackendEnabled: true, + SDSEnabled: true, + }, } inputFiles, err := filepath.Glob(filepath.Join("testdata", "*.in.yaml")) @@ -106,6 +117,7 @@ func TestTranslate(t *testing.T) { gatewayNamespaceMode := false runningOnHost := false luaEnvoyExtensionPolicyDisabled := false + sdsEnabled := false for _, config := range testCasesConfig { if config.name == strings.Split(filepath.Base(inputFile), ".")[0] { @@ -114,6 +126,7 @@ func TestTranslate(t *testing.T) { gatewayNamespaceMode = config.GatewayNamespaceMode runningOnHost = config.RunningOnHost luaEnvoyExtensionPolicyDisabled = config.LuaEnvoyExtensionPolicyDisabled + sdsEnabled = config.SDSEnabled } } @@ -123,6 +136,7 @@ func TestTranslate(t *testing.T) { GlobalRateLimitEnabled: true, EnvoyPatchPolicyEnabled: envoyPatchPolicyEnabled, BackendEnabled: backendEnabled, + SDSSecretRefEnabled: sdsEnabled, ControllerNamespace: "envoy-gateway-system", MergeGateways: IsMergeGatewaysEnabled(resources), GatewayNamespaceMode: gatewayNamespaceMode, diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 7b40befba0..ef88f8f900 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -2231,6 +2231,7 @@ _Appears in:_ | `enableEnvoyPatchPolicy` | _boolean_ | true | | EnableEnvoyPatchPolicy enables Envoy Gateway to
reconcile and implement the EnvoyPatchPolicy resources.
Warning: Enabling `EnvoyPatchPolicy` may lead to complete security compromise of your system.
Users with `EnvoyPatchPolicy` permissions can inject arbitrary configuration to proxies,
leading to high Confidentiality, Integrity and Availability risks. | | `enableBackend` | _boolean_ | true | | EnableBackend enables Envoy Gateway to
reconcile and implement the Backend resources. | | `disableLua` | _boolean_ | true | | DisableLua determines if Lua EnvoyExtensionPolicies should be disabled.
If set to true, the Lua EnvoyExtensionPolicy feature will be disabled. | +| `enableSDSSecretRef` | _boolean_ | true | | EnableSDSSecrectRef enables read SDS(Secret Discovery Service) settings from a secret(with type gateway.envoyproxy.io/sds-ref). | #### ExtensionHooks From 1505c0108c31ff96e9611d3f59a87678f1273e44 Mon Sep 17 00:00:00 2001 From: zirain Date: Mon, 20 Apr 2026 09:54:40 +0800 Subject: [PATCH 6/7] fix gen Signed-off-by: zirain --- site/content/en/latest/api/extension_types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index ef88f8f900..afd4437a31 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -2231,7 +2231,7 @@ _Appears in:_ | `enableEnvoyPatchPolicy` | _boolean_ | true | | EnableEnvoyPatchPolicy enables Envoy Gateway to
reconcile and implement the EnvoyPatchPolicy resources.
Warning: Enabling `EnvoyPatchPolicy` may lead to complete security compromise of your system.
Users with `EnvoyPatchPolicy` permissions can inject arbitrary configuration to proxies,
leading to high Confidentiality, Integrity and Availability risks. | | `enableBackend` | _boolean_ | true | | EnableBackend enables Envoy Gateway to
reconcile and implement the Backend resources. | | `disableLua` | _boolean_ | true | | DisableLua determines if Lua EnvoyExtensionPolicies should be disabled.
If set to true, the Lua EnvoyExtensionPolicy feature will be disabled. | -| `enableSDSSecretRef` | _boolean_ | true | | EnableSDSSecrectRef enables read SDS(Secret Discovery Service) settings from a secret(with type gateway.envoyproxy.io/sds-ref). | +| `enableSDSSecretRef` | _boolean_ | true | | EnableSDSSecretRef enables read SDS(Secret Discovery Service) settings from a secret(with type gateway.envoyproxy.io/sds-ref). | #### ExtensionHooks From bbd5ef0238dba62d2a8af56c53e536b7a4ae5edf Mon Sep 17 00:00:00 2001 From: zirain Date: Tue, 21 Apr 2026 17:09:01 +0800 Subject: [PATCH 7/7] rename Signed-off-by: zirain --- api/v1alpha1/envoygateway_types.go | 2 +- api/v1alpha1/shared_types.go | 2 +- internal/gatewayapi/testdata/sds-disabled.in.yaml | 4 ++-- internal/gatewayapi/testdata/sds-invalid.in.yaml | 4 ++-- internal/gatewayapi/testdata/sds.in.yaml | 4 ++-- site/content/en/latest/api/extension_types.md | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/api/v1alpha1/envoygateway_types.go b/api/v1alpha1/envoygateway_types.go index eac51d8c34..8d3fa614af 100644 --- a/api/v1alpha1/envoygateway_types.go +++ b/api/v1alpha1/envoygateway_types.go @@ -312,7 +312,7 @@ 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-ref). + // EnableSDSSecretRef enables read SDS(Secret Discovery Service) settings from a secret(with type gateway.envoyproxy.io/sds). EnableSDSSecretRef bool `json:"enableSDSSecretRef"` } diff --git a/api/v1alpha1/shared_types.go b/api/v1alpha1/shared_types.go index a70a748bcc..5450cb4913 100644 --- a/api/v1alpha1/shared_types.go +++ b/api/v1alpha1/shared_types.go @@ -17,7 +17,7 @@ import ( const ( // SDSSecretType is the type for secrets that reference SDS configuration - SDSSecretType = "gateway.envoyproxy.io/sds-ref" + SDSSecretType = "gateway.envoyproxy.io/sds" // DefaultDeploymentReplicas is the default number of deployment replicas. DefaultDeploymentReplicas = 1 diff --git a/internal/gatewayapi/testdata/sds-disabled.in.yaml b/internal/gatewayapi/testdata/sds-disabled.in.yaml index f12b212c2f..fe641e087d 100644 --- a/internal/gatewayapi/testdata/sds-disabled.in.yaml +++ b/internal/gatewayapi/testdata/sds-disabled.in.yaml @@ -78,7 +78,7 @@ secrets: metadata: name: sds-client-cert-default namespace: envoy-gateway-system - type: gateway.envoyproxy.io/sds-ref + type: gateway.envoyproxy.io/sds data: # /var/run/secrets/workload-spiffe-uds/socket url: L3Zhci9ydW4vc2VjcmV0cy93b3JrbG9hZC1zcGlmZmUtdWRzL3NvY2tldA== @@ -88,7 +88,7 @@ secrets: metadata: name: sds-client-cert-rootca namespace: default - type: gateway.envoyproxy.io/sds-ref + type: gateway.envoyproxy.io/sds data: # /var/run/secrets/workload-spiffe-uds/socket url: L3Zhci9ydW4vc2VjcmV0cy93b3JrbG9hZC1zcGlmZmUtdWRzL3NvY2tldA== diff --git a/internal/gatewayapi/testdata/sds-invalid.in.yaml b/internal/gatewayapi/testdata/sds-invalid.in.yaml index 6ba06f9f7c..d96ccf4fab 100644 --- a/internal/gatewayapi/testdata/sds-invalid.in.yaml +++ b/internal/gatewayapi/testdata/sds-invalid.in.yaml @@ -205,7 +205,7 @@ secrets: metadata: name: sds-ref-1 namespace: default - type: gateway.envoyproxy.io/sds-ref + type: gateway.envoyproxy.io/sds data: url: L3Zhci9ydW4vc2VjcmV0cy93b3JrbG9hZC1zcGlmZmUtdWRzL3NvY2tldA== # /var/run/secrets/workload-spiffe-uds/socket secretName: Uk9PVENB # ROOTCA @@ -214,7 +214,7 @@ secrets: metadata: name: sds-ref-2 namespace: default - type: gateway.envoyproxy.io/sds-ref + type: gateway.envoyproxy.io/sds data: url: L3Zhci9ydW4vc2VjcmV0cy93b3JrbG9hZC1zcGlmZmUtdWRzL3NvY2tldA== # /var/run/secrets/workload-spiffe-uds/socket secretName: Uk9PVENBMg== # ROOTCA2 diff --git a/internal/gatewayapi/testdata/sds.in.yaml b/internal/gatewayapi/testdata/sds.in.yaml index 4672450d2b..37a28ea6a6 100644 --- a/internal/gatewayapi/testdata/sds.in.yaml +++ b/internal/gatewayapi/testdata/sds.in.yaml @@ -78,7 +78,7 @@ secrets: metadata: name: sds-client-cert-default namespace: envoy-gateway-system - type: gateway.envoyproxy.io/sds-ref + type: gateway.envoyproxy.io/sds data: # /var/run/secrets/workload-spiffe-uds/socket url: L3Zhci9ydW4vc2VjcmV0cy93b3JrbG9hZC1zcGlmZmUtdWRzL3NvY2tldA== @@ -88,7 +88,7 @@ secrets: metadata: name: sds-client-cert-rootca namespace: default - type: gateway.envoyproxy.io/sds-ref + type: gateway.envoyproxy.io/sds data: # /var/run/secrets/workload-spiffe-uds/socket url: L3Zhci9ydW4vc2VjcmV0cy93b3JrbG9hZC1zcGlmZmUtdWRzL3NvY2tldA== diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index afd4437a31..693a90bacf 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -2231,7 +2231,7 @@ _Appears in:_ | `enableEnvoyPatchPolicy` | _boolean_ | true | | EnableEnvoyPatchPolicy enables Envoy Gateway to
reconcile and implement the EnvoyPatchPolicy resources.
Warning: Enabling `EnvoyPatchPolicy` may lead to complete security compromise of your system.
Users with `EnvoyPatchPolicy` permissions can inject arbitrary configuration to proxies,
leading to high Confidentiality, Integrity and Availability risks. | | `enableBackend` | _boolean_ | true | | EnableBackend enables Envoy Gateway to
reconcile and implement the Backend resources. | | `disableLua` | _boolean_ | true | | DisableLua determines if Lua EnvoyExtensionPolicies should be disabled.
If set to true, the Lua EnvoyExtensionPolicy feature will be disabled. | -| `enableSDSSecretRef` | _boolean_ | true | | EnableSDSSecretRef enables read SDS(Secret Discovery Service) settings from a secret(with type gateway.envoyproxy.io/sds-ref). | +| `enableSDSSecretRef` | _boolean_ | true | | EnableSDSSecretRef enables read SDS(Secret Discovery Service) settings from a secret(with type gateway.envoyproxy.io/sds). | #### ExtensionHooks