들어가며

클라우드 서비스의 장점은 리소스의 탄력적 사용, 비용 절감, 운영에 대한 부담 감소에 있다. 하지만 보안 관점에서는 이러한 클라우드 서비스의 장점이 취약점으로 작용한다. 클라우드 환경에서는 개발자가 직접 리소스를 관리할 수도 있다. 이는 편하고 빠르게 개발할 수 있다는 장점이 되지만, 반대로 보안을 챙기는 사람 입장에서는 시시각각 변화하는 리소스를 어떻게 안전하게 보호할 수 있는지에 대해 고민이 될 수 밖에 없다. 온프레미스 환경에서 인프라 변경 시 서버를 하나 추가하더라도 다수의 의사결정이 필요하다. 이 과정에서 다소 절차가 복잡할지라도 자연스럽게 자산관리가 될 수 있고, 기존에 튼튼하게 잘 구성된 인프라 내부에 보안수준을 해치지 않고 서버를 추가하게 된다. 하지만 클라우드 서비스에서는 서버 한 대를 추가하는 작업은 다수의 의사결정 없이도 개인이 1분 안에 생성하는 것이 가능하다. 그리고 삭제도 바로 가능하다.

이러한 특징을 보면 온프레미스와 클라우드는 게임 ‘요새 지키기’와 ‘마인크래프트’ 모습과 닮아있다.

[그림] 게임 요새 지키기와 마인크래프트

온프레미스 환경에서는 인프라를 구성할 때 요새처럼 최전방에 철벽과 철문을 두어 트래픽을 감시하고 차단한다. 클라우드 환경에서는 마인크래프트처럼 누구나 인프라를 구성할 수 있다. 튼튼한 벽과 하나의 철문을 둔 요새를 만들 수도 있고, 입구도 많고 누구나 제약 없이 들어올 수 있는 허름한 성을 만들 수도 있다. 그렇다고 클라우드 환경에서 온프레미스 환경처럼 모든 입구에 철벽과 철문을 두기에는 네트워크 아키텍처, 운영, 비용 측면에서 제약사항이 많다. 그래서 (AWS) 클라우드 환경에서는 Security Group이라는 서비스를 적극적으로 활용한다.

Security Group은 방화벽과 같이 네트워크 접근 통제 제어를 가능하게 하는 AWS에서 제공하는 보안 서비스이다. 기본적으로 “All Deny” 정책이며 직접 설정한 허용 정책에 정의한 트래픽만 허용된다. Security Group은 상세 설정에서 Protocol Type, Port Range, Source(CIDR, IPv4, IPv6), Description, Tag 설정을 제공한다. 그리고 무료이다.👍

[설명] Security Group

Security Group도 역시 직접 설정할 수 있는데, 여기서 보안 문제가 발생할 수 있다. 이를테면 누군가 실수로 인터넷 어디서나 내부 리소스의 SSH, FTP, DB, RDP 등과 같은 포트에 접근하도록 설정했다. 그리고 긴 시간 동안 이 사실을 아무도 몰랐다면…? 아주 암울 할 것이다.

공격자는 이렇게 실수로 인해 생겨난 공격 포인트를 찾아내기 위해 밤낮없이 부지런하게 스캔 중이다.

이러한 사태를 막기 위해 온프레미스 방화벽 운영 방식처럼 Security Group 적용하는 권한 제어나 절차를 추가하는 건 어떨까? 예를 들면 Security Group을 변경하거나 추가하려면 정보보안팀에 결재를 받는 것이다. 혹은 IAM에서 Security Group 관리 권한을 제거해버리고, 보안 담당자만 적용하거나 설정할 수 있게 한다. 보수적이고 좋은 방법처럼 보이지만, 검수하는 보안 담당자도 힘들 것이고 정책이 적용되길 기다리는 개발팀도 힘들어진다. 특히 개발팀에게 이러한 절차는 클라우드 서비스를 쓰는 매력을 사라지게 한다. 그리고 이러한 방법으로 보수적인 보안 정책을 세워가다 보면 결국 클라우드 서비스를 클라우드 서비스답게 쓰지 못하는 상황이 벌어진다.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Deny",
            "Action": [
                "ec2:DescribeSecurityGroups",
                "ec2:DescribeTags"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Deny",
            "Action": [
                "ec2:AuthorizeSecurityGroupIngress",
                "ec2:RevokeSecurityGroupIngress",
                "ec2:AuthorizeSecurityGroupEgress",
                "ec2:RevokeSecurityGroupEgress"
            ],
            "Resource": [
                "arn:aws:ec2:region:AccountId:security-group/*"
            ]
        }
    ]
}

