← 아티클 목록

시리즈AWS 실습 계정에서 흔적을 남기고 되짚기3편 · 공개 4편

Cloud · DevSecOps

내가 바꾼 권한을 CloudTrail에서 다시 찾아봤다

실습 계정에서 IAM 사용자를 만들고 정책을 바꾼 뒤, 그 호출을 조사자 입장에서 찾아봤다. 서울 리전에서는 아무것도 나오지 않았고, 그 이유를 찾는 것이 이 글의 대부분이다.

목차 10

2026-08-09 업데이트 — CloudTrail 이벤트 기록의 계정·리전 범위를 명확히 하고, IAM 실습의 us-east-1 결과를 모든 권한 변경에 일반화한 설명을 바로잡았다. 시간 범위 조회에는 AWS CLI 페이지네이션과 이어서 조회하는 방법을 보완했다.

내가 만든 이벤트를 로그에서 찾을 수 있는가

침해가 의심될 때 CloudTrail에서 「누가 무엇을 했는지」를 찾는다고들 한다. 그런데 필드 목록을 외우는 것과 실제로 찾아내는 것은 다르다.

그래서 순서를 바꿨다. 내가 권한을 바꾸고, 그 호출을 조사자 입장에서 찾아본다. 답은 이미 알고 있으니 로그가 그 답을 어떻게 담고 있는지만 보면 된다.

앞선 IAM 글에서 실습 사용자를 만들고 정책을 여러 번 바꿨다. 그 호출들이 이 글의 조사 대상이다.

시각(KST) 한 일 예상 이벤트
21:03 실습 사용자 생성 CreateUser
21:06~21:20 인라인 정책 교체 (6회) PutUserPolicy
21:18 실습 버킷 생성 CreateBucket
21:55~22:15 정책 시뮬레이션 SimulatePrincipalPolicy

CloudTrail이 기록하는 것과 판단하지 않는 것

CloudTrail은 계정에서 발생한 API 호출을 기록하는 감사 로그다. 판단하는 서비스가 아니라 증거의 원천이라는 점이 다른 서비스와 다르다.

서비스 하는 일
CloudTrail 누가 어떤 API를 호출했는지 기록한다. 판단은 하지 않는다
GuardDuty CloudTrail·VPC Flow Logs·DNS 로그를 분석해 위협 Finding을 만든다
CloudWatch 메트릭·로그 기반 운영 알람과 로그 검색

클라우드에서 이 로그가 특히 중요한 이유는, API 호출 하나가 인프라 자원을 직접 생성·변경·삭제하기 때문이다. 온프레미스에서 서버에 들어가 설정 파일을 고치는 일이 여기서는 한 줄의 호출로 끝나며, CloudTrail은 그 호출을 되짚는 주요 감사 근거다.

이벤트 기록에는 최근 90일치 관리 이벤트가 남는다. 다만 검색 범위는 단일 AWS 계정·단일 리전이고, 속성 조건도 한 번에 하나만 쓸 수 있다. 이 글은 한 실습 계정의 ap-northeast-2와 us-east-1을 각각 조회했으며, Trail은 따로 만들지 않았다. 여러 계정·리전을 한 번에 모아 본 결과가 아니다.

1. 이름을 알고 찾을 때

CreateBucket부터 찾아봤다.

bash
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=CreateBucket \
  --max-items 8 \
  --query 'Events[].[EventTime,EventName,Username]' --output table
|  2026-08-02T21:18:00+09:00 |  CreateBucket  |  lab-user |
|  2026-07-25T22:02:53+09:00 |  CreateBucket  |  root     |
|  2026-07-25T21:54:58+09:00 |  CreateBucket  |  root     |

21:18에 만든 버킷이 그대로 나온다. 이름을 알고 찾을 때는 이 방법이 빠르다.

같은 방식으로 CreateUser와 PutUserPolicy를 찾았다.

=== CreateUser ===
=== PutUserPolicy ===

아무것도 나오지 않았다. 방금 만든 사용자인데 기록이 없다.

2. 왜 같은 계정에서 어떤 이벤트는 보이고 어떤 이벤트는 안 보이는가

