使Fargate负载均衡器仅接受特定IP地址的流量。

huangapple go评论63阅读模式
英文:

Make Fargate Loadbalancer Only Accept Traffic from Specific IP Address

问题

我正在使用一个AWS示例来部署一个MLFlow堆栈,使用NetworkLoadBalancedFargateService。我能够创建堆栈并提供资源,但问题是服务是面向互联网的,即任何人都可以从任何IP地址访问该服务。我想将传入流量限制为特定的CIDR IP地址。

代码的相关部分如下(来自app.py)。我尝试附加安全组,但服务仍然是公共面向的。有人可以帮助我修改这段代码,以便堆栈只能通过特定的IP地址访问吗?谢谢。

# 版权所有 Amazon.com, Inc. 或其附属公司。保留所有权利。
# SPDX-License-Identifier: MIT-0

from aws_cdk import (
    aws_ec2 as ec2,
    aws_s3 as s3,
    aws_ecs as ecs,
    aws_rds as rds,
    aws_iam as iam,
    aws_secretsmanager as sm,
    aws_ecs_patterns as ecs_patterns,
    App,
    Stack,
    CfnParameter,
    CfnOutput,
    Aws,
    RemovalPolicy,
    Duration,
)
from constructs import Construct


class MLflowStack(Stack):
    def __init__(self, scope: Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)
        # ==============================
        # ======= CFN PARAMETERS =======
        # ==============================
        project_name_param = CfnParameter(scope=self, id="ProjectName", type="String")
        db_name = "mlflowdb"
        port = 3306
        username = "master"
        bucket_name = f"{project_name_param.value_as_string}-artifacts-{Aws.ACCOUNT_ID}"
        container_repo_name = "mlflow-containers"
        cluster_name = "mlflow"
        service_name = "mlflow"

        # ==================================================
        # ================= IAM ROLE =======================
        # ==================================================
        role = iam.Role(
            scope=self,
            id="TASKROLE",
            assumed_by=iam.ServicePrincipal(service="ecs-tasks.amazonaws.com"),
        )
        role.add_managed_policy(
            iam.ManagedPolicy.from_aws_managed_policy_name("AmazonS3FullAccess")
        )
        role.add_managed_policy(
            iam.ManagedPolicy.from_aws_managed_policy_name("AmazonECS_FullAccess")
        )

        # ==================================================
        # ================== SECRET ========================
        # ==================================================
        db_password_secret = sm.Secret(
            scope=self,
            id="DBSECRET",
            secret_name="dbPassword",
            generate_secret_string=sm.SecretStringGenerator(
                password_length=20, exclude_punctuation=True
            ),
        )

        # ==================================================
        # ==================== VPC =========================
        # ==================================================
        public_subnet = ec2.SubnetConfiguration(
            name="Public", subnet_type=ec2.SubnetType.PUBLIC, cidr_mask=28
        )
        private_subnet = ec2.SubnetConfiguration(
            name="Private", subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS, cidr_mask=28
        )
        isolated_subnet = ec2.SubnetConfiguration(
            name="DB", subnet_type=ec2.SubnetType.PRIVATE_ISOLATED, cidr_mask=28
        )

        vpc = ec2.Vpc(
            scope=self,
            id="VPC",
            ip_addresses=ec2.IpAddresses.cidr("10.0.0.0/24"),
            max_azs=2,
            nat_gateway_provider=ec2.NatProvider.gateway(),
            nat_gateways=1,
            subnet_configuration=[public_subnet, private_subnet, isolated_subnet],
        )
        vpc.add_gateway_endpoint(
            "S3Endpoint", service=ec2.GatewayVpcEndpointAwsService.S3
        )
        # ==================================================
        # ================= S3 BUCKET ======================
        # ==================================================
        artifact_bucket = s3.Bucket(
            scope=self,
            id="ARTIFACTBUCKET",
            bucket_name=bucket_name,
            public_read_access=False,
        )
        # # ==================================================
        # # ================== DATABASE  =====================
        # # ==================================================
        # Creates a security group for AWS RDS
        sg_rds = ec2.SecurityGroup(
            scope=self, id="SGRDS", vpc=vpc, security_group_name="sg_rds"
        )
        # Adds an ingress rule which allows resources in the VPC's CIDR to access the database.
        sg_rds.add_ingress_rule(
            peer=ec2.Peer.ipv4("10.0.0.0/24"), connection=ec2.Port.tcp(port)
        )

        database = rds.DatabaseInstance(
            scope=self,
            id="MYSQL",
            database_name=db_name,
            port=port,
            credentials=rds.Credentials.from_username(
                username=username, password=db_password_secret.secret_value
            ),
            engine=rds.DatabaseInstanceEngine.mysql(
                version=rds.MysqlEngineVersion.VER_8_0_26
            ),
            instance_type=ec2.InstanceType.of(
                ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL
            ),
            vpc=vpc,
            security_groups=[sg_rds],
            vpc_subnets=ec2.SubnetSelection(
                subnet_type=ec2.SubnetType.PRIVATE_ISOLATED
            ),
            # multi_az=True,
            removal_policy=RemovalPolicy.DESTROY,
            deletion_protection=False,
        )
        # ==================================================
        # =============== FARGATE SERVICE ==================
        # ==================================================
        cluster = ecs.Cluster(
            scope=self, id="CLUSTER", cluster_name=cluster_name, vpc=vpc
        )

        task_definition = ecs.FargateTaskDefinition(
            scope=self,
            id="MLflow",
            task_role=role,
            cpu=4 * 1024,
            memory_limit_mib=8 * 1024,
        )

        container = task_definition.add_container(
            id="Container",
            image=ecs.ContainerImage.from_asset(directory="container"),
            environment={
                "BUCKET": f"s3://{artifact_bucket.bucket_name}",
                "HOST": database.db_instance_endpoint_address,
                "PORT": str(port),
                "DATABASE": db_name,
                "USERNAME": username,
            },
            secrets={"PASSWORD": ecs.Secret.from_secrets_manager(db_password_secret)},
            logging=ecs.LogDriver.aws_logs(stream_prefix="mlflow"),
        )
        port_mapping = ecs.PortMapping(
            container_port=5000, host_port=5000, protocol=ecs.Protocol.TCP
        )
        container.add_port_mappings(port_mapping)

        fargate_service = ecs_patterns.NetworkLoadBalancedFargateService(
            scope=self,
            id="MLFLOW",
            service_name=service_name,
            cluster=cluster,
            task_definition=task_definition,
        )

        # Setup security group
        fargate_service.service.connections.security_groups[0].add_ingress_rule(
            peer=ec2.Peer.ipv4(vpc.vpc_cidr_block),
            connection=ec2.Port.tcp(5000),
            description="Allow inbound from VPC for mlflow",
        )

        # Setup autoscaling policy
        scaling = fargate_service.service.auto_scale_task_count(max_capacity=2)
        scaling.scale_on_cpu_utilization(
            id="AUTOSCALING",
            target_utilization_percent=70,
            scale_in_cooldown=Duration.seconds(60),
            scale_out_cooldown=Duration.seconds(60),
        )
        # ==================================================
        # =================== OUTPUTS ======================
        # ==================================================
        CfnOutput(
            scope=self,
            id="LoadBalancerDNS",
            value=fargate_service.load_balancer.load_balancer_dns_name,
        )


