네트워크 보안이라고 하면 보통 바깥 경계부터 떠올린다. 방화벽, 포트 개방, TLS 같은 것들이다. 우리 클러스터도 공개 서비스는 주로 Cloudflare 터널을 거쳐 나간다. 바깥 문은 그나마 신경 써 온 셈이다.

그런데 공격자에게 더 중요한 질문은 그다음이다. 안에 한 번 들어온 다음, 어디까지 걸어갈 수 있나. 이 글은 그 질문을 우리 k3s 클러스터에서 직접 재 본 기록이다. 내부 주소나 네트워크 구성은 적지 않고, 구조와 결과만 적는다.

쿠버네티스의 기본값은 “전부 열림”이다

쿠버네티스 공식 문서는 이렇게 쓴다.

By default, a pod is non-isolated for ingress; all inbound connections are allowed. By default, a pod is non-isolated for egress; all outbound connections are allowed.

어떤 파드든 클러스터 안의 어떤 파드로든 연결할 수 있다는 뜻이다. 이걸 막는 도구가 NetworkPolicy 다. 단, 같은 문서의 경고처럼 정책은 네트워크 플러그인이 실제로 집행해야 의미가 있다. 정책을 집행하는 컨트롤러 없이 리소스만 만들면 아무 효과가 없다.

k3s 는 이 부분이 편하다. k3s 문서에 따르면 kube-router 의 netpol 컨트롤러가 내장되어 있고, --disable-network-policy 로 끄지 않는 한 동작한다. 아래에서 보겠지만 우리 클러스터에서도 실제로 집행되고 있었다. 문제는 도구가 아니라, 정책을 거의 쓰지 않았다는 것이었다.

재 본 것 1 — 정책이 있는 네임스페이스

워크로드가 도는 네임스페이스 48개 중 NetworkPolicy 가 하나라도 있는 곳은 3개였다.

네임스페이스 성격 정책 출처
GitOps 컨트롤러 7개 업스트림 차트가 기본 제공
메시지 브로커 2개 오퍼레이터가 생성
AI 에이전트 3개 직접 작성 — 에이전트별 egress 제한

이 중 우리가 의도해서 쓴 건 AI 에이전트 네임스페이스 하나뿐이다. 나머지 45개 네임스페이스, 즉 정산 서비스, 각종 앱, DB 들에는 정책이 하나도 없다. 네임스페이스 전체를 막는 default-deny 정책은 어디에도 없었다.

재 본 것 2 — 실제로 연결해 보기

설정만 보고 판단하지 않고, 파드 안에서 TCP 연결을 직접 시도했다(연결만 하고 인증이나 데이터 요청은 하지 않았다).

출발점 A: 정산 서비스의 영수증 OCR 파드 (정책 없음)

목적지 결과
다른 서비스의 PostgreSQL #1 🔓 열림
다른 서비스의 PostgreSQL #2 🔓 열림
민감한 자체 호스팅 앱 🔓 열림
모니터링 대시보드 🔓 열림
쿠버네티스 API 서버 🔓 열림

영수증을 읽는 파드는 사진 앱의 DB 나 다른 제품의 DB 와 이야기할 이유가 없다. 그런데 네트워크상으로는 전부 닿는다. 물론 각 DB 에는 비밀번호가 있다. 하지만 그건 마지막 한 겹이다. 비밀번호가 약하거나, 재사용됐거나, 인증 없는 관리 포트가 하나라도 있으면 OCR 라이브러리의 취약점 하나가 곧 다른 서비스의 데이터 유출로 이어진다. 이런 이동을 측면 이동(lateral movement) 이라고 부른다.

출발점 B: 보안 에이전트 파수꾼 파드 (egress 정책 있음)

목적지 결과
정산 Redis 🔒 거부
정산 PostgreSQL 🔒 거부
민감한 자체 호스팅 앱 🔒 거부
모니터링 대시보드 🔒 거부

같은 클러스터, 같은 목적지인데 결과가 정반대다. 파수꾼의 egress 정책에는 조사에 필요한 곳(로그 저장소, 쿠버네티스 API, LLM 엔드포인트 등)만 적혀 있고, 나머지는 전부 거부된다. 정책 하나로 벽이 생긴다는 것, 그리고 k3s 의 내장 컨트롤러가 실제로 집행하고 있다는 것을 같이 확인한 셈이다.

