들어가며
지난 #133 Secrets Management에서 애플리케이션이 런타임에 어떻게 크리덴셜을 안전하게 받아가는지를 다뤘습니다. 그런데 한 층 더 아래에 질문이 남습니다. A 서비스가 B 서비스를 부를 때, 서로가 진짜 맞다는 건 누가 보증하는가?
과거 모델은 단순했습니다. "VPC 안이면 믿는다." 2026년에도 이 전제를 유지하는 조직이 많지만, 현실은 정반대로 흐르고 있습니다. 2024년 Snowflake 고객사 165곳 침해는 내부망 신뢰 모델의 한계를 보여준 대표 사례였고, 2025년 Okta·Cloudflare 공급망 토큰 사고도 "경계 안은 안전"이라는 가정이 깨진 결과였습니다.
해답이 Zero Trust입니다. 네트워크 위치와 상관없이 모든 요청에 신원·인증·인가를 강제합니다. 오늘은 이 원칙을 서비스 간 통신에 적용하는 방법을 구체적 코드 레벨로 정리합니다.
- 1부 - Zero Trust가 바꾸는 전제 5가지
- 2부 - mTLS 기초와 직접 구현의 한계
- 3부 - Istio와 Linkerd 비교 선택
- 4부 - SPIFFE/SPIRE로 플랫폼 중립 신원 부여
- 5부 - AuthorizationPolicy로 L7 인가
- 6부 - Spring Boot 애플리케이션 관점의 통합
- 7부 - 관측성·성능·도입 단계
1. Zero Trust가 바꾸는 전제 5가지
기존 경계 모델과 무엇이 다른지가 개념의 핵심입니다.
| 축 | 경계(Perimeter) 모델 | Zero Trust |
|---|---|---|
| 기본 신뢰 | 내부망이면 신뢰 | 아무도 기본 신뢰 없음 |
| 인증 단위 | 사람 로그인 1회 | 요청마다 서비스·사용자 신원 |
| 네트워크 | 평문 내부 통신 허용 | 전 구간 암호화(mTLS) |
| 인가 | IP/방화벽 룰 | L7 정책(서비스·경로·메서드) |
| 사고 반경 | 한 번 뚫리면 전체 | 마이크로 세그먼트로 봉쇄 |
구글의 BeyondCorp가 사내 VPN을 없애며 보여준 모델이 이 원칙의 원형입니다. NIST SP 800-207이 표준화했고, 2026년에는 FedRAMP·금융권 신규 프로젝트에서 사실상 요구사항이 됐습니다.
2. mTLS의 기초와 직접 구현의 한계
mTLS(Mutual TLS)는 서버만 인증서를 제시하는 일반 TLS와 달리, 클라이언트도 인증서를 제시해 양방향으로 신원을 검증합니다.
2-1. Spring Boot에서 직접 구현
# application.yml
server:
port: 8443
ssl:
enabled: true
client-auth: need # 핵심: 클라이언트 인증서 필수
key-store: classpath:server-keystore.p12
key-store-password: ${KS_PASSWORD}
trust-store: classpath:ca-truststore.p12
trust-store-password: ${TS_PASSWORD}
// WebClient 클라이언트 측
SslContext ctx = SslContextBuilder.forClient()
.keyManager(clientCert, clientKey)
.trustManager(caCert)
.build();
HttpClient http = HttpClient.create()
.secure(s -> s.sslContext(ctx));
WebClient client = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(http))
.build();
2-2. 왜 애플리케이션에서 직접 하면 안 되는가
돌아가긴 합니다. 문제는 운영입니다.
- 인증서 로테이션 - 90일 주기로 모든 서비스 재배포 필요, Secrets Management의 dual secret 설계를 서비스마다 구현해야 함
- CA 관리 - 자체 서명이면 다른 서비스는 이 CA를 어떻게 받나? 부트스트랩 문제 재발
- 언어별 구현 차이 - Java·Go·Node.js 각 언어에서 Trust store 관리가 제각각
- 실패 모드 공유 - 인증서 만료 하나에 수십 개 서비스가 동시에 장애
- 인가 부재 - mTLS는 "너가 누구냐"만 증명, "이 API 호출 권한"은 별도
그래서 이 책임을 사이드카나 프록시로 떼어내는 것이 서비스 메시의 출발점입니다.
3. Istio vs Linkerd - 현실적 선택 기준
OSS 서비스 메시 양대산맥입니다. 둘 다 Pod마다 사이드카(또는 앰비언트) 프록시를 붙여 mTLS와 정책을 자동으로 적용합니다.
| 항목 | Istio | Linkerd |
|---|---|---|
| 데이터 플레인 | Envoy(C++) | linkerd2-proxy(Rust, 자체) |
| 기능 범위 | 트래픽 관리·보안·관측 전체 | 핵심만, 단순성 추구 |
| 앰비언트 모드 | 지원(ztunnel+waypoint) | 지원(2.14+) |
| 러닝 커브 | 가파름 | 완만함 |
| 메모리 오버헤드 | 사이드카당 40~80MB | 사이드카당 10~20MB |
| 성숙도(CNCF) | Graduated | Graduated |
| 대표 도입 | 금융·대기업 | 스타트업·중견 |
선택 기준은 단순합니다.
- 트래픽 관리·카나리·외부 egress 세밀 제어까지 필요 → Istio
- mTLS·관측성만 빠르게 받고 싶다 → Linkerd
- 사이드카 오버헤드가 부담스럽다 → Istio Ambient 또는 Cilium Service Mesh
3-1. Istio 설치와 자동 mTLS
$ istioctl install --set profile=default
$ kubectl label namespace production istio-injection=enabled
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: production
spec:
mtls:
mode: STRICT # 비 mTLS 트래픽 차단
이 한 장으로 production 네임스페이스의 모든 Pod 간 통신이 mTLS로 강제됩니다. 애플리케이션은 평범한 HTTP를 그대로 쓰고, 사이드카 Envoy가 mTLS를 맺습니다.
3-2. Istio Ambient Mode
사이드카 없이 노드 단위 ztunnel로 L4 mTLS를 처리합니다. L7 정책이 필요한 네임스페이스만 waypoint 프록시를 덧붙입니다.
| 레이어 | 컴포넌트 | 역할 |
|---|---|---|
| L4 | ztunnel (DaemonSet) | 노드 단위 mTLS·HBONE |
| L7 | waypoint (Deployment) | AuthorizationPolicy·라우팅 |
Pod 수만큼 사이드카를 띄우지 않아 메모리 절감이 크고, 사이드카 주입으로 인한 초기 컨테이너 경쟁 조건 이슈도 사라집니다. 다만 L7 기능이 필요한 곳은 여전히 waypoint가 필요합니다.
4. SPIFFE/SPIRE - 플랫폼 중립 신원 표준
Kubernetes 안에서만 mTLS를 쓰면 충분할 것 같지만, 현실은 VM·베어메탈·타 클라우드·온프레미스가 섞여 있습니다. SPIFFE가 이 이질성을 흡수하는 표준입니다.
4-1. 핵심 개념 - SPIFFE ID와 SVID
SPIFFE ID는 URI 형식의 범용 신원입니다.
spiffe://acme.example.com/production/orders-service
spiffe://acme.example.com/production/payment-service
이 ID가 담긴 X.509 인증서 또는 JWT를 SVID(SPIFFE Verifiable Identity Document)라 부릅니다. 발급자가 SPIRE 서버이고, 에이전트가 각 워크로드에 주입합니다.
4-2. 왜 SPIFFE인가
- Kubernetes의 ServiceAccount 신원은 클러스터 바깥에서 검증이 어렵다
- EC2·Lambda·온프레미스 VM 등 혼합 환경에서 단일 신원 체계 필요
- 서비스 메시 별 신원 포맷 차이(Istio SA, Linkerd 등)를 추상화
- JWT-SVID로 애플리케이션 레벨 호출에도 재사용
4-3. SPIRE 등록 예시
$ spire-server entry create \
-spiffeID spiffe://acme.example.com/production/orders-service \
-parentID spiffe://acme.example.com/spire/agent/k8s_psat/prod/xxx \
-selector k8s:ns:production \
-selector k8s:sa:orders-app
selector는 "이 신원은 production 네임스페이스의 orders-app ServiceAccount로 뜬 Pod에만 부여"를 의미합니다. 위조하려면 쿠버네티스 kubelet 자체를 장악해야 합니다.
4-4. Istio와 SPIRE 연동
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
values:
global:
caAddress: spire-agent.spire.svc:8081
pilot:
env:
ENABLE_CA_SERVER: "false"
Istio의 내장 CA(Citadel) 대신 SPIRE를 CA로 사용합니다. 그러면 같은 SPIFFE 신원으로 Kubernetes 내부와 외부(VM·Lambda 등)가 한 신원 체계를 공유합니다. #133 Workload Identity의 확장판이라 볼 수 있습니다.
5. L7 인가 - 네트워크가 아닌 신원 기반으로
mTLS로 "누구인지"가 증명되면, 이제 "무엇을 할 수 있는지"를 정해야 합니다. Istio AuthorizationPolicy는 신원·HTTP 메서드·경로·헤더로 정책을 세밀하게 짭니다.
5-1. 기본 거부 정책
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: default-deny
namespace: production
spec: {} # 빈 스펙 = 모두 거부
Zero Trust의 "default deny"를 선언하는 한 장입니다. 이후에 필요한 접근만 allow로 열어줍니다.
5-2. 서비스 간 허용 정책
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: orders-allow-payment
namespace: production
spec:
selector:
matchLabels: {app: orders}
rules:
- from:
- source:
principals: ["cluster.local/ns/production/sa/payment-app"]
to:
- operation:
methods: ["POST"]
paths: ["/internal/orders/capture"]
"payment-app ServiceAccount가 orders의 /internal/orders/capture에 POST만 가능"이라는 정책을 한 장으로 표현합니다. IP 기반 방화벽으로는 표현이 어려운 L7 룰이 간결해집니다.
5-3. 최종 사용자 JWT와 조합
apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
name: jwt-auth
namespace: production
spec:
selector: {matchLabels: {app: api-gateway}}
jwtRules:
- issuer: "https://auth.example.com"
jwksUri: "https://auth.example.com/.well-known/jwks.json"
---
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata: {name: require-jwt, namespace: production}
spec:
selector: {matchLabels: {app: api-gateway}}
rules:
- from: [{source: {requestPrincipals: ["https://auth.example.com/*"]}}]
사이드카에서 JWT 검증까지 처리합니다. 애플리케이션 코드는 검증된 클레임만 받게 되어, Spring Security의 리소스 서버 설정이 얇아집니다.
6. Spring Boot 애플리케이션 관점
서비스 메시를 도입해도 애플리케이션이 신경 써야 할 것이 있습니다.
6-1. 재시도·타임아웃은 애플리케이션이 아닌 메시에
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: orders
spec:
hosts: ["orders"]
http:
- timeout: 2s
retries:
attempts: 3
perTryTimeout: 700ms
retryOn: "5xx,reset,connect-failure"
Resilience4j로 코드에 리트라이를 박으면 이중 리트라이로 폭탄이 됩니다. 인프라성 복원력은 메시로, 비즈니스성 보상은 애플리케이션으로 분리해야 합니다.
6-2. 신원 기반 로그 컨텍스트
// 요청 필터 (WebMvc)
String caller = request.getHeader("X-Forwarded-Client-Cert"); // 사이드카 주입
MDC.put("peer.spiffe", extractSpiffeId(caller));
"누가 이 요청을 보냈나"를 애플리케이션 로그에 직접 남길 수 있습니다. OpenTelemetry(#110) span attribute로 전달하면 트레이스에서도 신원이 보입니다.
6-3. 헬스체크의 함정
mTLS strict 모드에서 kubelet HTTP 프로브는 사이드카가 없어 mTLS를 못 맺습니다. Istio는 rewriteAppHTTPProbers로 자동 우회하지만, 커스텀 프로브는 직접 처리해야 합니다. Actuator의 /actuator/health가 사이드카를 거치지 않도록 별도 포트에 열어두는 패턴이 현실적입니다.
6-4. Virtual Threads와 사이드카 오버헤드
Spring Boot 4 + Virtual Threads(#64)는 초당 요청 수가 급증할 수 있습니다. 사이드카당 2~5ms p99 오버헤드가 누적되면 병목이 될 수 있어, 앰비언트 모드 또는 L7 필요 서비스만 사이드카 전략이 유리해집니다.
7. 관측성 - 메시가 공짜로 주는 것
서비스 메시의 보너스가 관측성입니다. 애플리케이션 코드 변경 없이 기본 지표가 다 잡힙니다.
| 지표 | 의미 | 용도 |
|---|---|---|
| istio_requests_total | 응답 코드별 카운터 | SLI - 에러율 |
| istio_request_duration_milliseconds | 히스토그램 | SLI - 레이턴시 |
| istio_request_bytes/response_bytes | 바디 크기 | 용량 이상 탐지 |
| istio_tcp_connections_* | TCP 커넥션 | 메시 상태 모니터링 |
Prometheus·Grafana(#93)로 자동 수집되고, Kiali가 서비스 맵을 실시간으로 그려줍니다. 새 서비스 추가 시 별도 대시보드 작업이 거의 없어집니다.
8. 성능 오버헤드 실측 기준
도입 전 가장 많이 묻는 질문입니다. 2026년 기준 범위입니다.
| 항목 | 사이드카 모드 | 앰비언트 모드 |
|---|---|---|
| p50 레이턴시 증가 | +1~2ms | +0.3~0.8ms |
| p99 레이턴시 증가 | +3~5ms | +1~2ms |
| 메모리/Pod | 40~80MB | 0 (노드 단위) |
| CPU/1000rps | 0.05~0.1 core | 0.02~0.05 core |
p99 요구가 빡빡하지 않은 일반 백엔드 API에서는 체감 불가 수준입니다. 실시간성이 강한 트레이딩·광고 입찰 시스템이라면 앰비언트 또는 eBPF 기반 메시가 현실적 선택입니다.
9. 도입 로드맵
빅뱅 도입은 절대 금물입니다. 단계별 전환이 정석입니다.
| 단계 | 목표 | 위험 |
|---|---|---|
| 1 | 1개 네임스페이스 자동 주입, PERMISSIVE mTLS | 매우 낮음 |
| 2 | Prometheus/Kiali 연동, SLO 관측 확립 | 낮음 |
| 3 | 해당 네임스페이스 STRICT mTLS 전환 | 중간 (헬스체크·외부 egress 주의) |
| 4 | default-deny + allow 정책 전환 | 높음 (정책 누락 시 장애) |
| 5 | 전체 네임스페이스 확산 | 중간 |
| 6 | SPIRE 통합, 외부 VM·Lambda 신원 통일 | 중간 |
| 7 | 앰비언트 모드로 오버헤드 축소 | 중간 |
핵심은 PERMISSIVE 단계를 충분히 길게 가져가는 것입니다. 기존 평문 트래픽을 강제 차단하기 전에, 사이드카 주입이 안 된 워크로드·외부 시스템 연결을 모두 식별해야 합니다.
10. 흔한 도입 실패 패턴
- STRICT mTLS를 첫날에 전 네임스페이스 적용 - 사이드카 미주입 DaemonSet·배치 잡·외부 모니터링이 일제히 끊김. PERMISSIVE → STRICT 단계적 전환이 정석.
- AuthorizationPolicy default-deny 직후 엔드유저 트래픽까지 차단 - Gateway(ingress) 정책을 별도로 풀어두지 않으면 서비스가 세상과 끊김. ingress 네임스페이스는 예외 처리 먼저.
- 메시와 애플리케이션 양쪽에 리트라이 - 재시도 폭증 → 업스트림 과부하. 메시를 쓰기로 했다면 애플리케이션 리트라이는 원칙적으로 제거.
- 사이드카 없는 Pod이 cluster-wide 정책에 걸림 - Job·CronJob은 단명이라 주입 타이밍 실패가 흔함. istio-cni·앰비언트로 해결하거나 해당 리소스를 mesh 밖 네임스페이스로 분리.
- 인증서 로테이션 관측 부재 - 만료 임박 알림·만료 실패 메트릭을 안 보면 대규모 장애로 귀결. Prometheus
citadel_server_csr_count등 기본 지표에 알림. - 외부 egress를 메시에서 처리 안 함 - 내부만 mTLS인 채 외부로는 평문.
ServiceEntry·Egress Gateway로 외부 TLS 강제 정책을 별도로 만들어야 완결. - SPIRE 도입을 너무 일찍 시도 - K8s 네이티브 신원으로도 초기에는 충분. 혼합 환경(VM·온프레미스)이 실제로 있을 때 도입.
11. 체크리스트
- [ ] 프로덕션 네임스페이스가 STRICT mTLS로 설정돼 있다
- [ ] default-deny AuthorizationPolicy 위에 화이트리스트로 운영된다
- [ ] 모든 서비스 호출이 IP가 아닌 ServiceAccount·SPIFFE ID로 허가된다
- [ ] 사이드카 인증서 만료 임박·만료 실패에 알림이 있다
- [ ] 헬스체크 프로브가 mTLS STRICT와 호환된다
- [ ] 외부 egress도 egress gateway를 경유하며 TLS가 강제된다
- [ ] 애플리케이션 리트라이와 메시 리트라이가 중복되지 않는다
- [ ] 메시 메트릭이 SLO 대시보드에 연결돼 있다
- [ ] Job/CronJob·DaemonSet 등 예외 리소스의 처리 원칙이 문서화돼 있다
- [ ] 사고 대응 시나리오에 "메시 장애" 항목이 포함돼 있다
12. Zero Trust의 적용 범위
서비스 메시는 Zero Trust의 "동서(East-West)" 트래픽만 담당합니다. 사용자 → 서비스의 "남북(North-South)"은 별도 레이어가 필요합니다.
| 구간 | 해답 | 본 글 범위 |
|---|---|---|
| User → API | IdP·OAuth·MFA·디바이스 포스처 | 해당 없음 |
| API Gateway → 서비스 | JWT 검증 + 메시 AuthZ | 일부 |
| 서비스 ↔ 서비스 | mTLS + AuthorizationPolicy | 중심 |
| 서비스 → DB/큐 | Workload Identity + KMS | #133 |
| 배포 파이프라인 | OIDC·서명·SLSA | #132 |
그림이 퍼즐처럼 맞아 들어갑니다. OWASP 기본기 → SBOM·서명 → Secrets → 서비스 간 mTLS·AuthZ가 한 줄로 연결됩니다.
마치며
Zero Trust 네트워킹 실전의 핵심을 정리합니다.
- "내부망이면 신뢰"는 이미 깨진 전제입니다. 공급망 공격과 토큰 유출 사례가 반복 증명했고, 2026년 규제 환경에서는 default-deny + 요청별 신원·인가가 사실상 기본값입니다. 네트워크 위치가 아니라 신원이 신뢰의 단위입니다.
- mTLS는 애플리케이션이 아니라 인프라 레이어에서 처리해야 확장됩니다. 인증서 로테이션·CA 배포·언어별 trust store 관리를 서비스마다 반복하면 운영 비용이 폭증합니다. 사이드카·앰비언트·eBPF 중 조직 성숙도에 맞춰 선택하세요.
- Istio와 Linkerd의 선택은 기능 범위가 아니라 운영 복잡도로 결정됩니다. 트래픽 관리·egress 제어까지 필요하면 Istio, mTLS와 관측성만 빠르게 받고 싶으면 Linkerd. 초기부터 앰비언트 또는 Cilium으로 가는 것도 현실적 선택입니다.
- SPIFFE/SPIRE는 혼합 환경의 신원 표준입니다. 쿠버네티스 단일 환경에서는 과잉이지만, VM·Lambda·온프레미스가 섞인 조직에는 대안이 없습니다. #133 Workload Identity와 같은 철학의 확장판입니다.
- L7 AuthorizationPolicy가 진짜 Zero Trust의 레이어입니다. mTLS만으로는 "누구인지"만 증명되고 "무엇을 할 수 있는지"는 여전히 열린 상태입니다. default-deny 위에 ServiceAccount·경로·메서드 단위 화이트리스트를 쌓아야 마이크로세그먼트가 완성됩니다.
- 도입은 PERMISSIVE 단계에서 오래 머물러야 합니다. 메트릭으로 예외 워크로드를 다 찾아낸 뒤 STRICT로 전환해야 사고를 피합니다. 한 네임스페이스에서 4~6주는 PERMISSIVE로 운영하는 것이 실전 권장 기간입니다.
OWASP(#131) → SBOM/Sigstore(#132) → Secrets(#133) → Zero Trust 메시로 애플리케이션 보안의 네 기둥이 섰습니다. 다음 편에서는 이 위에 런타임 위협 탐지 - eBPF·Falco·Tetragon을 얹어, 서명된 이미지가 예상과 다르게 움직일 때 어떻게 탐지하고 차단하는지를 다룰 예정입니다. "만들 때·보낼 때"에 이어 "실행 중" 단계의 방어선입니다.
'최신 트렌드' 카테고리의 다른 글
| GPT-5.5 출시 완전 정리 - Codex에 얹힌 '실무용 새 인텔리전스'와 7주 만의 역대 최단 업그레이드 (0) | 2026.04.24 |
|---|---|
| 런타임 위협 탐지 실전 - eBPF·Falco·Tetragon으로 Pod 안에서 벌어지는 일 보기 (0) | 2026.04.24 |
| Secrets Management 실전 - Vault·AWS Secrets Manager로 런타임 크리덴셜 안전하게 다루기 (0) | 2026.04.23 |
| SBOM과 Sigstore 실전 - 공급망 공격 시대의 A08 방어선 구축 (0) | 2026.04.23 |
| OWASP 오픈소스 완벽 가이드 - Top 10·ZAP·Dependency-Check로 백엔드 보안 기본기 다지기 (4) | 2026.04.23 |