app = App()
MLflowStack(app, "MLflowStack")
app.synth()
英文:

I am using an AWS example to deploy an MLFlow stack using a NetworkLoadBalancedFargateService. I am able to create the stack and provision the resources, but the issue is that the service is internet facing, i.e. anyone can access the service from any IP address. I want to restrict the incoming traffic to a specific CIDR ip address.

The relevant portion of the code is the following (from app.py). I have tried attaching a security group but the service is still public-facing. Can someone help me modify this code so that the stack is only accessible through a specific IP address? Thank you.

# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: MIT-0
from aws_cdk import (
aws_ec2 as ec2,
aws_s3 as s3,
aws_ecs as ecs,
aws_rds as rds,
aws_iam as iam,
aws_secretsmanager as sm,
aws_ecs_patterns as ecs_patterns,
App,
Stack,
CfnParameter,
CfnOutput,
Aws,
RemovalPolicy,
Duration,
)
from constructs import Construct
class MLflowStack(Stack):
def __init__(self, scope: Construct, id: str, **kwargs) -> None:
super().__init__(scope, id, **kwargs)
# ==============================
# ======= CFN PARAMETERS =======
# ==============================
project_name_param = CfnParameter(scope=self, id="ProjectName", type="String")
db_name = "mlflowdb"
port = 3306
username = "master"
bucket_name = f"{project_name_param.value_as_string}-artifacts-{Aws.ACCOUNT_ID}"
container_repo_name = "mlflow-containers"
cluster_name = "mlflow"
service_name = "mlflow"
# ==================================================
# ================= IAM ROLE =======================
# ==================================================
role = iam.Role(
scope=self,
id="TASKROLE",
assumed_by=iam.ServicePrincipal(service="ecs-tasks.amazonaws.com"),
)
role.add_managed_policy(
iam.ManagedPolicy.from_aws_managed_policy_name("AmazonS3FullAccess")
)
role.add_managed_policy(
iam.ManagedPolicy.from_aws_managed_policy_name("AmazonECS_FullAccess")
)
# ==================================================
# ================== SECRET ========================
# ==================================================
db_password_secret = sm.Secret(
scope=self,
id="DBSECRET",
secret_name="dbPassword",
generate_secret_string=sm.SecretStringGenerator(
password_length=20, exclude_punctuation=True
),
)
# ==================================================
# ==================== VPC =========================
# ==================================================
public_subnet = ec2.SubnetConfiguration(
name="Public", subnet_type=ec2.SubnetType.PUBLIC, cidr_mask=28
)
private_subnet = ec2.SubnetConfiguration(
name="Private", subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS, cidr_mask=28
)
isolated_subnet = ec2.SubnetConfiguration(
name="DB", subnet_type=ec2.SubnetType.PRIVATE_ISOLATED, cidr_mask=28
)
vpc = ec2.Vpc(
scope=self,
id="VPC",
ip_addresses=ec2.IpAddresses.cidr("10.0.0.0/24"),
max_azs=2,
nat_gateway_provider=ec2.NatProvider.gateway(),
nat_gateways=1,
subnet_configuration=[public_subnet, private_subnet, isolated_subnet],
)
vpc.add_gateway_endpoint(
"S3Endpoint", service=ec2.GatewayVpcEndpointAwsService.S3
)
# ==================================================
# ================= S3 BUCKET ======================
# ==================================================
artifact_bucket = s3.Bucket(
scope=self,
id="ARTIFACTBUCKET",
bucket_name=bucket_name,
public_read_access=False,
)
# # ==================================================
# # ================== DATABASE  =====================
# # ==================================================
# Creates a security group for AWS RDS
sg_rds = ec2.SecurityGroup(
scope=self, id="SGRDS", vpc=vpc, security_group_name="sg_rds"
)
# Adds an ingress rule which allows resources in the VPC's CIDR to access the database.
sg_rds.add_ingress_rule(
peer=ec2.Peer.ipv4("10.0.0.0/24"), connection=ec2.Port.tcp(port)
)
database = rds.DatabaseInstance(
scope=self,
id="MYSQL",
database_name=db_name,
port=port,
credentials=rds.Credentials.from_username(
username=username, password=db_password_secret.secret_value
),
engine=rds.DatabaseInstanceEngine.mysql(
version=rds.MysqlEngineVersion.VER_8_0_26
),
instance_type=ec2.InstanceType.of(
ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL
),
vpc=vpc,
security_groups=[sg_rds],
vpc_subnets=ec2.SubnetSelection(
subnet_type=ec2.SubnetType.PRIVATE_ISOLATED
),
# multi_az=True,
removal_policy=RemovalPolicy.DESTROY,
deletion_protection=False,
)
# ==================================================
# =============== FARGATE SERVICE ==================
# ==================================================
cluster = ecs.Cluster(
scope=self, id="CLUSTER", cluster_name=cluster_name, vpc=vpc
)
task_definition = ecs.FargateTaskDefinition(
scope=self,
id="MLflow",
task_role=role,
cpu=4 * 1024,
memory_limit_mib=8 * 1024,
)
container = task_definition.add_container(
id="Container",
image=ecs.ContainerImage.from_asset(directory="container"),
environment={
"BUCKET": f"s3://{artifact_bucket.bucket_name}",
"HOST": database.db_instance_endpoint_address,
"PORT": str(port),
"DATABASE": db_name,
"USERNAME": username,
},
secrets={"PASSWORD": ecs.Secret.from_secrets_manager(db_password_secret)},
logging=ecs.LogDriver.aws_logs(stream_prefix="mlflow"),
)
port_mapping = ecs.PortMapping(
container_port=5000, host_port=5000, protocol=ecs.Protocol.TCP
)
container.add_port_mappings(port_mapping)
fargate_service = ecs_patterns.NetworkLoadBalancedFargateService(
scope=self,
id="MLFLOW",
service_name=service_name,
cluster=cluster,
task_definition=task_definition,
)
# Setup security group
fargate_service.service.connections.security_groups[0].add_ingress_rule(
peer=ec2.Peer.ipv4(vpc.vpc_cidr_block),
connection=ec2.Port.tcp(5000),
description="Allow inbound from VPC for mlflow",
)
# Setup autoscaling policy
scaling = fargate_service.service.auto_scale_task_count(max_capacity=2)
scaling.scale_on_cpu_utilization(
id="AUTOSCALING",
target_utilization_percent=70,
scale_in_cooldown=Duration.seconds(60),
scale_out_cooldown=Duration.seconds(60),
)
# ==================================================
# =================== OUTPUTS ======================
# ==================================================
CfnOutput(
scope=self,
id="LoadBalancerDNS",
value=fargate_service.load_balancer.load_balancer_dns_name,
)
app = App()
MLflowStack(app, "MLflowStack")
app.synth()

