Repository: knative-sandbox/net-kourier
Pull request #987 (2023-02-06 04:52:25)

Allow to configure cipher suites for TLS external listener
This patch adds the new configuration to specify the cipher suites for TLS external listener.

It just specifies the cipher suites via `config-kourier` as:

```
$ kubectl -n "knative-serving" patch configmap/config-kourier \
  --type merge \
  -p '{"data":{"cipher-suites":"ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-ECDSA-CHACHA20-POLY1305"}}'
```

The default uses Envoy's default cipher suites as current behavior.

:gift: Add new feature

**Release Note**


```release-note
net-koruier allows to configure cipher suites for TLS external listener
```


knative-prow[bot]: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/knative-sandbox/net-kourier/pull/987#" title="Author self-approved">nak3</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=knative-sandbox%2Fnet-kourier).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/knative-sandbox/net-kourier/blob/main/OWNERS)~~ [nak3]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
codecov[bot]: # [Codecov](https://codecov.io/gh/knative-sandbox/net-kourier/pull/987?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) Report
> Merging [#987](https://codecov.io/gh/knative-sandbox/net-kourier/pull/987?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) (af2bac4) into [main](https://codecov.io/gh/knative-sandbox/net-kourier/commit/ac032c47c45b3bd60bb8a49c73906ead5db564da?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) (ac032c4) will **increase** coverage by `0.07%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##             main     #987      +/-   ##
==========================================
+ Coverage   80.64%   80.71%   +0.07%     
==========================================
  Files          18       18              
  Lines        1333     1338       +5     
==========================================
+ Hits         1075     1080       +5     
  Misses        205      205              
  Partials       53       53              
```


| [Impacted Files](https://codecov.io/gh/knative-sandbox/net-kourier/pull/987?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) | Coverage Δ | |
|---|---|---|
| [pkg/config/config.go](https://codecov.io/gh/knative-sandbox/net-kourier/pull/987?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-cGtnL2NvbmZpZy9jb25maWcuZ28=) | `0.00% <ø> (ø)` | |
| [pkg/config/configmap.go](https://codecov.io/gh/knative-sandbox/net-kourier/pull/987?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-cGtnL2NvbmZpZy9jb25maWdtYXAuZ28=) | `100.00% <100.00%> (ø)` | |
| [pkg/envoy/api/listener.go](https://codecov.io/gh/knative-sandbox/net-kourier/pull/987?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-cGtnL2Vudm95L2FwaS9saXN0ZW5lci5nbw==) | `76.81% <100.00%> (+0.10%)` | :arrow_up: |
| [pkg/generator/caches.go](https://codecov.io/gh/knative-sandbox/net-kourier/pull/987?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-cGtnL2dlbmVyYXRvci9jYWNoZXMuZ28=) | `72.46% <100.00%> (+0.20%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox)

nak3: /cc @skonto @ReToCode 
(SRVCOM-2272)
ReToCode: Nice work.

/lgtm
/approve

knative-prow[bot]: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/knative-sandbox/net-kourier/pull/987#" title="Author self-approved">nak3</a>*, *<a href="https://github.com/knative-sandbox/net-kourier/pull/987#issuecomment-1418609756" title="Approved">ReToCode</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=knative-sandbox%2Fnet-kourier).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/knative-sandbox/net-kourier/blob/main/OWNERS)~~ [nak3]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/README.md b/README.md
index 1717bcfc..e9f7512b 100644
--- a/README.md
+++ b/README.md
@@ -99,6 +99,7 @@ To change the Kourier gateway namespace, you will need to:
 - Support for gRPC services.
 - Timeouts and retries.
 - TLS
+- Cipher Suite
 - External Authorization support.
 - Proxy Protocol (AN EXPERIMENTAL / ALPHA FEATURE)
 - Traffic Isolation (AN EXPERIMENTAL / ALPHA FEATURE)
@@ -118,6 +119,19 @@ CERTS_SECRET_NAMESPACE: ${NAMESPACES_WHERE_THE_SECRET_HAS_BEEN_CREATED}
 CERTS_SECRET_NAME: ${CERT_NAME}
 ```
 
+### Cipher Suites
+
+You can specify the cipher suites for TLS external listener.
+To specify the cipher suites you want to allow, run the following command to patch `config-kourier` ConfigMap:
+
+```
+kubectl -n "knative-serving" patch configmap/config-kourier \
+  --type merge \
+  -p '{"data":{"cipher-suites":"ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-ECDSA-CHACHA20-POLY1305"}}'
+```
+
+The default uses the default cipher suites of the envoy version.
+
 ## External Authorization Configuration
 
 If you want to enable the external authorization support you can set these ENV
@@ -155,6 +169,7 @@ Ensure that the file was updated successfully:
 ```
 kubectl get configmap config-kourier --namespace knative-serving --output yaml
 ```
+
 ### LoadBalancer configuration:
 
 We need to:
diff --git a/config/200-config.yaml b/config/200-config.yaml
index a9524919..bcdc4172 100644
--- a/config/200-config.yaml
+++ b/config/200-config.yaml
@@ -74,13 +74,16 @@ data:
     # NOTE: This flag is in an alpha state.
     traffic-isolation: ""
 
-
     # Specifies whether to use CryptoMB private key provider in order to
     # acclerate the TLS handshake.
     # NOTE THAT THIS IS AN EXPERIMENTAL / ALPHA FEATURE.
     enable-cryptomb: "false"
 
-
     # Configures the number of additional ingress proxy hops from the
     # right side of the x-forwarded-for HTTP header to trust.
     trusted-hops-count: "0"
+
+    # Specifies the cipher suites for TLS external listener.
+    # Use ',' separated values like "ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-ECDSA-CHACHA20-POLY1305"
+    # The default uses the default cipher suites of the envoy version.
+    cipher-suites: ""
diff --git a/pkg/config/config.go b/pkg/config/config.go
index bc0f1f56..aaf423b1 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -79,6 +79,9 @@ const (
 	// trustedHopsCount Configure the number of additional ingress proxy hops from the
 	// right side of the x-forwarded-for HTTP header to trust.
 	trustedHopsCount = "trusted-hops-count"
+
+	// CipherSuites is the cipher suites for TLS external listener.
+	cipherSuites = "cipher-suites"
 )
 
 var disableHTTP2Annotation = kmap.KeyPriority{
diff --git a/pkg/config/configmap.go b/pkg/config/configmap.go
index 09b6f70e..eeece145 100644
--- a/pkg/config/configmap.go
+++ b/pkg/config/configmap.go
@@ -20,6 +20,7 @@ import (
 	"time"
 
 	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/util/sets"
 
 	cm "knative.dev/pkg/configmap"
 )
@@ -63,6 +64,7 @@ func DefaultConfig() *Kourier {
 		IdleTimeout:                0 * time.Second, // default value
 		TrafficIsolation:           "",
 		TrustedHopsCount:           0,
+		CipherSuites:               nil,
 		EnableCryptoMB:             false,
 	}
 }
@@ -78,6 +80,7 @@ func NewConfigFromMap(configMap map[string]string) (*Kourier, error) {
 		cm.AsDuration(IdleTimeoutKey, &nc.IdleTimeout),
 		cm.AsString(trafficIsolation, (*string)(&nc.TrafficIsolation)),
 		cm.AsUint32(trustedHopsCount, &nc.TrustedHopsCount),
+		cm.AsStringSet(cipherSuites, &nc.CipherSuites),
 		cm.AsBool(enableCryptoMB, &nc.EnableCryptoMB),
 	); err != nil {
 		return nil, err
@@ -116,4 +119,6 @@ type Kourier struct {
 	// EnableCryptoMB specifies whether Kourier enable CryptoMB private provider to accelerate
 	// TLS handshake. The default value is "false".
 	EnableCryptoMB bool
+	// CipherSuites specifies the cipher suites for TLS external listener.
+	CipherSuites sets.String
 }
diff --git a/pkg/config/zz_generated.deepcopy.go b/pkg/config/zz_generated.deepcopy.go
index 2cf929c8..3558ce84 100644
--- a/pkg/config/zz_generated.deepcopy.go
+++ b/pkg/config/zz_generated.deepcopy.go
@@ -21,9 +21,20 @@ limitations under the License.
 
 package config
 
+import (
+	sets "k8s.io/apimachinery/pkg/util/sets"
+)
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Kourier) DeepCopyInto(out *Kourier) {
 	*out = *in
+	if in.CipherSuites != nil {
+		in, out := &in.CipherSuites, &out.CipherSuites
+		*out = make(sets.String, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
 	return
 }
 
diff --git a/pkg/envoy/api/listener.go b/pkg/envoy/api/listener.go
index ed818329..5e0d5495 100644
--- a/pkg/envoy/api/listener.go
+++ b/pkg/envoy/api/listener.go
@@ -33,6 +33,8 @@ import (
 	"google.golang.org/protobuf/types/known/anypb"
 	"google.golang.org/protobuf/types/known/durationpb"
 	"k8s.io/apimachinery/pkg/types"
+
+	"knative.dev/net-kourier/pkg/config"
 )
 
 // SNIMatch represents an SNI match, including the hosts to match, the certificates and
@@ -121,8 +123,8 @@ func CreateFilterChainFromCertificateAndPrivateKey(
 // manager and applies a FilterChain with the given sniMatches.
 //
 // Ref: https://www.envoyproxy.io/docs/envoy/latest/faq/configuration/sni.html
-func NewHTTPSListenerWithSNI(manager *hcm.HttpConnectionManager, port uint32, sniMatches []*SNIMatch, enableProxyProtocol bool) (*listener.Listener, error) {
-	filterChains, err := createFilterChainsForTLS(manager, sniMatches)
+func NewHTTPSListenerWithSNI(manager *hcm.HttpConnectionManager, port uint32, sniMatches []*SNIMatch, kourierConfig *config.Kourier) (*listener.Listener, error) {
+	filterChains, err := createFilterChainsForTLS(manager, sniMatches, kourierConfig)
 	if err != nil {
 		return nil, err
 	}
@@ -134,7 +136,7 @@ func NewHTTPSListenerWithSNI(manager *hcm.HttpConnectionManager, port uint32, sn
 	// the SNI will not be parsed correctly if the proxy protocol listener filter is not executed first.
 	// Without SNI matching, you would get the wrong certificate, and traffic would drop.
 	// https://github.com/solo-io/gloo/issues/5116
-	if enableProxyProtocol {
+	if kourierConfig.EnableProxyProtocol {
 		proxyProtocolListenerFilter, err := createProxyProtocolListenerFilter()
 		if err != nil {
 			return nil, err
@@ -193,7 +195,7 @@ func createFilters(manager *hcm.HttpConnectionManager) ([]*listener.Filter, erro
 	}}, nil
 }
 
-func createFilterChainsForTLS(manager *hcm.HttpConnectionManager, sniMatches []*SNIMatch) ([]*listener.FilterChain, error) {
+func createFilterChainsForTLS(manager *hcm.HttpConnectionManager, sniMatches []*SNIMatch, kourierConfig *config.Kourier) ([]*listener.FilterChain, error) {
 	res := make([]*listener.FilterChain, 0, len(sniMatches))
 	for _, sniMatch := range sniMatches {
 		filters, err := createFilters(manager)
@@ -201,7 +203,7 @@ func createFilterChainsForTLS(manager *hcm.HttpConnectionManager, sniMatches []*
 			return nil, err
 		}
 
-		c := Certificate{Certificate: sniMatch.CertificateChain, PrivateKey: sniMatch.PrivateKey}
+		c := Certificate{Certificate: sniMatch.CertificateChain, PrivateKey: sniMatch.PrivateKey, CipherSuites: kourierConfig.CipherSuites.List()}
 
 		tlsContext, err := c.createTLSContext()
 		if err != nil {
@@ -235,6 +237,7 @@ type Certificate struct {
 	PrivateKey         []byte
 	PrivateKeyProvider string
 	PollDelay          *durationpb.Duration
+	CipherSuites       []string
 }
 
 // messageToAny converts from proto message to proto Any
@@ -276,6 +279,7 @@ func (c Certificate) createTLSContext() (*auth.DownstreamTlsContext, error) {
 			// Temporary fix until we start using envoyproxy image newer than v1.23.0 (envoyproxy has adopted TLS v1.2 as the default minimum version in https://github.com/envoyproxy/envoy/commit/f8baa480ec9c6cbaa7a9d5433102efb04145cfc8)
 			TlsParams: &auth.TlsParameters{
 				TlsMinimumProtocolVersion: auth.TlsParameters_TLSv1_2,
+				CipherSuites:              c.CipherSuites,
 			},
 			TlsCertificates: []*auth.TlsCertificate{tlsCertificates},
 		},
diff --git a/pkg/generator/caches.go b/pkg/generator/caches.go
index a0129871..e9b51501 100644
--- a/pkg/generator/caches.go
+++ b/pkg/generator/caches.go
@@ -313,16 +313,19 @@ func generateListenersAndRouteConfigs(
 	if len(sniMatches) > 0 {
 		externalHTTPSEnvoyListener, err := envoy.NewHTTPSListenerWithSNI(
 			externalTLSManager, config.HTTPSPortExternal,
-			sniMatches, cfg.Kourier.EnableProxyProtocol,
+			sniMatches, cfg.Kourier,
 		)
 		if err != nil {
 			return nil, nil, err
 		}
 
+		probeConfig := cfg.Kourier
+		probeConfig.EnableProxyProtocol = false // Disable proxy protocol for prober.
+
 		// create https prob listener with SNI
 		probHTTPSListener, err := envoy.NewHTTPSListenerWithSNI(
 			externalManager, config.HTTPSPortProb,
-			sniMatches, false,
+			sniMatches, probeConfig,
 		)
 		if err != nil {
 			return nil, nil, err
@@ -395,6 +398,7 @@ func newExternalEnvoyListenerWithOneCertFilterChain(ctx context.Context, manager
 		Certificate:        certificateChain,
 		PrivateKey:         privateKey,
 		PrivateKeyProvider: privateKeyProvider(cfg.EnableCryptoMB),
+		CipherSuites:       cfg.CipherSuites.List(),
 	})
 }
 
@@ -416,6 +420,7 @@ func newInternalEnvoyListenerWithOneCert(ctx context.Context, manager *httpconnm
 		Certificate:        certificateChain,
 		PrivateKey:         privateKey,
 		PrivateKeyProvider: privateKeyProvider(cfg.EnableCryptoMB),
+		CipherSuites:       cfg.CipherSuites.List(),
 	})
 	if err != nil {
 		return nil, err


Test patch:
diff --git a/pkg/config/configmap_test.go b/pkg/config/configmap_test.go
index 1132d209..d3c6a519 100644
--- a/pkg/config/configmap_test.go
+++ b/pkg/config/configmap_test.go
@@ -23,6 +23,7 @@ import (
 
 	"github.com/google/go-cmp/cmp"
 	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/util/sets"
 
 	_ "knative.dev/pkg/system/testing"
 )
@@ -84,6 +85,16 @@ func TestKourierConfig(t *testing.T) {
 		data: map[string]string{
 			enableProxyProtocol: "foo",
 		},
+	}, {
+		name: "set cipher suites",
+		want: &Kourier{
+			EnableServiceAccessLogging: false,
+			CipherSuites:               sets.NewString("foo", "bar"),
+		},
+		data: map[string]string{
+			enableServiceAccessLoggingKey: "false",
+			cipherSuites:                  "foo, bar",
+		},
 	}, {
 		name: "set timeout to 200",
 		want: &Kourier{
@@ -110,19 +121,17 @@ func TestKourierConfig(t *testing.T) {
 		data: map[string]string{
 			trafficIsolation: "port",
 		},
-	},
-		{
-			name: "add 3 trusted hops",
-			want: &Kourier{
-				EnableServiceAccessLogging: false,
-				TrustedHopsCount:           3,
-			},
-			data: map[string]string{
-				enableServiceAccessLoggingKey: "false",
-				trustedHopsCount:              "3",
-			},
+	}, {
+		name: "add 3 trusted hops",
+		want: &Kourier{
+			EnableServiceAccessLogging: false,
+			TrustedHopsCount:           3,
 		},
-	}
+		data: map[string]string{
+			enableServiceAccessLoggingKey: "false",
+			trustedHopsCount:              "3",
+		},
+	}}
 
 	for _, tt := range configTests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/envoy/api/listener_test.go b/pkg/envoy/api/listener_test.go
index c5dc4ac8..c30d218d 100644
--- a/pkg/envoy/api/listener_test.go
+++ b/pkg/envoy/api/listener_test.go
@@ -32,6 +32,7 @@ import (
 	"google.golang.org/protobuf/types/known/durationpb"
 	"gotest.tools/v3/assert"
 	is "gotest.tools/v3/assert/cmp"
+	"k8s.io/apimachinery/pkg/util/sets"
 	"knative.dev/net-kourier/pkg/config"
 )
 
@@ -158,6 +159,41 @@ func TestNewHTTPSListenerWithPrivatekeyProvider(t *testing.T) {
 	// check proxy protocol is not configured
 	assert.Check(t, len(l.ListenerFilters) == 0)
 }
+
+func TestNewHTTPSListenerWithSNIWithCipherSuites(t *testing.T) {
+	sniMatches := []*SNIMatch{{
+		Hosts:            []string{"some_host.com"},
+		CertificateChain: []byte("cert1"),
+		PrivateKey:       []byte("key1"),
+	}, {
+		Hosts:            []string{"another_host.com"},
+		CertificateChain: []byte("cert2"),
+		PrivateKey:       []byte("key2"),
+	}}
+	kourierConfig := config.Kourier{
+		EnableServiceAccessLogging: true,
+		EnableProxyProtocol:        false,
+		IdleTimeout:                0 * time.Second,
+		CipherSuites:               sets.NewString("foo", "bar"),
+	}
+	manager := NewHTTPConnectionManager("test", &kourierConfig)
+	listener, err := NewHTTPSListenerWithSNI(manager, 8443, sniMatches, &kourierConfig)
+	assert.NilError(t, err)
+
+	downstreamTLSContext := &auth.DownstreamTlsContext{}
+	err = anypb.UnmarshalTo(listener.FilterChains[0].GetTransportSocket().GetTypedConfig(), downstreamTLSContext, proto.UnmarshalOptions{})
+	assert.NilError(t, err)
+
+	assert.DeepEqual(t, kourierConfig.CipherSuites.List(), downstreamTLSContext.CommonTlsContext.TlsParams.CipherSuites)
+
+	assert.Equal(t, core.SocketAddress_TCP, listener.Address.GetSocketAddress().Protocol)
+	assert.Equal(t, "0.0.0.0", listener.Address.GetSocketAddress().Address)
+	assert.Equal(t, uint32(8443), listener.Address.GetSocketAddress().GetPortValue())
+
+	assertListenerHasSNIMatchConfigured(t, listener, sniMatches[0])
+	assertListenerHasSNIMatchConfigured(t, listener, sniMatches[1])
+}
+
 func TestNewHTTPSListenerWithProxyProtocol(t *testing.T) {
 	kourierConfig := config.Kourier{
 		EnableServiceAccessLogging: true,
@@ -203,7 +239,7 @@ func TestNewHTTPSListenerWithSNI(t *testing.T) {
 		IdleTimeout:                0 * time.Second,
 	}
 	manager := NewHTTPConnectionManager("test", &kourierConfig)
-	listener, err := NewHTTPSListenerWithSNI(manager, 8443, sniMatches, false)
+	listener, err := NewHTTPSListenerWithSNI(manager, 8443, sniMatches, &kourierConfig)
 	assert.NilError(t, err)
 
 	assert.Equal(t, core.SocketAddress_TCP, listener.Address.GetSocketAddress().Protocol)
@@ -235,7 +271,7 @@ func TestNewHTTPSListenerWithSNIWithProxyProtocol(t *testing.T) {
 		IdleTimeout:                0 * time.Second,
 	}
 	manager := NewHTTPConnectionManager("test", &kourierConfig)
-	listener, err := NewHTTPSListenerWithSNI(manager, 8443, sniMatches, true)
+	listener, err := NewHTTPSListenerWithSNI(manager, 8443, sniMatches, &kourierConfig)
 	assert.NilError(t, err)
 
 	assert.Equal(t, core.SocketAddress_TCP, listener.Address.GetSocketAddress().Protocol)
diff --git a/test/e2e-kind.sh b/test/e2e-kind.sh
index 6b78f0ae..21768025 100755
--- a/test/e2e-kind.sh
+++ b/test/e2e-kind.sh
@@ -63,6 +63,17 @@ echo ">> Scale down after HA tests"
 kubectl -n "${KOURIER_GATEWAY_NAMESPACE}" scale deployment 3scale-kourier-gateway --replicas=1
 kubectl -n "${KOURIER_CONTROL_NAMESPACE}" scale deployment net-kourier-controller --replicas=1
 
+echo ">> Running TLS Cipher suites"
+echo ">> Setup cipher suites"
+kubectl -n "${KOURIER_CONTROL_NAMESPACE}" patch configmap/config-kourier --type merge -p '{"data":{"cipher-suites":"ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-ECDSA-CHACHA20-POLY1305"}}'
+
+go test -v  -tags=e2e ./test/tls/... \
+  --ingressendpoint="${IPS[0]}" \
+  --ingressClass=kourier.ingress.networking.knative.dev \
+  --cluster-suffix="$CLUSTER_SUFFIX"
+
+kubectl -n "${KOURIER_CONTROL_NAMESPACE}" patch configmap/config-kourier --type merge -p '{"data":{"cipher-suites":""}}'
+
 echo ">> Setup one certificate"
 $(dirname $0)/generate-cert.sh
 kubectl -n "${KOURIER_CONTROL_NAMESPACE}" set env deployment net-kourier-controller CERTS_SECRET_NAMESPACE="${KOURIER_CONTROL_NAMESPACE}" CERTS_SECRET_NAME=wildcard-certs
diff --git a/test/tls/cipher_test.go b/test/tls/cipher_test.go
new file mode 100644
index 00000000..6029cb92
--- /dev/null
+++ b/test/tls/cipher_test.go
@@ -0,0 +1,124 @@
+//go:build e2e
+// +build e2e
+
+/*
+Copyright 2023 The Knative Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tls
+
+import (
+	"context"
+	"crypto/tls"
+	"net/http"
+	"strings"
+	"testing"
+
+	"k8s.io/apimachinery/pkg/util/intstr"
+	"knative.dev/networking/pkg/apis/networking"
+	"knative.dev/networking/pkg/apis/networking/v1alpha1"
+	"knative.dev/networking/test"
+	"knative.dev/networking/test/conformance/ingress"
+)
+
+const (
+	// The cipher which is configured in config-network for test. See test/e2e-kind.sh.
+	allowedCipher = tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
+
+	// The cipher which is NOT configured in config-network for test.
+	disabledCipher = tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA
+
+	// TLS handshake failure message which is implemented in Go TLS library.
+	handshakeFailure = "handshake failure"
+)
+
+// TestCipherSuites verifies that tls client only works with the cipher suites configured.
+func TestCipherSuites(t *testing.T) {
+	t.Parallel()
+	ctx, clients := context.Background(), test.Setup(t)
+
+	tests := []struct {
+		cipherSuites []uint16
+		expErr       string
+	}{{
+		cipherSuites: []uint16{allowedCipher},
+	}, {
+		cipherSuites: []uint16{disabledCipher},
+		expErr:       handshakeFailure,
+	}}
+
+	for _, test := range tests {
+		host, client := create(ctx, t, clients, test.cipherSuites)
+		err := sendRequest(ctx, t, client, host)
+		if err != nil && !strings.Contains(err.Error(), test.expErr) {
+			t.Fatalf("Unexpected error: %v, wanted %q", err, test.expErr)
+		}
+		if err == nil && test.expErr != "" {
+			t.Fatalf("Failed to disallow cipher, wanted error: %q", test.expErr)
+		}
+	}
+}
+
+func create(ctx context.Context, t *testing.T, clients *test.Clients, cipherSuites []uint16) (string, *http.Client) {
+	name, port, _ := ingress.CreateRuntimeService(ctx, t, clients, networking.ServicePortNameHTTP1)
+	hosts := []string{name + ".example.com"}
+
+	secretName, tlsConfig, _ := ingress.CreateTLSSecret(ctx, t, clients, hosts)
+	tlsConfig.CipherSuites = cipherSuites
+	// TLS 1.3 ciphersuites are not configurable. See https://pkg.go.dev/crypto/tls#Config
+	tlsConfig.MaxVersion = tls.VersionTLS12
+
+	_, client, _ := ingress.CreateIngressReadyWithTLS(ctx, t, clients, v1alpha1.IngressSpec{
+		Rules: []v1alpha1.IngressRule{{
+			Hosts:      hosts,
+			Visibility: v1alpha1.IngressVisibilityExternalIP,
+			HTTP: &v1alpha1.HTTPIngressRuleValue{
+				Paths: []v1alpha1.HTTPIngressPath{{
+					Splits: []v1alpha1.IngressBackendSplit{{
+						IngressBackend: v1alpha1.IngressBackend{
+							ServiceName:      name,
+							ServiceNamespace: test.ServingNamespace,
+							ServicePort:      intstr.FromInt(port),
+						},
+					}},
+				}},
+			},
+		}},
+		TLS: []v1alpha1.IngressTLS{{
+			Hosts:           hosts,
+			SecretName:      secretName,
+			SecretNamespace: test.ServingNamespace,
+		}},
+	}, tlsConfig)
+	return hosts[0], client
+}
+
+func sendRequest(ctx context.Context, t *testing.T, client *http.Client, hostname string) error {
+	// Check without TLS.
+	ingress.RuntimeRequest(ctx, t, client, "http://"+hostname)
+
+	// Check with TLS.
+	resp, err := client.Get("https://" + hostname)
+	if err != nil {
+		return err
+	}
+
+	defer resp.Body.Close()
+	if resp.StatusCode != http.StatusOK {
+		t.Errorf("Unexpected status code: %d, wanted %v", resp.StatusCode, http.StatusOK)
+		ingress.DumpResponse(ctx, t, resp)
+	}
+	return nil
+}