Ejemplo n.º 1
0
def describe_fargate_cluster(
        cluster_name: str) -> DescribeClustersResponseTypeDef:
    """
    Fetches the status for a given cluster
    """
    client: ECSClient = fetch_boto3_client("ecs")
    return client.describe_clusters(clusters=[cluster_name])
Ejemplo n.º 2
0
def load_running_task_info(
    cluster_name: str,
    instance_name: Optional[str] = None,
    bastion_id: Optional[str] = None,
) -> List[TaskTypeDef]:
    """
    Loads and returns all running bastion tasks in the given cluster with the
    selected instance name
    """
    client: ECSClient = fetch_boto3_client("ecs")

    task_list = client.list_tasks(cluster=cluster_name, family=DEFAULT_NAME)
    task_response = describe_task(cluster_name, task_list["taskArns"])

    if not task_response:
        return []

    response = task_response["tasks"]

    if instance_name:
        response = [
            t for t in response
            if f"{DEFAULT_NAME}/{instance_name}" == get_tag_value(
                "ecs", t["tags"], "Name")
        ]

    if bastion_id:
        response = [
            t for t in response
            if bastion_id == get_tag_value("ecs", t["tags"], "BastionId")
        ]

    return response
Ejemplo n.º 3
0
def attach_policies_to_role(role_name: str, policy_arns: List[str]) -> None:
    """
    Attaches a list of IAM policies to a given IAM role
    """
    client: IAMClient = fetch_boto3_client("iam")
    for policy_arn in policy_arns:
        client.attach_role_policy(RoleName=role_name, PolicyArn=policy_arn)
Ejemplo n.º 4
0
def delete_task_definition() -> None:
    """
    Inactivates all serverless-aws-bastion task definitions
    """
    client: ECSClient = fetch_boto3_client("ecs")
    task_definitions = client.list_task_definitions(familyPrefix=DEFAULT_NAME)

    log_info("Deregistering task definitions")
    for td in task_definitions["taskDefinitionArns"]:
        client.deregister_task_definition(taskDefinition=td)
Ejemplo n.º 5
0
def delete_role(role_name: str) -> None:
    """
    Safely deletes a given role by first detaching any policies
    and then deleting the role and handling any exceptions
    """
    client: IAMClient = fetch_boto3_client("iam")

    try:
        log_info(f"Deleting {role_name} role")
        detach_policies_from_role(role_name)
        client.delete_role(RoleName=role_name)
    except client.exceptions.NoSuchEntityException:
        return None
Ejemplo n.º 6
0
def create_fargate_cluster(cluster_name: str) -> CreateClusterResponseTypeDef:
    """
    Creates a Fargate cluster to launch the bastion task into
    """
    client: ECSClient = fetch_boto3_client("ecs")

    log_info("Creating Fargate cluster")
    response = client.create_cluster(
        clusterName=cluster_name,
        capacityProviders=["FARGATE"],
        tags=build_tags("ecs"),
    )
    wait_for_fargate_cluster_status(cluster_name, ClusterStatus.ACTIVE)
    return response
Ejemplo n.º 7
0
def detach_policies_from_role(role_name: str) -> None:
    """
    Detaches all policies from a role
    """
    client: IAMClient = fetch_boto3_client("iam")

    try:
        policies = client.list_attached_role_policies(RoleName=role_name)
        policy_arns = [p["PolicyArn"] for p in policies["AttachedPolicies"]]
    except client.exceptions.NoSuchEntityException:
        return None

    for policy_arn in policy_arns:
        client.detach_role_policy(RoleName=role_name, PolicyArn=policy_arn)
Ejemplo n.º 8
0
def fetch_role_arn(role_name: str) -> Optional[str]:
    """
    Checks if the given role name exists

    Returns role arn if it exists
    """
    client: IAMClient = fetch_boto3_client("iam")

    try:
        response = client.get_role(RoleName=role_name)
    except client.exceptions.NoSuchEntityException:
        return None

    return response["Role"]["Arn"]