[IAM 참고] IAM Security Group 권한 제한(Deny) Role 예시

다른 방법으로는 AWS의 Trusted Advisor 서비스를 이용하여 취약한 설정이 있는지 주기적으로 확인하는 방법이 있다. 이를 활용하는 것도 보안 조치를 하는 관점에서 좋은 방법이지만 실시간 대응이 힘들뿐더러 사고분석, 감사 관점에서는 생성자를 찾기 위해 다시 Cloudtrail 로그를 찾아보아야 하는 수고가 필요하다. 그리고 서비스가 확대되고 10.. 20.. 40.. 100 개의 계정이 생성되어간다면 더욱 효율성은 떨어진다.

Trusted Advisor는 AWS 리소스의 사용 비용, 퍼포먼스, 보안, 저가용 결함성, 서비스 사용한도 측면에서 권장사항을 준수하고 있는지 모니터링 해주는 서비스이다.

[설명] Trusted Advisor 서비스

Security Group의 Cloudtrail API 호출 Rules 설정

이 장에서는 AWS 멀티 계정 환경에서 Security Group을 취약하게 설정하는 것을 효과적으로 모니터링하는 방법에 관해 설명한다. Security Group이 변경, 추가될 때 발생하는 Cloudtrail API 호출 이벤트를 Cloudwatch 서비스에서 모니터링하여 누가 취약한 설정을 하는지 모니터링 할 수 있다. 우선 취약함의 정의는 내부 기준에 따라 달라질 수 있는데, 이 글에서는 일반적인 웹 포트 80, 443, 8080 포트를 제외한 포트 중 0.0.0.0/0으로 출발지를 지정한 Security Group을 취약한 설정이라 가정하겠다.

취약한 Security Group을 모니터링할 때 필요한 이벤트는 ‘AuthorizeSecurityGroupIngress’이다. Cloudtrail API 호출 이벤트를 모니터링하는 Cloudwatch 서비스에 접속해 Events > Rules > Step 1: Create rule에서 다음과 같이 작성한다.


[그림] Create Rule

아래 ‘이벤트 패턴 미리보기’에 다음과 같이 생성되면 성공!

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

위와 같은 Cloudwatch 서비스의 Rules를 생성할 때 다음 Security Group과 연관된 이벤트들을 참고할 수 있다.

[보안그룹 생성] createsecuritygroup
[보안그룹 삭제] deletesecuritygroup
[보안그룹 설명] describesecuritygroup
[보안그룹에 규칙추가] authorizesecuritygroupIngress
[보안그룹에 규칙삭제] revokesecuritygroupingress

[참고] Security Group API 호출 이벤트

Event Buses를 통한 멀티 계정의 이벤트 로그 중앙화

다음 단계로 Cloudwatch 서비스 이벤트를 lambda 서비스로 트리거하려 한다. 멀티 계정 환경이라면 이 단계에서 로그 중앙화 작업을 수행하는 것이 좋다.

이번 글에서 다루는 작업을 떠나서 멀티 계정 환경에서 하나의 컨트롤 계정을 지정하여 로그를 중앙 수집하는 것은 실시간 탐지 및 로그 기반 침해사고 분석, 의도하지 않은 로그데이터 삭제 등에 대비할 수 있어 중요하게 생각해야 할 부분이다.



