Repository: zalando-incubator/kube-ingress-aws-controller
Pull request #400 (2021-02-09 17:54:21)

ALB Listener rule to deny internal domain requests
This commit addresses #402. It adds the following new configuration
flags:
  - internal-domains
  - deny-internal-domains
  - deny-internal-domains-response
  - deny-internal-domains-response-content-type
  - deny-internal-domains-response-status-code

The main flag is the `deny-internal-domains`, a boolean that enables and
disables the feature. The other flags allow to set the internal domains
that will be protected as well as the response that will be returned.
Defaults are also set: an HTTP `text/plain` `401 - Unauthorized`
response for the `*.ingress.cluster.local` domain.

The ALB cloudformation template will add a
[`AWS::ElasticLoadBalancingV2::ListenerRule`][0] resource, configured
with the [condition][1] values from the `internal-domains` and the
[action `fixedresponseconfig`][2] with the respective response
`deny-internal-domains-response` flags.

[0]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-listenerrule.html
[1]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticloadbalancingv2-listenerrule-hostheaderconfig.html
[2]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticloadbalancingv2-listenerrule-action.html#cfn-elasticloadbalancingv2-listenerrule-action-fixedresponseconfig

Signed-off-by: Jonathan Juares Beber <jonathanbeber@gmail.com>

jonathanbeber: @aermakov-zalando I will mark the PR as ready to review opening it.
jonathanbeber: @aermakov-zalando please, now it's ready for review!
mikkeloscar: :+1: 
szuecs: I miss the documentation in the readme 
jonathanbeber: @szuecs thanks for reviewing, I added some docs.
szuecs: :+1: 
mikkeloscar: :+1: 

Patch:
diff --git a/README.md b/README.md
index 3eba71c..9c0a8dc 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,7 @@ This information is used to manage AWS resources for each ingress objects of the
 - Support for both [Application Load Balancers][alb] and [Network Load Balancers][nlb].
 - Support for internet-facing and internal load balancers
 - Support for ignoring cluster-internal ingress, that only have `--cluster-local-domain=cluster.local` domains.
+- Support for denying traffic for internal domains.
 - Support for multiple Auto Scaling Groups
 - Support for instances that are not part of Auto Scaling Group
 - Support for SSLPolicy, set default and per ingress