Ejemplo n.º 9
0
def delete_deregister_ssm_policy() -> None:
    """
    Deletes the IAM policy that allows the bastion ECS task to
    deregister itself from SSM.
    """
    client: IAMClient = fetch_boto3_client("iam")

    try:
        log_info(f"Deleting {SSM_DEREGISTER_POLICY_NAME} policy")
        account_id = load_aws_account_id()
        client.delete_policy(
            PolicyArn=
            f"arn:aws:iam::{account_id}:policy/{SSM_DEREGISTER_POLICY_NAME}", )
    except client.exceptions.NoSuchEntityException:
        return None
Ejemplo n.º 10
0
def delete_fargate_cluster(cluster_name: str) -> None:
    """
    Deletes a given Fargate cluster
    """
    client: ECSClient = fetch_boto3_client("ecs")

    log_info("Deleting Fargate cluster")

    try:
        client.delete_cluster(cluster=cluster_name)
    except client.exceptions.ClusterNotFoundException:
        log_error(f"Failed to find {cluster_name} Fargate cluster")
        raise Abort()

    wait_for_fargate_cluster_status(cluster_name, ClusterStatus.INACTIVE)
Ejemplo n.º 11
0
def describe_task(
    cluster_name: str,
    task_arns: List[str],
) -> Optional[DescribeTasksResponseTypeDef]:
    """
    Fetches the statuses for a group of tasks
    """
    client: ECSClient = fetch_boto3_client("ecs")

    if len(task_arns) == 0:
        return None

    return client.describe_tasks(
        cluster=cluster_name,
        tasks=task_arns,
        include=["TAGS"],
    )
Ejemplo n.º 12
0
def create_bastion_task_role() -> str:
    """
    Creates the role that will be used by the bastion ECS task.
    Skips creation if the role already exists.

    Returns role arn
    """
    client: IAMClient = fetch_boto3_client("iam")

    current_role_arn = fetch_role_arn(TASK_ROLE_NAME)
    if current_role_arn:
        return current_role_arn

    log_info(f"Creating {TASK_ROLE_NAME} role")
    response = client.create_role(
        RoleName=TASK_ROLE_NAME,
        Description="Used by serverless-aws-bastion ECS tasks",
        AssumeRolePolicyDocument=json.dumps(
            {
                "Version":
                "2012-10-17",
                "Statement": [
                    {
                        "Sid": "",
                        "Effect": "Allow",
                        "Principal": {
                            "Service":
                            ["ecs-tasks.amazonaws.com", "ssm.amazonaws.com"],
                        },
                        "Action": "sts:AssumeRole",
                    },
                ],
            }, ),
        Tags=build_tags("iam"),
    )

    deregister_ssm_arn = create_deregister_ssm_policy()
    attach_policies_to_role(
        TASK_ROLE_NAME,
        [
            deregister_ssm_arn,
            "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore",
        ],
    )

    return response["Role"]["Arn"]
Ejemplo n.º 13
0
def create_task_definition(task_role_arn: str,
                           execution_role_arn: str) -> None:
    """
    Creates the task definition that will be used to launch the
    serverless bastion container
    """
    client: ECSClient = fetch_boto3_client("ecs")

    log_info("Creating bastion ECS task")
    client.register_task_definition(
        family=DEFAULT_NAME,
        networkMode="awsvpc",
        cpu=TASK_CPU,
        memory=TASK_MEMORY,
        taskRoleArn=task_role_arn,
        executionRoleArn=execution_role_arn,
        containerDefinitions=[
            {
                "image":
                f"nplutt/{DEFAULT_NAME}",
                "name":
                DEFAULT_NAME,
                "essential":
                True,
                "portMappings": [
                    {
                        "hostPort": 22,
                        "protocol": "tcp",
                        "containerPort": 22,
                    },
                ],
                "logConfiguration": {
                    "logDriver": "awslogs",
                    "options": {
                        "awslogs-group": "/ecs/ssh-bastion",
                        "awslogs-region": load_aws_region_name(),
                        "awslogs-stream-prefix": "ecs",
                    },
                },
            },
        ],
        tags=build_tags("ecs"),
    )