CreateBucket은 나오고 CreateUser는 안 나온다. 둘 다 같은 자격 증명으로, 15분 사이에 호출한 것이다.

차이는 이 실습에서 호출한 IAM 이벤트의 리전이다. CreateUser와 PutUserPolicy 레코드의 awsRegion은 us-east-1이었는데, 위 명령은 프로필의 기본 리전인 ap-northeast-2를 조회했다. 이벤트 기록과 lookup-events는 리전별 조회이므로 같은 계정이어도 다른 리전의 결과를 합쳐 주지 않는다.

bash
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=PutUserPolicy \
  --region us-east-1 \
  --query 'Events[].[EventTime,EventName,Username]' --output table
|  2026-08-02T21:20:11+09:00 |  PutUserPolicy  |  lab-user |
|  2026-08-02T21:20:07+09:00 |  PutUserPolicy  |  lab-user |
|  2026-08-02T21:18:30+09:00 |  PutUserPolicy  |  lab-user |
|  2026-08-02T21:18:13+09:00 |  PutUserPolicy  |  lab-user |
|  2026-08-02T21:07:15+09:00 |  PutUserPolicy  |  lab-user |
|  2026-08-02T21:06:55+09:00 |  PutUserPolicy  |  lab-user |

여섯 번의 정책 교체가 전부 있다.

콘솔에서도 같은 일이 일어난다. 리전을 서울로 둔 채 CreateUser를 검색하면 이렇게 나온다.

서울 리전에서 CreateUser를 검색해 결과가 없는 CloudTrail 이벤트 기록 화면 아시아 태평양(서울) — 「일치하는 항목 없음」

리전만 버지니아 북부로 바꾸면 이렇게 된다.

버지니아 북부 리전에서 같은 검색을 해 결과가 나온 화면 미국 동부(버지니아 북부) — 같은 검색어, 4건

검색 조건은 한 글자도 바꾸지 않았다. 화면 오른쪽 위의 리전만 다르다.

조사에서 이게 왜 문제인지는 분명하다. 「이 범위에 기록이 없다」와 「내가 다른 리전을 봤다」가 같은 빈 화면으로 보인다. 조회 범위를 확인하지 않으면 빈 결과의 이유를 화면만으로 구분할 수 없다.

이 결과를 모든 권한 변경은 us-east-1에 있다고 일반화하면 안 된다. 이 글의 IAM 호출은 그곳에서 확인됐지만, S3 버킷 정책처럼 리전 리소스의 권한 변경은 해당 리전에서 찾는다. STS도 리전별 엔드포인트를 쓰면 그 리전에서 발생한 이벤트로 조회된다. 글로벌 서비스 이벤트도 일부는 다른 리전에 기록될 수 있으므로, 먼저 대상 서비스와 호출이 발생한 리전을 정하고 필요한 리전을 각각 확인해야 한다.

3. 이벤트 하나에 무엇이 들어 있는가

PutUserPolicy 하나를 펼쳐 봤다.

JSON
{
  "eventTime": "2026-08-02T12:20:11Z",
  "eventSource": "iam.amazonaws.com",
  "eventName": "PutUserPolicy",
  "awsRegion": "us-east-1",
  "sourceIPAddress": "203.0.113.10",
  "userAgent": "aws-cli/2.36.2 ... md/command#iam.put-user-policy",
  "userIdentity": {
    "type": "IAMUser",
    "principalId": "AIDAEXAMPLEEXAMPLE",
    "arn": "arn:aws:iam::123456789012:user/lab-user",
    "accountId": "123456789012",
    "accessKeyId": "AKIAIOSFODNN7EXAMPLE",
    "userName": "lab-user"
  },
  "requestParameters": {
    "userName": "lab-lp",
    "policyName": "lab",
    "policyDocument": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":[\"s3:ListBucket\"],\"Resource\":\"arn:aws:s3:::example-bucket\"}, ...]}"
  }
}

세 가지가 눈에 띈다.

이 이벤트의 requestParameters에는 정책 본문이 들어 있었다. 따라서 이 관찰 건은 로그에서 그 시점에 준 권한을 복원할 수 있고, 정책을 나중에 지워도 당시 변경 사실과 내용이 이벤트 기록에 남는다. 다만 CloudTrail의 requestParameters는 100KB를 넘으면 생략될 수 있으므로, 모든 호출에서 정책 전문이 항상 보장된다는 뜻은 아니다.

