AWS ssm:SendCommand with SNS - 限制性 IAM 策略

vku*_*hyn 5 amazon-web-services amazon-sns amazon-iam aws-lambda aws-ssm

我正在尝试创建一个限制性 SSM 角色 IAM 策略,该策略能够在 SendCommand 命令执行失败时发送 SNS 通知。我目前的以下策略为我提供“AccessDenied”,没有其他信息(占位符已替换):

{
  "Statement": {
    "Effect": "Allow",
    "Action": [ "ssm:SendCommand" ],
    "Resource": [
      "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:instance/*",
      "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:document/${DocumentName}",
      "arn:aws:s3:::${S3BucketName}",
      "arn:aws:s3:::${S3BucketName}/*",
      "arn:aws:iam::${AWS::AccountId}:role/${RoleThatHasSNSPublishPerms}",
      "arn:aws:sns:${AWS::RegionId}:${AWS::AccountId}:${SNSTopicName}"
    ]
  }
}
Run Code Online (Sandbox Code Playgroud)

我还拥有 ${RoleThatHasSNSPublishPerms} 的 iam::PassRole 权限。我以这种方式使用 python boto3 从 lambda 调用它:

        ssm = boto3.client('ssm')
        ssm.send_command(
            InstanceIds = [ instance_id ],
            DocumentName = ssm_document_name,
            TimeoutSeconds = 300,
            OutputS3Region = aws_region,
            OutputS3BucketName = output_bucket_name,
            OutputS3KeyPrefix = ssm_document_name,
            ServiceRoleArn = ssm_service_role_arn,
            NotificationConfig = {
                'NotificationArn': sns_arn,
                'NotificationEvents': ['TimedOut', 'Cancelled', 'Failed'],
                'NotificationType': 'Command'
            }
        )
Run Code Online (Sandbox Code Playgroud)

我知道问题出在我的 IAM 策略的“资源”部分,因为当我将资源块更改为简单的“*”时,运行命令可以正确执行。另外,当我删除 python 命令的 NotificationConfig 和 ServiceRoleArn 部分时,SendCommand 也会成功。

我不希望这个 lambda 角色有一个宽松的策略来在任何地方和任何东西上执行命令。问题是,如何限制此策略并仍然发送失败通知?

编辑:不确定这是新的还是我之前错过了它,但AWS发布了一些有关如何将权限缩小到仅标记的EC2的说明: https://docs.aws.amazon.com/systems-manager/latest/userguide /sysman-rc-setting-up-cmdsec.html

这仍然没有回答问题的 SNS/S3 部分,但至少这是朝着正确方向迈出的一步。

Sco*_*ter 1

我刚刚创建了一个定期计划的 lambda 函数,该函数调用 ssm send-message API 来调用位于 EC2 实例上的 shell 脚本。此脚本会对实例上运行的各种服务执行 ping 操作。如果有任何不健康的情况,脚本会返回非零退出代码,并且我会收到电子邮件通知。无论退出代码如何,脚本的 stderr 和 stdout 都会转到我选择的 S3 存储桶。

IaC 全部包含在 CDKv2 中。我删除了一些无关的内容,例如调度函数,以便您可以专注于 IAM 组件。有两个重要的组成部分:

  1. 用于发布到 SNS 的服务角色(此处记录为任务 2-3)
  2. Lambda 函数的角色(与任务 4-5 记录在同一位置)。在任务 4 的文档中,他们说使用“AmazonSSMFullAccess 托管策略,或提供类似权限的策略”。我的代码中提供“可比较”(即范围缩小)权限的策略是ssmSendMessagePolicypassRolePolicy,它们作为内联策略添加到 Lambda 函数的默认执行角色中。

关于将脚本的输出写入 S3,还有一个更重要的 IAM 组件,我稍后会介绍它。

const { readFileSync } = require('fs');
import * as path from 'path';
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as sns from "aws-cdk-lib/aws-sns";
import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as iam from 'aws-cdk-lib/aws-iam';

interface Ec2InstanceConfig {
  instanceId: string,
}

export class HealthCheckStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: cdk.StackProps) {
    super(scope, id, props);
    
    // This JSON config has stuff like EC2 instance ids, bucket names, etc
    const config = JSON.parse(readFileSync(path.join(__dirname, '..', 'config.json')).toString())

    // Bucket for storing ssm send-message responses
    const ssmSendCommandLogBucket = s3.Bucket.fromBucketName(
      this,'ssmSendCommandLogBucket',
      config.ssmSendCommandLogBucket
    )

    // SNS Topic and subscriptions
    const infaAlarmTopic = new sns.Topic(this, 'infaAlarmTopic', {
      displayName: 'infaAlarmTopic',
      topicName: 'infaAlarmTopic',
    })
    config.alarmSubscribers.forEach((email: string) => {
      infaAlarmTopic.addSubscription(new subscriptions.EmailSubscription(email));
    })