Ejemplo n.º 14
0
def create_bastion_task_execution_role() -> str:
    """
    Creates the role that will be used by ECS to launch the bastion ECS task.
    Skips creation if the role already exists.

    Returns role arn
    """
    client: IAMClient = fetch_boto3_client("iam")

    current_role_arn = fetch_role_arn(TASK_EXECUTION_ROLE_NAME)
    if current_role_arn:
        return current_role_arn

    log_info(f"Creating {TASK_EXECUTION_ROLE_NAME} role")
    response = client.create_role(
        RoleName=TASK_EXECUTION_ROLE_NAME,
        Description=
        "Used by Fargate to launch serverless-aws-bastion ECS tasks",
        AssumeRolePolicyDocument=json.dumps(
            {
                "Version":
                "2012-10-17",
                "Statement": [
                    {
                        "Sid": "",
                        "Effect": "Allow",
                        "Principal": {
                            "Service": ["ecs-tasks.amazonaws.com"]
                        },
                        "Action": "sts:AssumeRole",
                    },
                ],
            }, ),
        Tags=build_tags("iam"),
    )
    attach_policies_to_role(
        TASK_EXECUTION_ROLE_NAME,
        [
            "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy",
        ],
    )

    return response["Role"]["Arn"]
Ejemplo n.º 15
0
def load_public_ips_for_network_interfaces(
    interface_ids: List[str], ) -> Dict[str, str]:
    """
    Loads the public ip addresses for a list of network
    interface ids
    """
    client: EC2Client = fetch_boto3_client("ec2")
    interfaces = client.describe_network_interfaces(
        NetworkInterfaceIds=interface_ids, )

    public_ips = {}
    for interface in interfaces["NetworkInterfaces"]:
        try:
            public_ips[get_tag_value(
                "ec2", interface["TagSet"],
                "BastionId")] = interface["Association"]["PublicIp"]
        except KeyError:
            pass

    return public_ips
Ejemplo n.º 16
0
def create_activation(
    iam_role_name: str,
    instance_name: str,
    bastion_id: str,
) -> CreateActivationResultTypeDef:
    """
    Creates an SSM activation code that is used to connect the agent
    back to SSM
    """
    instance_name = f"{DEFAULT_NAME}/{instance_name}"

    client: SSMClient = fetch_boto3_client("ssm")
    response = client.create_activation(
        Description=f"Used to activate ssm agent in {DEFAULT_NAME}",
        DefaultInstanceName=instance_name,
        IamRole=iam_role_name,
        RegistrationLimit=1,
        ExpirationDate=datetime.utcnow() + timedelta(minutes=5),
        Tags=build_tags("ssm", {"Name": instance_name, "BastionId": bastion_id}),
    )
    return response
Ejemplo n.º 17
0
def load_instance_ids(
    instance_name: str = None,
    bastion_ids: List[str] = None,
) -> Dict[str, str]:
    """
    Loads all of the ssm instance ids for instances that were
    created by this cli. If the instance name is passed in, then
    instances are also filtered by name.
    """
    client: SSMClient = fetch_boto3_client("ssm")
    filters: List[InstanceInformationStringFilterTypeDef] = [
        {
            "Key": "tag:CreatedBy",
            "Values": ["serverless-aws-bastion:cli"],
        },
    ]

    if instance_name:
        filters.append(
            {
                "Key": "tag:Name",
                "Values": [f"{DEFAULT_NAME}/{instance_name}"],
            },
        )

    if bastion_ids:
        filters.append(
            {
                "Key": "tag:BastionId",
                "Values": bastion_ids,
            },
        )

    response = client.describe_instance_information(Filters=filters)
    return {
        i["ActivationId"]: i["InstanceId"] for i in response["InstanceInformationList"]
    }
