본문 바로가기

AWS

[Lambda] IAM, SG 변경 사항을 Slack으로 알람 받기

반응형

개요

IAM 혹은 Security Group에서의 변경 사항이 감지되면 Slack으로 통보를 받고 싶습니다.

 

⇒ Notification을 받는 방법은 여러 가지가 있을 것이다.

 

 

AWS Config를 통해 감지할 수도 있고, 필자의 방식대로 EventBridge를 통해 특정 API CALL이 발생될 경우에 감지될 수 있도록 설정할 수도 있다.

 

간단하게는 API CALL이 감지될 경우 SNS - Email로 보내는 방법이 있는데 보기에 이쁘지가 않다.

 

그렇기 때문에 Lambda를 Trigger하여 문자를 Parsing하고 Template대로 Slack에 Webhook을 보내 Slack에서 Notification을 받는 방법이 보기에 깔끔할 것이라 생각한다.

 

전제 조건

  1. SLACK APP을 미리 생성해둔 후에 WEBHOOK_URL을 발급받아 놓는다.
  2. CloudTrail이 활성화되어 있어야 한다.

 

아키텍처

아키텍처는 정말 간단하다.

 

1. EventBridge에서 특정 API CALL이 감지 (Event Pattern으로 정의)

 

2. Target(SNS)으로 보내짐

 

3. SNS는 Lambda를 트리거하여 event의 내용을 보내게 된다.

 

4. Lambda에서는 Event 내용을 적절히 분해하여 Slack으로 Noti를 보내주면 끝이다.

 

 

주의할 점 몇 가지가 존재한다.

1. IAM 관련 API는 Virginia 리전에서 감지가 가능하다.

(글로벌 리소스이기 때문이며, CloudFront의 SSL 인증서가 Virginia ACM만 가능한 이유를 생각해보면 된다.)

2. EventBridge에서 특정 API CALL을 감지해야 하는데, 어떤 API CALL만 감지할 것인지 Event Pattern에 대한 설정을 잘 해두어야 한다.

3. Seoul과 Tokyo 리전을 주로 사용한다면 SG의 변경 사항은 주로 Seoul, Tokyo에서만 발생하기 때문에 Seoul, Tokyo 리전에서는 SG 관련된 API만 Event Pattern에 설정하면 될 것이다.

4. CloudTrail이 활성화 되어 있어야 한다.

(AWS에서 발생하는 모든 API CALL은 CloudTrail을 통해 수집이 되며, EventBridge는 본인 Region에서 발생하는 모든 API CALL을 검사하게 된다.)

 

설정 방법

1. Lambda 함수 생성 (Virginia Region)

  1. Runtime : Python 3.8
  2. Role : CloudWatch Log groups 관련된 권한을 부여
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "logs:CreateLogGroup",
            ],
            "Resource": "*"
        }
    ]
}

 

3) Environment Variables

환경 변수를 설정해두고, Lambda code에서는 Python의 os 모듈을 사용하여 가져올 수 있도록 함

 

HOOK_URL

SLACK_CHANNEL

 

4) Python Code

- urllib3의 Poolmanager() 클래스를 통해 http 객체를 생성하고

  http의 request 함수를 사용하여 Webhook을 보낸다. 인자로는 method, url, body 3개를 설정해서 보내준다.

- iam_event = json.loads(event.get('Records')[0].get('Sns').get('Message'))
>> event가 SNS로부터 JSON 형식으로 전달을 받기 때문에 분해하여 오직 원하는 event 내용을 받기 위해 분해.

     또한, json 형식으로 만들기 위해 json 모듈의 loads 함수를 사용
     (iam_event가 아니라 sg event 또한 같이 들어오는데 Naming을 잘못 하였음..)

- Event Name에 Delete 혹은 Detach 혹은 Revoke가 포함되어 있으면 빨간색, 아니면 파란색 Noti 보내도록 설정

 

Python Code

import json
import logging
import os
import urllib3

http = urllib3.PoolManager()

# Reference
# <https://blog.nodeswat.com/simple-node-js-and-slack-webhook-integration-d87c95aa9600>
   
# Get values from Environments variables
SLACK_CHANNEL = os.environ['SLACK_CHANNEL']
HOOK_URL = os.environ['HOOK_URL']
   
logger = logging.getLogger()
logger.setLevel(logging.INFO)

def send_message(message):
    
    data = json.dumps(message).encode('utf-8')

    res = http.request(
        method='POST',
        url=HOOK_URL,
        body=data
    )

    print(res.data, res.status)

   
