Skip to main content

AWS 루트 계정 보호하기

ProtectRoot1

AWS의 루트 계정은 초기 AWS 계정 생성 후를 제외하고는 사용할 일이 없기 때문에 루트 계정의 액세스 키는 잠금 처리하거나 파기하고 MFA를 설정하여 보안을 강화해주는 것이 좋습니다. 보안 사고를 살펴보면 루트 계정 탈취로 인한 비용 처리로 고생하는 회사들이 많은 것 같습니다.

따라서 이번 실습에서는 다음 그림과 같이 아키텍처를 구성하여 보다 안전하게 AWS 클라우드를 사용하겠습니다.

먼저 루트 계정의 활동 중 모니터링해야 할 행동 몇가지가 있는데여. 액세스 키가 발급되었는지와 MFA가 활성화 되었는지 그리고 루트 계정으로 콘솔에 로그인을 진행하였을 때 입니다.

액세스키와 MFA 활성화 여부는 AWS Config 서비스를 통해 이미 만들어져 있는 AWS 관리형 규칙을 사용하여 액세스 키가 생성되었거나 MFA가 활성화되지 않는다면 1시간의 간격으로 트리거되어 해당 토픽에 대하여 제가 구독한 이메일로 메일이 발송되게 됩니다.

AWS Config 생성

위와 같이 AWS Config를 설정하고 SNS를 생성하여 자신의 이메일로 구독 설정을 하면 액세스 키가 활성화되거나 루트 계정의 MFA가 비활성화될 경우 설정한 트리거 시간에 따라 규칙을 만족하는지 여부를 이메일로 전송받을 수 있습니다. 구독 등록 중에 이메일에 오타가 포함되거나 잘못 들어갈 경우 삭제 버튼이 비활성화되는데 3일 동안 이메일 수신을 확인하지 않으면 자동으로 삭제된다고 합니다.


note

레코더

  • [✅] 기록 활성화

AWS Config 생성

기록할 리소스 유형

  • 이 리전 내에서 지원되는 모든 리소스 유형 기록
  • [✅] 특정 리소스 유형 기록

리소스 범주

  • AWS IAM User

데이터 보존 기간

  • 7년 동안 AWS Config 데이터 보존

AWS Config 역할

  • 기존 AWS Config 서비스 연결 역할 사용

전송 방법

  • [✅] 구성 변경 사항과 알림을 Amazon SNS 주제에 스트리밍 합니다.

이메일을 SNS 주제에 대해 알림 엔드포인트로 선택하면, 이메일 양이 증가할 수 있습니다.

사용자 계정에서 주제 선택

AWS Config 규칙

  • iam-root-access-key-check
  • root-account-mfa-enabled

SNS 구독 생성 (이메일)

ProtectRoot3

AWS Config와 SNS 구독 설정을 통해 루트 계정의 mfa 설정과 accessKey 확인 유무에 따라 이메일 알람을 받게 됩니다.

S3 액세스 로그 활성화

추가로 저는 서울 리전에 생성된 S3 버킷(AWS Config 액세스 파일 객체)에 액세스 로그를 활성화하여 다른 버킷에 이 S3 버킷의 액세스 로그가 추가로 쌓이게 했습니다. 여기서 만약 같은 버킷에 로그 기록을 담는다면 로깅 루프가 발생하여 엄청난 비용이 청구될 수 있으니 주의하세요.

다음으로는 루트 계정이 콘솔창에 로그인할 시에 문자 알림이 전송되도록 설정하겠습니다. 처음에는 카카오톡 알림톡으로 메시지 수신을 받고자하여 카카오 비즈니스에 채널을 등록하였지만 무조건 비즈니스 채널을 등록해야 한다고 해서 결국에는 SMS 메시지 수신으로 진행하였습니다. 비즈니스 채널을 등록하기 위해서는 사업자 등록을 해야합니다.

CloudTrail 생성

CloudTrail을 생성하여 읽기와 쓰기 액세스를 모두 허용하여 생성해줍니다. 만약 다중 리전 추적이 활성화되지 않았다면 CLI를 통해 활성화해줍니다.

aws cloudtrail update-trail --name cloudtraril-log --is-multi-region-trail

CloudTrail이 생성되었다면 리전별로 조금씩 상이하게 로그가 쌓이는 것을 확인하실 수 있습니다. 로깅 레코드를 한 번 살펴보시고 EventBridge에서 규칙 생성을 통해 원하는 해당 이벤트를 트리거할 텐데요. 제가 원하는 이벤트는 루트 계정이 콘솔에 입력했을 때의 이벤트이므로 해당 이벤트에 맞게 설정해주겠습니다.

여기서부터는 서울 리전이 아니라 us-east-1 버지니아 북부 리전에서 진행해주세요! 나중에 콘솔에 로그인하면 Consolelogin 이벤트를 트리거하는데 해당 이벤트는 서울 리전에 있는 CloudTrail에는 절대 레코드가 등록되지 않습니다. 이 레코드는 버지니아 북부 레코드에만 등록이 됩니다. 따라서 버지니아 북부에 Rule을 설정해주겠습니다.

