# EKS 컨트롤 플레인을 잠그는 순서 - 엔드포인트, 보안 그룹, 감사 로그
EKS 클러스터를 만들면 API 서버 엔드포인트가 기본으로 인터넷에 열립니다. IAM과 RBAC으로 보호되지만 주소 자체는 공개돼 있고, 보안 검토에서 가장 먼저 지적되는 항목입니다.
그래서 퍼블릭 접근에 CIDR 제한을 겁니다. 그리고 몇 분 뒤 노드가 전부 NotReady가 됩니다. 이 글에서는 엔드포인트 설정의 조합이 각각 무엇을 의미하는지, 왜 순서가 중요한지, 그리고 감사 로그를 켤 때 걸리는 조건들을 정리합니다.
# 1. 세 가지 조합
퍼블릭과 프라이빗 엔드포인트를 각각 켜고 끌 수 있어 조합이 셋입니다.
| 퍼블릭 | 프라이빗 | 노드 → API 경로 | kubectl 경로 |
|---|---|---|---|
| 켬 | 끔 | VPC 밖으로 나감 | 인터넷 |
| 켬 | 켬 | VPC 내부 | 인터넷 또는 VPC |
| 끔 | 켬 | VPC 내부 | VPC 또는 연결된 네트워크 |
첫 줄이 기본값이고, 여기 이 글의 핵심이 들어 있습니다.
Kubernetes API requests that originate from within your cluster's VPC (such as node to control plane communication) leave the VPC but not Amazon's network.
프라이빗 엔드포인트가 꺼져 있으면 노드도 퍼블릭 엔드포인트를 씁니다. 노드가 클러스터 안에 있다고 해서 내부 경로로 통신하는 것이 아닙니다. VPC를 나가서 다시 들어옵니다.
이 사실을 모른 채 퍼블릭 접근을 사무실 IP로 제한하면 어떻게 될까요? 노드의 요청도 퍼블릭 엔드포인트로 가는데, 그 출발지는 NAT 게이트웨이의 EIP입니다. 허용 목록에 그 주소가 없으면 노드가 API 서버에 도달하지 못합니다. 문서가 그래서 이렇게 권고합니다.
If you limit access to specific CIDR blocks, then we recommend that you also enable the private endpoint, or ensure that the CIDR blocks that you specify include the addresses that nodes and Fargate Pods (if you use them) access the public endpoint from.
정리하면 순서가 정해집니다.
- 먼저 프라이빗 엔드포인트를 켠다. 이 시점에 노드 통신이 VPC 내부로 옮겨 갑니다.
- 노드가 정상인지 확인한다.
- 그다음에 퍼블릭 CIDR를 제한하거나 퍼블릭을 끈다.
반대로 하면 노드가 먼저 끊깁니다. "보안을 강화한다"는 변경이 클러스터를 멈추는 전형적인 경로입니다.
# 2. 통제 수단마다 적용 대상이 다르다
접근을 막는 수단이 셋인데, 각각 다른 것을 막습니다. 문서가 명시적으로 경계를 그어 둡니다.
Note that each of the following methods to control endpoint access only affect the respective endpoint.
퍼블릭 액세스 CIDR.
the public access CIDRs don't affect the private endpoint
퍼블릭 엔드포인트만 제한합니다. 프라이빗 엔드포인트로 오는 요청은 통과합니다.
클러스터 보안 그룹.
The cluster security group controls two types of connections: connections to the kubelet API and the private endpoint. The connections to the
kubeletAPI are used in thekubectl attach,kubectl cp,kubectl exec,kubectl logs, andkubectl port-forwardcommands. The cluster security group doesn't affect the public endpoint.
두 가지를 통제하는데, 그중 하나가 kubelet API입니다. kubectl exec, kubectl logs, kubectl port-forward가 여기를 씁니다. 그리고 마지막 문장이 중요합니다. 보안 그룹은 퍼블릭 엔드포인트에 아무 영향이 없습니다.
"보안 그룹으로 막아 두었다"는 진술이 틀리는 지점이 여기입니다. 퍼블릭 엔드포인트가 켜져 있으면 보안 그룹과 무관하게 인터넷에서 API 서버에 도달할 수 있습니다.
정리하면 이렇습니다.
| 통제 수단 | 퍼블릭 엔드포인트 | 프라이빗 엔드포인트 | kubelet API |
|---|---|---|---|
| 퍼블릭 액세스 CIDR | 통제 | 영향 없음 | 영향 없음 |
| 클러스터 보안 그룹 | 영향 없음 | 통제 | 통제 |
| IAM + RBAC | 통제 | 통제 | 통제 |
세 수단이 겹치지 않으므로, 하나만 설정하고 안심하면 구멍이 남습니다.
# 3. 프라이빗 전용으로 갈 때 필요한 것
퍼블릭을 끄면 DNS와 접근 경로가 함께 문제가 됩니다.
DNS 조건. EKS가 프라이빗 호스티드 존을 만들어 주지만 전제가 있습니다.
In order for the private hosted zone to properly route traffic to your API server, your VPC must have
enableDnsHostnamesandenableDnsSupportset totrue, and the DHCP options set for your VPC must includeAmazonProvidedDNSin its domain name servers list.
VPC의 DNS 설정 두 개와 DHCP 옵션이 조건입니다. 사내 DNS 서버를 DHCP 옵션에 넣어 둔 VPC라면 AmazonProvidedDNS가 빠져 있을 수 있고, 그러면 엔드포인트 이름이 안 풀립니다.
기존 클러스터에서 프라이빗 IP로 해석되지 않는 경우를 위한 우회도 문서에 있습니다.
If your endpoint does not resolve to a private IP address within the VPC for an existing cluster, you can: Enable public access and then disable it again. You only need to do so once for a cluster and the endpoint will resolve to a private IP address from that point forward.
퍼블릭을 켰다가 다시 끄면 해결됩니다. 직관적이지 않지만 알아 두면 유용한 절차입니다.
접근 경로. 퍼블릭을 끄면 kubectl은 VPC 안이나 연결된 네트워크에서만 됩니다. 선택지는 전용선/Transit Gateway로 연결된 사내망, VPC 안의 배스천, 또는 CloudShell입니다. 어느 쪽이든 컨트롤 플레인 보안 그룹에 443 인바운드를 열어 주어야 합니다.
그리고 여기에 가장 위험한 함정이 있습니다.
be sure to use AWS credentials that are already mapped to your cluster's RBAC configuration, or add the IAM principal that your bastion will use to the RBAC configuration before you remove endpoint public access
퍼블릭을 끄기 전에 배스천이 쓸 IAM 주체를 클러스터 권한에 등록해 두어야 합니다. 순서를 바꾸면 배스천은 네트워크로는 닿는데 인증에서 거부되고, 고치려면 클러스터에 들어가야 하는 상태가 됩니다. IAM 주체를 클러스터 권한에 등록하는 방법은 aws-auth ConfigMap에서 EKS Access Entries로 (opens new window)에 정리했는데, Access Entries를 쓰고 있다면 이 교착이 원천적으로 사라집니다. 권한 부여가 EKS API 쪽에 있기 때문입니다.
따라서 전환 순서는 이렇게 됩니다.
- Access Entries(또는 aws-auth)에 배스천·CI 주체를 등록한다
- 프라이빗 엔드포인트를 켠다
- 배스천에서
kubectl이 되는지 확인한다 - 노드가 정상인지 확인한다
- 퍼블릭을 끈다
# 4. 감사 로그는 기본으로 꺼져 있다
접근을 좁히는 것과 별개로, 누가 무엇을 했는지 남기는 설정이 필요합니다.
By default, cluster control plane logs aren't sent to CloudWatch Logs. You must enable each log type individually.
다섯 종류가 있고 성격이 다릅니다.
| 종류 | 내용 | 우선순위 |
|---|---|---|
audit | 사용자·관리자·시스템 컴포넌트가 클러스터에 한 일의 기록 | 최우선 |
authenticator | IAM 자격 증명 기반 인증 과정 (EKS 고유) | 높음 |
api | API 서버 로그. 시작 시 플래그 포함 | 중간 |
controllerManager | 컨트롤 루프 | 진단용 |
scheduler | 파드 배치 결정 | 진단용 |
보안 목적이라면 audit과 authenticator가 핵심입니다. 앞의 것은 "무엇을 했는가", 뒤의 것은 "누구로 인증됐는가"를 답합니다. 접근이 거부됐을 때 원인을 찾는 것도 authenticator입니다.
aws eks update-cluster-config --region ap-northeast-2 --name my-cluster \
--logging '{"clusterLogging":[{"types":["audit","authenticator"],"enabled":true}]}'
여기서 세 가지 제약을 알아 두어야 합니다.
서브넷에 여유 IP가 필요합니다.
To update the control plane logging configuration, Amazon EKS requires up to five available IP addresses in each subnet.
로깅 설정을 바꾸는 것뿐인데 IP가 필요합니다. 서브넷이 꽉 찬 클러스터에서는 이 변경이 실패하고, 원인이 로깅과 무관해 보여 진단이 오래 걸립니다.
API 서버 플래그는 나중에 켜면 못 봅니다.
If you don't see the API server logs at the beginning of the log stream, then it is likely that the API server log file was rotated on the server before you enabled API server logging.
api 로그의 첫 부분에 API 서버 시작 플래그가 들어가는데, 이미 회전된 로그는 내보낼 수 없습니다. 클러스터의 API 서버 설정을 확인하고 싶다면 생성 시점에 켜 두어야 합니다.
전달이 최선 노력입니다.
Amazon EKS control plane logs are delivered to CloudWatch Logs within a few minutes. However, log delivery is best effort.
규정 준수 목적으로 "빠짐없이 남는다"를 전제하면 안 됩니다. 보완이 필요하다면 CloudTrail(EKS API 호출 기록)과 함께 봐야 합니다.
# 5. 감사 로그를 켜 두기만 하면 소용이 없다
로그를 켜면 CloudWatch 비용이 발생합니다. 규모가 있는 클러스터의 audit은 양이 상당합니다. 그래서 켜 놓고 아무도 안 보는 상태가 되기 쉽습니다.
문서가 언급하는 실시간 처리 경로가 이 문제의 답입니다.
You can use CloudWatch subscription filters to do real time analysis on the logs or to forward them to other services
몇 가지 질의는 미리 걸어 두면 값어치를 합니다.
# 익명 접근 시도
fields @timestamp, user.username, verb, objectRef.resource, responseStatus.code
| filter user.username = "system:anonymous"
| sort @timestamp desc
# Secret 읽기 - 누가 어떤 시크릿을 봤는가
fields @timestamp, user.username, objectRef.namespace, objectRef.name
| filter objectRef.resource = "secrets" and verb in ["get", "list"]
| filter user.username not like /^system:/
| sort @timestamp desc
# 권한 거부 - 잘못된 설정과 침해 시도가 함께 나타난다
fields @timestamp, user.username, verb, objectRef.resource
| filter responseStatus.code = 403
| stats count(*) as cnt by user.username, objectRef.resource
| sort cnt desc
세 번째 질의가 특히 실용적입니다. 403이 갑자기 늘어나는 것은 대개 배포 실수지만, 한 주체가 여러 리소스에 걸쳐 403을 쌓고 있다면 권한 탐색일 수 있습니다. "보고 있는 지표가 있는가"가 로그를 켜는 것보다 중요합니다.
보존 기간도 정해야 합니다. 로그 그룹이 자동 생성되면서 보존 기간이 무기한으로 남는 경우가 많고, 그러면 비용이 계속 누적됩니다.
aws logs put-retention-policy \
--log-group-name /aws/eks/my-cluster/cluster --retention-in-days 90
# 6. 직접 확인하는 방법
현재 상태부터 봅니다.
# 엔드포인트 설정과 로깅 설정을 한 번에
aws eks describe-cluster --name my-cluster --query '{
public: cluster.resourcesVpcConfig.endpointPublicAccess,
private: cluster.resourcesVpcConfig.endpointPrivateAccess,
cidrs: cluster.resourcesVpcConfig.publicAccessCidrs,
clusterSG: cluster.resourcesVpcConfig.clusterSecurityGroupId,
logging: cluster.logging.clusterLogging
}'
public: true, private: false, cidrs: ["0.0.0.0/0"]이면 기본 상태입니다. cidrs가 좁혀져 있는데 private: false라면 1절의 위험 구간에 있습니다.
노드가 어느 경로로 API에 닿는지 확인합니다.
# 노드 안에서 엔드포인트가 어떤 IP로 풀리는지
ENDPOINT=$(aws eks describe-cluster --name my-cluster --query 'cluster.endpoint' --output text)
kubectl debug node/<node> -it --image=busybox -- nslookup "${ENDPOINT#https://}"
프라이빗 IP(VPC 대역)로 풀리면 내부 경로, 공인 IP면 VPC를 나가는 경로입니다. CIDR 제한 전에 이 값이 프라이빗인지 확인하는 것이 1절의 사고를 막는 유일한 검증입니다.
VPC DNS 조건도 미리 봅니다.
VPC=$(aws eks describe-cluster --name my-cluster --query 'cluster.resourcesVpcConfig.vpcId' --output text)
aws ec2 describe-vpc-attribute --vpc-id "$VPC" --attribute enableDnsHostnames
aws ec2 describe-vpc-attribute --vpc-id "$VPC" --attribute enableDnsSupport
로깅이 실제로 흐르는지도 확인합니다.
aws logs describe-log-streams \
--log-group-name /aws/eks/my-cluster/cluster \
--order-by LastEventTime --descending --max-items 5 \
--query 'logStreams[].{name:logStreamName,last:lastEventTimestamp}'
lastEventTimestamp가 최근이 아니면 전달이 멈춘 것입니다.
# 7. 트러블슈팅
| 증상 | 원인 | 조치 |
|---|---|---|
CIDR 제한 후 노드가 NotReady | 노드가 퍼블릭 엔드포인트를 사용 | 프라이빗 먼저 켜기, NAT EIP 포함 |
| 프라이빗을 켰는데 이름이 안 풀림 | VPC DNS 속성·DHCP 옵션 | 두 속성 확인, AmazonProvidedDNS 추가 |
| 기존 클러스터가 공인 IP로 해석 | 프라이빗 존 반영 안 됨 | 퍼블릭을 켰다가 다시 끄기 |
| 퍼블릭을 끈 뒤 접근 불가 | 배스천 주체가 미등록 | 끄기 전에 권한 등록 |
| SG로 막았는데 인터넷에서 접근됨 | SG는 퍼블릭에 영향 없음 | 퍼블릭 CIDR 또는 퍼블릭 비활성화 |
kubectl exec만 실패 | kubelet API가 SG로 막힘 | 클러스터 SG 규칙 확인 |
| 로깅 설정 변경이 실패 | 서브넷 여유 IP 부족 | IP 확보 후 재시도 |
| API 서버 시작 플래그가 안 보임 | 로그 회전 후 활성화 | 생성 시점에 활성화 |
| CloudWatch 비용 급증 | 보존 기간 무기한 | 보존 정책 설정, 로그 종류 선별 |
# 8. 마무리
- 프라이빗 엔드포인트가 꺼져 있으면 노드도 퍼블릭 엔드포인트를 씁니다. 이 사실을 모르고 CIDR를 제한하면 노드가 먼저 끊깁니다.
- 순서가 정해져 있습니다. 권한 등록 → 프라이빗 켜기 → 확인 → 퍼블릭 제한입니다. 반대로 하면 복구 경로가 사라집니다.
- 통제 수단은 적용 대상이 서로 다릅니다. 보안 그룹은 퍼블릭 엔드포인트에 아무 영향이 없고, 퍼블릭 CIDR는 프라이빗에 영향이 없습니다.
- 프라이빗 전환에는 VPC DNS 조건이 붙습니다. 기존 클러스터가 프라이빗 IP로 해석되지 않으면 퍼블릭을 켰다 끄는 절차가 있습니다.
- 감사 로그는 기본으로 꺼져 있고, 켜는 데도 서브넷 여유 IP가 필요합니다. 전달은 최선 노력이므로 완전성을 전제하면 안 됩니다.
- 로그를 켜는 것보다 무엇을 보고 있는지가 중요합니다. 익명 접근, 시크릿 읽기, 403 급증 정도는 미리 질의로 걸어 둡니다.