Skip to content

Commit

Permalink
fix(kuma-cp): specifying IPv6 Envoy Admin address breaks readiness/li…
Browse files Browse the repository at this point in the history
…veness probes (#7909)

Put a specified admin address to proxy metadata and use it when generating an admin cluster.

Signed-off-by: Ilya Lobkov <[email protected]>
  • Loading branch information
lobkovilya authored Sep 29, 2023
1 parent ccda25e commit 8481ec2
Show file tree
Hide file tree
Showing 20 changed files with 516 additions and 6 deletions.
10 changes: 10 additions & 0 deletions pkg/core/xds/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ var metadataLog = core.Log.WithName("xds-server").WithName("metadata-tracker")
const (
// Supported Envoy node metadata fields.
FieldDataplaneAdminPort = "dataplane.admin.port"
FieldDataplaneAdminAddress = "dataplane.admin.address"
FieldDataplaneDNSPort = "dataplane.dns.port"
FieldDataplaneDNSEmptyPort = "dataplane.dns.empty.port"
FieldDataplaneDataplaneResource = "dataplane.resource"
Expand Down Expand Up @@ -51,6 +52,7 @@ const (
type DataplaneMetadata struct {
Resource model.Resource
AdminPort uint32
AdminAddress string
DNSPort uint32
EmptyDNSPort uint32
DynamicMetadata map[string]string
Expand Down Expand Up @@ -114,6 +116,13 @@ func (m *DataplaneMetadata) GetAdminPort() uint32 {
return m.AdminPort
}

func (m *DataplaneMetadata) GetAdminAddress() string {
if m == nil {
return ""
}
return m.AdminAddress
}

func (m *DataplaneMetadata) GetDNSPort() uint32 {
if m == nil {
return 0
Expand Down Expand Up @@ -154,6 +163,7 @@ func DataplaneMetadataFromXdsMetadata(xdsMetadata *structpb.Struct, tmpDir strin
metadata.ProxyType = mesh_proto.ProxyType(field.GetStringValue())
}
metadata.AdminPort = uint32Metadata(xdsMetadata, FieldDataplaneAdminPort)
metadata.AdminAddress = xdsMetadata.Fields[FieldDataplaneAdminAddress].GetStringValue()
metadata.DNSPort = uint32Metadata(xdsMetadata, FieldDataplaneDNSPort)
metadata.EmptyDNSPort = uint32Metadata(xdsMetadata, FieldDataplaneDNSEmptyPort)
if value := xdsMetadata.Fields[FieldDataplaneDataplaneResource]; value != nil {
Expand Down
1 change: 1 addition & 0 deletions pkg/xds/bootstrap/template_v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ func genConfig(parameters configParameters, proxyConfig xds.Proxy, enableReloada
}
if parameters.AdminPort != 0 {
res.Node.Metadata.Fields[core_xds.FieldDataplaneAdminPort] = util_proto.MustNewValueForStruct(strconv.Itoa(int(parameters.AdminPort)))
res.Node.Metadata.Fields[core_xds.FieldDataplaneAdminAddress] = util_proto.MustNewValueForStruct(parameters.AdminAddress)
res.Admin = &envoy_bootstrap_v3.Admin{
Address: &envoy_core_v3.Address{
Address: &envoy_core_v3.Address_SocketAddress{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ node:
id: default.dp-1.default
metadata:
accessLogSocketPath: /tmp/kuma-al-dp-1.default-default.sock
dataplane.admin.address: 127.0.0.1
dataplane.admin.port: "1234"
dataplane.proxyType: dataplane
features: []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ node:
id: mesh.name.namespace
metadata:
accessLogSocketPath: /tmp/kuma-al-name.namespace-mesh.sock
dataplane.admin.address: 192.168.0.1
dataplane.admin.port: "9902"
dataplane.proxyType: dataplane
features: []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ node:
id: mesh.name.namespace
metadata:
accessLogSocketPath: /tmp/kuma-al-name.namespace-mesh.sock
dataplane.admin.address: 192.168.0.1
dataplane.admin.port: "1234"
dataplane.proxyType: dataplane
dataplane.resource: |2-
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ node:
id: mesh.name.namespace
metadata:
accessLogSocketPath: /tmp/kuma-al-name.namespace-mesh.sock
dataplane.admin.address: 127.0.0.1
dataplane.admin.port: "1234"
dataplane.dns.empty.port: "53002"
dataplane.dns.port: "53001"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ node:
id: mesh.name.namespace
metadata:
accessLogSocketPath: /tmp/kuma-al-name.namespace-mesh.sock
dataplane.admin.address: 127.0.0.1
dataplane.admin.port: "1234"
dataplane.dns.empty.port: "53002"
dataplane.dns.port: "53001"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ node:
id: mesh.name.namespace
metadata:
accessLogSocketPath: /tmp/kuma-al-name.namespace-mesh.sock
dataplane.admin.address: 127.0.0.1
dataplane.admin.port: "1234"
dataplane.proxyType: dataplane
features: []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ node:
id: mesh.name.namespace
metadata:
accessLogSocketPath: /tmp/kuma-al-name.namespace-mesh.sock
dataplane.admin.address: 127.0.0.1
dataplane.admin.port: "1234"
dataplane.proxyType: dataplane
features: []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ node:
id: mesh.name.namespace
metadata:
accessLogSocketPath: /tmp/kuma-al-name.namespace-mesh.sock
dataplane.admin.address: 127.0.0.1
dataplane.admin.port: "1234"
dataplane.proxyType: dataplane
features: []
Expand Down
31 changes: 27 additions & 4 deletions pkg/xds/generator/admin_proxy_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ package generator
import (
"context"

"github.com/asaskevich/govalidator"
"github.com/pkg/errors"
"golang.org/x/exp/maps"

core_xds "github.com/kumahq/kuma/pkg/core/xds"
xds_context "github.com/kumahq/kuma/pkg/xds/context"
envoy_common "github.com/kumahq/kuma/pkg/xds/envoy"
Expand Down Expand Up @@ -32,6 +36,14 @@ var staticTlsEndpointPaths = []*envoy_common.StaticEndpointPath{
// By default, Admin API is exposed only on loopback interface because of security reasons.
type AdminProxyGenerator struct{}

var adminAddressAllowedValues = map[string]struct{}{
"127.0.0.1": {},
"0.0.0.0": {},
"::1": {},
"::": {},
"": {},
}

func (g AdminProxyGenerator) Generate(ctx context.Context, xdsCtx xds_context.Context, proxy *core_xds.Proxy) (*core_xds.ResourceSet, error) {
if proxy.Metadata.GetAdminPort() == 0 {
// It's not possible to export Admin endpoints if Envoy Admin API has not been enabled on that dataplane.
Expand All @@ -41,14 +53,25 @@ func (g AdminProxyGenerator) Generate(ctx context.Context, xdsCtx xds_context.Co
adminPort := proxy.Metadata.GetAdminPort()
// We assume that Admin API must be available on a loopback interface (while users
// can override the default value `127.0.0.1` in the Bootstrap Server section of `kuma-cp` config,
// the only reasonable alternative is `0.0.0.0`).
// the only reasonable alternatives are `::1`, `0.0.0.0` or `::`).
// In contrast to `AdminPort`, we shouldn't trust `AdminAddress` from the Envoy node metadata
// since it would allow a malicious user to manipulate that value and use Prometheus endpoint
// as a gateway to another host.
adminAddress := "127.0.0.1"
envoyAdminClusterName := envoy_names.GetEnvoyAdminClusterName()
adminAddress := proxy.Metadata.GetAdminAddress()
if _, ok := adminAddressAllowedValues[adminAddress]; !ok {
return nil, errors.Errorf("envoy admin cluster is not allowed to have addresses other than %v", maps.Keys(adminAddressAllowedValues))
}
switch adminAddress {
case "", "0.0.0.0":
adminAddress = "127.0.0.1"
case "::":
adminAddress = "::1"
}
cluster, err := envoy_clusters.NewClusterBuilder(proxy.APIVersion, envoyAdminClusterName).
Configure(envoy_clusters.ProvidedEndpointCluster(false, core_xds.Endpoint{Target: adminAddress, Port: adminPort})).
Configure(envoy_clusters.ProvidedEndpointCluster(
govalidator.IsIPv6(adminAddress),
core_xds.Endpoint{Target: adminAddress, Port: adminPort})).
Configure(envoy_clusters.DefaultTimeout()).
Build()
if err != nil {
Expand All @@ -62,7 +85,7 @@ func (g AdminProxyGenerator) Generate(ctx context.Context, xdsCtx xds_context.Co
}

// We bind admin to 127.0.0.1 by default, creating another listener with same address and port will result in error.
if g.getAddress(proxy) != "127.0.0.1" {
if g.getAddress(proxy) != adminAddress {
filterChains := []envoy_listeners.ListenerBuilderOpt{
envoy_listeners.FilterChain(envoy_listeners.NewFilterChainBuilder(proxy.APIVersion, envoy_common.AnonymousResource).
Configure(envoy_listeners.StaticEndpoints(envoy_names.GetAdminListenerName(), staticEndpointPaths)),
Expand Down
60 changes: 58 additions & 2 deletions pkg/xds/generator/admin_proxy_generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ var _ = Describe("AdminProxyGenerator", func() {
type testCase struct {
dataplaneFile string
expected string
adminAddress string
}

DescribeTable("should generate envoy config",
Expand All @@ -49,7 +50,8 @@ var _ = Describe("AdminProxyGenerator", func() {

proxy := &xds.Proxy{
Metadata: &xds.DataplaneMetadata{
AdminPort: 9901,
AdminPort: 9901,
AdminAddress: given.adminAddress,
},
EnvoyAdminMTLSCerts: xds.ServerSideMTLSCerts{
CaPEM: []byte("caPEM"),
Expand All @@ -76,9 +78,63 @@ var _ = Describe("AdminProxyGenerator", func() {
// and output matches golden files
Expect(actual).To(MatchGoldenYAML(filepath.Join("testdata", "admin", given.expected)))
},
Entry("should generate admin resources", testCase{
Entry("should generate admin resources, empty admin address", testCase{
dataplaneFile: "01.dataplane.input.yaml",
expected: "01.envoy-config.golden.yaml",
adminAddress: "",
}),
Entry("should generate admin resources, IPv4 loopback", testCase{
dataplaneFile: "02.dataplane.input.yaml",
expected: "02.envoy-config.golden.yaml",
adminAddress: "127.0.0.1",
}),
Entry("should generate admin resources, IPv6 loopback", testCase{
dataplaneFile: "03.dataplane.input.yaml",
expected: "03.envoy-config.golden.yaml",
adminAddress: "::1",
}),
Entry("should generate admin resources, unspecified IPv4", testCase{
dataplaneFile: "04.dataplane.input.yaml",
expected: "04.envoy-config.golden.yaml",
adminAddress: "0.0.0.0",
}),
Entry("should generate admin resources, unspecified IPv6", testCase{
dataplaneFile: "05.dataplane.input.yaml",
expected: "05.envoy-config.golden.yaml",
adminAddress: "::",
}),
)

It("should return error when admin address is not allowed", func() {
ctx := xds_context.Context{
Mesh: xds_context.MeshContext{
Resource: &core_mesh.MeshResource{
Meta: &test_model.ResourceMeta{
Name: "default",
},
},
},
}

proxy := &xds.Proxy{
Metadata: &xds.DataplaneMetadata{
AdminPort: 9901,
AdminAddress: "192.168.0.1", // it's not allowed to use such address
},
EnvoyAdminMTLSCerts: xds.ServerSideMTLSCerts{
CaPEM: []byte("caPEM"),
ServerPair: tls.KeyPair{
CertPEM: []byte("certPEM"),
KeyPEM: []byte("keyPEM"),
},
},
Dataplane: core_mesh.NewDataplaneResource(),
APIVersion: envoy_common.APIV3,
}

// when
_, err := generator.Generate(context.Background(), ctx, proxy)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(Equal(`envoy admin cluster is not allowed to have addresses other than [127.0.0.1 0.0.0.0 ::1 :: ]`))
})
})
9 changes: 9 additions & 0 deletions pkg/xds/generator/testdata/admin/02.dataplane.input.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
type: Dataplane
name: web-1
mesh: default
networking:
address: 192.168.0.1
inbound:
- port: 1234
tags:
kuma.io/service: web
94 changes: 94 additions & 0 deletions pkg/xds/generator/testdata/admin/02.envoy-config.golden.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
resources:
- name: kuma:envoy:admin
resource:
'@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
altStatName: kuma_envoy_admin
connectTimeout: 10s
loadAssignment:
clusterName: kuma:envoy:admin
endpoints:
- lbEndpoints:
- endpoint:
address:
socketAddress:
address: 127.0.0.1
portValue: 9901
name: kuma:envoy:admin
type: STATIC
- name: kuma:envoy:admin
resource:
'@type': type.googleapis.com/envoy.config.listener.v3.Listener
address:
socketAddress:
address: 192.168.0.1
portValue: 9901
enableReusePort: false
filterChains:
- filters:
- name: envoy.filters.network.http_connection_manager
typedConfig:
'@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
httpFilters:
- name: envoy.filters.http.router
typedConfig:
'@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
routeConfig:
validateClusters: false
virtualHosts:
- domains:
- '*'
name: kuma:envoy:admin
routes:
- match:
prefix: /ready
route:
cluster: kuma:envoy:admin
prefixRewrite: /ready
statPrefix: kuma_envoy_admin
- filterChainMatch:
transportProtocol: tls
filters:
- name: envoy.filters.network.http_connection_manager
typedConfig:
'@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
httpFilters:
- name: envoy.filters.http.router
typedConfig:
'@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
routeConfig:
validateClusters: false
virtualHosts:
- domains:
- '*'
name: kuma:envoy:admin
routes:
- match:
prefix: /
route:
cluster: kuma:envoy:admin
prefixRewrite: /
statPrefix: kuma_envoy_admin
transportSocket:
name: envoy.transport_sockets.tls
typedConfig:
'@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
commonTlsContext:
tlsCertificates:
- certificateChain:
inlineBytes: Y2VydFBFTQ==
privateKey:
inlineBytes: a2V5UEVN
validationContext:
matchTypedSubjectAltNames:
- matcher:
exact: kuma-cp
sanType: DNS
trustedCa:
inlineBytes: Y2FQRU0=
requireClientCertificate: true
listenerFilters:
- name: envoy.filters.listener.tls_inspector
typedConfig:
'@type': type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector
name: kuma:envoy:admin
trafficDirection: INBOUND
9 changes: 9 additions & 0 deletions pkg/xds/generator/testdata/admin/03.dataplane.input.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
type: Dataplane
name: web-1
mesh: default
networking:
address: 192.168.0.1
inbound:
- port: 1234
tags:
kuma.io/service: web
Loading

0 comments on commit 8481ec2

Please sign in to comment.