@@ -357,7 +358,49 @@ spec:
 ```
 
 If you pass `--cluster-local-domain=".cluster.local"`, you can change
-what domain is considered cluster internal.
+what domain is considered cluster internal. If you're using the [deny
+internal traffic](#deny-traffic-for-internal-domains) feature, you might
+want to sync this configuration with the `--internal-domains` one.
+
+#### Deny traffic for internal domains
+
+Since `>=v0.11.18` the controller supports the flag
+`--deny-internal-domains`. It's a boolean config item that when enabled
+configures the ALBs' cloudformation templates with a
+[`AWS::ElasticLoadBalancingV2::ListenerRule`][ListenerRule] resource.
+This rule will be configured with the [condition][HostHeaderConfig]
+values from the `--internal-domains` flag and the
+[action `fixedresponseconfig`][FixedResponse] with the respective response
+`--deny-internal-domains-response` flags. This feature is not enabled by
+default. The following are the default values to its config flags:
+
+- `internal-domains`: `*.cluster.local`
+- `deny-internal-domains`: `false`
+- `deny-internal-domains-response`: `Unauthorized`
+- `deny-internal-domains-response-content-type`: `text/plain`
+- `deny-internal-domains-response-status-code`: `401`
+
+Note that `--internal-domains` differs from `--cluster-local-domain`,
+which is used exclusively to [avoid load balancers creation for the
+cluster internal
+domain](#omit-to-create-a-load-balancer-for-cluster-internal-domains).
+The `--internal-domains` flag can be set multiple times and accept AWS'
+wildcard characters. Check the AWS' docs on the [Host Header
+config][HostHeaderConfig] for more details.
+
+This feature is not supported by NLBs.
+
+Example:
+
+Running the controller with `--deny-internal-domains=true` and
+`--internal-domains=*.cluster.local` will generate a rule in the ALB
+that matches any request to domains ending in `.cluster.local` and answer
+the request with an [HTTP 401 Unauthorized][401].
+
+[ListenerRule]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-listenerrule.html
+[HostHeaderConfig]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticloadbalancingv2-listenerrule-hostheaderconfig.html
+[FixedResponse]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticloadbalancingv2-listenerrule-action.html#cfn-elasticloadbalancingv2-listenerrule-action-fixedresponseconfig
+[401]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401
 
 #### Create Load Balancer with SSL Policy
 
diff --git a/aws/adapter.go b/aws/adapter.go
index e2bd2dc..3c26f32 100644
--- a/aws/adapter.go
+++ b/aws/adapter.go
@@ -37,31 +37,36 @@ type Adapter struct {
 	iam            iamiface.IAMAPI
 	cloudformation cloudformationiface.CloudFormationAPI
 
-	manifest                   *manifest
-	healthCheckPath            string
-	healthCheckPort            uint
-	healthCheckInterval        time.Duration
-	healthCheckTimeout         time.Duration
-	targetPort                 uint
-	creationTimeout            time.Duration
-	idleConnectionTimeout      time.Duration
-	deregistrationDelayTimeout time.Duration
-	TargetedAutoScalingGroups  map[string]*autoScalingGroupDetails
-	OwnedAutoScalingGroups     map[string]*autoScalingGroupDetails
-	ec2Details                 map[string]*instanceDetails
-	singleInstances            map[string]*instanceDetails
-	obsoleteInstances          []string
-	stackTerminationProtection bool
-	stackTags                  map[string]string
-	controllerID               string
-	sslPolicy                  string
-	ipAddressType              string
-	albLogsS3Bucket            string
-	albLogsS3Prefix            string
-	httpRedirectToHTTPS        bool
-	nlbCrossZone               bool
-	nlbHTTPEnabled             bool
-	customFilter               string
+	manifest                    *manifest
+	healthCheckPath             string
+	healthCheckPort             uint
+	healthCheckInterval         time.Duration
+	healthCheckTimeout          time.Duration
+	targetPort                  uint
+	creationTimeout             time.Duration
+	idleConnectionTimeout       time.Duration
+	deregistrationDelayTimeout  time.Duration
+	TargetedAutoScalingGroups   map[string]*autoScalingGroupDetails
+	OwnedAutoScalingGroups      map[string]*autoScalingGroupDetails
+	ec2Details                  map[string]*instanceDetails
+	singleInstances             map[string]*instanceDetails
+	obsoleteInstances           []string
+	stackTerminationProtection  bool
+	stackTags                   map[string]string
+	controllerID                string
+	sslPolicy                   string
+	ipAddressType               string
+	albLogsS3Bucket             string
+	albLogsS3Prefix             string
+	httpRedirectToHTTPS         bool
+	nlbCrossZone                bool
+	nlbHTTPEnabled              bool
+	customFilter                string
+	internalDomains             []string
+	denyInternalDomains         bool
+	denyInternalRespBody        string
+	denyInternalRespContentType string
+	denyInternalRespStatusCode  int
 }
 
 type manifest struct {
@@ -365,6 +370,48 @@ func (a *Adapter) WithCustomFilter(customFilter string) *Adapter {
 	return a
 }
 
+// WithInternalDomains returns the receiver adapter after changing the
+// internal domains that will be used by the resources created by the
+// adapter.
+func (a *Adapter) WithInternalDomains(domains []string) *Adapter {
+	a.internalDomains = domains
+	return a
+}
+
+// WithDenyInternalDomains returns the receiver adapter after setting
+// the denyInternalDomains config.
+func (a *Adapter) WithDenyInternalDomains(deny bool) *Adapter {
+	a.denyInternalDomains = deny
+	return a
+}
+
+// WithInternalDomainsDenyResponse returns the receiver adapter after
+// changing the Body of the response that will be returned by the
+// resources created by the adapter when denyInternalDomains is set to
+// true.
+func (a *Adapter) WithInternalDomainsDenyResponse(body string) *Adapter {
+	a.denyInternalRespBody = body
+	return a
+}
+
+// WithInternalDomainsDenyResponseStatusCode returns the receiver
+// adapter after changing the status code of the response that will be
+// returned by the resources created by the adapter when
+// denyInternalDomains is set to true.
+func (a *Adapter) WithInternalDomainsDenyResponseStatusCode(code int) *Adapter {
+	a.denyInternalRespStatusCode = code
+	return a
+}
+
+// WithInternalDomainsDenyResponseContenType returns the receiver
+// adapter after changing the content-type of the response that will be
+// returned by the resources created by the adapter when
+// denyInternalDomains is set to true.
+func (a *Adapter) WithInternalDomainsDenyResponseContenType(contentType string) *Adapter {
+	a.denyInternalRespContentType = contentType
+	return a
+}
+
 // ClusterID returns the ClusterID tag that all resources from the same Kubernetes cluster share.
 // It's taken from the current ec2 instance.
 func (a *Adapter) ClusterID() string {
@@ -558,6 +605,13 @@ func (a *Adapter) CreateStack(certificateARNs []string, scheme, securityGroup, o
 		nlbHTTPEnabled:                    a.nlbHTTPEnabled,
 		http2:                             http2,
 		tags:                              a.stackTags,
+		internalDomains:                   a.internalDomains,
+		denyInternalDomains:               a.denyInternalDomains,
+		denyInternalDomainsResponse: denyResp{
+			body:        a.denyInternalRespBody,
+			statusCode:  a.denyInternalRespStatusCode,
+			contentType: a.denyInternalRespContentType,
+		},
 	}
 
 	return createStack(a.cloudformation, spec)
@@ -601,6 +655,13 @@ func (a *Adapter) UpdateStack(stackName string, certificateARNs map[string]time.
 		nlbHTTPEnabled:                    a.nlbHTTPEnabled,
 		http2:                             http2,
 		tags:                              a.stackTags,
+		internalDomains:                   a.internalDomains,
+		denyInternalDomains:               a.denyInternalDomains,
+		denyInternalDomainsResponse: denyResp{
+			body:        a.denyInternalRespBody,
+			statusCode:  a.denyInternalRespStatusCode,
+			contentType: a.denyInternalRespContentType,
+		},
 	}
 
 	return updateStack(a.cloudformation, spec)
diff --git a/aws/cf.go b/aws/cf.go
index 6fc4b42..0d2cdfd 100644
--- a/aws/cf.go
+++ b/aws/cf.go
@@ -147,6 +147,9 @@ type stackSpec struct {
 	nlbCrossZone                      bool
 	nlbHTTPEnabled                    bool
 	http2                             bool
+	denyInternalDomains               bool
+	denyInternalDomainsResponse       denyResp
+	internalDomains                   []string
 	tags                              map[string]string
 }
 
@@ -157,6 +160,12 @@ type healthCheck struct {
 	timeout  time.Duration
 }
 
+type denyResp struct {
+	statusCode  int
+	contentType string
+	body        string
+}
+
 func createStack(svc cloudformationiface.CloudFormationAPI, spec *stackSpec) (string, error) {
 	template, err := generateTemplate(spec)
 	if err != nil {
diff --git a/aws/cf_template.go b/aws/cf_template.go
index e450945..86b9324 100644
--- a/aws/cf_template.go
+++ b/aws/cf_template.go
@@ -11,6 +11,21 @@ import (
 	cloudformation "github.com/mweagle/go-cloudformation"
 )
 
+const (
+	// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticloadbalancingv2-listenerrule-rulecondition.html#cfn-elasticloadbalancingv2-listenerrule-rulecondition-field
+	listenerRuleConditionHostField = "host-header"
+
+	// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticloadbalancingv2-listenerrule-action.html#cfn-elasticloadbalancingv2-listenerrule-action-type
+	listenerRuleActionTypeFixedRes = "fixed-response"
+
+	// internalTrafficDenyRulePriority defines the [priority][0] for the
+	// rule that denies requests directed to internal domains on the HTTP
+	// and HTTPS listener.
+	//
+	// [0]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-listenerrule.html#cfn-elasticloadbalancingv2-listenerrule-priority
+	internalTrafficDenyRulePriority int64 = 1
+)
+
 func hashARNs(certARNs []string) []byte {
 	hash := sha256.New()
 
@@ -124,7 +139,8 @@ func generateTemplate(spec *stackSpec) (string, error) {
 			Protocol:        cloudformation.String("HTTP"),
 		})
 	} else if spec.loadbalancerType == LoadBalancerTypeApplication || spec.nlbHTTPEnabled {
-		template.AddResource("HTTPListener", &cloudformation.ElasticLoadBalancingV2Listener{
+		listenerName := "HTTPListener"
+		template.AddResource(listenerName, &cloudformation.ElasticLoadBalancingV2Listener{
 			DefaultActions: &cloudformation.ElasticLoadBalancingV2ListenerActionList{
 				{
 					Type:           cloudformation.String("forward"),
@@ -135,6 +151,17 @@ func generateTemplate(spec *stackSpec) (string, error) {
 			Port:            cloudformation.Integer(80),
 			Protocol:        cloudformation.String(protocol),
 		})
+		if spec.denyInternalDomains {
+			template.AddResource(
+				"HTTPRuleBlockInternalTraffic",
+				generateDenyInternalTrafficRule(
+					listenerName,
+					internalTrafficDenyRulePriority,
+					spec.internalDomains,
+					spec.denyInternalDomainsResponse,
+				),
+			)
+		}
 	}
 
 	if len(spec.certificateARNs) > 0 {
@@ -148,7 +175,8 @@ func generateTemplate(spec *stackSpec) (string, error) {
 		})
 
 		// Add an HTTPS Listener resource with the first certificate as the default one
-		template.AddResource("HTTPSListener", &cloudformation.ElasticLoadBalancingV2Listener{
+		listenerName := "HTTPSListener"
+		template.AddResource(listenerName, &cloudformation.ElasticLoadBalancingV2Listener{
 			DefaultActions: &cloudformation.ElasticLoadBalancingV2ListenerActionList{
 				{
 					Type:           cloudformation.String("forward"),
@@ -165,6 +193,17 @@ func generateTemplate(spec *stackSpec) (string, error) {
 			Protocol:        cloudformation.String(tlsProtocol),
 			SslPolicy:       cloudformation.Ref(parameterListenerSslPolicyParameter).String(),
 		})
+		if spec.denyInternalDomains {
+			template.AddResource(
+				"HTTPSRuleBlockInternalTraffic",
+				generateDenyInternalTrafficRule(
+					listenerName,
+					internalTrafficDenyRulePriority,
+					spec.internalDomains,
+					spec.denyInternalDomainsResponse,
+				),
+			)
+		}
 
 		// Add a ListenerCertificate resource with all of the certificates, including the default one
 		certificateList := make(cloudformation.ElasticLoadBalancingV2ListenerCertificateCertificateList, 0, len(certificateARNs))
@@ -179,8 +218,9 @@ func generateTemplate(spec *stackSpec) (string, error) {
 		resourceName := fmt.Sprintf("HTTPSListenerCertificate%x", hashARNs(certificateARNs))
 		template.AddResource(resourceName, &cloudformation.ElasticLoadBalancingV2ListenerCertificate{
 			Certificates: &certificateList,
-			ListenerArn:  cloudformation.Ref("HTTPSListener").String(),
+			ListenerArn:  cloudformation.Ref(listenerName).String(),
 		})
+
 	}
 
 	// Build up the LoadBalancerAttributes list, as there is no way to make attributes conditional in the template
@@ -351,3 +391,31 @@ func generateTemplate(spec *stackSpec) (string, error) {
 
 	return string(stackTemplate), nil
 }
+
+func generateDenyInternalTrafficRule(listenerName string, rulePriority int64, internalDomains []string, resp denyResp) cloudformation.ElasticLoadBalancingV2ListenerRule {
+	values := cloudformation.StringList()
+	for _, domain := range internalDomains {
+		values.Literal = append(values.Literal, cloudformation.String(domain))
+	}
+
+	return cloudformation.ElasticLoadBalancingV2ListenerRule{
+		Conditions: &cloudformation.ElasticLoadBalancingV2ListenerRuleRuleConditionList{
+			cloudformation.ElasticLoadBalancingV2ListenerRuleRuleCondition{
+				Field:  cloudformation.String(listenerRuleConditionHostField),
+				Values: values,
+			},
+		},
+		Actions: &cloudformation.ElasticLoadBalancingV2ListenerRuleActionList{
+			cloudformation.ElasticLoadBalancingV2ListenerRuleAction{
+				Type: cloudformation.String(listenerRuleActionTypeFixedRes),
+				FixedResponseConfig: &cloudformation.ElasticLoadBalancingV2ListenerRuleFixedResponseConfig{
+					ContentType: cloudformation.String(resp.contentType),
+					MessageBody: cloudformation.String(resp.body),
+					StatusCode:  cloudformation.String(fmt.Sprintf("%d", resp.statusCode)),
+				},
+			},
+		},
+		Priority:    cloudformation.Integer(rulePriority),
+		ListenerArn: cloudformation.Ref(listenerName).String(),
+	}
+}
diff --git a/controller.go b/controller.go
index 5cfe82a..f2418d9 100644
--- a/controller.go
+++ b/controller.go
@@ -74,6 +74,12 @@ var (
 	nlbCrossZone                  bool
 	nlbHTTPEnabled                bool
 	ingressAPIVersion             string
+	internalDomains               []string
+	denyInternalDomains           bool
+	denyInternalRespBody          string
+	denyInternalRespContentType   string
+	denyInternalRespStatusCode    int
+	defaultInternalDomains        = fmt.Sprintf("*%s", kubernetes.DefaultClusterLocalDomain)
 )
 
 func loadSettings() error {
@@ -148,6 +154,16 @@ func loadSettings() error {
 		Default("false").BoolVar(&nlbHTTPEnabled)
 	kingpin.Flag("ingress-api-version", "APIversion used for listing/updating ingresses.").
 		Default(kubernetes.IngressAPIVersionNetworking).EnumVar(&ingressAPIVersion, kubernetes.IngressAPIVersionNetworking, kubernetes.IngressAPIVersionExtensions)
+	kingpin.Flag("deny-internal-domains", "Sets a rule on ALB's Listeners that denies requests with the Host header as a internal domain. Domains can be set with the -internal-domains flag.").
+		Default("false").BoolVar(&denyInternalDomains)
+	kingpin.Flag("internal-domains", "Define the internal domains to be blocked when -deny-internal-domains is set to true. Set it multiple times for multiple domains. The maximum size of each name is 128 characters. The following wildcard characters are supported: * (matches 0 or more characters) and ? (matches exactly 1 character).").
+		Default(defaultInternalDomains).StringsVar(&internalDomains)
+	kingpin.Flag("deny-internal-domains-response", "Defines the response body for a request identified as to an internal domain when -deny-internal-domains is set.").
+		Default("Unauthorized").StringVar(&denyInternalRespBody)
+	kingpin.Flag("deny-internal-domains-response-content-type", "Defines the response conten-type for a request identified as to an internal domain when -deny-internal-domains is set.").
+		Default("text/plain").StringVar(&denyInternalRespContentType)
+	kingpin.Flag("deny-internal-domains-response-status-code", "Defines the response status code for a request identified as to an internal domain when -deny-internal-domains is set.").
+		Default("401").IntVar(&denyInternalRespStatusCode)
 	kingpin.Parse()
 
 	blacklistCertArnMap = make(map[string]bool)
@@ -249,7 +265,12 @@ func main() {
 		WithNLBCrossZone(nlbCrossZone).
 		WithNLBHTTPEnabled(nlbHTTPEnabled).
 		WithCustomFilter(customFilter).
-		WithStackTags(additionalStackTags)
+		WithStackTags(additionalStackTags).
+		WithInternalDomains(internalDomains).
+		WithDenyInternalDomains(denyInternalDomains).
+		WithInternalDomainsDenyResponse(denyInternalRespBody).
+		WithInternalDomainsDenyResponseStatusCode(denyInternalRespStatusCode).
+		WithInternalDomainsDenyResponseContenType(denyInternalRespContentType)
 
 	log.Debug("certs.NewCachingProvider")
 	certificatesProvider, err := certs.NewCachingProvider(


Test patch:
diff --git a/aws/cf_template_test.go b/aws/cf_template_test.go
index 4c1a153..33a9219 100644
--- a/aws/cf_template_test.go
+++ b/aws/cf_template_test.go
@@ -2,7 +2,10 @@ package aws
 
 import (
 	"encoding/json"
+	"fmt"
+	"sort"
 	"testing"
+	"time"
 
 	cloudformation "github.com/mweagle/go-cloudformation"
 	"github.com/stretchr/testify/assert"
@@ -10,6 +13,42 @@ import (
 )
 
 func TestGenerateTemplate(t *testing.T) {
+	internalDomains := []string{"*.ingress.cluster.local", "*.internal.domain.local"}
+	denyResp := denyResp{
+		statusCode:  404,
+		contentType: "text/plain",
+		body:        "Not Found",
+	}
+
+	validateDenyRule := func(t *testing.T, resource *cloudformation.Resource) {
+		rules, ok := resource.Properties.(*cloudformation.ElasticLoadBalancingV2ListenerRule)
+		require.True(t, ok, "couldn't convert resource to ElasticLoadBalancingV2ListenerRule")
+
+		conditions := *rules.Conditions
+		require.Equal(t, len(conditions), 1)
+		condition := conditions[0]
+
+		actions := *rules.Actions
+		require.Equal(t, len(actions), 1)
+		action := actions[0]
+
+		require.Equal(t, internalTrafficDenyRulePriority, rules.Priority.Literal)
+
+		require.Equal(t, listenerRuleConditionHostField, condition.Field.Literal)
+		var conditionValues []string
+		for _, v := range condition.Values.Literal {
+			conditionValues = append(conditionValues, v.Literal)
+		}
+		require.Equal(t, len(internalDomains), len(conditionValues))
+		sort.Strings(conditionValues)
+		require.Equal(t, internalDomains, conditionValues)
+
+		require.Equal(t, listenerRuleActionTypeFixedRes, action.Type.Literal)
+		require.Equal(t, denyResp.contentType, action.FixedResponseConfig.ContentType.Literal)
+		require.Equal(t, denyResp.body, action.FixedResponseConfig.MessageBody.Literal)
+		require.Equal(t, fmt.Sprintf("%d", denyResp.statusCode), action.FixedResponseConfig.StatusCode.Literal)
+	}
+
 	for _, test := range []struct {
 		name     string
 		spec     *stackSpec
@@ -233,6 +272,75 @@ func TestGenerateTemplate(t *testing.T) {
 				require.NotNil(t, tg.HealthCheckTimeoutSeconds)
 			},
 		},
+		{
+			name: "deny internal traffic for HTTPS correctly",
+			spec: &stackSpec{
+				loadbalancerType:    LoadBalancerTypeApplication,
+				certificateARNs:     map[string]time.Time{"domain.company.com": time.Now()},
+				httpRedirectToHTTPS: true,
+
+				denyInternalDomains:         true,
+				internalDomains:             internalDomains,
+				denyInternalDomainsResponse: denyResp,
+			},
+			validate: func(t *testing.T, template *cloudformation.Template) {
+				require.NotNil(t, template.Resources["HTTPSRuleBlockInternalTraffic"])
+				resource := template.Resources["HTTPSRuleBlockInternalTraffic"]
+				validateDenyRule(t, resource)
+				require.NotContains(t, template.Resources, "HTTPRuleBlockInternalTraffic")
+			},
+		},
+		{
+			name: "deny internal traffic for HTTP correctly",
+			spec: &stackSpec{
+				loadbalancerType: LoadBalancerTypeApplication,
+
+				denyInternalDomains:         true,
+				internalDomains:             internalDomains,
+				denyInternalDomainsResponse: denyResp,
+			},
+			validate: func(t *testing.T, template *cloudformation.Template) {
+				require.NotNil(t, template.Resources["HTTPRuleBlockInternalTraffic"])
+				resource := template.Resources["HTTPRuleBlockInternalTraffic"]
+				validateDenyRule(t, resource)
+				require.NotContains(t, template.Resources, "HTTPSRuleBlockInternalTraffic")
+			},
+		},
+		{
+			name: "deny internal traffic for HTTP/HTTPS correctly",
+			spec: &stackSpec{
+				loadbalancerType:    LoadBalancerTypeApplication,
+				certificateARNs:     map[string]time.Time{"domain.company.com": time.Now()},
+				httpRedirectToHTTPS: false,
+
+				denyInternalDomains:         true,
+				internalDomains:             internalDomains,
+				denyInternalDomainsResponse: denyResp,
+			},
+			validate: func(t *testing.T, template *cloudformation.Template) {
+				require.NotNil(t, template.Resources["HTTPRuleBlockInternalTraffic"])
+				resource := template.Resources["HTTPRuleBlockInternalTraffic"]
+				validateDenyRule(t, resource)
+
+				require.NotNil(t, template.Resources["HTTPSRuleBlockInternalTraffic"])
+				resource = template.Resources["HTTPSRuleBlockInternalTraffic"]
+				validateDenyRule(t, resource)
+			},
+		},
+		{
+			name: "Does not deny internal traffic if not required",
+			spec: &stackSpec{
+				loadbalancerType:    LoadBalancerTypeApplication,
+				certificateARNs:     map[string]time.Time{"domain.company.com": time.Now()},
+				httpRedirectToHTTPS: false,
+
+				denyInternalDomains: false,
+			},
+			validate: func(t *testing.T, template *cloudformation.Template) {
+				require.NotContains(t, template.Resources, "HTTPSRuleBlockInternalTraffic")
+				require.NotContains(t, template.Resources, "HTTPRuleBlockInternalTraffic")
+			},
+		},
 	} {
 		t.Run(test.name, func(t *testing.T) {
 			generated, err := generateTemplate(test.spec)