userAgent는 호출 도구를 추정하는 단서다. 이번에 비교한 두 이벤트에서는 CLI 문자열과 브라우저 문자열이 다음처럼 달랐다.

CLI  : aws-cli/2.36.2 ... md/command#iam.put-user-policy
콘솔 : Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/151.0.0.0

하지만 이 필드 하나만으로 「사람이 콘솔에서 눌렀는가, 자동화가 실행했는가」를 확정할 수는 없다. AWS 서비스·SDK·프록시도 각자의 값으로 남을 수 있고, 콘솔 세션에서 나온 자격 증명을 외부 클라이언트가 쓴 경우에는 sessionCredentialFromConsole 같은 추가 필드가 단서가 된다. userIdentity, sourceIPAddress, 세션 정보와 함께 읽어야 한다.

accessKeyId가 남는다. 어떤 키로 들어왔는지가 기록되므로, 유출이 의심되는 키가 있으면 그 키로 무엇을 했는지 되짚을 수 있다.

콘솔의 이벤트 상세 화면은 같은 내용을 요약해 보여 준다.

CloudTrail 이벤트 상세 화면의 세부 정보 패널 이벤트 상세 — 사용자 이름·액세스 키·소스 IP·리전이 한 화면에. 액세스 키와 소스 IP는 가렸다

AWS 리전에 us-east-1이 박혀 있는 것이 2번의 답이기도 하다. 그리고 읽기 전용 필드가 있다 — 이 값으로 조회성 호출과 변경성 호출을 갈라 볼 수 있다.

4. 이벤트가 보이기까지 걸리는 시간

소급해서는 잴 수 없으니 새 호출을 하나 던지고 시각을 적었다.

호출         13:23:10Z   (aws iam get-user)
조회 1회차   13:23:43Z   → 없음
조회 2회차   13:31:35Z   → 있음 (eventTime 13:23:12Z)

33초 뒤에는 없었고 8분 25초 뒤에는 있었다. 그 사이 어디쯤인지는 이번 실습으로 좁히지 못했다.

값보다 성질이 중요하다. 방금 일어난 일은 이벤트 기록에 아직 없을 수 있다. 사고 대응에서 「로그에 없으니 아직 안 일어났다」로 읽으면 안 된다.

시간 범위 조회는 첫 40건만 보고 결론 내릴 수 없다

실제 조사는 무엇이 호출됐는지 모르는 상태에서 시작한다. 그럴 때는 이름이 아니라 시간으로 훑는다.

초판에는 --max-results 40으로 적었지만, 이는 LookupEvents API의 MaxResults와 AWS CLI 옵션을 섞은 표기였다. AWS CLI에서 전체 출력 수를 제한하는 옵션은 --max-items다. 당시처럼 첫 40건만 확인하려면 다음 형태가 맞다.

bash
aws cloudtrail lookup-events \
  --region ap-northeast-2 \
  --start-time 2026-08-02T10:20:00Z --end-time 2026-08-02T13:20:00Z \
  --max-items 40 \
  --output json

당시 기록에 남은 첫 40건은 이렇게 채워졌다.

|  2026-08-02T22:19:22+09:00 |  Decrypt            |  app-observer  |
|  2026-08-02T22:19:21+09:00 |  GetCallerIdentity  |  app-observer  |
|  2026-08-02T22:19:21+09:00 |  AssumeRole         |  None          |
|  2026-08-02T22:14:22+09:00 |  Decrypt            |  app-observer  |
|  ... 5분 간격으로 계속                                            |

5분 주기로 도는 워크로드가 AssumeRole → GetCallerIdentity → Decrypt 세 건씩 남겼고, 확인한 첫 40건에는 실습 이벤트가 없었다. 여기서 말할 수 있는 것은 첫 구간이 주기 작업으로 채워졌다는 것까지다. 더 오래된 다음 페이지에도 없었다거나 시간 범위 조회 자체가 통하지 않는다는 증거는 아니다.

