AWS 보안 설정 가이드: 초기 스타트업이 하루에 잠글 5가지

AWS 보안 설정을 루트 계정, IAM, S3, 보안 그룹, 감사 로그 순으로 정리했다. 소규모 팀이 하루 안에 점검하고 고칠 수 있는 명령어와 수정 전후 예시, 판단 기준을 담았다.

보안 점검 의뢰 팀 16분 분량

보안 담당자가 없는 팀이라면 AWS 보안 설정은 루트 계정 → IAM 권한과 액세스 키 → S3 공개 범위 → 보안 그룹 → 감사 로그와 알림 순서로 고치는 것이 좋다. 앞쪽일수록 한 번 뚫렸을 때 피해 범위가 넓기 때문이다. 루트와 관리자 키는 계정 전체를 넘겨주고, S3와 보안 그룹은 데이터나 서버 하나를 노출하며, 로그는 사고를 막지는 못해도 무슨 일이 있었는지 밝혀 준다.

이 다섯 가지는 하루면 1차 조치를 끝낼 수 있다. 모든 모범 사례를 지키려 들기보다 "인터넷에서 바로 닿는가", "유출되면 어디까지 가는가", "나중에 되돌릴 수 있는가"를 기준으로 위험한 것부터 없애는 편이 현실적이다.

아래 명령어는 읽기 전용 점검부터 시작한다. 변경 작업은 서비스에 영향을 줄 수 있으니 트래픽이 적은 시간에 하고, 가능하면 스테이징에서 먼저 확인한다.

하루 작업 순서

소요 시간은 서버와 버킷이 수십 개 이하인 팀을 가정한 대략적인 기준이다.

순서대상끝난 상태예상 시간
1루트 계정키 0개, MFA 2개 이상, 회사 소유 복구 수단1시간
2IAM관리자 최소화, 안 쓰는 키 제거, CI 키를 OIDC로 교체2~3시간
3S3공개 버킷 목록 정리, 계정 단위 차단1~2시간
4보안 그룹22·3389·DB 포트의 전체 공개 제거1~2시간
5감사 로그멀티 리전 추적, 루트 로그인 알림, 위협 탐지1시간

리전을 여러 개 쓴다면 EC2와 보안 그룹 점검은 리전마다 반복해야 한다. 잊고 있던 리전에 테스트 서버가 남아 있는 경우가 많다.

1단계: 루트 계정 잠그기

AWS는 2025년 6월 멤버 계정까지 포함해 모든 계정 유형의 루트 사용자에 MFA를 요구하도록 확대했다. 그래서 지금 점검할 것은 MFA 유무만이 아니다. 그 MFA와 복구 수단을 누가 쥐고 있는가가 더 중요하다. 초기 스타트업은 창업자 개인 메일과 개인 휴대폰에 묶여 있는 경우가 흔하고, 그 사람이 자리를 비우면 회사가 자기 계정에 들어가지 못할 수 있다.

aws iam get-account-summary \
  --query 'SummaryMap.{MFA:AccountMFAEnabled,RootKeys:AccountAccessKeysPresent}'

MFA는 1, RootKeys는 0이어야 한다. 루트 키가 있다면 사용처를 확인해 IAM 역할로 옮긴 뒤 비활성화하고, 며칠 지켜본 다음 삭제한다.

체크리스트

  • 루트 비밀번호는 고유하고 긴 값으로 바꾸고 팀용 비밀번호 관리 도구에 보관한다.
  • MFA는 패스키나 보안 키를 우선하고 2개 이상 등록한다. 루트 사용자는 최대 8개까지 등록할 수 있다.
  • 루트 이메일을 개인 주소에서 [email protected] 같은 회사 그룹 주소로 바꾼다. 다른 용도로는 쓰지 않는다.
  • 계정의 연락처 전화번호를 최신으로 유지한다. MFA를 모두 잃으면 이메일과 전화번호가 복구 수단이 된다.
  • 가능하면 이메일 수신함과 전화번호를 한 사람이 모두 갖지 않게 나눈다.
  • 일상 업무에는 루트를 쓰지 않는다. 루트 비밀번호를 해당 AWS 계정 위에서 돌아가는 도구에 보관하지 않는다.

2단계: IAM 권한과 액세스 키 줄이기

IAM에서 가장 위험한 조합은 권한이 넓고 수명이 긴 키다. 깃허브 시크릿, 개발자 노트북, 오래된 서버에 흩어진 관리자 키가 대표적이다. 먼저 현황을 뽑는다.

aws iam generate-credential-report
aws iam get-credential-report --query Content --output text | base64 -d > credential-report.csv

