본문으로 바로가기

Contents

    개요

    AWS의 Web Console에 로그인 시 이에 대한 알람을 Slack으로 받아볼 수 있도록 설정해 봅시다.

    AWS Chatbot을 써볼까?

    AWS 서비스 중 하나인 Chatbot을 사용하여 Web Console에서 발생하는 이벤트들에 대해서 알람을 설정할 수 있습니다. Chatbot을 사용하면 별도의 Lambda 함수를 생성하지 않아서 비용이 들어가지 않고 SNS 비용만 들어갑니다.

    하지만 Chatbot은 메시지 필드의 커스터마이징이 불가능하다는 단점이 존재합니다.

    위의 사진은 Chatbot으로 알람 발송을 설정한 메시지 샘플인데 해당 사진에서 보이는 필드를 제외하고는 사용이 불가능합니다.

    아래의 사진처럼 제가 원하는 필드 (IP Address)를 보내고 싶은데 말이죠

    출처 :  https://longtermsad.tistory.com/53

    Chatbot으로는 원하는 필드를 추가할 수 없으니 Lambda 코드를 직접 만듭니다.

    💡 커스텀 필드가 필요하지 않을 경우 Chatbot을 사용하는 것만으로도 충분합니다.

     

    Slack APP 생성

    먼저 Lambda 코드를 작성하기 전 채널에 메시지를 보내는 Slack App을 생성하겠습니다.

    1️⃣ Slack App 생성

    1. https://api.slack.com/apps 접근 후 ‘Create New App’ 클릭
    2. ‘From Scratch’ 클릭 후 생성할 App의 이름 지정 후 생성

    2️⃣ 초기 설정 (권한 및 Token 발급)

    1. 생성한 App 클릭 후 Install your app안에 있는 Install to Workspace 클릭해서 워크스페이스에 추가

    2. 좌측 메뉴에서 ‘OAuth & Permissions’ 클릭

    3. 아래로 내려서 ‘Scopes’ 블록의 Bot Token Scopes → Add an OAuth Scope 클릭

    4. chat:write 권한 추가

     

    5. ‘OAuth Tokens for Your Workspace’에 기입되어 있는 Token 값 복사

     

    Lambda 함수 작성

    이 글에서 Lambda 함수는 Go언어로 작성합니다.

    1️⃣ Console Login 이벤트 Struct 타입으로 변환

    AWS에서 발생하는 모든 이벤트들은 Json 형식으로 이루어져 있습니다.

    Json 형식을 쉽게 사용하려면 Go언어의 Struct 형식으로 변환을 해주고 사용해야 합니다.

    • 이러한 일련의 과정을 마샬링(Marshalling)이라고 합니다.
    💡 AWS에서 공식적으로 제공하는 라이브러리 중 Go언어의 Struct 형식으로 변환이 되어있는 라이브러리가 존재합니다

    그러나 지금 구현하려는 Console Login 이벤트는 해당 라이브러리에 존재하지 않으므로 직접 구조체와 필드를 정의해야 합니다.

    변환은 struct의 Tag 기능을 사용하여 변환합니다.

     

    변환된 결과는 아래와 같습니다.

    // Define the structure of the AWS Console Sign In event
    type AWSConsoleSignInEvent struct {
        Version     string        `json:"version"`
        ID          string        `json:"id"`
        DetailType  string        `json:"detail-type"`
        Source      string        `json:"source"`
        Account     string        `json:"account"`
        Time        string        `json:"time"`
        Region      string        `json:"region"`
        Resources   []interface{} `json:"resources"`
        EventDetail EventDetail   `json:"detail"`
    }
    
    // Define the structure of the event detail
    type EventDetail struct {
        EventVersion        string              `json:"eventVersion"`
        UserIdentity        UserIdentity        `json:"userIdentity"`
        EventTime           string              `json:"eventTime"`
        EventSource         string              `json:"eventSource"`
        EventName           string              `json:"eventName"`
        AWSRegion           string              `json:"awsRegion"`
        SourceIPAddress     string              `json:"sourceIPAddress"`
        UserAgent           string              `json:"userAgent"`
        RequestParameters   interface{}         `json:"requestParameters"`
        ResponseElements    ResponseElements    `json:"responseElements"`
        AdditionalEventData AdditionalEventData `json:"additionalEventData"`
    }
    
    // Define the structure of the user identity
    type UserIdentity struct {
        Type        string `json:"type"`
        PrincipalID string `json:"principalId"`
        ARN         string `json:"arn"`
        AccountID   string `json:"accountId"`
    }
    
    // Define the structure of the response elements
    type ResponseElements struct {
        ConsoleLogin string `json:"ConsoleLogin"`
    }
    
    // Define the structure of the additional event data
    type AdditionalEventData struct {
        LoginTo       string `json:"LoginTo"`
        MobileVersion string `json:"MobileVersion"`
        MFAUsed       string `json:"MFAUsed"`
    }

    2️⃣ Slack Alert 함수 작성

    구조체를 정의했으니 이제 Slack으로 알람을 발송하는 구문을 작성합니다.

    • 메시지 템플릿은 Slack Builder 사이트에서 미리 볼 수 있습니다.
    • 슬랙 채널에 알람을 발송할 때 필요한 정보는 Lambda의 환경변수에서 얻도록 설정합니다.
      • SLACK_AUTH_TOKEN
      • CHANNEL_ID
    // Define the Lambda function handler
    func handler(ctx context.Context, event AWSConsoleSignInEvent) {
        // Set Slack authentication token and channel ID
        SLACK_AUTH_TOKEN := slack.New(os.Getenv("SLACK_AUTH_TOKEN"))
        CHANNEL_ID := os.Getenv("CHANNEL_ID")
    
        // Create Slack message template
        blocks := []slack.Block{
            slack.NewSectionBlock(
                nil,
                []*slack.TextBlockObject{
                    slack.NewTextBlockObject("mrkdwn", ":mega: *AWS Web Console Login*", false, false),
                },
                nil,
            ),
            slack.NewSectionBlock(
                nil,
                []*slack.TextBlockObject{
                    buildTextBlockObject("Account ID", event.EventDetail.UserIdentity.AccountID),
                    buildTextBlockObject("Type", event.EventDetail.UserIdentity.Type),
                    buildTextBlockObject("Source IP", event.EventDetail.SourceIPAddress),
                    buildTextBlockObject("Region", event.Region),
                    buildTextBlockObject("MFA Used", event.EventDetail.AdditionalEventData.MFAUsed),
                    buildTextBlockObject("ARN", event.EventDetail.UserIdentity.ARN),
                    buildTextBlockObject("Date", event.Time),
                },
                nil,
            ),
        }
    
        // Send Slack message
        CHANNEL_ID, timestamp, err := SLACK_AUTH_TOKEN.PostMessage(
            CHANNEL_ID,
            slack.MsgOptionBlocks(blocks...),
            slack.MsgOptionAsUser(true), // Add this if you want that the bot would post message as a user, otherwise it will send response using the default slackbot
        )
        // Check for errors and print message if successful
        if err != nil {
            fmt.Printf("%s\n", err)
            return
        }
        fmt.Printf("Message successfully sent to channel %s at %s", CHANNEL_ID, timestamp)
    }

    3️⃣ main() 함수 설정

    이제 main() 함수에서 위에서 생성한 Hanlder() 함수를 실행하도록 합니다.

    // Define the main function which starts the Lambda handler
    func main() {
        lambda.Start(handler)
    }

    4️⃣ 최종 코드

    최종적으로 완성된 코드입니다.

    package main
    
    import (
        "context"
        "fmt"
        "github.com/aws/aws-lambda-go/lambda"
        "github.com/slack-go/slack"
        "os"
    )
    
    // Define the structure of the AWS Console Sign In event
    type AWSConsoleSignInEvent struct {
        Version     string        `json:"version"`
        ID          string        `json:"id"`
        DetailType  string        `json:"detail-type"`
        Source      string        `json:"source"`
        Account     string        `json:"account"`
        Time        string        `json:"time"`
        Region      string        `json:"region"`
        Resources   []interface{} `json:"resources"`
        EventDetail EventDetail   `json:"detail"`
    }
    
    // Define the structure of the event detail
    type EventDetail struct {
        EventVersion        string              `json:"eventVersion"`
        UserIdentity        UserIdentity        `json:"userIdentity"`
        EventTime           string              `json:"eventTime"`
        EventSource         string              `json:"eventSource"`
        EventName           string              `json:"eventName"`
        AWSRegion           string              `json:"awsRegion"`
        SourceIPAddress     string              `json:"sourceIPAddress"`
        UserAgent           string              `json:"userAgent"`
        RequestParameters   interface{}         `json:"requestParameters"`
        ResponseElements    ResponseElements    `json:"responseElements"`
        AdditionalEventData AdditionalEventData `json:"additionalEventData"`
    }
    
    // Define the structure of the user identity
    type UserIdentity struct {
        Type        string `json:"type"`
        PrincipalID string `json:"principalId"`
        ARN         string `json:"arn"`
        AccountID   string `json:"accountId"`
    }
    
    // Define the structure of the response elements
    type ResponseElements struct {
        ConsoleLogin string `json:"ConsoleLogin"`
    }
    
    // Define the structure of the additional event data
    type AdditionalEventData struct {
        LoginTo       string `json:"LoginTo"`
        MobileVersion string `json:"MobileVersion"`
        MFAUsed       string `json:"MFAUsed"`
    }
    
    // Create a function to build a Slack TextBlockObject
    func buildTextBlockObject(title string, value string) *slack.TextBlockObject {
        return slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*%s*\n%s", title, value), false, false)
    }
    
    // Define the Lambda function handler
    func handler(ctx context.Context, event AWSConsoleSignInEvent) {
        // Set Slack authentication token and channel ID
        SLACK_AUTH_TOKEN := slack.New(os.Getenv("SLACK_AUTH_TOKEN"))
        CHANNEL_ID := os.Getenv("CHANNEL_ID")
    
        // Create Slack message template
        blocks := []slack.Block{
            slack.NewSectionBlock(
                nil,
                []*slack.TextBlockObject{
                    slack.NewTextBlockObject("mrkdwn", ":mega: *AWS Web Console Login*", false, false),
                },
                nil,
            ),
            slack.NewSectionBlock(
                nil,
                []*slack.TextBlockObject{
                    buildTextBlockObject("Account ID", event.EventDetail.UserIdentity.AccountID),
                    buildTextBlockObject("Type", event.EventDetail.UserIdentity.Type),
                    buildTextBlockObject("Source IP", event.EventDetail.SourceIPAddress),
                    buildTextBlockObject("Region", event.Region),
                    buildTextBlockObject("MFA Used", event.EventDetail.AdditionalEventData.MFAUsed),
                    buildTextBlockObject("ARN", event.EventDetail.UserIdentity.ARN),
                    buildTextBlockObject("Date", event.Time),
                },
                nil,
            ),
        }
    
        // Send Slack message
        CHANNEL_ID, timestamp, err := SLACK_AUTH_TOKEN.PostMessage(
            CHANNEL_ID,
            slack.MsgOptionBlocks(blocks...),
            slack.MsgOptionAsUser(true), // Add this if you want that the bot would post message as a user, otherwise it will send response using the default slackbot
        )
        // Check for errors and print message if successful
        if err != nil {
            fmt.Printf("%s\n", err)
            return
        }
        fmt.Printf("Message successfully sent to channel %s at %s", CHANNEL_ID, timestamp)
    }
    
    // Define the main function which starts the Lambda handler
    func main() {
        lambda.Start(handler)
    }

    AWS 설정

    AWS Console Login의 이벤트는 콘솔 로그인의 URL에 따라 어느 리전에서 이벤트가 발생되는지 결정됩니다.

    만약, 사용자가 ap-northeast-2가 붙은 URL을 통해 로그인할 경우 서울 리전에 이벤트가 발생하는 것이고 서울 리전에 Eventbridge와 Lambda 함수가 있어야 합니다.

    결국 사용자의 로그인 알람을 놓치지 않고 모두 받고 싶다면 모든 리전에 Lambda와 Eventbridge를 추가해야 하지만 이는 공수가 너무 크므로 자주 접속하는 리전에 대해서만 설정합니다.

    필자는 아래의 리전에 대해 Eventbridge와 Lambda를 생성했습니다.

    • ap-northeast-2
    • us-east-1
    • us-east-2
    💡 AWS Identity Center를 사용하면 지정한 리전 외에는 접근이 불가능하게 설정할 수 있기 때문에 Identity Center를 사용할 수 있는 여건이 된다면 사용하길 권장합니다.

    1️⃣ Lambda 함수 생성 및 설정

    1. Lambda Function 생성

    • 런타임 Go.1x

    2. 핸들러 이름 변경

    • Lambda 함수 생성 시 기본값이 hello인데 go build시 나오는 바이너리의 이름으로 변경합니다.

    3. Lambda 환경변수 설정

    • SLACK_AUTH_TOKEN
    • CHANNEL_ID

    4. 코드 업로드 (Lambda 함수)

    2️⃣ Eventbridge Rule 생성 및 설정

    1. 규칙 생성

     

    2. 이벤트 패턴 지정

    • detail.eventName을 ConsoleLogin으로 지정하지 않으면 MFACheck 이벤트도 같이 알람으로 발송되기 때문에 꼭 넣어줍시다.
    {
      "source": ["aws.signin"],
      "detail-type": ["AWS Console Sign In via CloudTrail"],
      "detail": {
        "eventName": ["ConsoleLogin"]
      }
    }

    3. 조금 전 생성한 Lambda 함수를 대상으로 지정

     

    알람 확인

    이제 AWS 콘솔에 로그인 시 지정한 Slack의 채널에 아래와 같은 알람이 발송됩니다.

     

    해당 글에서 작업한 Lambda 코드는 아래의 링크에서 확인하실 수 있습니다.

     

    GitHub - SeungHyeonShin/aws-console-login-alert: Lambda function that sends an alarm to Slack when Webconsole login event occurs

    Lambda function that sends an alarm to Slack when Webconsole login event occurs on AWS - GitHub - SeungHyeonShin/aws-console-login-alert: Lambda function that sends an alarm to Slack when Webconsol...

    github.com

     

    Ref.