Contents
개요
AWS의 Web Console에 로그인 시 이에 대한 알람을 Slack으로 받아볼 수 있도록 설정해 봅시다.
AWS Chatbot을 써볼까?
AWS 서비스 중 하나인 Chatbot을 사용하여 Web Console에서 발생하는 이벤트들에 대해서 알람을 설정할 수 있습니다. Chatbot을 사용하면 별도의 Lambda 함수를 생성하지 않아서 비용이 들어가지 않고 SNS 비용만 들어갑니다.
하지만 Chatbot은 메시지 필드의 커스터마이징이 불가능하다는 단점이 존재합니다.
위의 사진은 Chatbot으로 알람 발송을 설정한 메시지 샘플인데 해당 사진에서 보이는 필드를 제외하고는 사용이 불가능합니다.
아래의 사진처럼 제가 원하는 필드 (IP Address)를 보내고 싶은데 말이죠
Chatbot으로는 원하는 필드를 추가할 수 없으니 Lambda 코드를 직접 만듭니다.
💡 커스텀 필드가 필요하지 않을 경우 Chatbot을 사용하는 것만으로도 충분합니다.
Slack APP 생성
먼저 Lambda 코드를 작성하기 전 채널에 메시지를 보내는 Slack App을 생성하겠습니다.
1️⃣ Slack App 생성
- https://api.slack.com/apps 접근 후 ‘Create New App’ 클릭
- ‘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 사이트에서 미리 볼 수 있습니다.
- Block Type에 따라서 어떤 함수를 사용할지는 아래의 공식 문서를 참고합니다.
- 슬랙 채널에 알람을 발송할 때 필요한 정보는 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 코드는 아래의 링크에서 확인하실 수 있습니다.
Ref.
'AWS > 관리 및 거버넌스' 카테고리의 다른 글
[SSM] SSM(System Manager)를 이용하여 Private Subnet 리소스에 접근하기 (0) | 2023.01.29 |
---|