CSV에서 password_last_used, mfa_active, access_key_1_last_used_date, access_key_1_last_rotated를 본다. 판단 기준은 다음과 같다.

  • 퇴사자 계정, 몇 달간 쓰지 않은 사용자와 키는 비활성화 후 삭제한다.
  • 콘솔 비밀번호가 있는데 MFA가 없는 사용자는 오늘 MFA를 켠다.
  • 90일이 넘은 키는 교체하거나 역할로 옮길 대상으로 표시한다. AWS Security Hub 통제도 90일을 기준으로 삼는다.
  • AdministratorAccess는 1~2명으로 줄인다.

사람의 접속은 임시 자격 증명으로 옮기는 것이 AWS 권장 방향이다. 계정이 여럿이면 IAM Identity Center를 쓰고, 단일 계정이면 IAM 역할을 쓴다. 오늘 전환이 어렵다면 IAM 사용자는 유지하되 MFA를 켜고 개인 액세스 키부터 없앤다. EC2·ECS·Lambda의 앱에는 키를 넣지 말고 역할을 붙인다.

정책: 와일드카드 줄이기

// 수정 전: 앱이 모든 S3 버킷을 지울 수 있다
{
  "Effect": "Allow",
  "Action": "s3:*",
  "Resource": "*"
}
// 수정 후: 필요한 동작과 경로만 허용한다
{
  "Effect": "Allow",
  "Action": ["s3:GetObject", "s3:PutObject"],
  "Resource": "arn:aws:s3:::example-app-uploads/*"
}

어떤 권한이 필요한지 모르겠다면 IAM Access Analyzer의 정책 생성 기능을 활용한다. CloudTrail 기록을 바탕으로 실제 사용한 동작에 맞춘 정책 초안을 만들어 주므로, 5단계 이후 다시 돌아와 좁혀 가면 된다.

CI: 고정 키를 OIDC로 교체

# 수정 전: 깃허브 시크릿에 장기 키를 저장
- uses: aws-actions/configure-aws-credentials@v4
  with:
    aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
    aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    aws-region: ap-northeast-2
# 수정 후: 실행할 때마다 임시 자격 증명을 받음
permissions:
  id-token: write
  contents: read

steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::111122223333:role/example-deploy
      aws-region: ap-northeast-2

이때 역할의 신뢰 정책에는 sub 조건을 꼭 넣는다. 이 조건이 없거나 넓으면 의도하지 않은 저장소나 브랜치의 워크플로가 역할을 맡을 수 있다.

{
  "Effect": "Allow",
  "Principal": {
    "Federated": "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com"
  },
  "Action": "sts:AssumeRoleWithWebIdentity",
  "Condition": {
    "StringEquals": {
      "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
      "token.actions.githubusercontent.com:sub": "repo:example-org/example-app:ref:refs/heads/main"
    }
  }
}

깃허브 문서에 따르면 2026년 7월 15일 이후 만든 저장소나 옵트인한 저장소는 sub 값에 소유자·저장소 ID가 포함될 수 있다. 위 형식을 그대로 복사하지 말고 실제 토큰 값과 맞춰 본다.

키가 코드나 로그에 남아 있는지는 API 키 유출 방지 체크리스트로 함께 점검한다. 유출이 의심되면 영향 범위를 파악하고 키를 바로 비활성화한다. 삭제보다 비활성화가 먼저인 이유는 앱이 멈췄을 때 되돌릴 수 있어서다. 다만 유출된 키로 이미 발급된 임시 자격 증명은 키 교체만으로 무효화되지 않으므로, CloudTrail에서 해당 키의 활동과 새로 생긴 사용자·역할·키를 함께 확인해야 한다.

3단계: S3 공개 범위 닫기

2023년 4월 이후 새 버킷은 기본적으로 퍼블릭 액세스가 차단되고 ACL이 비활성화된다. 위험한 쪽은 그 전에 만들었거나, 배포를 서두르다 차단을 풀어 둔 버킷이다.

공개가 의도된 버킷을 먼저 찾아야 한다. 그래야 계정 단위 차단을 켰을 때 정적 사이트가 멈추는 일을 피할 수 있다.

aws s3control get-public-access-block --account-id 111122223333

for b in $(aws s3api list-buckets --query 'Buckets[].Name' --output text); do
  status=$(aws s3api get-bucket-policy-status --bucket "$b" \
    --query 'PolicyStatus.IsPublic' --output text 2>/dev/null)
  echo "$b: ${status:-정책 없음}"
done

이 루프는 버킷 정책만 확인한다. ACL을 통한 공개와 외부 계정 공유까지 보려면 콘솔의 IAM Access Analyzer for S3 결과를 함께 본다.