    // Service Role for SSM to publish to SNS
    const servicePolicy = new iam.PolicyStatement({
      actions: [
        'sns:Publish',
      ],
      resources: [infaAlarmTopic.topicArn],
      effect: iam.Effect.ALLOW,
    })
    const serviceRole = new iam.Role(this, 'serviceRole', {
      assumedBy: new iam.ServicePrincipal("ssm.amazonaws.com"),
    })
    serviceRole.attachInlinePolicy(
      new iam.Policy(this, `servicePolicy`, {
        statements: [servicePolicy],
      })
    )

    // One Lambda Function per EC2 Instance (although you can send commands to multiple instances, I haven't worked that out yet)
    const ec2Instances: Ec2InstanceConfig[] = config.ec2Instances;
    ec2Instances.forEach(ec2Instance => {
      // Lambda Function
      const healthCheckLambda = new lambda.Function(this, `healthCheckLambda${ec2Instance.instanceId}`, {
        runtime: lambda.Runtime.PYTHON_3_9,
        handler: 'app.handler',
        code: lambda.Code.fromAsset(path.join(__dirname, '..', 'lambdas', 'health_check')),
      })
      
      // Environment Variables (you need to provide these to the send-message command)
      healthCheckLambda.addEnvironment('SNS_TOPIC_ARN', infaAlarmTopic.topicArn)
      healthCheckLambda.addEnvironment('SERVICE_ROLE_ARN', serviceRole.roleArn)
      
      // IAM for Lambda
      const ssmSendMessagePolicy = new iam.PolicyStatement({
        actions: [
          'ssm:SendCommand',
        ],
        resources: [
          `arn:aws:ssm:${props.env?.region}::document/AWS-RunShellScript`,
          `arn:aws:ec2:${props.env?.region}:${props.env?.account}:instance/${ec2Instance.instanceId}`,
        ],
        effect: iam.Effect.ALLOW,
      })
      const passRolePolicy = new iam.PolicyStatement({
        actions: [
          'iam:PassRole',
        ],
        resources: [serviceRole.roleArn],
        effect: iam.Effect.ALLOW,
      })
      healthCheckLambda.role?.attachInlinePolicy(
        new iam.Policy(this, `SMSendCommandPolicyHealthCheckLambda${ec2Instance.instanceId}`, {
          statements: [ssmSendMessagePolicy],
        })
      )
      healthCheckLambda.role?.attachInlinePolicy(
        new iam.Policy(this, `passRolePolicyHealthCheckLambda${ec2Instance.instanceId}`, {
          statements: [passRolePolicy],
        })
      )
    })
  }
}
Run Code Online (Sandbox Code Playgroud)

以下是 Lambda 函数中的 ssm 发送命令调用(使用适用boto3于 AWS 的 SDK 的 Python):

import os

import boto3

BUCKET_NAME = os.getenv("BUCKET_NAME")
INSTANCE_ID = os.getenv("INSTANCE_ID")
SNS_TOPIC_ARN = os.getenv("SNS_TOPIC_ARN")
SERVICE_ROLE_ARN = os.getenv("SERVICE_ROLE_ARN")
USER = os.getenv("USER")

ssm_client = boto3.client("ssm")

def handler(event, context):
    response = ssm_client.send_command(
        InstanceIds=[
            INSTANCE_ID,
        ],
        DocumentName='AWS-RunShellScript',
        TimeoutSeconds=60,
        Comment='Health check',
        Parameters={
            'commands': [f"""sudo -H -u {USER} bash -c '/path/to/my/health_check.sh'"""]
        },
        OutputS3BucketName=BUCKET_NAME,
        OutputS3KeyPrefix="ssm-send-commands",
        NotificationConfig={
            'NotificationArn': SNS_TOPIC_ARN,
            'NotificationEvents': [
                'Failed',
            ],
            'NotificationType': 'Command'
        },
        ServiceRoleArn=SERVICE_ROLE_ARN,
    )
Run Code Online (Sandbox Code Playgroud)

此时,您可能想知道将脚本输出写入 s3 的权限在哪里。我找不到任何相关文档,因为SERVICE_ROLE_ARN根据文档,这是特定于 SNS 的:“身份和访问管理 (IAM) 服务角色的 ARN,用于发布用于运行的 Amazon Simple Notification Service (Amazon SNS) 通知指挥指挥。” 我发现有效的方法是通过 EC2 实例的 IAM 角色提供这些权限。其背后的直觉是 ssm 代理正在实例上运行,因此在写入 s3 时它将获取实例角色。s3:PutObject范围为存储桶前缀的操作(ssm-send-commands在我的示例中)将满足最低权限要求。