无论您何时拥有VPC,您都可能需要某种方式才能从您的本地设备访问VPC内的资源。通常,做到这一点的方法是运行一个堡垒(或跳箱),您和您的团队可以通过SSH连接到该堡垒(或跳箱)。缺点是您暴露了一个进入网络的入口点,该入口点可供多人访问并全天候运行。根据您管理权限的方式,您可能无法通过IAM限制对计算机的访问。显然,这并不理想。
有了法尔盖特,我们不再需要维护永久的堡垒实例--我们可以在需要时创建堡垒,在不再使用时拆除它们。我们可以根据SSH密钥和IP地址将堡垒实例锁定到单个用户。我们可以限制通过IAM访问用于管理堡垒的API和使用SSH密钥登录实例的API。
在本指南中,我将引用我的堡垒按需回购中的代码。如果您想跳过解释,只需克隆它并按照Readme.md中的说明开始。
如果你遇到任何麻烦,制造一个问题,我会尽我所能作出回应。
堡垒按需有两个关键组件-用于管理堡垒映像的基础设施和堡垒服务本身。
容器基础设施由ECR存储库、Docker容器、用于获取用户公钥的IAM角色以及用于构建和推送镜像的脚本组成。通常,我跨多个服务共享此基础设施,因为需求差别不大。但是,如果一个团队想要完全的服务隔离,或者需要定制他们的堡垒映像,那么要做到这一点是微不足道的。
堡垒服务由一个ECS任务、一个允许访问任何所需资源的任务角色、一个用于创建和销毁堡垒实例的API以及一组使团队成员可以轻松执行此操作的脚本组成。堡垒服务模块应该包括在任何需要堡垒的服务中-将其保存在服务的存储库中以便于访问,并将其与父服务一起部署。
我要感谢以下作者,因为他们提供了宝贵的例子,帮助我开发了这种方法:
虽然我建议使用多个AWS帐户来实现安全和隔离,但在本指南中,我将使用单个帐户,以便我们可以将重点放在要点上。如果有兴趣,我将在单独的指南中介绍如何修改此方法以用于多个帐户。本指南中的所有内容都假设您使用的是默认配置文件,但您可以通过AWS_PROFILE环境变量进行覆盖。
在本指南中,我还故意不使用Terraform远程状态,以便您可以更容易地试用我的代码。如果您要在活动帐户中使用它,您绝对应该使用远程状态-在适当的地方插入您自己的后端配置。如果您没有远程后端,请查看我的远程状态存储库,了解如何使用DynamoDB和KMS在S3上设置远程后端。
数据";AWS_IAM_POLICY_DOCUMENT";";AWS_ROLE";{Statement{Actions=[";STS:AssumeRole";]主体{标识符=[";apigateway.amazonaws.com";]type=";Service";}数据";AWS_IAM_POLICY_DOCUMENT";";LOGGER"。日志:CreateLogGroup";,";日志:CreateLogStream";,";日志:DescribeLogGroups";,";日志:DescribeLogStreams";,";日志:FilterLogEvents";,";日志:GetLogEvents";,";日志:PutLogEvents";,]resources=[";,*&。{name=";api-ateway-cloudwatch-logger";ASSET_ROLE_POLICY=DATA。AWS_IAM_POLICY_DOCUMENT。承担角色(_Role)。json}resource";AWS_IAM_ROLE_POLICY";";LOGER";{name=";api-ateway-cloudwatch-logger";policy=data。AWS_IAM_POLICY_DOCUMENT。伐木者。JSON ROLE=AWS_IAM_ROLE。伐木者。name}resource";AWS_API_Gateway_Account";";global";{CLOUDWATCH_ROLE_ARN=AWS_IAM_ROLE。伐木者。ARN}。
一般来说,记录API网关访问是一个好主意。遗憾的是,这是一个全局帐户设置,因此请谨慎使用。API Gateway有很多有状态的角落。我通常在单独的存储库中管理此记录器,因为它在一个帐户中运行的所有服务之间共享。
接下来,我们需要创建一个可以获取用户公钥的角色。
数据";aws_CALLER_IDENTITY";";环境";{}#…。DATA";AWS_IAM_POLICY_DOCUMENT";";AWS_IAM_POLICY_DOCUMENT";{语句{ACTIONS=[";STS:AssumeRole";]主体{标识符=[";arn:aws:iam::${data.aws_caller_identity.env.account_id}:root";]TYPE=";AWS";}DATA";AWS_IAM_POLICY_DOCUMENT";";PUBLIC_KEY_FETCHER";{语句{ACTIONS=[";IAM:GetSSHPublicKey";,";]RESOURCES=[";arn:aws:iam::${data.aws_caller_identity.env.account_id}:user/*";]}}RESOURCE";AWS_IAM_ROLE";";PUBLIC_KEY_FETCHER";{NAME=";PUBLIC-KEY-FETCHER";ASSET_ROLE_POLICY=data。AWS_IAM_POLICY_DOCUMENT。承担角色(_Role)。json}resource";AWS_IAM_ROLE_POLICY";";PUBLIC_KEY_FETCHER";{name=";PUBLIC-KEY-FETCHER";POLICY=DATA。AWS_IAM_POLICY_DOCUMENT。PUBLIC_KEY_FETCHER。JSON ROLE=AWS_IAM_ROLE。PUBLIC_KEY_FETCHER。ID}。
当用户尝试通过SSH进入实例时,堡垒实例将使用此角色来获取用户的密钥,如下所示。
创建容器映像相当简单。我们从Alpine Linux开始,因为它很小且面向安全,并添加了启动sshd和处理登录所需的脚本。
接下来,我们安装依赖项,包括AWS CLI。如果您通常需要为您的堡垒提供任何其他软件包,请将它们添加到列表中。(AWS没有为其CLI包提供校验和,这仍然困扰着我。哦,好吧。)。
运行ECHO安装依赖项.";&;&;\apk--no-cache\add\bash\curl\openssh\python\tini\&;&cho;安装AWS CLI.&34;&;&;\wget https://s3.amazonaws.com/aws-cli/awscli-bundle.zip&;\unzip awscli-bundle.zip&;&;\rm awscli-bundle.zip&;&;\./awscli-bundle/install-i/usr/local/aws-b/usr/local/bin/aws&;&;\rm-R awscli-bundle&;&;\/usr/local/bin/aws-version。
您可以使用root。我过去曾与需要额外灵活性的小团队合作过。但是,如果您向独立于负责安全事务的团队提供图像,您不一定希望人们对堡垒进行可能会在您的网络中打开漏洞的更改。
运行echo&34;创建用户\";ops\";.&;&;\adduser ops--Disabled-Password Run echo";Unlock\";ops\";.";&;&;\sed-i";s/ops:!:/ops:*:/g";/etc/dow。
运行ECHOD.";&;&;\sed-i&34;s:#AuthorizedKeysCommand None:AuthorizedKeysCommand/usr/local/bin/fetch_authorized_keys.sh:g";/etc/ssh/sshd_config&;&;\sed-i&34;s:#AuthorizedKeysCommandUser noser:AuthorizedKeysCommandUser noser:G";/etc/ssh/sshd_config&;&。s:#GatewayPorts no:GatewayPorts yes:g";/etc/ssh/sshd_config&;&;\sed-i";s:#PasswordAuthentication yes:PasswordAuthentication no:g";/etc/ssh/sshd_config&;&;\sed-i";s:#PermitChannel no:PermitChannel yes:g";/etc/ssh/。s:AllowTcp Forwarding no:AllowTcp Forwarding yes:g";/etc/ssh/sshd_config&;&;\sed-i&34;s:AuthorizedKeysFile.ssh/AuthorizedKeysFile None:G";/etc/ssh/sshd_config。
我们将AuthorizedKeysCommand设置为使用FETCH_AUTHORIZED_keys.sh,并且禁用密码登录和在实例上使用AuthorizedKeysFile的功能。这里的目的是使其能够仅使用FETCH_AUTHORIZED_keys.sh中的逻辑来验证用户。
这里唯一有趣的是我们用的是提尼。如果您对原因感兴趣,请查看这期GitHub。
然后,我们导出全局环境,其中包括注入到容器中的AWS凭据、获取SSH密钥的角色以及此堡垒实例的目标用户的用户名。
导出AWSCONTAINER_Credentials_Relative_URI=$AWSCONTAINER_Credentials_Relative_URI";>;/etc/profile.d/authorized_keys_configuration.sh ECHO";导出AWSDEFAULT_REGION=$AWSDEFAULT_REGION";>;/etc/profile.d/authorized_keys_configuration.sh ECHO";导出AWSExecution_ENV=$AWSExecution_ENV";&>。/etc/profile.d/authorized_keys_configuration.sh ECHO";导出AWS_REGION=$AWS_REGION";&>;/etc/profile.d/authorized_keys_configuration.sh ECHO";导出ECS_CONTAINER_METADATA_URI=$ECS_CONTAINER_METADATA_URI";>;>;/etc/profile.d/authorized_keys_configuration.sh ECHO";导出AWS_ROLE_FOR_AUTHORIZED_KEYS=$ASSUME_ROLE_FOR_AUTHORIZED_KEYS";>;>;/etc/profile.d/authorized_keys_configuration.sh ECHO&34;EXPORT USER_NAME=$USER_NAME&34;>;>;/etc/profile.d/authorized_keys_configuration.sh。
因为FETCH_AUTHORIZED_KEYS.sh是以无人身份运行的,并且没有办法在运行时将必要的变量注入到环境中,所以我们将它们导出到/etc/profile.d/authorized_keys_configuration.sh,以便当有人试图登录到实例时,可以在已知位置使用它们。
这就是魔术发生的地方。sshd的AuthorizedKeysCommand设置使您几乎可以执行任何操作,就像获取密钥一样。在我们的例子中,我们将从IAM取回它们。
首先,我们获取容器环境的源(通过entrypoint.sh创建),并承担我们在上面创建的公钥获取器角色。
源/etc/profile.d/authorized_keys_configuration.sh STS_Credentials=$(/USR/LOCAL/BIN/AWS STS Asmise-Role\--Role-Arn";${Asmise_Role_For_Authorized_Key}";\--Role-Session-Name获取堡垒授权密钥\--查询';[Credentials.SessionToken,Credentials.AccessKeyId,Credentials.SecretAccessKey]';\--Output。
然后,我们将假定角色的凭据注入到环境中,并将堡垒用户的所有活动公钥打印到STDOUT。
AWS_ACCESS_KEY_ID=$(ECHO";${STS_Credentials}";|AWK';{Print$2}';)AWS_SECRET_ACCESS_KEY=$(ECHO";${STS_Credentials}";|AWK';{Print$3}';)AWS_SESSION_TOKEN=$(ECHO";${STS_Credentials}";|AWK';)AWS_SECURITY_TOKEN=$(ECHO";${STS_Credentials}";|AWK';{PRINT$1}';)export AWS_ACCESS_KEY_ID AWS_SECURE_ACCESS_KEY AWS_SESSION_TOKEN AWS_SECURITY_TOKEN/usr/local/bin/AWS IAM list-ssh-public-key--user-name";$user_name";--query";SSHPublic.。].[SSHPublicKeyId]";--输出文本|同时读取-r key_id;do/usr/local/bin/AWS IAM get-ssh-public-key--user-name";$user_name";--ssh-public-key-id";$key_id";--编码SSH--query";SSHPublicKey.SSHPublicKeyBody";--。
sshd将使用此输出授权试图登录堡垒的用户。所有凭据都在IAM中管理,没有凭据持久化到堡垒实例本身。如果从IAM中删除用户或其密钥,他们将无法登录(即使他们让堡垒实例处于运行状态)。
您也可以根据需要轻松使用SSM参数存储或Vault。
到目前为止,没有什么比bastion/bin中的支持脚本更有趣的了。它们只处理构建、标记和将容器图像推送到ECR的一些细节。有关详细信息,请参阅自述文件。
堡垒服务为用户提供创建和销毁堡垒实例的API。在下面,我们使用的是ECS,就编排而言,它是相当简单的,但对于我们这里的目的来说已经足够好了。
当使用ECS和Fargate时,我们需要为实例定义两个不同的IAM角色。首先,我们需要定义用于获取容器映像和旋转的执行角色。其次,我们需要定义容器在运行时将使用的任务角色。
任务角色定义容器将有权访问哪些资源。您创建的每个服务的任务角色权限都不同,因此,为了模块化,我们将通过变量传递策略的JSON。
执行角色相当简单。我们需要授权ECR访问和许可下载堡垒图像。我们还需要授权CloudWatch日志记录。
资源";AWS_IAM_ROLE";";EXECUTION_ROLE";{name=";堡垒-EXECUTION";ASSUME_ROLE_POLICY=DATA。AWS_IAM_POLICY_DOCUMENT。承担角色(_Role)。JSON}DATA";AWS_IAM_POLICY_DOCUMENT";";EXECUTE_ROLE";{STATEMENT{ACTIONS=[";ECR:GetAuthorizationToken";]RESOURCES=[";*";]}STATION{ACTIONS=[";ECR:BatchCheckLayerAvailability";,";ECR:BatchGetImage";,。IMAGE_REPORATION_ARN]}语句{Actions=[";logs:CreateLogStream";,";,]Resources=[";*";]}}resource";AWS_IAM_ROLE_POLICY";";EXECUTION_ROLE";{name=";bastion-ecution";policy=data。AWS_IAM_POLICY_DOCUMENT。EXECUTION_ROLE。JSON ROLE=AWS_IAM_ROLE。EXECUTION_ROLE。ID}。
对于任务角色,您可以添加所需的任何权限。但至少,您需要授予承担我们上面创建的公钥获取器角色的能力。
Data";AWS_IAM_POLICY_DOCUMENT";";BASSION_TASK_ROLE";{语句{ACTIONS=[";STS:AssumeRole";]RESOURCES=[MODULE。堡垒。PUBLIC_KEY_FETCHER_ROLE_ARN]}##在此处添加所需的任何其他权限#}。
因为我们要将服务作为一个模块包含在父服务中,所以我们在顶级模块中创建策略并通过变量注入它。这样,我们就可以将服务模块泛化为在多个服务中使用。
资源";AWS_IAM_ROLE";";TASK_ROLE";{名称=";堡垒-TASK";ASSET_ROLE_POLICY=DATA。AWS_IAM_POLICY_DOCUMENT。承担角色(_Role)。json}资源";AWS_IAM_ROLE_POLICY";";TASK_ROLE";{POLICY=var.。TASK_ROLE_POLICY_JSON ROLE=AWS_IAM_ROLE。TASK_ROLE。ID}。
ECS任务使用JSON文件定义。因为我们需要注入从Terraform派生的变量,所以我们将使用TEMPLATE_FILE数据源。
[{";name";:";${name}";,";image";:";${image}";,";portMappings";:[{";tainerPort";:22}],";环境";:[{";name";:";AME_ROLE_FOR_AUTHED_KEYS";,";值";:";${Asmise_Role_for_Authorized_Key}";}],";logConfiguration";:{";logDriver";:";awslogs";,";选项";:{";awslogs-group";:";${log_group_name}";,";awslog-group";:";${region}";,";awslogs-stream-prefix";:";ssh";}]。
DATA";TEMPLATE_FILE";";CONTAINER_DEFINITIONS";{TEMPLATE=FILE(";${path.module}/container-definitions.tpl.json";)变量={假设角色_授权密钥=var.。PUBLIC_KEY_FETCHER_ROLE_ARN image=";${var.image_pository_url}:最新";log_group_name=AWS_cloudwatch_log_group。堡垒。名称名称=";堡垒";区域=var。地区}}。
资源";AWS_ECS_TASK_DEFINITION";";堡垒";{CONTAINER_DEFINITIONS=DATA。模板文件。CONTAINER_DEFINGS。渲染CPU=";256";EXECUTION_ROLE_ARN=AWS_IAM_ROLE。EXECUTION_ROLE。ARN family=";bastions";memory=";512";network_mode=";awsvpc";Requires_Compatibilies=[";Fargate";]TASK_ROLE_ARN=AWS_IAM_ROLE。TASK_ROLE。ARN}。
您可以随心所欲地调整CPU和内存,但由于我们可能只是通过此实例进行代理,所以我将两者都设置为支持的最低值。
当我使用API Gateway构建API时,我通常从创建单个函数开始。
我正在使用Clojure和David Chlimsky出色的AWS-API。如果您不想将Clojure引入到您的环境中,那么将我的代码翻译成您选择的语言应该不会太难。不过,我鼓励您认真考虑一下Clojure。我已经使用它大约十年了,它将改变您思考代码的方式。
本接口需要3个函数:一个用于创建堡垒实例,一个用于触发堡垒实例销毁,一个用于实际销毁实例。在本例中,创建和销毁函数共享相当多的代码,因此我选择将它们保留在单个Leiningen项目中。(对于在座的Clojure开发人员,我最终会将其迁移到tools.deps。但莱恩目前会做得很好。)。
首先,我们需要检查该用户是否存在已有的安全组。如果没有,我们知道该用户当前没有运行堡垒,我们只需创建锁定到该用户当前IP的安全组并启动堡垒任务。
如果该用户存在安全组,我们需要检查入口IP是否需要更新。如果IP匹配,那么很可能已经有一个运行堡垒,所以我们尝试为用户查找任务。如果它存在,就没有理由启动另一个,所以我们只需要用现有的堡垒IP来回应。这可以防止用户启动多个相同的堡垒并浪费资源。这也使得这个过程是幂等的,即使之前启动堡垒的请求似乎因为这样或那样的原因而失败了。
如果IP不匹配,我们需要检查是否已经有正在运行的任务。如果有,我们需要在删除过时的安全组之前将其停止。然后,我们删除安全组,使用新的IP重新创建,并开始新的堡垒任务。
请注意,我们根据用于签署请求的AWS凭据来识别用户。
(defn-handleRequest[_input-stream output-stream_](let[event(json/parse-stream(io/read input-stream)true)CIDR-IP(str(get-in event[:requestContext:Identity:SourceIp]))";/32";)user(last(cs/Split(get-in event[:requestContext:Identity:userArn]))#";/";)])](if-let[security-group-id(sg/get-id-for user)](if(sg/ip-matches?security-group-id CIDR-ip)(if-let[task(task/get-for user)](stream-response(:bastion-ip task)output-stream)(start-bastion-and-stream-response user CIDR-IP output-stream security-group-id)(if-let[task(task/get-for user)](do(task/stop-for user task)(sg/delete-for user)(start-bastion-and-stream-response user CIDR-IP。output-stream))(do(sg/delete-for user)(start-bastion-and-stream-response user CIDR-IP output-stream)(start-bastion-and-stream-response user CIDR-IP output-stream))
需要注意的一件事是,在编写本文时,查询正在运行的任务的好方法还不是很多。我选择使用startedBy,它在值方面有相当多的限制(有关更多详细信息,请参阅AWS文档)。要解决此问题,我使用SHA-256散列用户名,并将散列修剪为适当的长度。根据您需要支持的用户数,YMMV。
摧毁一座堡垒要简单得多(而且仍然是幂等的)。如果用户有任务正在运行,我们会停止它。然后我们删除该用户的安全组。
(defn-handleRequest[_input-stream__](let[event(json/parse-stream(io/read input-stream)true)user(:user event)](if-let[task(task/。
..