← 아티클 목록

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

Cloud · DevSecOps

버킷이 공개인지는 버킷 정책만 봐서는 알 수 없다

새로 만든 S3 버킷의 기본 설정과 버킷 수준 Public Access Block의 동작을 실습 계정에서 직접 확인했다. 공개 정책의 저장 거부와 S3의 public-policy 판정이 고정된 신뢰 값으로 접근 범위를 제한하는 방식을 조건별 정책으로 비교한 기록이다.

목차 10

2026-08-09 정정 — file:// 실행 실패를 인코딩 문제로 설명한 부분을 바로잡았다. 요청은 S3까지 도달했으며 BlockPublicPolicy가 공개 정책 저장을 거부한 것이 원인이었다. 버킷 이름 치환과 IsPublic·Public Access Block의 판단 범위도 보완했다.

2026-09-25 보완 — 실습 후 버킷 정책과 Public Access Block을 초기 상태로 되돌리는 절차를 추가했다. 복원 명령은 공식 문서로 확인한 안내이며 이번에 AWS에서 실행한 결과는 아니다.

S3 버킷이 공개되었는지 어디서 확인해야 할까

S3 버킷이 공개인지 아닌지를 판단해야 할 때, 어디를 봐야 답이 나오는가.

버킷 정책 하나만 열어 보면 될 것 같지만 그렇지 않다. 이 글에서는 직접 버킷·객체에 접근하는 실습에서 확인한 IAM 정책, 버킷 정책, ACL/Object Ownership, 버킷 수준 Public Access Block을 중심으로 본다. 실제 유효 권한에는 조직·계정·access point 수준의 통제, VPC endpoint 정책, KMS 키 정책 같은 다른 요소도 참여할 수 있다.

이 실습에서 확인한 네 가지 설정

Amazon S3는 객체 단위로 데이터를 저장하는 서비스다. 이 실습에서 사용한 일반 목적 버킷 이름은 해당 AWS partition의 모든 계정·리전이 공유하는 namespace 안에서 고유해야 한다. 다른 버킷 유형과 namespace까지 모두 같은 규칙이라는 뜻은 아니다.

이 실습의 직접 접근을 읽을 때는 다음 네 항목을 함께 확인했다. 이것이 S3의 모든 권한 평가 요소를 빠짐없이 열거한 목록은 아니다.

설정 역할
IAM 정책 이 계정의 주체가 무엇을 할 수 있는가
Bucket Policy 이 버킷에 누가 접근할 수 있는가 (Effect·Principal·Action·Resource·Condition)
ACL / Object Ownership 객체 단위 개별 권한. Object Ownership 설정에 따라 아예 비활성화될 수 있다
Public Access Block 조직·계정·버킷·access point 수준의 설정 중 가장 제한적인 조합을 적용해 정책·ACL이 만드는 공개 액세스를 설정별 방식으로 차단하거나 제한한다 (BlockPublicAcls·IgnorePublicAcls·BlockPublicPolicy·RestrictPublicBuckets)

Pre-Signed URL은 이 네 가지와 성격이 다르다. 정책이 아니라 URL을 만든 IAM 주체의 권한을 빌려 쓰는 방식이라, 만료 시간과 허용 동작을 URL 자체에 새겨서 내보낸다.

여기까지가 문서로 확인한 범위다. 아래부터가 실제 확인한 결과다.

1. 새 버킷의 기본값은 무엇인가

「최신 버킷은 BucketOwnerEnforced가 기본」이라고 알고 있었는데, 그 「기본」을 직접 확인한 적은 없었다. 버킷을 만들고 아무 설정도 건드리지 않은 채 세 가지를 조회했다.

