Secretの参照・Podの作成・実行時の安全性は別の境界
한국어 원문으로 표시합니다.
한 줄 요약
Secret API를 읽을 권한, 파드를 만들 권한, 파드의 실행 보안 설정은 서로 다른 경계입니다. Restricted 파드도 같은 네임스페이스의 Secret을 볼륨으로 읽을 수 있습니다.
왜 이게 필요했나
예시 시나리오입니다. 배포용 서비스 계정에서 get secrets를 지웠습니다.
kubectl auth can-i도 no라고 답하니 비밀에 접근할 수 없다고 보고했습니다.
하지만 그 계정에는 create pods가 남아 있었습니다. 공격자는 Secret을 읽는 API 대신
Secret 볼륨을 가진 파드를 만드는 API를 선택했습니다. kubelet이 볼륨을 준비하자
컨테이너는 평범한 파일 읽기로 값을 얻었습니다. 직접 접근 거절과 간접 접근 차단은 다릅니다.
어떻게 동작하나
세 번 묻는 질문
| 경계 | 묻는 질문 | 이 실습에서 보는 것 |
|---|---|---|
| 인증 | 요청자는 누구인가? | 관리자 인증서와 대리 요청한 runner SA를 구분 |
| 인가 | 이 동사·자원·범위를 허용하는가? | Secret 조회는 거절, app의 Pod 생성은 허용 |
| 어드미션 | 허용된 생성 요청의 객체를 받아들일 것인가? | Restricted 실행 조건을 만족하는 Pod인지 검사 |
Pod 생성 요청의 주체와 Pod의 serviceAccountName은 같은 개념이 아닙니다.
관리자가 runner를 지정한 파드를 만들었다고 해서 runner의 생성 권한을 사용한 것은 아닙니다.
다음 실습은 --as=system:serviceaccount:app:runner로 실제 생성 요청을 보냅니다.
관리자의 impersonate 권한을 이용한 교육용 대리 요청이지 SA 토큰 탈취나 위조가 아닙니다.
Restricted가 막는 것과 남기는 것
Pod Security Standards의 Restricted는 privilege escalation, root 실행 등 컨테이너의
실행 설정을 제한합니다. baseline부터 hostPath·hostPID·hostNetwork·privileged 같은
위험한 설정을 제한합니다. 그러나 Restricted의 허용 볼륨에는 secret과 configMap이
들어 있습니다. 이 정책은 어떤 사용자가 특정 Secret을 업무상 써도 되는지 판단하지 않습니다.
runAsNonRoot: true, capabilities drop ALL, allowPrivilegeEscalation: false,
seccompProfile: RuntimeDefault를 모두 넣어도 컨테이너가 정당하게 마운트받은 파일의
읽기까지 금지되지는 않습니다. automountServiceAccountToken: false도 자동 API 토큰
마운트를 끄는 옵션이지 Secret 볼륨을 막는 옵션은 아닙니다.
그래서 create pods가 항상 노드 root라는 설명도 틀립니다. 같은 권한이라도
어드미션 정책, 선택 가능한 SA, 네임스페이스 범위, 볼륨 종류에 따라 도달 범위가 다릅니다.
pods/exec는 들어갈 수 있는 기존 컨테이너의 권한과 데이터에 영향을 받습니다.
bind·escalate·impersonate도 허용된 자원과 주체 범위를 확인해야 하며,
이 동사가 하나 있다는 이유만으로 무조건 cluster-admin이라고 단정하지 않습니다.
방어는 데이터와 워크로드의 경계를 함께 정한다
서로 신뢰하지 않는 작업은 같은 네임스페이스에 민감한 Secret과 함께 두지 않는 것이
출발점입니다. 워크로드 생성 권한과 선택 가능한 서비스 계정을 최소화하고, 필요하면
별도 어드미션 정책으로 허용된 Secret 참조·SA 선택을 제한합니다. 직접 get 권한에
resourceNames를 걸어도 Pod 생성으로 참조하는 볼륨까지 자동으로 제한되지는 않습니다.
NetworkPolicy는 네트워크 연결 통제이므로 이미 마운트한 로컬 파일 읽기를 막지 않습니다.
PSA는 그 위에서 여전히 필요합니다. 노드로 넘어가는 위험한 실행 설정을 줄이는 통제와 네임스페이스 안의 업무 데이터를 누가 쓸 수 있는지 정하는 통제를 함께 적용해야 합니다. 어느 하나를 켰다는 체크 표시로 다른 경계의 검증을 생략하지 않는 것이 핵심입니다.
저장 암호화도 다른 경계다
Secret의 API data 값은 base64 표현이며 암호화가 아닙니다. 저장 시 암호화를 구성하지
않은 경우 저장소의 데이터가 기밀성을 보장받지 못합니다. 이번 k3s/kine 환경에서는
SQLite 안의 직렬화된 Secret에서 합성 표식을 확인합니다. 모든 배포판이 같은 파일·
직렬화 형식을 쓴다는 뜻은 아니며, 관리형 서비스의 암호화 기본값도 별도로 확인해야 합니다.
EncryptionConfiguration은 첫 제공자로 새 값을 쓰고 뒤 제공자들로 이전 값을 읽습니다. identity를 앞에 두면 새 데이터도 암호화되지 않습니다. 설정을 켜거나 키를 바꿨다고 기존 객체가 자동으로 다시 저장되는 것도 아닙니다. 재작성·검증·옛 읽기 경로 제거가 필요합니다. 정상적으로 인가된 API 요청은 복호화된 값을 받으므로 저장 암호화가 위의 마운트 경로를 차단하지는 않습니다. 키와 백업을 함께 노출하면 암호화의 효과도 잃을 수 있습니다.
현장에서 만나는 모습
예시 점검 순서는 다음과 같습니다. 먼저 특정 SA에 대해 네임스페이스를 지정해
can-i get secrets, can-i create pods를 각각 묻습니다. 다음으로 어떤 Pod를 만들 수
있는지 어드미션 정책과 SA 선택 범위를 봅니다. 마지막으로 생성된 워크로드가 어떤
데이터를 마운트받는지 확인합니다. can-i --list는 출발점이지 모든 외부 인가·어드미션
결과를 완전히 설명하는 증거는 아닙니다. 실제 요청과 실패 원인을 같이 봐야 합니다.
서비스 계정의 자동 토큰 마운트를 꺼도 Pod에 명시한 설정이 우선할 수 있습니다. 이 옵션을 고친다고 이미 실행 중인 모든 Pod가 즉시 바뀌지는 않으므로, 영향을 확인하고 새 Pod로 적용합니다. 토큰 불필요와 Secret 불필요는 따로 판단합니다.
401과 403도 구분해서 읽습니다. 익명 인증이 허용된 경로의 무자격 요청은
system:anonymous 사용자와 system:unauthenticated 그룹으로 처리될 수 있습니다.
따라서 익명 요청이 항상 401인 것은 아닙니다. 경로별 익명 허용, 유효하지 않은 자격증명,
인가 결과에 따라 달라집니다. 403만 보고 어느 어드미션 통제까지 통과했다고 단정하지 말고
응답의 이유와 감사 기록을 함께 확인합니다.
다음 실습에서 할 것
합성 Secret으로 저장 암호화와 재작성을 확인한 뒤, Restricted가 적용된 app에서 runner가 만든 비루트 Pod의 Secret 파일 해시를 비교합니다. 실제 비밀값은 출력하지 않습니다. API 직접 조회 거절, Pod 생성 성공, Secret 볼륨 읽기 성공을 별개의 증거로 기록합니다. 그 다음 인증 경계와 본문 없는 감사 로그를 확인합니다. 이 VM은 교육용 단일 노드이며, 이 결과를 다른 테넌트나 운영 클러스터 전체의 보안 보장으로 확대하지 않습니다.