答案1

得分: 0

要限制访问支持NetworkLoadBalancedFargateService的网络负载均衡器(NLB)的特定CIDR IP地址,您需要调整服务使用的安全组入站规则。

但是:NetworkLoadBalancedFargateService自动附带一个安全组并将其关联到NLB。因此,您在代码中尝试修改的安全组(fargate_service.service.connections.security_groups[0])实际上是Fargate任务的安全组,而不是NLB(类似于aws/aws-cdk问题9972)。

这在过去引起了问题,正如aws/aws-cdk问题4091所证明的那样。

您可以在aws/aws-cdk问题1490中看到在使用AWS CDK创建Fargate服务时处理安全组问题的另一种更直接的方法。


提议

(正如OP在评论中中所指出的

> 我无法解决这个问题。服务要么是公共面向的,要么根本无法访问。
我最终使用了一个oauth2代理服务器来限制对组织中的用户的访问。
>
> 如果可以接受基于oauth2的解决方案,mjedrasz/oauth2-mlflow-aws可能会引起兴趣。

)

如果您的组织权限阻止开发人员执行ec2:CreateSecurityGroup(如"IAM策略/Amazon EC2控制台"中所示),您可以在创建Fargate服务时使用现有的安全组。通过在创建服务时提供此安全组,CDK将不会自动创建新的安全组。