AWS CLI는 --no-paginate를 주지 않으면 여러 서비스 페이지를 자동으로 따라가 전체 결과를 모은다. 반대로 --max-items 40처럼 전체 출력을 의도적으로 자르면 루트 응답에 CLI용 NextToken이 나오고, 같은 조건에 --starting-token으로 넘겨 이어서 조회한다.

bash
aws cloudtrail lookup-events \
  --region ap-northeast-2 \
  --start-time 2026-08-02T10:20:00Z --end-time 2026-08-02T13:20:00Z \
  --max-items 40 --starting-token '<이전 응답의 NextToken>' \
  --output json

--page-size 40은 서비스 호출 한 번의 페이지 크기만 바꾸며 최종 출력 수를 40건으로 제한하지 않는다. 이 계정처럼 주기 이벤트가 많은 곳에서는 이름이나 주체로 후보를 줄이면 검토가 빨라지지만, 시간 범위로 확인할 때도 필요한 페이지를 끝까지 봐야 한다. 이벤트 기록은 속성 하나와 시간 범위만 결합할 수 있으므로 여러 속성·리전·계정을 함께 질의해야 한다면 CloudTrail Lake 같은 별도 조회 수단이 필요하다.

권한 변경이 의심될 때 던지는 질문

위 실습은 답을 알고 한 조사였다. 답을 모르는 상황이라면 다음 순서로 묻는다.

  1. 어느 계정·리전을 보고 있는가 — 대상 서비스와 리소스의 리전을 먼저 정하고, 필요한 리전을 각각 조회한다
  2. 권한을 바꾸는 호출(PutUserPolicy, AttachUserPolicy, CreatePolicyVersion, PutBucketPolicy)이 언제 있었는가
  3. 그 전후에 AssumeRole 같은 역할 사용 호출이 있었는가 — 이는 권한 변경이 아니라 임시 자격 증명을 얻는 접근 경로다
  4. 그 호출의 userIdentity는 누구이고, userAgent와 다른 필드는 어떤 도구·세션을 가리키는가
  5. requestParameters에 무엇을 줬는가
  6. 그 주체에게 권한을 바꿀 권한이 왜 있었는가

6번이 나머지와 성격이 다르다. 1~5는 로그를 찾는 질문이지만 6번은 권한 설계를 묻는 질문이고, 대개 이 답이 재발 여부를 결정한다.

다시 확인하는 방법

bash
# 1. 리전을 명시한다 — 이 IAM 실습 호출은 us-east-1에서 확인했다
aws cloudtrail lookup-events --region us-east-1 \
  --lookup-attributes AttributeKey=EventName,AttributeValue=PutUserPolicy

# 2. 주체로
aws cloudtrail lookup-events --region us-east-1 \
  --lookup-attributes AttributeKey=Username,AttributeValue=lab-user

# 3. 이벤트 하나를 통째로 펼친다
aws cloudtrail lookup-events --region us-east-1 \
  --lookup-attributes AttributeKey=EventName,AttributeValue=PutUserPolicy \
  --max-items 1 --query 'Events[0].CloudTrailEvent' --output text

우선 확인 대상으로 두는 변경 이벤트는 CreateUser, PutUserPolicy, AttachUserPolicy, CreatePolicyVersion, PutBucketPolicy다. 리전 리소스의 호출은 해당 리전에서 찾는다. AssumeRole은 권한을 바꾸지 않고 역할의 임시 보안 자격 증명을 받는 호출이므로, 변경 이벤트와 분리해 누가 어떤 역할을 사용했는지 추적한다.

이번에는 Trail을 만들지 않았으므로 90일이 지난 뒤의 보관과 조직 단위 Trail의 동작은 확인하지 못했다. 객체 단위 GetObject 같은 데이터 이벤트는 기본으로 기록되지 않아서, 권한이 바뀐 뒤 실제로 데이터를 읽어 갔는지는 이 로그만으로 알 수 없다. 그쪽을 보려면 데이터 이벤트를 따로 켜야 한다.

참고 자료


출력의 계정 ID·액세스 키·소스 IP·버킷 이름과 운영 워크로드의 역할 이름은 예시 값으로 치환했다.