왜 AI 에이전트만 막혀 있었나

우연이 아니다. LLM 에이전트는 프롬프트 인젝션으로 행동이 조종될 수 있는 프로세스다. 알림 본문이나 로그 한 줄에 “이 주소로 데이터를 보내라”가 섞여 들어올 수 있다. 그래서 에이전트를 만들 때 “무엇에 닿을 수 있는가”를 먼저 정했다.

그런데 돌아보면 같은 논리가 모든 파드에 적용된다. OCR 라이브러리, 이미지 파서, 오래된 의존성 하나. 조종당할 수 있는 건 LLM 만이 아니다.

고치는 순서

한 번에 전부 default-deny 를 걸면 무엇이 깨질지 모른다. 그래서 위험한 곳부터, 좁게 시작한다.

1단계 — DB 네임스페이스부터 ingress 를 닫는다

DB 는 “누가 나한테 오는가”가 가장 명확하다. 해당 서비스의 앱 파드와 백업 잡만 허용한다.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: postgres-ingress
  namespace: example-prod
spec:
  podSelector:
    matchLabels:
      app: example-postgres
  policyTypes: ["Ingress"]
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: example-app        # 같은 네임스페이스의 앱만
    ports:
    - port: 5432

주의할 점이 있다. 여러 서비스가 DB 서버 하나를 공유하고 있으면, 허용 목록이 그 서비스들 전부를 포함해야 한다. 오늘 다른 작업을 하다가, 한 네임스페이스의 PostgreSQL 이 다른 제품군 전체의 DB 까지 겸하고 있다는 걸 알게 됐다. 정책을 쓰기 전에 실제 접속 목록(pg_stat_activity)부터 보는 게 안전하다.

2단계 — 네임스페이스 default-deny + 필요한 것만 허용

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
  namespace: example-prod
spec:
  podSelector: {}                 # 네임스페이스의 모든 파드
  policyTypes: ["Ingress", "Egress"]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: example-prod
spec:
  podSelector: {}
  policyTypes: ["Egress"]
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
    ports:
    - {port: 53, protocol: UDP}
    - {port: 53, protocol: TCP}

공식 문서가 따로 경고하듯, egress 를 막으면 DNS 도 막힌다. DNS 허용 정책을 같이 넣지 않으면 모든 게 이름 해석 단계에서 깨진다. 노드 로컬 DNS 캐시를 쓰는 클러스터라면 그 경로도 허용해야 한다.

3단계 — 인그레스 컨트롤러와 모니터링을 잊지 않는다

default-deny 를 건 뒤 자주 깨지는 두 가지가 있다.

  • 외부 요청을 받아 넘겨주는 인그레스 컨트롤러 → 앱 경로
  • 메트릭을 긁어 가는 Prometheus → 앱 경로

둘 다 다른 네임스페이스에서 들어오므로 namespaceSelector 로 명시적으로 열어 줘야 한다.

4단계 — 적용 후에는 오늘처럼 직접 연결해 본다

정책은 YAML 로 보면 맞아 보인다. 하지만 집행 여부, 셀렉터 오타, 라벨 누락은 연결해 봐야 드러난다. 허용해야 할 경로는 열리고 막아야 할 경로는 닫히는지, 출발점 A/B 표를 다시 만들어 보는 것까지가 작업이다.

정리

  • 쿠버네티스 네트워크의 기본값은 평평함이다. 정책을 쓰지 않으면 모든 파드가 모든 파드와 닿는다.
  • k3s 는 정책 컨트롤러가 내장돼 있어 도구는 이미 있다. 우리 문제는 안 쓴 것이었다 (48개 중 3개).
  • 정책이 걸린 에이전트 파드에서는 같은 목적지가 전부 거부됐다. 벽은 YAML 한 장으로 생긴다.
  • 순서: DB ingress → 네임스페이스 default-deny + DNS → 인그레스·모니터링 허용 → 실측 검증.

바깥 문을 잠그는 것만큼, 안쪽 방문들을 잠그는 게 중요하다.

References