공개로 표시된 버킷은 아래처럼 분류한다.

  • 업로드 파일, 백업, 로그, DB 덤프: 공개할 이유가 없으니 바로 닫는다. 사용자별 파일은 서버가 권한을 확인한 뒤 짧은 만료 시간의 Presigned URL로 내려 준다.
  • 이미지, JS 같은 정적 파일: CloudFront 뒤로 옮기고 Origin Access Control(OAC)로 CloudFront만 읽게 한다.
  • 판단이 안 되는 버킷: 최근 접근 로그와 사용처를 확인한 뒤 닫는다.
// 수정 전: 누구나 모든 객체를 읽을 수 있다
{
  "Effect": "Allow",
  "Principal": "*",
  "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::example-static/*"
}
// 수정 후: 지정한 CloudFront 배포만 읽을 수 있다
{
  "Effect": "Allow",
  "Principal": { "Service": "cloudfront.amazonaws.com" },
  "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::example-static/*",
  "Condition": {
    "StringEquals": {
      "AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/EDFDVBD6EXAMPLE"
    }
  }
}

정리가 끝나면 계정 단위로 네 가지 설정을 모두 켠다.

aws s3control put-public-access-block \
  --account-id 111122223333 \
  --public-access-block-configuration \
  BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

S3는 계정과 버킷 설정 중 더 제한적인 쪽을 적용한다. 따라서 이 설정을 켜면 버킷별로 공개를 다시 열어도 차단이 유지되고, 실수를 계정 수준에서 막을 수 있다. 반대로 이 설정은 기존 정책과 ACL을 지우지 않는다. 나중에 차단을 끄면 공개 상태가 되살아나므로 공개 정책 자체도 함께 삭제한다.

4단계: 보안 그룹과 인스턴스 접근 정리

전체 인터넷에 열린 인바운드 규칙을 뽑는다. IPv6 규칙도 같이 봐야 한다.

aws ec2 describe-security-group-rules \
  --query "SecurityGroupRules[?IsEgress==\`false\` && (CidrIpv4=='0.0.0.0/0' || CidrIpv6=='::/0')].[GroupId,IpProtocol,FromPort,ToPort]" \
  --output table

aws rds describe-db-instances \
  --query "DBInstances[?PubliclyAccessible==\`true\`].DBInstanceIdentifier"

판단 기준은 단순하다.

  • 80, 443: 로드 밸런서라면 공개해도 된다. 앱 서버는 로드 밸런서 보안 그룹에서 오는 트래픽만 받는다.
  • 22, 3389: 전체 공개를 제거한다. Security Hub의 EC2.19 통제도 이런 고위험 포트의 전체 공개를 Critical로 분류한다.
  • 3306, 5432, 6379, 9200 같은 데이터 저장소 포트: 앱 서버 보안 그룹에서만 받는다.
  • 프로토콜 전체 허용(-1): 사유를 확인할 수 없으면 제거 대상이다. 지우기 전에 실제 사용 포트를 확인한다.
# 수정 전: DB가 인터넷 전체에 열려 있다
resource "aws_vpc_security_group_ingress_rule" "db" {
  security_group_id = aws_security_group.db.id
  cidr_ipv4         = "0.0.0.0/0"
  from_port         = 5432
  to_port           = 5432
  ip_protocol       = "tcp"
}
# 수정 후: 앱 서버 보안 그룹에서 오는 연결만 허용한다
resource "aws_vpc_security_group_ingress_rule" "db" {
  security_group_id            = aws_security_group.db.id
  referenced_security_group_id = aws_security_group.app.id
  from_port                    = 5432
  to_port                      = 5432
  ip_protocol                  = "tcp"
}

서버 접속은 Session Manager로 옮기는 것을 권장한다. 인바운드 포트, 배스천 호스트, SSH 키 관리가 필요 없고, 인스턴스에 프로파일을 붙이고 Systems Manager 엔드포인트로 나가는 경로만 있으면 된다. 작업이 끝나면 22번 규칙을 지운다. 당장 SSH가 꼭 필요하다면 사무실 IP 등 특정 대역으로만 좁힌다. 먼저 Session Manager로 접속되는지 확인하고 규칙을 지워야 스스로 잠기지 않는다.

EC2를 쓴다면 IMDSv2 강제도 같이 처리한다. 메타데이터 서비스의 역할 자격 증명이 SSRF 같은 취약점을 통해 탈취되는 위험을 줄이는 조치다. 다만 계정 기본값은 기존 인스턴스에 적용되지 않으므로 인스턴스별로 바꿔야 하고, 오래된 SDK가 IMDSv1에 의존하지 않는지 먼저 확인한다.

aws ec2 modify-instance-metadata-options \
  --instance-id i-0123456789abcdef0 \
  --http-tokens required --http-endpoint enabled

# 이 리전의 새 인스턴스 기본값 (컨테이너 환경이면 hop limit 2)
aws ec2 modify-instance-metadata-defaults \
  --http-tokens required --http-put-response-hop-limit 2