Ejemplo n.º 18
0
def create_deregister_ssm_policy() -> str:
    """
    Creates an IAM policy that allows the bastion ECS task to
    deregister itself from SSM.

    Returns the policy arn
    """
    client: IAMClient = fetch_boto3_client("iam")
    try:
        log_info(f"Creating {SSM_DEREGISTER_POLICY_NAME} policy")
        response = client.create_policy(
            Description="Used by serverless-aws-bastion ECS task to "
            "deregister itself from SSM",
            PolicyName=SSM_DEREGISTER_POLICY_NAME,
            PolicyDocument=json.dumps(
                {
                    "Version":
                    "2012-10-17",
                    "Statement": [
                        {
                            "Action": [
                                "ssm:DeregisterManagedInstance",
                                "ssm:DescribeInstanceInformation",
                            ],
                            "Effect":
                            "Allow",
                            "Resource":
                            "*",
                        },
                    ],
                }, ),
        )
        return response["Policy"]["Arn"]
    except client.exceptions.EntityAlreadyExistsException:
        account_id = load_aws_account_id()
        return f"arn:aws:iam::{account_id}:policy/{SSM_DEREGISTER_POLICY_NAME}"
Ejemplo n.º 19
0
def launch_fargate_task(
    cluster_name: str,
    subnet_ids: str,
    security_group_ids: str,
    authorized_keys: str,
    instance_name: str,
    timeout_minutes: int,
    bastion_type: BastionType,
) -> RunTaskResponseTypeDef:
    """
    Launches the ssh bastion Fargate task into the proper subnets & security groups,
    also sends in the authorized keys.
    """
    client: ECSClient = fetch_boto3_client("ecs")

    bastion_id = str(uuid4())

    activation: Dict[str, str] = {}
    if bastion_type == BastionType.ssm:
        activation = create_activation(TASK_ROLE_NAME, instance_name,
                                       bastion_id)  # type: ignore

    log_info("Starting bastion task")
    try:
        response = client.run_task(
            cluster=cluster_name,
            taskDefinition=DEFAULT_NAME,
            overrides={
                "containerOverrides": [
                    {
                        "name":
                        DEFAULT_NAME,
                        "environment": [
                            {
                                "name": "AUTHORIZED_SSH_KEYS",
                                "value": authorized_keys
                            },
                            {
                                "name": "ACTIVATION_ID",
                                "value": activation.get("ActivationId", ""),
                            },
                            {
                                "name": "ACTIVATION_CODE",
                                "value": activation.get("ActivationCode", ""),
                            },
                            {
                                "name": "AWS_REGION",
                                "value": load_aws_region_name()
                            },
                            {
                                "name": "TIMEOUT",
                                "value": str(timeout_minutes * 60)
                            },
                            {
                                "name": "BASTION_TYPE",
                                "value": bastion_type.value
                            },
                        ],
                    },
                ],
            },
            count=1,
            launchType="FARGATE",
            networkConfiguration={
                "awsvpcConfiguration": {
                    "subnets": subnet_ids.split(","),
                    "securityGroups": security_group_ids.split(","),
                    "assignPublicIp": "ENABLED",
                },
            },
            tags=build_tags(
                "ecs",
                {
                    "Name": f"{DEFAULT_NAME}/{instance_name}",
                    "BastionId": bastion_id,
                    "ActivationId": activation.get("ActivationId", ""),
                },
            ),
        )

    except client.exceptions.ClusterNotFoundException:
        log_error(
            "Specified cluster to launch bastion task into doesn't exist")
        raise Abort()

    except (
            client.exceptions.ClientException,
            client.exceptions.InvalidParameterException,
    ) as e:
        log_error(e.response["Error"]["Message"])
        raise Abort()

    wait_for_tasks_to_start(cluster_name, response["tasks"])
    return response
Ejemplo n.º 20
0
def stop_fargate_tasks(cluster: str, tasks: List[TaskTypeDef]) -> None:
    client: ECSClient = fetch_boto3_client("ecs")

    log_info(f"Stopping {len(tasks)} tasks...")
    for t in tasks:
        client.stop_task(cluster=cluster, task=t["taskArn"])