[5주차] 마이크로서비스 통신 보안 : 자동 mTLS
🔐 Istio로 자동 mTLS 쉽게 이해하기
🔒 9.2 자동 mTLS: 서비스 간 안전한 통신
- Istio에서는 sidecar proxy가 주입된 서비스 간 트래픽이 기본적으로 암호화되고 상호 인증(Mutual Authentication)됩니다. 이는 mTLS를 통해 구현되는데, mTLS는 두 서비스가 서로 신뢰할 수 있는 인증서를 제시해 신원을 확인하고 데이터를 암호화하는 방식입니다.
- 과거에는 인증서를 수동으로 발급하고 갱신하는 과정에서 오류가 자주 발생해 서비스 장애로 이어졌습니다. Istio는 이 과정을 자동화해 오류를 줄이고 보안을 강화합니다. Istio의 컨트롤 플레인은 서비스에 필요한 인증서를 발급하고 주기적으로 갱신합니다.
- 하지만 Istio가 "기본적으로 안전하다" 해도, 완전한 보안을 위해 추가 설정이 필요합니다:
- 상호 인증된 트래픽만 허용: 기본적으로 Istio는 암호화되지 않은 트래픽도 허용해 서비스 마이그레이션을 쉽게 하지만, 이를 제한해 보안을 강화할 수 있습니다.
- 최소 권한 원칙(Principle of Least Privilege): 각 서비스에 필요한 최소한의 접근 권한만 부여해, 인증서가 유출되더라도 피해를 최소화합니다.
Istio는 mTLS를 통해 서비스 간 통신을 암호화하고 상호 인증을 자동화하며,
최소 권한 원칙으로 보안을 강화합니다.
🛠️ 9.2.1 환경 설정하기
mTLS의 기능을 보여주기 위해 간단한 환경을 설정하겠습니다. 이 환경에는 세 가지 서비스가 포함됩니다:
- webapp: 웹 애플리케이션 서비스.
- catalog: 데이터 카탈로그 서비스.
- sleep: sidecar proxy가 없는 레거시 워크로드로, mTLS를 지원하지 않습니다.
- 그 다음, 서비스를 설치합니다:
(⎈|kind-myk8s:N/A) $ kubectl apply -f services/catalog/kubernetes/catalog.yaml -n istioinaction
serviceaccount/catalog created
service/catalog created
deployment.apps/catalog created
(⎈|kind-myk8s:N/A) $ kubectl apply -f services/webapp/kubernetes/webapp.yaml -n istioinaction
serviceaccount/webapp created
service/webapp created
deployment.apps/webapp created
(⎈|kind-myk8s:N/A) $ kubectl apply -f services/webapp/istio/webapp-catalog-gw-vs.yaml -n istioinaction
gateway.networking.istio.io/coolstore-gateway created
virtualservice.networking.istio.io/webapp-virtualservice created
- default 네임스페이스에 sleep 앱 배포
(⎈|kind-myk8s:N/A) $ cat ch9/sleep.yaml
...
spec:
serviceAccountName: sleep
containers:
- name: sleep
image: governmentpaas/curl-ssl
command: ["/bin/sleep", "3650d"]
imagePullPolicy: IfNotPresent
volumeMounts:
- mountPath: /etc/sleep/tls
name: secret-volume
volumes:
- name: secret-volume
secret:
secretName: sleep-secret
optional: true
---
(⎈|kind-myk8s:N/A) $ kubectl apply -f ch9/sleep.yaml -n default
serviceaccount/sleep created
service/sleep created
deployment.apps/sleep created
- 설치가 완료되었는지 확인하기 위해, sleep 서비스에서 webapp 서비스로 암호화되지 않은(clear-text) 요청을 보냅니다:
(⎈|kind-myk8s:N/A) $ kubectl exec deploy/sleep -c sleep -- curl -s webapp.istioinaction/api/catalog -o /dev/null -w "%{http_code}\n"
200
- 결과: 200
- 서비스가 정상적으로 설정되었고, webapp이 암호화되지 않은 요청을 수락했음을 보여줍니다.
Istio는 기본적으로 암호화되지 않은 트래픽을 허용해 서비스 마이그레이션을 용이하게 하지만, 이를 제한하려면 PeerAuthentication 리소스를 사용해 mTLS를 강제할 수 있습니다.
- 서비스가 정상적으로 설정되었고, webapp이 암호화되지 않은 요청을 수락했음을 보여줍니다.
테스트 환경에서 webapp, catalog, sleep 서비스를 설정하고,
Istio의 기본 설정이 암호화되지 않은 트래픽을 허용함을 확인했습니다.
📌 핵심 요약
- Istio는 mTLS를 통해 서비스 간 트래픽을 자동으로 암호화하고 상호 인증합니다.
- 컨트롤 플레인이 인증서를 발급 및 갱신해 수동 관리의 오류를 줄입니다.
- 최소 권한 원칙을 적용해 인증서 유출 시 피해를 최소화합니다.
- 기본적으로 암호화되지 않은 트래픽이 허용되지만, PeerAuthentication 리소스로 이를 제한할 수 있습니다.
- 테스트 환경에서 webapp, catalog, sleep 서비스를 설정해 mTLS 동작을 확인했습니다.
🔐 Istio PeerAuthentication 쉽게 이해하기
PeerAuthentication은 서비스 간 트래픽의 보안을 설정하는 강력한 도구로, mTLS(Mutual TLS)를 강제하거나 암호화되지 않은 트래픽을 허용하는 방식으로 서비스 메시의 보안을 관리합니다.
🔒 9.2.2 PeerAuthentication 리소스 이해하기
- PeerAuthentication 리소스는 서비스가 mTLS를 엄격하게 요구할지(STRICT 모드), 아니면 암호화되지 않은(clear-text) 트래픽도 허용할지(PERMISSIVE 모드)를 설정합니다. 이 설정은 다음 세 가지 범위로 적용할 수 있습니다:
- Mesh-wide: 서비스 메시 전체 워크로드에 적용.
- Namespace-wide: 특정 네임스페이스의 모든 워크로드에 적용.
- Workload-specific: 특정 워크로드에만 적용.
PeerAuthentication은 mTLS 설정을 통해 서비스 간 트래픽 보안을 관리하며,
메시, 네임스페이스, 워크로드 단위로 적용 가능합니다.
🛡️ 모든 비인증 트래픽 차단: Mesh-wide 정책
- 서비스 메시의 보안을 강화하려면 암호화되지 않은 트래픽을 차단하는 Mesh-wide PeerAuthentication 정책을 설정할 수 있습니다. 이 정책은 STRICT 모드를 사용해 모든 트래픽이 mTLS로 인증되도록 강제합니다.
- Mesh-wide 정책은 다음 두 조건을 만족해야 합니다:
- Istio가 설치된 네임스페이스 에 적용.
- 정책 이름은 default로 설정
- 다음은 Mesh-wide 정책의 예제입니다:
(⎈|kind-myk8s:N/A) $ cat ch9/meshwide-strict-peer-authn.yaml
apiVersion: "security.istio.io/v1beta1"
kind: "PeerAuthentication" #PeerAuthentication은 Istio에서 서비스 간의 인증 정책을 설정하는 리소스
metadata:
name: "default"
namespace: "istio-system" #리소스가 "istio-system" 네임스페이스에 생성됨
spec:
mtls: #상호 TLS(mTLS) 설정
mode: STRICT #모든 서비스 간 통신이 반드시 mTLS를 사용해야 한다
- 이 정책을 클러스터에 적용합니다:
$ kubectl apply -f ch9/meshwide-strict-peer-authn.yaml -n istio-system
peerauthentication.security.istio.io/default created
- 이제 sleep 서비스(레거시 워크로드, sidecar proxy 없음)에서 webapp으로 암호화되지 않은 요청을 보내 확인해봅니다:
$ kubectl exec deploy/sleep -c sleep -- curl -s http://webapp.istioinaction/api/catalog -o /dev/null -w "%{http_code}\n"
000
command terminated with exit code 56
- command terminated with exit code 56
- 요청이 거부된 것을 확인할 수 있습니다. 이는 STRICT 모드가 암호화되지 않은 트래픽을 차단했기 때문입니다.
하지만, 모든 서비스를 한 번에 STRICT 모드로 전환하는 것은 대규모 프로젝트에서 팀 간 협업이 필요할 수 있습니다. 따라서 점진적으로 보안을 강화하는 PERMISSIVE 모드를 사용할 수 있습니다.
- 요청이 거부된 것을 확인할 수 있습니다. 이는 STRICT 모드가 암호화되지 않은 트래픽을 차단했기 때문입니다.
Mesh-wide PeerAuthentication 정책은 STRICT 모드로 암호화되지 않은 트래픽을 차단하며,
istio-system 네임스페이스에 default 이름으로 적용됩니다.
🌐 비인증 트래픽 허용: Namespace-wide 정책
- Namespace-wide PeerAuthentication 정책은 특정 네임스페이스의 모든 워크로드에 적용됩니다.
예를 들어, istioinaction 네임스페이스에서 PERMISSIVE 모드를 설정하면, 레거시 워크로드(sleep) 같은 비인증 트래픽도 허용됩니다. 이는 Mesh-wide STRICT 정책을 덮어씌웁니다. - 다음은 Namespace-wide 정책 예제입니다:
$ cat << EOF | kubectl apply -f -
apiVersion: "security.istio.io/v1beta1"
kind: "PeerAuthentication"
metadata:
name: "default"
namespace: "istioinaction"
spec:
mtls:
mode: PERMISSIVE
EOF
peerauthentication.security.istio.io/default created
- 요청 실행
$ kubectl exec deploy/sleep -c sleep -- curl -s http://webapp.istioinaction/api/catalog -o /dev/null -w "%{http_code}\n"
200
- 하지만, 모든 워크로드에 PERMISSIVE 모드를 적용하면 보안 취약점이 커질 수 있습니다. 대신, 특정 워크로드(예: webapp)에만 PERMISSIVE 모드를 적용하는 것이 더 안전합니다.
- 다음 실습을 위해 삭제
$ kubectl delete pa default -n istioinaction
peerauthentication.security.istio.io "default" deleted
Namespace-wide 정책은 네임스페이스 단위로 PERMISSIVE 모드를 설정해 비인증 트래픽을 허용하지만,
보안 강화를 위해 특정 워크로드에만 적용하는 것이 좋습니다.
🎯 워크로드별 PeerAuthentication 정책 적용
- 더 세밀한 제어를 위해 Workload-specific PeerAuthentication 정책을 사용해 특정 워크로드에만 PERMISSIVE 모드를 적용할 수 있습니다. 예를 들어, webapp 워크로드에만 비인증 트래픽을 허용하고, catalog 워크로드는 STRICT 모드를 유지합니다.
- 다음은 webapp에 적용하는 정책 예제입니다:
$ cat ch9/workload-permissive-peer-authn.yaml
apiVersion: "security.istio.io/v1beta1"
kind: "PeerAuthentication"
metadata:
name: "webapp"
namespace: "istioinaction"
spec:
selector:
matchLabels:
app: webapp #app=webapp 라벨이 있는 워크로드에만 이 정책을 적용
mtls:
mode: PERMISSIVE #mTLS 모드를 "허용(PERMISSIVE)"으로 설정
#PERMISSIVE 모드는 mTLS와 일반 텍스트 트래픽을 모두 허용합니다.
#즉, TLS로 암호화된 연결과 암호화되지 않은 평문 연결 모두 수락합니다.
- 이 정책을 적용합니다:
$ kubectl apply -f ch9/workload-permissive-peer-authn.yamlsive-peer-authn.yaml
peerauthentication.security.istio.io/webapp created
- 이제 sleep 서비스에서 webapp으로 요청을 다시 보냅니다:
$ kubectl exec deploy/sleep -c sleep -- curl -s http://webapp.istioinaction/api/catalog -o /dev/null -w "%{http_code}\n"
200
$ kubectl -n default exec deploy/sleep -c sleep -- curl -s webapp.istioinaction/api/catalog | jq
[
{
"id": 1,
"color": "amber",
"department": "Eyewear",
"name": "Elinor Glasses",
"price": "282.00"
},
...
- 요청이 성공적으로 처리되었습니다! 이는 webapp이 PERMISSIVE 모드로 설정되어 비인증 트래픽을 허용했기 때문입니다. 반면, catalog는 여전히 STRICT 모드를 유지해 보안을 강화합니다.
$ kubectl exec deploy/sleep -c sleep -- curl -s http://catalog.istioinaction/api/items -o /dev/null -w "%{http_code}\n"
000
command terminated with exit code 56
Workload-specific 정책은 특정 워크로드(예: webapp)에만 PERMISSIVE 모드를 적용해
보안과 유연성을 균형 있게 관리합니다.
⚙️ 추가 mTLS 모드
- STRICT와 PERMISSIVE 외에 두 가지 추가 모드가 있습니다:
- UNSET: 상위 정책(Mesh-wide 또는 Namespace-wide)을 상속합니다.
- DISABLE: 트래픽을 프록시를 거치지 않고 서비스로 직접 보냅니다.
- 이 모드들은 특정 상황에서 유용하지만, 대부분 STRICT 또는 PERMISSIVE를 사용합니다.
UNSET과 DISABLE 모드는 특정 상황에서 유연성을 제공하지만,
STRICT와 PERMISSIVE가 주로 사용됩니다.
🔍 서비스 간 트래픽 도청: tcpdump 사용
- Istio 프록시에는 tcpdump 유틸리티가 포함되어 네트워크 트래픽을 캡처하고 분석할 수 있습니다. 하지만 tcpdump은 권한이 필요하므로, Istio 설치 시 privileged 모드를 활성화해야 합니다:
demo 프로파일 컨트롤 플레인 배포 시 istio-proxy 에 privileged 설정 >> 이미 설정 되어 있음
- webapp Pod이 준비되면, 트래픽을 캡처합니다:
- sudo tcpdump: 루트 권한으로 tcpdump 도구를 실행합니다.
-l: 라인 버퍼링 모드를 사용하여 출력을 즉시 표시합니다.
--immediate-mode: 패킷을 즉시 처리하고 표시합니다.
-vv: 매우 상세한(very verbose) 출력을 제공합니다.
-s 0: 패킷의 전체 내용을 캡처합니다(스냅샷 길이 제한 없음). - (ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0: "페이로드가 있는 TCP 패킷"만 캡처하는 필터
- ip[2:2]: IP 헤더의 3번째와 4번째 바이트를 가져옵니다; IP 패킷의 전체 길이
((ip[0]&0xf)<<2): 첫 번째 바이트의 하위 4비트만 추출하고 왼쪽으로 2비트 시프트하여 바이트 단위로 변환 ; IP 헤더의 길이
((tcp[12]&0xf0)>>2): TCP 헤더의 13번째 바이트를 가져옵니다. > 상위 4비트만 추출 > 오른쪽으로 2비트 시프트; TCP 헤더의 길이
패킷 전체 길이 - IP 헤더 길이 - TCP 헤더 길이 = 데이터 페이로드 길이
계산된 페이로드 길이가 0이 아닌지 확인
이 세 값을 사용하여 패킷의 페이로드 길이를 계산하고, 페이로드가 있는 패킷만 캡처합니다. - and not (port 53): DNS 트래픽(포트 53)은 제외합니다.
- ip[2:2]: IP 헤더의 3번째와 4번째 바이트를 가져옵니다; IP 패킷의 전체 길이
- sudo tcpdump: 루트 권한으로 tcpdump 도구를 실행합니다.
$ kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy \
-- sudo tcpdump -l --immediate-mode -vv -s 0 '(((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0) and not (port 53)'
- 다른 터미널에서 sleep 서비스에서 webapp으로 요청을 보냅니다:
$ kubectl exec deploy/sleep -c sleep -- curl -s webapp.istioinaction/api/catalog -o /dev/null -w "%{http_code}\n"
200
- 첫 번째 터미널에서 캡처된 트래픽을 확인하면, 데이터가 clear-text로 전송된 것을 볼 수 있습니다:
# sleep -> webapp 호출 HTTP
01:10:00.107105 IP (tos 0x0, ttl 63, id 8987, offset 0, flags [DF], proto TCP (6), length 146)
10-10-0-14.sleep.default.svc.cluster.local.53950 > webapp-7685bcb84-tp5fd.http-alt: Flags [P.], cksum 0x14b3 (incorrect -> 0xd6a4), seq 3310570377:3310570471, ack 3138045211, win 502, options [nop,nop,TS val 1463167787 ecr 460000615], length 94: HTTP, length: 94
GET /api/catalog HTTP/1.1
Host: webapp.istioinaction
User-Agent: curl/8.5.0
Accept: */*
# webapp -> catalog 호출 HTTPS
01:10:00.110830 IP (tos 0x0, ttl 64, id 19518, offset 0, flags [DF], proto TCP (6), length 1304)
webapp-7685bcb84-tp5fd.59800 > 10-10-0-12.catalog.istioinaction.svc.cluster.local.3000: Flags [P.], cksum 0x1937 (incorrect -> 0xa3c3), seq 3859477536:3859478788, ack 594120, win 501, options [nop,nop,TS val 388368956 ecr 1794695550], length 1252
# catalog -> webapp 응답 HTTPS
01:10:00.113592 IP (tos 0x0, ttl 63, id 55101, offset 0, flags [DF], proto TCP (6), length 1788)
10-10-0-12.catalog.istioinaction.svc.cluster.local.3000 > webapp-7685bcb84-tp5fd.59800: Flags [P.], cksum 0x1b1b (incorrect -> 0xb29e), seq 1:1737, ack 1252, win 501, options [nop,nop,TS val 1794807257 ecr 388368956], length 1736
# webapp -> sleep 응답 HTTP
01:10:00.114496 IP (tos 0x0, ttl 64, id 26730, offset 0, flags [DF], proto TCP (6), length 662)
webapp-7685bcb84-tp5fd.http-alt > 10-10-0-14.sleep.default.svc.cluster.local.53950: Flags [P.], cksum 0x16b7 (incorrect -> 0x886e), seq 1:611, ack 94, win 509, options [nop,nop,TS val 460000622 ecr 1463167787], length 610: HTTP, length: 610
HTTP/1.1 200 OK
content-length: 357
content-type: application/json; charset=utf-8
date: Wed, 07 May 2025 01:10:00 GMT
x-envoy-upstream-service-time: 5
server: istio-envoy
x-envoy-decorator-operation: webapp.istioinaction.svc.cluster.local:80/*
[{"id":1,"color":"amber","department":"Eyewear","name":"Elinor
...
mTLS를 사용하면 트래픽이 암호화되어 도청이 불가능하지만,
레거시 워크로드는 clear-text로 데이터를 노출할 수 있습니다.
🛡️ 워크로드 신원 확인: SVID 인증서 검증
- 마지막으로, Istio가 발급한 인증서가 유효한 SVID(SPIFFE Verifiable Identity Document) 인지, 그리고 워크로드의 서비스 계정과 일치하는지 확인합니다.
openssl 명령어를 사용해 catalog 서비스의 X.509 인증서를 확인합니다:
$ kubectl -n istioinaction exec deploy/webapp -c istio-proxy \
-- openssl s_client -showcerts \
-connect catalog.istioinaction.svc.cluster.local:80 \
-CAfile /var/run/secrets/istio/root-cert.pem | \
openssl x509 -in /dev/stdin -text -noout
depth=1 O = cluster.local
verify return:1
depth=0
verify return:1
40C757AB307F0000:error:0A00045C:SSL routines:ssl3_read_bytes:tlsv13 alert certificate required:../ssl/record/rec_layer_s3.c:1584:SSL alert number 116
Certificate:
...
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication, TLS Web Client Authentication
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Authority Key Identifier:
A2:42:16:BF:6E:00:5D:D4:75:F9:72:6E:9D:F9:08:1D:CC:E2:2C:F6
X509v3 Subject Alternative Name: critical
URI:spiffe://cluster.local/ns/istioinaction/sa/catalog
...
- SPIFFE ID가 catalog 워크로드의 서비스 계정과 일치함을 확인할 수 있습니다.
X509v3 Subject Alternative Name: critical
URI:spiffe://cluster.local/ns/istioinaction/sa/catalog
- 이제 인증서의 유효성을 검증합니다: (Pod 내부에서 다음 명령어를 실행)
$ kubectl -n istioinaction exec -it deploy/webapp -c istio-proxy -- /bin/bash
istio-proxy@webapp-7685bcb84-tp5fd:/$ openssl verify -CAfile /var/run/secrets/istio/root-cert.pem \
<(openssl s_client -connect \
catalog.istioinaction.svc.cluster.local:80 -showcerts 2>/dev/null)
/dev/fd/63: OK
- 결과: /dev/fd/63: OK => Istio CA가 발급한 인증서가 유효함을 보여줍니다.
Istio가 발급한 SVID 인증서는 워크로드의 서비스 계정과 연결되며,
유효성 검증을 통해 신뢰할 수 있음을 확인할 수 있습니다.
📌 핵심 요약
- PeerAuthentication 리소스는 mTLS를 설정해 서비스 간 트래픽 보안을 관리하며, Mesh-wide, Namespace-wide, Workload-specific 범위로 적용됩니다.
- STRICT 모드는 모든 트래픽에 mTLS를 강제하고, PERMISSIVE 모드는 비인증 트래픽을 허용해 마이그레이션을 지원합니다.
- Workload-specific 정책은 특정 워크로드(예: webapp)에만 PERMISSIVE 모드를 적용해 보안과 유연성을 균형 있게 관리합니다.
- tcpdump로 트래픽을 분석하면, mTLS는 데이터를 암호화하지만 레거시 워크로드는 clear-text로 노출될 수 있습니다.
- SVID 인증서는 SPIFFE ID를 포함하며, openssl로 유효성을 검증해 워크로드 신원을 확인할 수 있습니다.