首先,创建或获取对现有安全组的引用:

# 假设您已经创建了一个安全组,并且通过其ID进行导入
my_service_security_group = ec2.SecurityGroup.from_security_group_id(
    self, 'ExistingSG', security_group_id='YOUR_SECURITY_GROUP_ID'
)

然后,在创建FargateService时提供此安全组。
但是,您提供的代码使用了NetworkLoadBalancedFargateService模式,该模式自动设置了网络负载均衡器、监听器和服务。如果您选择像GitHub评论中所示直接向Fargate服务提供安全组,您可能需要单独设置NLB和监听器。

例如:

service = ecs.FargateService(
    self, "MyFargateService",
    cluster=myCluster,
    task_definition=task_definition,
    security_groups=[my_service_security_group]
)

如果选择此方法,请记住,您需要手动设置NetworkLoadBalancedFargateService模式将自动为您创建的任何其他资源。

鉴于原始问题与创建默认安全组有关,提供预先存在的安全组(如GitHub评论中建议的)是一种可行的解决方法。

但是,Paolo评论中提到

> 这要求网络负载均衡器的目标组具有客户端IP保留

来自Elastic Load Balancing / Network Load Balancers/ Client IP preservation

> ## 客户端IP保留
>
> 当将请求路由到后端目标时,网络负载均衡器可以保留客户端的源IP地址。当禁用客户端IP保留时,网络负载均衡器的私有IP地址成为所有传入流量的客户端IP地址。
>
> 默认情况下,对于具有UDP和TCP_UDP协议的实例和IP类型目标组,启用或禁用客户端IP保留。但是,您可以使用preserve_client_ip.enabled目标组属性为TCP和TLS目标组启用或禁用客户端IP保留。

