K8s Security - 08, mtls supply-chain
전송 구간 암호화와 이미지 서명이 막는 다른 위협 — mTLS와 공급망 보안
한 팀이 두 가지 사고를 겪었다. 첫째, 같은 클러스터 안의 두 서비스가 평문으로 통신하고 있었는데 노드가 탈취됐다. 공격자는 그 노드에서 패킷을 가로채 비밀을 빼냈다. "내부니까 안전하다"는 가정이 무너진 순간이었다. 둘째, 같은 팀이 배포 파이프라인을 점검하다가 레지스트리의 이미지가 빌드한 것과 다르다는 걸 발견했다. 중간에 누군가 바꿔치기한 것이다. 두 사고는 전혀 다른 공격 경로에서 왔고, 그래서 방어도 다른 두 축이 필요하다 — 전송을 암호화하는 mTLS와, 이미지 출처를 증명하는 서명(cosign/Sigstore). 이 글은 이 두 축이 각각 무엇을 막고, 어떻게 공급망 보안의 한 벽돌이 되는지를 따진다.
전송과 출처는 다른 공격 경로다
세 가지 위협이 자주 뒤섞이므로 먼저 갈라놓는다.
| 축 | 막는 위협 | 언제 |
|---|---|---|
| mTLS | 패킷 도청·변조·위장 | 통신이 이동 중일 때 |
| 이미지 서명 | 위조 이미지 배포 | 이미지가 빌드→레지스트리→노드로 흐를 때 |
| SBOM + 스캔 | 알려진 취약점 포함 이미지 | 이미지 안의 구성 요소에 CVE가 있을 때 |
한 축만으로 다른 축의 위협은 못 막는다. mTLS를 켠다고 위조 이미지가 걸리지 않고, 이미지에 서명한다고 통신 도청이 막히지 않는다. 그래서 "공급망 보안"이란 세 축이 각자 자기 공격 경로를 담당하는 일이다.
mTLS — 내부 통신을 왜 암호화하는가
mTLS의 핵심은 서버만 인증하는 일반 HTTPS와 달리 양쪽이 서로 인증서로 상대를 인증한다는 점이다. 서비스 A가 서비스 B를 호출할 때 B는 A의 인증서를, A는 B의 인증서를 검증한다. 도청자는 암호문만 보고, 변조를 시도하면 TLS 검증에서 거부된다.
왜 "내부" 통신까지 암호화해야 하나. 노드가 하나라도 탈취되면 그 노드에서 돌아가는 패킷 가로채기는 공짜다. "클러스터 내부는 믿을 수 있다"는 성벽 모델(castle-and-moat)의 전제가 깨지는 순간이 바로 노드 침해다. PCI-DSS·HIPAA 같은 규정과 zero-trust 모델이 내부 mTLS를 요구하는 이유가 여기에 있다.
서비스 메시(Istio·Cilium)가 mTLS를 다루는 방식은 "앱이 모르게"다. 사이드카나 eBPF가 트래픽을 가로채 mTLS를 대신 종료·설정하므로 앱 코드는 바뀌지 않는다. Istio에선 PeerAuthentication 하나로 namespace 전체의 mTLS 모드를 정한다. (Istio docs - Security)
# Istio — namespace 기본 mTLS 강제
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: default
namespace: app
spec:
mtls:
mode: STRICT # PERMISSIVE(점진 전환) / STRICT(강제)
PERMISSIVE는 평문과 mTLS를 둘 다 받아들이는 전환기용 모드이고, STRICT는 mTLS만 허용한다. Istio는 또 클라이언트가 mTLS를 지원하면 자동으로 mTLS로 올리는 자동 mTLS 기능이 있어, 서버측 STRICT 없이도 대부분의 내부 트래픽이 암호화된다.
인증서 생명주기 — 짧은 수명과 자동 갱신이 곧 보안이다
mTLS가 작동하려면 양쪽이 서로의 인증서를 신뢰해야 한다. 그런데 인증서는 만료된다. 서비스가 수백 개인 클러스터에서 이걸 사람이 관리할 수는 없다. 메시의 컨트롤 플레인(Istio의 Istiod 등)이 이 생명주기를 자동화한다.
flowchart LR
CP["컨트롤플레인<br/>(Istiod / SPIRE)"] -->|"발급"| S1["서비스1 인증서"]
CP -->|"발급"| S2["서비스2 인증서"]
S1 -. 만료 임박 .-> CP
CP -->|"자동 갱신"| S1
S1 == "mTLS<br/>(인증서 상호 검증)" ==> S2
메시가 발급하는 워크로드 인증서 수명은 보통 수 시간에서 24시간이다. 이렇게 짧은 이유는 침해 시 노출 창을 최소화하기 위해서다. 인증서가 탈취돼도 몇 시간 뒤면 만료한다. 1년짜리 인증서는 한 번 탈취되면 취소되기 전까지 계속 유효하므로 위험이 크다. 이것이 07장의 cert-manager(앱용 인증서, 보통 90일 갱신)와 메시 mTLS 인증서가 다른 점이다 — 메시 쪽이 더 짧고 더 자동이다.
이미지 서명 — 배포되는 것이 정말 빌드한 것인가
배포 파이프라인이 이미지를 빌드해 레지스트리에 푸시한다. 그 사이에 이미지가 바뀌면, 팀이 푸시한 것과 클러스터가 받는 것이 달라진다. cosign(Sigstore 프로젝트)이 이를 막는다 — 이미지에 서명하고, 클러스터 측에서 그 서명을 검증한다. (Sigstore / cosign)
# 빌드 시 서명 (키 기반)
cosign sign --key cosign.key myregistry/app:v1.2.3
# 배포 시 검증 (클러스터 어드미션 또는 별도 컨트롤러)
cosign verify --key cosign.pub myregistry/app:v1.2.3
서명만 하면 끝이 아니다. 클러스터 어드미션 단(04장의 ValidatingAdmissionPolicy, 또는 Sigstore policy-controller)에서 검증을 통과한 이미지만 배포하도록 정책을 걸어야 서명이 의미를 갖는다. (Kubernetes docs - Admission controllers)
flowchart LR
BUILD["CI: 이미지 빌드"] --> SIGN["cosign 서명"]
SIGN --> REG["레지스트리<br/>(이미지 + 서명)"]
REG -. 배포 .-> ADM["클러스터 어드미션"]
ADM -->|"서명 검증"| POLICY{"정책"}
POLICY -->|통과| DEPLOY["배포"]
POLICY -->|실패| DENY["거부"]
keyless 서명 — 키 관리 부담을 없앤 방식
cosign엔 두 모드가 있다. 키 기반(cosign.key/.pub 쌍으로 서명·검증)은 단순하지만 비밀키 관리·유출이 부담이다. 최근 강조되는 keyless 모드는 서명키 자체가 없다. 대신 OIDC 신원(GitHub Actions 워크플로 ID 등)으로 서명한다. (Sigstore - Keyless signing overview)
keyless가 작동하는 과정:
- cosign이 임시 공개/개인키 쌍을 메모리에서 만든다.
- OIDC ID 토큰(GitHub Actions면
https://token.actions.githubusercontent.com발급)을 가져온다. - Fulcio(Sigstore의 공개 CA)가 OIDC 토큰을 검증하고, 그 신원을 SAN에 넣은 단기 서명 인증서를 공개키에 묶어 발급한다.
- 인증서로 아티팩트에 서명한다.
- 서명·인증서·해시를 Rekor(변경 불가 투명 로그)에 타임스탬프와 함께 기록한다.
- 임시 개인키는 즉시 파기하고, 인증서는 수분 안에 만료한다.
이 과정이 신뢰를 만드는 두 기둥은 Fulcio와 Rekor다. Fulcio는 "이 서명은 이 OIDC 신원(예: 이 GitHub 워크플로)이 했다"는 인증서를 발급하고, Rekor는 그 서명 이벤트를 누구나 검증 가능한 변경 불가 로그에 남긴다. 비밀키 유출 우려도, 키 순환 부담도 없다. "이 이미지가 언제 누구에 의해 서명됐나"를 제3자가 독립적으로 확인할 수 있다는 게 keyless의 핵심 혁신이다.
SBOM + 취약점 스캔 — 이미지 안의 구성 요소
서명은 이미지가 원본임을 증명할 뿐, 그 원본이 안전하다는 뜻은 아니다. 원본 이미지 안의 오래된 log4j에 CVE가 있으면 서명된 채로 배포된다. SBOM(Software Bill of Materials)이 이미지의 구성 요소 목록을 만들고, Trivy·Grype 같은 도구가 그 목록을 CVE 데이터베이스와 대조해 취약점을 찾는다. (Trivy)
가치는 단발 스캔이 아니라 지속 매칭에 있다. 이미지를 빌드할 때 SBOM을 만들어 두면, 나중에 새 CVE가 발표됐을 때 "내 클러스터의 이미지 중 이 취약 버전을 쓰는 게 있나"를 SBOM 기반으로 즉시 답할 수 있다. 매칭되면 알림 → 패치된 이미지로 재빌드. 이 "SBOM + CVE DB + 자동 매칭" 루프가 공급망 보안의 운영 사이클이다.
SLSA — 공급망 성숙도를 등급으로 따지다
SLSA(Supply-chain Levels for Software Artifacts)는 공급망 보안의 성숙도를 등급으로 매긴 프레임워크다. 주의할 점 — 예전 문서에서 자주 보이는 "SLSA 1-4"는 v1.0(2023)에서 폐기됐다. 현재 스펙은 빌드 무결성에 집중한 Build 트랙 L0-L3로 개편됐다. (SLSA - Build track)
| Build 레벨 | 요구 사항 | 막는 것 |
|---|---|---|
| L0 | (없음) | — |
| L1 | provenance(빌드 출처 증명)가 존재 | 실수·문서화 부족 |
| L2 | 호스티드 빌드 플랫폼이 provenance를 서명 | 빌드 후 변조 |
| L3 | 강화된 빌드 플랫폼(빌드 간 격리, 서명키 보호) | 빌드 중 변조 |
구 SLSA 1-4가 대략 Build L1-L3로 옮겨갔고, 구 L4(재현 가능 빌드 등)는 제외됐다. 실제 빌더의 수준은 공식 매트릭스를 따른다 — GitHub Actions과 Google Cloud Build는 Build L3, FRSCA는 L2로 알려져 있다. (SLSA - Get started)
도구(Sigstore)와 등급(SLSA)의 관계를 헷갈리면 안 된다. Sigstore(cosign/Fulcio/Rekor)는 provenance에 서명하고 검증하게 해 주는 도구다. "Sigstore를 깔았으니 SLSA L3"가 아니라 — 도구가 L2·L3의 요구(서명된 provenance, 격리된 빌드)를 충족하게 돕되, 등급 자체는 빌드 플랫폼의 격리·신뢰성에 달린다.
SPIFFE — 메시 신원의 표준
메시 mTLS의 "신원"은 인증서의 SAN에 든 URI 하나로 표현된다. 이 형식을 표준화한 게 SPIFFE다. (SPIFFE ID spec)
spiffe://<trust-domain>/<path>
trust domain은 신원을 발급하는 신뢰 영역(예: Istio 기본값 cluster.local)이고, path가 그 안의 워크로드를 식별한다. Istio의 형식은 spiffe://cluster.local/ns/<namespace>/sa/<serviceaccount>다. 이 신원을 담은 문서가 SVID(SPIFFE Verifiable Identity Document)이며, 보통 X.509 인증서의 URI SAN 또는 JWT로 쓰인다.
이 표준화의 의미는 "인증서 = 신원"이 된다는 것이다. 과거엔 인증서가 연결 암호화 수단이었지만, 메시에선 인증서가 서비스 신원 그 자체다. "이 요청은 spiffe://cluster.local/ns/app/sa/web-sa에서 왔다"를 암호학적으로 증명할 수 있다. 이것이 다음 글(09장)에서 신원 기반 인가 정책이 IP가 아닌 신원으로 평가하는 근거가 된다.
mTLS가 막지 못하는 것 — 인가가 그 위에 필요하다
mTLS는 강력하지만 만능이 아니다. 정확히 짚으면:
- 막는다: 통신 도청, 중간자 변조, 신원 없는 서비스의 위장 접근.
- 못 막는다: 인증받은 서비스가 악의적으로 행동하는 것(내부자 위협), 앱 자체 취약점, 권한 상승 뒤 합법적 신원으로 접근하는 것.
mTLS는 "통신 채널의 신원"을 보증할 뿐 "통신 내용의 안전"을 주지 않는다. 침해된 web-sa가 mTLS로 합법적으로 db-sa에 접근해 비밀을 빼가는 건 mTLS가 *허용한다 — 신원은 맞으니까. 이걸 막으려면 mTLS(신원) 위에 *인가 정책(AuthorizationPolicy)이 또一层 필요하다. 신원 + 권한이 함께야 심층 방어가 성립한다.
직접 확인하기
# Kubernetes 1.36 — 서명 없는 이미지 배포 시 어드미션이 거부하는지
# (Sigstore policy-controller 또는 cosigned admission 설치·정책 설정 후)
kubectl run x --image=untrusted/app:v1
확인할 것: 정책이 켜져 있으면 서명 없는 이미지가 denied로 거부된다.
# Istio 설치 클러스터에서 mTLS 적용 상태 (Istio 공식 문서 기반 명령)
istioctl authn tls-check app.default.svc.cluster.local
cosign/Sigstore 파이프라인 구축과 Istio 세부 플래그는 각 공식 문서로 실측.
실무 도입의 우선순위
세 축(mTLS·서명·SBOM)을 한 번에 도입하긴 어렵다. 현실적 순서:
- etcd 암호화 + 외부 비밀 — 가장 기본. 평문 비밀 제거.
- 이미지 서명(cosign) — 공급망. 신뢰된 이미지만 배포하도록 어드미션 정책.
- mTLS — 메시 도입 뒤. 서비스 간 암호화.
- SBOM + 스캔 — CI에 지속적으로 통합.
"기본 보호 → 공급망 → 통신 암호화 → 지속 스캔"의 점진 강화다. 한 번에 다 끼얹으면 복잡도가 폭발한다.
흔히 묻는 것, 흔히 틀리는 것
| 오해 | 정정 |
|---|---|
| "mTLS와 이미지 서명은 같다" | 다른 위협(도청 vs 위조). 별개 계층 |
| "내부 통신은 암호화 불필요" | 노드 침해 시 도청 가능. zero-trust는 내부 mTLS 요구 |
| "이미지 서명은 빌드만 하면 된다" | 클러스터 어드미션 검증이 짝. 둘 다 있어야 |
| "mTLS는 일반 HTTPS와 같다" | mTLS는 상호 인증. 서버만 인증하는 HTTPS와 다름 |
| "SBOM은 빌드 후 쓸모없다" | 배포 전 스캔의 기반. 새 CVE 발표 시 실시간 매칭 |
| "SLSA 4가 최고 등급이다" | 구 SLSA 1-4는 v1.0에서 폐지. 현재는 Build L0-L3 |
| "Sigstore 깔면 SLSA L3" | 도구가 요구를 돕될 뿐. 등급은 빌드 플랫폼 격리에 달림 |
요약 — 이 글의 결론
- mTLS와 이미지 서명은 다른 위협을 막는 별개 축. mTLS는 통신 도청·변조(전송), 서명은 위조 이미지(출처/무결성), SBOM은 취약 구성 요소.
- mTLS는 양쪽이 상호 인증. 메시(Istio/Cilium)가 사이드카/eBPF로 앱 몰래 자동화하고, 인증서는 짧은 수명(수 시간) + 자동 갱신이 곧 보안.
- cosign/Sigstore가 이미지에 서명하고 어드미션이 검증. keyless 모드는 Fulcio(OIDC→단기 인증서) + Rekor(변경 불가 투명 로그)로 키 관리 부담을 없애면서 투명성을 확보.
- SBOM + 스캔(Trivy/Grype)이 이미지 구성 요소의 CVE를 지속 매칭. 단발이 아니라 새 CVE마다 즉시 영향 평가.
- SLSA는 공급망 성숙도 등급. 구 "1-4"는 폐지됐고 현재는 Build L0-L3. Sigstore는 그 요구를 돕는 도구일 뿐.
- SPIFFE(
spiffe://trust-domain/path)가 메시 신원을 표준화 — 인증서 = 서비스 신원. 이게 09장 신원 기반 인가의 기반. - mTLS는 못 막는 게 있다(내부자 위협). 신원 위에 인가 정책이 또 필요하다.
생각해 볼 문제
- 노드가 탈취됐다. mTLS 없는 평문 통신이었다. 공격자가 얻을 수 있는 것은? mTLS가 있었다면?
- 빌드는 서명했는데 클러스터 어드미션 검증을 안 켰다. 의미가 있나?
- keyless 서명에서 Fulcio가 발급한 인증서가 10분 뒤 만료한다. 그래도 서명이 검증 가능한 이유는? (Rekor의 역할)
- 메시 없이 mTLS를 구현하려 한다. 앱 부담은? (언어/프레임워크별)
- SBOM으로 CVE를 찾았다. 이미 배포된 이미지엔 그 취약점이 있다. 어떻게 대응하나?
- cosign 키 기반 모드의 비밀키가 유출됐다. 공급망 보안에 어떤 영향? keyless였다면 달랐을까?
- Istio의 자동 mTLS가 켜져 있는데
PeerAuthentication을STRICT로 안 설정했다. 보안상 차이는?
참고
- Istio docs - Security concepts (mTLS / PeerAuthentication) - 접근 2026-07-16
- Sigstore - Cosign keyless signing overview - 접근 2026-07-16 (Fulcio / Rekor / OIDC)
- Sigstore - Certificate issuing overview (Fulcio) - 접근 2026-07-16
- SLSA - Build track (v1.2, L0-L3) - 접근 2026-07-16 (구 SLSA 1-4 폐지)
- SLSA - Get started (빌더별 수준) - 접근 2026-07-16
- SPIFFE ID specification - 접근 2026-07-16
- Trivy - 취약점 스캔 - 접근 2026-07-16
- Kubernetes docs - Admission controllers (이미지 정책) - 접근 2026-07-16
'Tech Artifacts > K8s Security' 카테고리의 다른 글
| K8s Security - 09. network security (0) | 2026.07.16 |
|---|---|
| K8s Security - 07. cert-manager (0) | 2026.07.16 |
| K8s Security - 06. secrets management (0) | 2026.07.16 |
| K8s Security - 05. pod-security-admission (0) | 2026.07.16 |
| K8s Security - 04. admission-control (0) | 2026.07.16 |