5단계: 감사 로그와 알림 켜기

CloudTrail의 이벤트 기록은 기본으로 켜져 있지만 관리 이벤트 90일분만, 리전별로 보여 준다. 사고는 한참 뒤에 발견되기도 하므로 추적(trail)을 만들어 S3에 따로 쌓아야 한다.

콘솔에서 만든 추적은 멀티 리전이고 로그 파일 검증도 기본으로 켜진다. 관리 이벤트 첫 사본은 CloudTrail 요금이 없고 S3 저장 비용만 든다. 추가 사본은 과금되니 이미 추적이 있는지부터 확인한다.

aws cloudtrail describe-trails \
  --query 'trailList[].{Name:Name,MultiRegion:IsMultiRegionTrail,Validation:LogFileValidationEnabled}'
aws cloudtrail get-trail-status --name example-audit-trail

설정 체크리스트

  • 로그 버킷은 앱 버킷과 분리하고 퍼블릭 액세스를 차단한다.
  • 버킷 정책에서 불필요한 접근을 제거하고, CloudTrail 쓰기 권한에 aws:SourceArn 조건을 건다.
  • 삭제 권한은 최소화하고, 필요하면 MFA Delete로 실수나 고의 삭제를 막는다.
  • 보관 기간은 S3 수명 주기 규칙으로 관리한다.
  • 개인정보가 담긴 버킷은 데이터 이벤트 기록을 검토한다. 관리 이벤트만으로는 객체를 누가 읽었는지 남지 않는다. 데이터 이벤트는 건수만큼 과금되므로 대상 버킷만 지정한다.

로그는 아무도 보지 않으면 의미가 없다. 소규모 팀은 아래 세 가지만 메신저나 공용 메일로 받아도 충분히 시작할 수 있다.

{
  "detail-type": ["AWS Console Sign In via CloudTrail"],
  "detail": { "userIdentity": { "type": ["Root"] } }
}
  1. 루트 로그인 알림: 위 패턴으로 EventBridge 규칙을 만들고 SNS로 보낸다. 규칙을 만든 뒤 실제 로그인으로 알림이 오는지 시험한다. 이벤트가 기록되는 리전이 예상과 다를 수 있다.
  2. GuardDuty: 새 계정은 리전별 30일 무료 체험이 있고, 이후 사용량 과금이 시작된다. 체험 기간의 탐지 결과와 비용 추정치를 보고 유지를 결정한다.
  3. 비용 알림: AWS Budgets 알림을 걸어 둔다. 탈취된 키로 자원이 생성되면 청구 금액 급증이 먼저 눈에 띄기도 한다.

CloudTrail은 AWS 제어 영역의 기록이다. 애플리케이션 관리자 화면에서 누가 어떤 고객 정보를 조회했는지는 남지 않으므로, 개인정보 접속기록은 앱 로그에서 따로 설계해야 한다. 법적 요구 사항은 개인정보 안전성 확보조치 기준 개발자 가이드를 참고한다.

자주 묻는 질문

계정 하나로 운영해도 되나요?

처음에는 가능하지만 운영 데이터가 생기면 개발과 운영 계정을 나누는 것이 좋다. 개발 환경의 키가 유출돼도 운영으로 번지지 않기 때문이다. 다만 하루 작업의 범위는 아니므로 이 글의 다섯 단계를 먼저 끝내고, 이후 AWS Organizations 도입과 함께 진행한다.

비용이 많이 드나요?

이 글의 대부분은 설정 변경이라 비용이 들지 않는다. 비용이 생길 수 있는 항목은 CloudTrail 로그 저장, 데이터 이벤트, GuardDuty 정도다. 데이터 이벤트와 GuardDuty만 사용량을 보고 범위를 정하면 된다.

스캐너나 Security Hub를 먼저 켜는 편이 낫지 않나요?

수십 개의 결과가 쏟아지면 소규모 팀은 우선순위를 잡기 어렵다. 이 다섯 단계를 끝낸 뒤 켜면 남은 항목을 차분히 처리할 수 있고, 조치 결과를 확인하는 수단으로도 쓸 수 있다.

정리

AWS 보안 설정은 피해 범위가 큰 순서로 처리한다. 루트 계정의 소유권을 회사로 돌리고, 장기 키를 없애고, 공개 버킷과 열린 포트를 닫은 뒤, 로그와 알림을 켠다.

오늘 끝낼 목표는 완벽한 최소 권한이 아니라 치명적인 경로를 없애는 것이다. 정책 세분화는 CloudTrail 기록이 쌓인 뒤 분기마다 조금씩 좁혀 가면 된다.

직접 점검이 어렵다면 무료 분석 신청으로 현재 상태를 확인받아 볼 수 있다.

참고 자료

함께 읽으면 좋은 글