在创建网络负载均衡器(NLB)时,可以在将请求路由到后端目标(例如Fargate任务)时保留客户端的源IP地址。这对于需要了解客户端的原始IP地址以进行功能或安全性原因的应用程序非常重要。

因此:

  • 如果您使用的是TCP或TLS目标组:如果您使用基于源IP地址的预先存在的安全组规则,应确保在与NLB关联的目标组上启用客户端IP保留。

  • 如果您使用的是UDP或TCP_UDP目标组:您无需进行任何更改,因为客户端IP保留始终启用。

根据协议类型和预先存在的安全组中设置的规则,可能需要调整NLB的目标组的设置。

英文:

To restrict access to the Network Load Balancer (NLB) backing your NetworkLoadBalancedFargateService to a specific CIDR IP address, you will need to adjust the security group ingress rules that the service uses.

But: NetworkLoadBalancedFargateService automatically comes with a security group and associates it to the NLB. Therefore, the security group that you are trying to modify in your code (fargate_service.service.connections.security_groups[0]) is essentially the security group of the Fargate tasks, not the NLB (a bit as in aws/aws-cdk issue 9972).

That has caused issues in the past, as aws/aws-cdk issue 4091 attests.

You can see an alternative and more direct method to handle the security group issue when creating a Fargate service using the AWS CDK in aws/aws-cdk issue 1490


Proposal

(as noted by the OP in the comments

> I was unable to figure this out. The service was either public facing or not accessible at all.
I ended up using an oauth2 proxy server to restrict access to users in my organization.
>
> If a solution based on oauth2 is acceptable, mjedrasz/oauth2-mlflow-aws might be of interest.

)

If your organization's permissions prevent developers from performing ec2:CreateSecurityGroup (as illustrated in "IAM policies / Amazon EC2 console"), you can use a pre-existing security group when creating your Fargate service. By supplying this security group when creating the service, the CDK will not automatically create a new security group.

First, create or obtain a reference to your pre-existing security group:

# Assuming you already have a security group created and you are importing it by its ID
my_service_security_group = ec2.SecurityGroup.from_security_group_id(
    self, 'ExistingSG', security_group_id='YOUR_SECURITY_GROUP_ID'
)

Then provide this security group when creating your FargateService.
However, the code you provided uses the NetworkLoadBalancedFargateService pattern, which automatically sets up a Network Load Balancer, a listener, and the service. If you choose to supply a security group directly to the Fargate service as shown in the GitHub comment, you might need to set up the NLB and listener separately.

For instance:

service = ecs.FargateService(
    self, "MyFargateService",
    cluster=myCluster,
    task_definition=task_definition,
    security_groups=[my_service_security_group]
)

If you opt for this method, remember that you will need to manually set up any other resources that the NetworkLoadBalancedFargateService pattern would have automatically created for you.

Given that the original problem is related to the creation of the default security group, supplying a pre-existing one, as suggested in the GitHub comment, is a viable workaround.

However, Paolo mentions in the comments

> This requires the target group of the network load balancer to have client IP preservation

From Elastic Load Balancing / Network Load Balancers/ Client IP preservation

> ## Client IP preservation
>
> Network Load Balancers can preserve the source IP address of clients when routing requests to backend targets. When you disable client IP preservation, the private IP address of the Network Load Balancer becomes the client IP address for all incoming traffic.
>
> By default, client IP preservation is enabled (and can't be disabled) for instance and IP type target groups with UDP and TCP_UDP protocols. However, you can enable or disable client IP preservation for TCP and TLS target groups using the preserve_client_ip.enabled target group attribute.

When creating a Network Load Balancer (NLB), the client's source IP address can be preserved when routing requests to backend targets (e.g., Fargate tasks). That can be vital for applications that need to know the original IP address of the client for functionality or security reasons.

So:

  • if you are using a TCP or TLS target group: You should make sure that client IP preservation is enabled on the target group associated with the NLB if you are using a pre-existing security group that has rules based on source IP addresses.

  • If you are using a UDP or TCP_UDP target group: You do not need to make any changes, as client IP preservation is always enabled.

The settings for the target group of the NLB might need adjustment based on the type of protocol and the rules set in your pre-existing security group.

huangapple
  • 本文由 发表于 2023年8月9日 08:07:48
  • 转载请务必保留本文链接:https://go.coder-hub.com/76863814.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定