본문으로 건너뛰기

보안

Kubernetes 컨트롤러는 클러스터 리소스를 생성하고 수정하는 권한을 가집니다. 침해 시 영향 범위가 넓으므로, 설계 단계에서부터 최소 권한과 격리를 적용합니다.

위협 모델

컨트롤러가 침해되면 어떤 일이 벌어지는지 먼저 이해합니다.

시나리오공격 경로영향
Pod 침해취약한 의존성, 컨테이너 탈출ServiceAccount 토큰으로 API 서버 접근
RBAC 과잉ClusterRole에 * verb침해 시 클러스터 전체 리소스 조작
이미지 변조레지스트리 침해, 태그 재사용악성 코드 실행
네트워크 노출admission webhook 포트 공개외부에서 webhook 엔드포인트 접근

핵심 원칙: 침해가 발생해도 영향 범위를 최소화하는 것이 목표입니다.

RBAC 설계

ClusterRole vs Role

구분ClusterRoleRole
범위클러스터 전체특정 네임스페이스
용도CRD 정의, 클러스터 스코프 리소스네임스페이스 내 리소스 관리
위험도높음 — 모든 네임스페이스에 영향낮음 — 해당 네임스페이스만

가능하면 Role + RoleBinding으로 범위를 제한합니다. CRD 정의 자체는 클러스터 스코프이므로 CRD 등록용과 런타임 운영용을 분리합니다.

CRD 분리 전략

CRD 등록용 — CI/CD 파이프라인에서 실행
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: my-controller-crd-admin
rules:
- apiGroups: ["apiextensions.k8s.io"]
resources: ["customresourcedefinitions"]
verbs: ["create", "get", "list", "patch"]
런타임용 — 컨트롤러 Pod의 ServiceAccount
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: my-controller-runtime
rules:
# 주 리소스 (CRD)
- apiGroups: ["example.com"]
resources: ["documents"]
verbs: ["get", "list", "watch", "patch"]
- apiGroups: ["example.com"]
resources: ["documents/status"]
verbs: ["patch"]
# 자식 리소스
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch", "create", "patch", "delete"]
# 이벤트 기록
- apiGroups: ["events.k8s.io"]
resources: ["events"]
verbs: ["create"]

최소 권한 원칙

규칙설명
verb 최소화get, list, watch만 필요한 리소스에 *를 주지 않습니다
resource 명시resources: ["*"] 대신 정확한 리소스를 나열합니다
status 분리주 리소스 patchstatus 서브리소스 patch를 별도 rule로 관리합니다
apiGroup 제한빈 문자열("")은 core API만, 필요한 group만 명시합니다
필요한 RBAC 파악하기

컨트롤러가 실제로 어떤 API를 호출하는지 audit log를 켜서 확인합니다. kubectl auth can-i --list --as=system:serviceaccount:ns:sa-name으로 현재 권한을 점검합니다.

컨테이너 보안

SecurityContext

deployment.yaml
spec:
containers:
- name: controller
securityContext:
runAsNonRoot: true
runAsUser: 65534
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault

컨트롤러는 네트워크 호출(API 서버) 외에 시스템 권한이 필요하지 않습니다. 모든 capability를 제거하고 읽기 전용 파일시스템으로 실행합니다.

최소 이미지

Rust의 정적 링킹을 활용하면 극도로 작은 이미지를 만들 수 있습니다.

Dockerfile (musl 정적 링킹)
FROM rust:1.88 AS builder
RUN rustup target add x86_64-unknown-linux-musl
WORKDIR /app
COPY . .
RUN cargo build --release --target x86_64-unknown-linux-musl

FROM scratch
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/controller /controller
# TLS 루트 인증서 (API 서버 연결용)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
USER 65534
ENTRYPOINT ["/controller"]
베이스 이미지크기공격 표면
ubuntu:24.04~78MB쉘, 패키지 관리자 포함
gcr.io/distroless/static~2MB쉘 없음, C 라이브러리 없음
scratch0MB바이너리만 존재

scratch를 쓸 때는 TLS 인증서를 수동으로 복사해야 합니다. kube의 Client는 API 서버에 TLS로 연결하므로 루트 인증서가 반드시 필요합니다.

musl과 TLS 크레이트

rustls feature를 사용하면 OpenSSL 의존성 없이 순수 Rust TLS가 가능합니다. openssl-tls feature는 musl 환경에서 추가 설정이 필요합니다.

kube = { version = "3.0.1", features = ["runtime", "derive", "rustls-tls"] }

NetworkPolicy

컨트롤러 Pod는 API 서버와만 통신하면 됩니다. deny-all 기본 정책에 필요한 egress만 허용합니다.

networkpolicy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: controller-netpol
namespace: controller-system
spec:
podSelector:
matchLabels:
app: my-controller
policyTypes: ["Ingress", "Egress"]
# Ingress: 기본 deny (admission webhook 사용 시만 허용)
ingress: []
egress:
# API 서버
- to:
- ipBlock:
cidr: 0.0.0.0/0 # API 서버 IP로 좁히면 더 좋음
ports:
- protocol: TCP
port: 443
# DNS
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53

Admission webhook를 사용하면 API 서버에서 오는 ingress를 추가로 허용해야 합니다:

webhook용 ingress 추가
ingress:
- from:
- ipBlock:
cidr: 0.0.0.0/0 # API 서버 IP로 제한
ports:
- protocol: TCP
port: 8443 # webhook 리스닝 포트

공급망 보안

의존성 감사

# 알려진 취약점 검사
cargo audit

# 라이선스 · 중복 · 금지된 크레이트 검사
cargo deny check

cargo-deny는 CI에 통합하여 매 빌드마다 실행합니다. deny.toml에서 허용할 라이선스와 금지할 크레이트를 지정합니다.

deny.toml (예시)
[licenses]
allow = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC"]

[bans]
deny = [
# openssl 대신 rustls 사용
{ name = "openssl-sys" },
]

SBOM 생성

# cargo-cyclonedx로 CycloneDX BOM 생성
cargo cyclonedx --format json

컨테이너 이미지 레벨에서는 syfttrivy로 SBOM을 생성합니다.

이미지 서명과 검증

# cosign으로 이미지 서명
cosign sign --key cosign.key ghcr.io/org/controller:v1.0.0

# 배포 시 서명 검증 (Kyverno 정책 또는 admission controller)

태그 대신 digest(@sha256:...)로 이미지를 참조하면 이미지 변조 위험을 줄일 수 있습니다.

ServiceAccount 토큰 관리

Kubernetes 1.22+에서는 바운드 토큰이 기본입니다. 추가 주의사항:

설정이유
automountServiceAccountToken: false필요 없는 Pod에 토큰 마운트 방지
expirationSeconds: 3600토큰 수명 제한 (기본 1시간)
전용 ServiceAccountdefault SA를 쓰지 않습니다

kube의 Client::try_default()는 마운트된 토큰을 자동으로 사용하며, 만료 시 토큰 갱신도 자동입니다.

보안 체크리스트

항목확인
RBAC에 * verb나 * resource가 없는가?
CRD 등록과 런타임 권한이 분리되었는가?
SecurityContext에 runAsNonRoot, readOnlyRootFilesystem 설정했는가?
capability ALL drop 설정했는가?
NetworkPolicy로 불필요한 트래픽을 차단했는가?
cargo audit / cargo deny가 CI에 통합되었는가?
이미지에 digest 핀이 적용되었는가?
admission webhook 사용 시 TLS 인증서를 자동 갱신하는가?