[그림] 논리적 연동 구성인 컨트롤(Control Account)계정과 서비스(Player Account)계정

이후부터는 컨트롤 계정과 서비스 계정 구조인 플레이어 계정을 구분해서 설명한다. 이어서 플레이어 계정에서 Cloudwatch 서비스의 Events > Rules > Step 1: Create rule > Targets를 지정하는 단계에서 Event bus in another AWS account로 타겟을 지정하여 각 플레이어 계정에서 발생하는 Security Group API 호출 이벤트를 컨트롤 계정으로 중앙화할 수 있다.



[그림] 플레이어 계정에서의 Targets 설정

Account ID는 이벤트를 중앙 수집하는 계정인 컨트롤 계정 ID를 넣어준다. Event Buses를 이용하여 Targets에 이벤트를 보내기 위해서는 IAM Role이 하나 필요한데 IAM 서비스에 접속하여 다음 IAM Role(Event_Bus_IAM Roles)을 생성하여 지정해주자.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "events:PutEvents"
            ],
            "Resource": [
                "arn:aws:events:ap-northeast-2:컨트롤 계정ID:event-bus/default"
            ]
        }
    ]
}

여기까지 플레이어 계정 하나에서 해줘야할 설정은 끝났다. 이어서 컨트롤 계정에 접속하여 각 플레이어 계정의 Event Buses가 컨트롤 계정으로 데이터를 보낼 수 있도록 ‘너(플레이어)는 나(컨트롤)에게 이벤트를 전송할 수 있는 신뢰받는 존재야’라는 설정이 필요하다. 아래 그림처럼 Cloudwatch > Events > Event Buses에서 각 플레이어 계정 ID를 넣어 권한을 추가해주자.


[그림] Event Buses설정

멀티 계정 환경에서 AWS Organization이 활성화되어 있다면, 각 플레이어 계정과 컨트롤 계정에서 다음 그림처럼 Type을 Organization으로 변경하여 Organization ID를 지정할 수 있다.

[그림] Add Permission

만약, 그렇지 않다면 컨트롤 계정과 플레이어 계정들(A, B, C, D, E, F…)간의 Event Buses를 연동하기 위해 컨트롤 계정에서 플레이어 계정들(A, B, C, D, E, F…)의 ID를 직접 등록해주어야 한다.

Lambda 트리거 작업

모든 플레이어 계정과 컨트롤 계정 간의 Rules 및 Event Buses가 생성이 완료되었고, Targets에서 Event Bus in another account 설정이 끝났다. 이제 각 멀티 계정(플레이어 계정)들에서 전송받은 이벤트에 대해 컨트롤 계정이 어떤 Action을 취할것인지 구현해야 한다. 이번 장에서는 이벤트들을 Lambda function으로 트리거 하는 작업을 진행해본다. 컨트롤 계정에서 다음처럼 연동된 플레이어 계정들의 이벤트를 수신할 수 있도록 Rules를 다음과 같이 만들어보자.

Cloudwatch > Events > Rules > Step1: Create rule



[그림] 컨트롤 계정 (Create rule)

컨트롤 계정에서는 Targets를 Lambda function으로 지정한다.



[그림] 컨트롤 계정 (Targets)

이 작업 이후부터는 컨트롤 계정에서 Targets으로 트리거된 Lambda function을 어떻게 구현하느냐에 따라 달라진다. Jira API를 이용하여 티켓을 자동으로 생성하여 이력 관리를 함과 동시에 담당자 Assign을 수행 후 메일 알람을 줄 수도 있고, 문제가 되는 AWS Security Group을 제거할 수도 있다. 이같은 코드구현이 힘들다면 cloudcustodian 같은 오픈소스의 도움을 받을 수도 있다. 즉, Security Group 관리 정책을 정의해서 생산, 효율성을 계산해 어느 단계까지 보안 오케스트레이션을 할지는 어떤 Lambda function을 구현하느냐에 따라 달려있다. 이번 글에서 예제로 사용하는 Lambda function(Security-SecurityGroup-AllPort-Alert-To-Slack_py)은 Slack API를 이용하여 Slack 채널에 알람을 준다.