PowerShell
# 1. 버킷 생성 (서울 리전: ap-northeast-2)
aws s3api create-bucket `
    --bucket <이름> `
    --region ap-northeast-2 `
    --create-bucket-configuration LocationConstraint=ap-northeast-2

# 2. 퍼블릭 액세스 차단 설정 조회
aws s3api get-public-access-block --bucket <이름>

# 3. 버킷 소유권 설정 조회
aws s3api get-bucket-ownership-controls --bucket <이름>

# 4. 버킷 정책 상태 조회
aws s3api get-bucket-policy-status --bucket <이름>

버킷 생성 명령 실행 결과 버킷 생성

퍼블릭 액세스 차단 설정 조회 결과 퍼블릭 액세스 차단 설정

버킷 소유권 설정 조회 결과 Object Ownership

정책이 없는 버킷의 정책 상태 조회 결과 버킷 정책 상태 — 정책이 없을 때

콘솔의 버킷 권한 탭 콘솔 — 버킷 권한 탭

새 버킷은 퍼블릭 액세스 차단이 적용된 상태였고, Object Ownership은 BucketOwnerEnforced였다. 즉 ACL이 비활성화된 채로 시작한다.

정책이 없는 버킷에 get-bucket-policy-status를 실행하면 값이 아니라 NoSuchBucketPolicy 오류가 돌아온다. 이 오류 자체가 「붙어 있는 정책이 없다」는 신호다.

2. 공개 여부를 확인할 기준 객체 만들기

어떤 업무에서도 읽을 이유가 없는 객체를 하나 둔다. 뒤의 확인과, 이어서 쓸 IAM 최소 권한 글의 Negative 테스트가 같은 객체를 대상으로 삼는다.

PowerShell
# 1. canary.txt 파일 생성
Set-Content -Path canary.txt -Value "canary" -Encoding utf8

# 2. S3 버킷으로 파일 업로드
aws s3api put-object --bucket $bucketName --key canary.txt --body canary.txt

canary.txt 업로드 결과 canary.txt 업로드

3. Public Access Block은 공개 정책 저장을 막는가

「Public Access Block이 공개를 막는다」는 문장은 여러 곳에서 읽었지만, 막는다가 정확히 무엇인지는 확인하지 않았다. 정책 저장 자체가 거부되는가, 저장은 되고 접근만 막히는가.

public.json:

JSON
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": "*",
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::example-bucket/*"
  }]
}

여기와 아래 정책 파일의 example-bucket은 공개용으로 바꾼 자리표시자다. 실행할 때는 JSON 안의 값을 실제 $bucketName 값으로 직접 바꿔야 한다. file://는 파일 내용을 그대로 읽을 뿐, JSON 안의 $bucketName 같은 PowerShell 변수를 확장하지 않는다.

PowerShell
aws s3api put-bucket-policy `
--bucket $bucketName `
--policy file://public.json

실제 캡처의 명령은 file://public.json이다. 응답도 로컬 JSON 파싱 오류가 아니라 S3의 PutBucketPolicy AccessDenied이며, 메시지는 BlockPublicPolicy를 원인으로 적는다. 따라서 이 실행에서 직접 확인한 것은 AWS CLI가 파일을 읽어 요청을 보냈고, S3가 공개 정책으로 판정해 거부했다는 것까지다. 인코딩 실패나 fileb:// 우회는 이 캡처의 결과가 아니다.

AWS CLI put-bucket-policy 참조에서 --policy는 문자열이고 공식 예제도 file://policy.json을 쓴다. file://와 fileb:// 안내에 따르면 file://는 텍스트 파일의 내용을 읽고, fileb://는 바이너리 입력 파라미터를 위해 파일을 바이너리로 읽는 접두사다. 이 정책 JSON 실습에는 캡처와 문서에 맞춰 file://를 사용한다.

CLI에서 공개 정책 저장을 시도해 실패한 결과 CLI — 공개 정책 저장 시도

콘솔에서 같은 정책을 저장해 실패한 화면 콘솔 — 같은 정책 저장 시도

BlockPublicPolicy가 켜져 있으면 공개 액세스를 허용하는 새 버킷 정책은 PutBucketPolicy 단계에서 거부된다. 위 캡처가 보여 주는 것도 이 동작이다. 콘솔에서 같은 정책을 저장할 때도 오류가 났다.

이 절에서 확인한 “막는다”는 BlockPublicPolicy의 정책 저장 거부다. Public Access Block의 네 설정이 모두 같은 단계에서 같은 방식으로 거부한다는 뜻은 아니다.

4. IsPublic은 언제 true가 되는가

아래처럼 접근 설정을 완화하는 비교는 새로 만든 실습 전용 버킷에서만 한다. 변경 전에 버킷 이름과 정책·PAB 조회 결과를 보관해 복원할 상태를 확인한다. 이 글의 초기 상태는 정책 없음·PAB 네 항목 모두 true다. 실습을 중단하거나 마치면 접근 설정 복원 절차로 돌아간다.

PowerShell
# 1. 버킷 수준 퍼블릭 액세스 차단(PAB) 해제
aws s3api delete-public-access-block --bucket $bucketName

# 2. 정책 없이 PAB만 껐을 때 정책 상태 조회
aws s3api get-bucket-policy-status --bucket $bucketName

# 3. 공개 정책 적용
aws s3api put-bucket-policy --bucket $bucketName --policy file://public.json

# 4. 공개 정책 적용 후 정책 상태 조회
aws s3api get-bucket-policy-status --bucket $bucketName

정책 없이 PAB만 해제한 뒤 정책 상태 조회 결과 정책 없이 PAB만 껐을 때

공개 정책 적용 결과 공개 정책 적용

공개 정책 적용 후 정책 상태 조회 결과 공개 정책 적용 후 정책 상태

공개 정책을 적용한 뒤 IsPublic이 true로 바뀌었다.

여기까지만 보면 「정책이 있으면 공개」로 읽힌다.

5. HTTPS 조건만으로는 공개 범위가 줄어들지 않는다

Principal: "*" 에 Condition이 붙어 있으면 「제한된 정책」으로 읽기 쉽다. 하지만 조건마다 제한하는 축이 다르다.

aws:SecureTransport는 전송 구간이 HTTPS인지만 본다. 누가 접근하는지는 건드리지 않는다. 그래서 이 조건이 붙은 Principal: "*" 정책은 「HTTPS를 쓰는 모든 인터넷 사용자에게 허용」이고, 여전히 공개 정책이다.

이 실습에서 비교한 예시를 공개 범위를 줄이는 축과 구분하면 이렇게 된다. S3가 신뢰 값으로 인정하는 조건 키 전체를 열거한 표는 아니다.

구분 정책 공개 범위
인터넷 공개 Principal: "*" 누구나
인터넷 공개 (전송만 제한) Principal: "*" + aws:SecureTransport 누구나 (HTTPS로)
조직 내부 Principal: "*" + aws:PrincipalOrgID 조직 내부
교차 계정 특정 외부 계정·역할 ARN 그 계정만
계정 내부 로컬 IAM 주체 계정 내부

SecureTransport.json:

JSON
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::example-bucket/*",
      "Condition": {
        "Bool": {
          "aws:SecureTransport": "true"
        }
      }
    }
  ]
}

HTTPS 조건만 붙인 정책의 정책 상태 조회 결과 HTTPS만 조건으로 걸었을 때

PrincipalOrgID.json:

JSON
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::example-bucket/*",
      "Condition": {
        "StringEquals": {
          "aws:PrincipalOrgID": "o-1234567890"
        },
        "Bool": {
          "aws:SecureTransport": "true"
        }
      }
    }
  ]
}

조직 조건을 붙인 정책의 정책 상태 조회 결과 조직 구성원으로 제한했을 때

두 정책 모두 버킷에 붙어 있고 Principal도 "*"다. 그런데 IsPublic은 갈렸다. aws:SecureTransport만 붙은 정책은 true, aws:PrincipalOrgID로 좁힌 정책은 false였다.

그래서 앞 절의 「정책이 있으면 공개」는 정확하지 않다. S3의 public-policy 판정은 정책을 먼저 공개로 가정한 뒤, 고정된 Principal이나 aws:SourceIp, aws:SourceVpc, aws:SourceVpce, aws:SourceArn, aws:SourceAccount, aws:PrincipalOrgID 같은 허용된 조건의 고정된 신뢰 값으로 접근이 제한되는지를 분석한다. 이 목록에도 키별 값 형식과 예외가 있으므로 단순히 Condition이 있다는 사실만으로 비공개가 되지는 않는다.

이 실습에서 4번의 무조건 Principal: "*" 정책과 HTTPS 조건만 둔 정책은 신뢰 대상을 좁히지 않아 true였고, 고정된 조직 ID 조건은 S3가 신뢰 경계로 인정해 false였다. 즉 여기서 확인한 차이는 Principal 하나가 아니라 Principal과 신뢰 조건의 조합이다.

HTTPS 조건은 접근 주체를 좁히지 못한다. AWS도 그 정책을 공개로 판정한다.

한 가지 더 확인된 것이 있다. 위 정책에 넣은 조직 ID o-1234567890은 실제로 존재하지 않는 더미 값이다. 그런데도 IsPublic은 false가 됐다. 이 관찰에서 S3는 문서의 형식에 맞는 고정된 조직 ID를 신뢰 값으로 분류했지만, 상태값만으로 그 조직의 실재 여부나 현재 구성원을 검증하지는 못한다.

그래서 false를 곧 「안전하다」로 읽을 수는 없다. 조건이 가리키는 대상이 실제로 맞는지는 사람이 확인해야 한다.

버킷 공개 여부를 확인하는 순서

PowerShell
# 1. 버킷 정책의 public-policy 분류를 본다 (IsPublic)
aws s3api get-bucket-policy-status --bucket $bucketName

# 2. 정책을 바꾸지 않고 공개 요청을 막거나 제한할 수 있는 버킷 수준 PAB 상태
aws s3api get-public-access-block --bucket $bucketName

# 3. ACL이 평가에 참여하는 버킷인지 (버킷 소유권 설정)
aws s3api get-bucket-ownership-controls --bucket $bucketName

# 4. 정책 본문 — Principal과 Condition을 함께 읽는다
aws s3api get-bucket-policy --bucket $bucketName

IsPublic은 버킷 정책의 공개 여부를 분류하는 값이고, Public Access Block은 정책이나 ACL을 반드시 지우지 않은 채 새 공개 설정을 거부하거나 기존 공개 권한을 무시·제한할 수 있다. 하나가 다른 하나의 상태값을 “덮어쓴다”고 읽지 않고, 정책 분류와 실제 요청 차단을 나눠 확인한다.

이 실습에서는 버킷 수준 PAB만 직접 바꾸고 확인했다. 조직·계정·access point 수준의 PAB, 조직 SCP, VPC endpoint 정책, KMS 키 정책이 함께 있을 때의 유효 접근은 검증하지 않았다. 따라서 위 네 명령만으로 모든 경로의 접근 가능성을 확정할 수는 없다. 이런 설정을 바꾼 호출이 로그에 어떻게 남는지는 뒤의 글에서 본다.

실습이 끝나면 접근 설정을 되돌린다

이 글에서 새로 만든 버킷은 처음에 버킷 정책이 없었고 PAB 네 항목이 모두 켜져 있었다. 그 상태로 되돌리려면 PAB를 다시 켠 뒤 실습 중 붙인 버킷 정책을 삭제한다. 다음 명령은 이 실습 전용 버킷의 초기 상태를 복원하는 안내다. 기존 버킷에 적용하면 필요한 정책까지 지울 수 있으므로, 기존 자원은 변경 전에 보관한 정책과 PAB 설정을 기준으로 복원해야 한다.

$bucketName이 실습 버킷을 가리키는지 확인한 뒤 한 명령씩 실행하고 성공 여부를 확인한다.

PowerShell
# 1. 버킷 수준 PAB 네 항목 복원
aws s3api put-public-access-block `
    --bucket $bucketName `
    --public-access-block-configuration "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

# 2. 실습에서 붙인 정책 제거 — 초기에는 정책이 없었다
aws s3api delete-bucket-policy --bucket $bucketName

# 3. 초기 설정과 다시 대조
aws s3api get-public-access-block --bucket $bucketName
aws s3api get-bucket-policy --bucket $bucketName
aws s3api get-bucket-ownership-controls --bucket $bucketName

확인할 값은 PAB 네 항목의 true, 정책 조회의 NoSuchBucketPolicy, 소유권 설정의 BucketOwnerEnforced다. AccessDenied 같은 다른 오류를 정책이 없다는 뜻으로 읽으면 안 된다. 후속 IAM 실습에서 사용할 canary.txt는 접근 설정을 복원한 상태로 남겨 두고, 시리즈 실습까지 끝나면 생성한 객체와 버킷을 정리한다.

put-public-access-block은 PAB 설정을 바꾸지만 기존 버킷 정책을 지우지는 않는다. delete-bucket-policy와 재조회를 함께 둔 이유다. 이 절은 공식 명령 참조에 따라 보완한 절차이며, 당시 복원 여부를 입증하는 실행 기록은 아니다.

참고 자료


출력과 화면의 계정 ID·버킷 이름·조직 ID는 임의의 값이다.