이벤트 소스는 AWS 이벤트 또는 EventBridge 파트너 이벤트를 선택하시어 샘플 이벤트와 비교하여 원하는 이벤트 양식 패턴을 설정해주시면 됩니다.

{
"source": ["aws.signin"],
"detail-type": ["AWS Console Sign In via CloudTrail"],
"detail": {
"userIdentity": {
"arn": ["arn:aws:iam::AccountID:root"]
}
}
}

ProtectRoot4

타겟에는 API 게이트웨이를 설정합니다. 여기서 잠깐 생성 과정을 중지하고 먼저 API 게이트웨이를 만들 수 있습니다. API 게이트웨이를 생성하였다면 타겟으로 등록해줍니다. 리소스와 메소드는 람다를 생성한 후에 지정할 것입니다.

여기까지 잘 진행하셨다면 루트 계정으로 로그인 시 북부 버지니아의 클라우드 트레일에 이벤트 레코드가 생성되고 이 레코드 형식을 이벤트 브릿지 규칙에서 설정한 이벤트 패턴으로 API 게이트웨이를 호출합니다.

람다를 생성하기 전에 카카오 비즈니스 채널에서 다양한 아웃소싱 업체들을 확인할 수 있는데 여기서 선택하시어 API를 생성하실 수 있습니다. 저는 SOLAPI를 통해 API를 구현해보겠습니다. 공식문서도 깔끔하고 군더더기 없으니까요. 무엇보다 회원가입시 300포인트를 준다고 하니 샘플용으로 충분합니다. 회원가입을 진행하시어 계정인증을 하신다면 API Key를 등록하여 사용할 수 있습니다. 대쉬보드를 통해 API키 관리에 들어가셔서 새 API 키를 생성하시고 API SIGNATURE 발급기를 통해 헤더에 들어갈 인증 정보를 저장합니다.

코드는 문서를 참고하셔도 됩니다. 여러 언어가 지원되니 편한 언어로 선택하시면 됩니다. 저는 Go언어를 사용하겠습니다. Lambda에서 Go 언어는 코드 편집기가 지원되지 않으니 참고하세요.

테스트 용도로 인증 정보를 코드에 포함하겠습니다. 인증 정보는 람다 환경변수에 값을 지정하여 사용하시길 바랍니다.

아래는 예시 코드입니다.

package main

import (
"context"
"fmt"
"io/ioutil"
"net/http"
"strings"

"github.com/aws/aws-lambda-go/lambda"
)

func HandleLambdaEvent(ctx context.Context) (string, error) {
uri := "http://api.solapi.com/messages/v4/send"
data := strings.NewReader(`{"message":{"to":"보내는 사람","from":"받는 사람","text":"AWS 루트 계정 로그인 알림","type":"SMS"}}`)

req, err := http.NewRequest("POST", uri, data)
if err != nil {
panic(err)
}

req.Header.Set("Authorization", "HMAC-SHA256 apiKey=API키, date=키발급날짜, salt=쏠트, signature=시그니처")
req.Header.Set("Content-Type", "application/json")

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()

bytes, _ := ioutil.ReadAll(resp.Body)
str := string(bytes)
fmt.Println(str)

return "send message", nil
}

func main() {
lambda.Start(HandleLambdaEvent)
}
go mod init lambda-login
go get github.com/aws/aws-lambda-go/lambda
GOARCH=amd64 GOOS=linux go build main.go
zip main.zip main

최종적으로 생성된 zip 파일을 업로드해주세요.

10MB 용량이 넘어간다면 S3 버킷에 업로드한 후에 S3 버킷에서 파일을 전송하는 것이 더 빠릅니다. 위 코드는 빌드시 바이너리 용량이 4.6MB입니다. 핸들러는 꼭 main으로 변경해주세요.

람다가 업데이트 되었다면 배포를 완료하여 API 게이트웨이에 람다 ARN을 등록해줍니다. 리소스나 메소드는 자유롭게 하시면 됩니다.

ProtectRoot5

다시 한 번 람다 트리거에 API 게이트웨이가 등록되었는지 확인한 후 SAM CLI로 람다 코드를 테스트하여 문자 메시지를 수신해봅니다. 만약 문자가 정상적으로 수신되었다면 루트 계정을 로그인하여 문자가 수신되는지 확인하세요!

테스트시 2회 이상 연속적으로 문자 발송을 하다보면 API 키가 무효화되어 재발급받아야 하니 시간 간격을 두어 테스트하세요! 또는 개인 설정을 통해 중복 수신을 허용할 수 있습니다.

ProtectRoot2

위 과정에서는 SOLAPI를 사용하여 메시지 알림을 받는 과정이라면 이번 과정은 텔레그램 API와 NHN Cloud Notification를 사용하여 메시지 알림을 받는 과정입니다.