import json
import os
import accountId_list
from botocore.vendored import requests

ENCRYPTED_HOOK_URL = os.environ['kmsEncryptedHookUrl']
# The Slack channel to send a message to stored in the slackChannel environment variable
SLACK_CHANNEL = os.environ['slackChannel']
# Slack Hook URL
HOOK_URL = "https://" + ENCRYPTED_HOOK_URL

def Send_Message(slack_message):
    headers={'Content-Type': 'application/json'}
    req = requests.post(HOOK_URL, data = json.dumps(slack_message), headers=headers)

def event_set(event):
    data = event
    accountId = (data['userIdentity']['accountId'])
    sourceIP = (data['sourceIPAddress'])
    awsRegion = (data['awsRegion'])
    eventTime =(data['eventTime'])
    groupId = (data['requestParameters']['groupId'])
    principalId = (data['userIdentity']['principalId'])
    arn = (data['userIdentity']['arn'])
    port = (data['requestParameters']['ipPermissions']['items'][0]['toPort'])
    protocol = (data['requestParameters']['ipPermissions']['items'][0]['ipProtocol'])
    ipv4 = (data['requestParameters']['ipPermissions']['items'][0]['ipRanges']['items'][0]['cidrIp'])
    ipv6 = (data['requestParameters']['ipPermissions']['items'][0]['ipv6Ranges'])
    
    try:
        description = (data['requestParameters']['ipPermissions']['items'][0]['ipRanges']['items'][0]['description'])
    except KeyError as e:
        description = "None"
        
    IP_Port_Checker(data, arn, principalId, accountId, sourceIP, awsRegion, eventTime, groupId, protocol, port, description, ipv4, ipv6)

def IP_Port_Checker(data, arn, principalId, accountId, sourceIP, awsRegion, eventTime, groupId, protocol, port, description, ipv4, ipv6):
    if 'oneid' in arn.split('/')[1]:
        Username = principalId.split(':')[1]
    else:
        Username = (arn.split('/')[1]).split('@')[0]
        
    notice = ("<@{}>님, {}계정 {}리전의 {}에 대해 삭제 혹은 출발지 지정 바랍니다." .format(Username, accountId_list.accountId_find(accountId), awsRegion, groupId))
    if (port not in [80, 443, 8080]):
        if (ipv4 == '0.0.0.0/0' or ipv6 =='::/0' in ipv4):
            srcip = 'ANY(0.0.0.0/0)'
            Message_data = {
                
                [...slack 전송 메시지 포맷 코드 중략...]
                
            }
            Send_Message(Message_data)
        else:
            pass
    else:
        pass 
def lambda_handler(event, context):
    event_set(event['detail'])

Lambda function 환경변수의 SLACK_CHANNEL과 HOOK_URL로 지정한 채널로 다음과 같이 알람을 줄 수 있다.



[그림] Slack Notice

마치며

이번 글에서 다룬 Security Group의 모니터링 방법은 실수나 의도하지 않은 Security Group 변경에 빠르게 대응할 수 있어 AWS 계정의 보안 수준을 한층 강화할 수 있다. 만약 모니터링 체계가 잡히기 전이거나 막 구축한 상황이라면 적용 전에 위에서 언급한 Trusted Advisor 서비스를 이용하여 기존에 있는 취약한 Security Group에 대해 우선 조치하는 것도 도움이 될 것이다. 또한, 반드시 Any Open(0.0.0.0/0)이 아니어도 네트워크 레벨에서의 이상 트래픽은 유입될 수 있으니 vpc flow 로그를 Cloudwatch 서비스 및 SIEM 솔루션과 연동하여 네트워크 트래픽을 가시화하여 감시하는 등의 추가적인 보안 모니터링 활동이 필요하다.