def lambda_handler(event, context):
    
    iam_event = json.loads(event.get('Records')[0].get('Sns').get('Message'))
    
    logger.info("Event        : " + str(event))
    
    # 정보
    event_id = iam_event['detail']['eventID']
    
    
    # 주체
    user_name = iam_event['detail']['userIdentity']['userName']
    source_ip = iam_event['detail']['sourceIPAddress']
    
    # 이벤트 내역
    event_time = iam_event['detail']['eventTime']
    event_name = iam_event['detail']['eventName']
    aws_region = iam_event['detail']['awsRegion']
    event_request_parameters = iam_event['detail']['requestParameters']
    event_source = iam_event['detail']['eventSource']
    
    changed_resource = ""
    if event_source == "ec2.amazonaws.com":
        changed_resource += "Security Group"
    else:
        changed_resource += "IAM"
    
    
    # cloudtrail url
    ct_url = f"https://{aws_region}.console.aws.amazon.com/cloudtrail/home?region={aws_region}#/events/{event_id}"
    
    print(ct_url)
    
    current_event = ""
    
    for key, value in event_request_parameters.items():
        current_event += f"{key} : {value}\\n"
    
    
    logger.info("SLACK Channel: " + SLACK_CHANNEL)
    logger.info("HOOK URL     : " + HOOK_URL)
    
    check_list = ["Delete", "Detach", "Revoke"]
    if any(x in event_name for x in check_list):
        color = "#eb4034"
    else:
        color = "#0c3f7d"
    
    # color = "#eb4034" if event_name.find("delete") >= 0 else "#0c3f7d"

    slack_message = {
        "channel": SLACK_CHANNEL,
        "text": f"{changed_resource} 변경 사항이 감지되었습니다.",
        "blocks": [
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": '*'+ changed_resource + ' 변경 사항이 감지되었습니다.*\\n\\n*Request Parameters*'
                }
            },
            # {"type": "divider"},
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": current_event + '\\n'
                }
            },
            {"type": "divider"}
        ],
        "attachments": [{
            "fallback": "Fallback 입니다.",
            "color": color,
            "blocks": [
            {
                "type": "section",
                "fields": [
                    {
                        "type": "mrkdwn",
                        "text": '*행위 주체*\\n' + user_name
                    },
                    {
                        "type": "mrkdwn",
                        "text": '*소스 IP*\\n' + source_ip + ' (<https://ko.infobyip.com/ip-' + source_ip + '.html|*IP 위치 조회*>)'
                    },
                    {
                        "type": "mrkdwn",
                        "text": '*이벤트 시간 (UTC)*\\n' + event_time
                    },
                    {
                        "type": "mrkdwn",
                        "text": '*이벤트 이름*\\n' + event_name
                    },
                    {
                        "type": "mrkdwn",
                        "text": '*이벤트 리전*\\n' + aws_region
                    }
                ]
            },
            {
                "type": "actions",
                "elements": [
                    {
                        "type": "button",
                        "text": {
                            "type": "plain_text",
                            "text": "상세 내용 확인  :waving_white_flag:"
                        },
                        "style": "primary",
                        "url": ct_url
                    }
                ]
            },
            {
            "type": "divider"
            }
            ]
        }]
    }
    
    logger.info("Slack Message        : " + str(slack_message))
    
    send_message(slack_message)

 


 

2. SNS 설정 (Virginia Region)

 

Subscription : 위에서 생성한 Lambda를 Invoke 하도록 설정

(권한은 자동으로 AWS에서 내부적으로 Lambda를 Invoke 할 수 있도록 권한이 추가된다.)

 


 

3. EventBridge 설정 (Virginia Region)

 

1) Event Pattern

 

Virginia에서는 IAM 관련 내역을,

 

Tokyo, Seoul의 Event Bridge로부터 인입되는 SG 관련 내역을 감지해야 하기 때문에 아래처럼 설정

 

{
  "source": ["aws.iam", "aws.ec2"],
  "detail-type": ["AWS API Call via CloudTrail"],
  "detail": {
    "eventSource": ["iam.amazonaws.com", "ec2.amazonaws.com"],
    "eventName": [
      "AttachGroupPolicy",
      "AttachRolePolicy",
      "AttachUserPolicy",
      "ChangePassword",
      "CreateAccessKey",
      "CreateGroup",
      "CreatePolicy",
      "CreateRole",
      "CreateUser",
      "DeleteAccessKey",
      "DeleteGroup",
      "DeleteGroupPolicy",
      "DeletePolicy",
      "DeleteRole",
      "DeleteRolePolicy",
      "DeleteUser",
      "DeleteUserPolicy",
      "DetachGroupPolicy",
      "DetachRolePolicy",
      "DetachUserPolicy",
      "PutGroupPolicy",
      "PutRolePolicy",
      "PutUserPolicy",
      "AuthorizeSecurityGroupIngress",
      "AuthorizeSecurityGroupEgress",
      "RevokeSecurityGroupIngress",
      "RevokeSecurityGroupEgress"
    ]
  }
}

 

2) Targets

위에서 생성한 SNS를 타겟으로 설정한다.

 


 

4. Tokyo, Seoul 리전에서의 EventBridge 설정

 

관련 공식 문서 : https://aws.amazon.com/ko/premiumsupport/knowledge-center/monitor-security-group-changes-ec2/

 

 

1) Event Pattern

{
  "source": [
    "aws.ec2"
  ],
  "detail-type": [
    "AWS API Call via CloudTrail"
  ],
  "detail": {
    "eventSource": [
      "ec2.amazonaws.com"
    ],
    "eventName": [
      "AuthorizeSecurityGroupIngress",
      "AuthorizeSecurityGroupEgress",
      "RevokeSecurityGroupIngress",
      "RevokeSecurityGroupEgress"
    ]
  }
}

 

2) Targets

Virginia 리전에 있는 EventBridge로 전달될 수 있도록 EventBus의 ARN을 기입해준다.

 


 

5. Test

 

1 - AccessKey를 보유한 IAM User를 생성해본다.

 

 

Slack으로 Notification이 옴

 

 

2 - SG 규칙을 변경해본다.

 

 

Slack으로 Notification이 옴

 

 

'상세 내용 확인’을 클릭하면 CloudTrail의 Event로 접근하도록 함

 

⇒ API Event 발생 후 약 2~4분 후에 CloudTrail에 생성된다. 바로 클릭하면 없는 Event ID 오류 발생

 


 

6. 결론

Slack으로 할 수 있는 것들이 굉장히 많다.

 

이번에는 Webhook URL로 간단하게 보냈지만 Token 값을 통해서도 할 수 있고,

Slash Command를 활용하여 Slack에서 자동화 프로세스를 구현할 수 있다.

 

반응형