위 예시에서는 솔라피의 SMS API를 통해 메시지를 전달했지만 아래와 같이 가격 측면에서 더 저렴한 NHN Cloud의 Nofitication SMS를 사용하여 루트 로그인 시에 메시지 알림과 텔레그램 알림 두 가지를 모두 알람받도록 하겠습니다.

솔라피 vs NHN Cloud

  • NHN Cloud Notification SMS는 1건당 9.9원
  • SOLAPI SMS 1건당 13원 (API를 이용한 발송시)

먼저 텔레그램의 봇 토큰과 CHAT_ID를 준비합니다. 텔레그램 봇 토큰과 CHAT_ID를 생성하는 방법은 https://ghdwlsgur.github.io/docs/AWS-Workshop/Alert_Budget 이곳에서 확인하실 수 있습니다.

CloudTrail과 나머지 설정은 위 과정에서 진행했기에 생략하고 아래 과정에서는 람다를 신규 생성하고 API Gateway 리소스에 생성한 람다를 지정(람다 트리거 설정), EventBridge 타겟을 기존 API Gateway 메서드에서 신규 생성한 람다를 리소스로 가지고 있는 API Gateway의 메서드로 변경하는 과정입니다. 즉, 메시지 알림과 텔레그램 알림을 모두 받을 수 있지만 텔레그램 알람만 받도록 설정을 변경하는 과정입니다.

람다 생성 -> 런타임 설정

ProtectRoot11

환경 변수 입력

  • 텔레그램의 봇 토큰과 CHAT ID를 람다의 환경변수로 설정합니다.
  • SMS_SEND_NO는 SMS을 전송할 발신 전화번호입니다.

ProtectRoot12

ProtectRoot21

  • SMS_APP_KEYSMS_SECRET_KEY는 아래 그림과 같이 서비스 선택 탭에서 NHN Cloud의 Notification 서비스를 활성화한 이후 탭으로 이동 후 상단 URL & AppKey를 클릭하여 조회한 Appkey와 SecretKey 입니다.
  • 발신자 전화번호를 등록하기 위해서 진행되는 인증과정은 생략하였습니다.

ProtectRoot22

코드 구성 (Go)

package main

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"

"github.com/aws/aws-lambda-go/lambda"
)

func HandleLambdaEvent(ctx context.Context) (string, error) {
// Telegram
//==================================================================================
botToken := os.Getenv("TELEGRAM_BOT_TOKEN")
chatID := os.Getenv("TELEGRAM_CHAT_ID")
uri := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", botToken)
content := "AWS 루트 계정의 콘솔 로그인이 감지되었습니다."

postData := map[string]interface{}{
"chat_id": chatID,
"disable_web_page_preview": true,
"text": content,
}

jsonData, err := json.Marshal(postData)
if err != nil {
panic(err)
}

req, err := http.NewRequest("POST", uri, bytes.NewBuffer(jsonData))
if err != nil {
panic(err)
}

req.Header.Set("Content-Type", "application/json")

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()

b, _ := ioutil.ReadAll(resp.Body)
str := string(b)
fmt.Println(str)

// NHN Cloud (Message - SMS)
//==================================================================================
appKey := os.Getenv("SMS_APP_KEY")
senderNo := os.Getenv("SMS_SEND_NO")
smsUrl := "https://api-sms.cloud.toast.com"
smsUri := fmt.Sprintf("/sms/v3.0/appKeys/%s/sender/sms", appKey)
SecretKey := os.Getenv("SMS_SECRET_KEY")

postData = map[string]interface{}{
"body": "AWS 루트 계정의 콘솔 로그인이 감지되었습니다.",
"sendNo": senderNo,
"recipientList": []map[string]interface{}{
{
"recipientNo": senderNo,
"countryCode": "82",
},
},
}

jsonData, err = json.Marshal(postData)
if err != nil {
panic(err)
}

smsTarget := fmt.Sprintf("%s%s", smsUrl, smsUri)

req, err = http.NewRequest("POST", smsTarget, bytes.NewBuffer(jsonData))
if err != nil {
panic(err)
}

req.Header.Set("Content-Type", "application/json; charset=UTF-8")
req.Header.Set("X-Secret-Key", SecretKey)

client = &http.Client{}
resp, err = client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()

b, _ = ioutil.ReadAll(resp.Body)
str = string(b)
fmt.Println(str)

return "send message", nil
}

func main() {
lambda.Start(HandleLambdaEvent)
}

go mod init lambda-login
go get github.com/aws/aws-lambda-go/lambda
GOARCH=amd64 GOOS=linux go build main.go
zip main.zip main

# 람다에 main.zip 압축 바이너리 업로드

API 게이트웨이 메서드 설정

ProtectRoot13

  • 변경 후 API Gateway 배포를 잊지 마세요.

메서드 테스트

ProtectRoot14

EventBridge 타겟 설정

ProtectRoot15 ProtectRoot16

텔레그램 알람 확인

ProtectRoot17