S-exec 명령어가 승인 제어에 걸리는 이유
개요
E-검증 승인 정책 실습, E-Kyverno 기본 실습에서 exec을 막는 정책을 짜보다가, 문득 이상한 지점을 느꼈다.
exec은 기본적으로 get 요청으로 날아가는데, 왜 승인 제어에 걸리는 걸까?
Admission Control에서도 다뤘지만, 승인 제어는 기본적으로 create, delete처럼 클러스터에 영향을 미치는 요청에 대해서만 적용된다.
실습하다가 삼천포로 샜던 내용을 여기에 옮겨 적는다.
검증 승인 정책으로 분석을 진행했다.
사실 원래 처음 실습하려던 것은 log 조회를 승인 제어로 막겠다는.. 얼빠진 생각이었다.
exec은 get 요청일 지언대
그러니까 log, exec 등은 기본적으로 http 상에서 get이라 요청에 대해서는 동작하지 않을 거라 생각했다.
keti debug -v 6 -- curl google.com
이렇게 요청을 날려보면
GET https://192.168.80.11:6443/api/v1/namespaces/default/pods/debug/exec?command=curl&command=google.com&container=debug&stdin=true&stdout=true&tty=true 101 Switching Protocols in 5 milliseconds
이런 말이 뜨는 것을 알 수 있다.
내가 아는 한에서도 웹소켓 프로토콜(1.32 기준으로 웹소켓이다)은 get 요청으로 날아가는 게 맞고, 다른 글에서도 말하고 있다.[1]
exec 같은 요청은 중간에 프로토콜이 스위칭되면서 다시 한 번 요청이 날아가나 봤는데 그런 것도 아닌 것 같다.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: "deny-log-policy"
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["*"]
resources: ["*"]
validations:
- expression: "false"
무조건 거부가 일어나도록 검증 정책을 작성해봤다.
분명 create에 대해서는 검증 실패가 발생하니 정책이 잘 적용된 것이다.
그러나 exec은 그냥 잘만 실행되는 것을 확인할 수 있다.
조금 생각해봤을 때는 역시 get 요청이니까 이게 당연하다는 생각이 들었다.
올바르게 서브리소스에 적용되는 정책
resources: ["*/*"]
그런데 생각해보니 단순 와일드카드가 서브 리소스를 포함하지 않는다는 걸 깨달아서 이렇게 수정해봤다.
이번에는 이렇게 제대로 막히는 모습이 보인다.
그렇지만 이렇게 하더라도 logs는 잡히지 않는다.
logs는 그냥 그대로 get 요청이긴 한가 보다.
로그 뜯기
요청 실패의 경우
이대로 끝나면 뒤가 구려서 조금 더 로그를 자세히 뜯어봤다.
POST https://192.168.80.11:6443/api/v1/namespaces/default/pods/debug/exec?command=curl&command=google.com&container=debug&stdin=true&stdout=true&tty=true
중간에 POST 메서드가 사용되는 것이 확인된다.
문서를 조금 더 뒤져보니까 이 동작에 대한 힌트가 살짝 보인다.[2]
처음에는 분명 get으로 exec이 일어나는 것이 맞다.
그러나 그 이후에는 post가 다시 날아가는 게 로그를 통해서도 확인됐었는데, 실제 api 문서에도 관련 요청에 대한 인터페이스가 명시돼있다.
요청 성공의 경우
처음에는 요청 실패 케이스 로그만 자세히 보다가 무조건 POST가 날아가는 것이라 생각했는데, 다시 보니 그건 또 아니다.
요청 성공의 케이스에는 그냥 일반 웹소켓 프로토콜 마냥 POST가 발생하지 않는다.
위의 요청 거부가 일어나는 케이스는 GET 메서드가 일어난 상태에서 발생하는 게 명확하다.
그렇다면 현재로서는 가장 의심스러운 것은 쿠버네티스 인가 상의 동사이다.
http 메서드와 별개로 쿠버네티스는 자체적인 동사 매커니즘이 존재한다.
이때 exec에 해당하는 요청은 아마도 일반적인 조회 동사와 다른 것으로 생각된다.
문서를 서칭하다 봤던 것 중, CONNECT가 여기에 해당하는 게 아닐까 생각한다.
CONNECT 동사로 제한
operations: ["CONNECT"]
이번엔 operations에 CONNECT만 남겨본다.
요청은 동일하게 실패하니, exec은 CONNECT 동사인 것이 확실시된다.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: "deny-log-policy"
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CONNECT"]
resources: ["pods/exec"]
validations:
- expression: "false"
확실하게 지정해서 이렇게 다시 시도해도 정확하게 exec 동작이 막히는 것이 확인됐다.
결론적으로, exec은 CONNECT 동사로서 승인 제어에서 평가를 받는 요청이다.
kubectl exec 로그 정리
완벽하지는 않지만, 쿠버네티스에서의 exec은 다음의 요청 흐름을 따른다.
요청 성공 시
GET https://192.168.80.11:6443/api/v1/namespaces/default/pods/debug/exec?command=curl&command=google.com&container=debug&stdin=true&stdout=true&tty=true
Request Headers:
Sec-Websocket-Protocol: v5.channel.k8s.io
User-Agent: kubectl/v1.32.2 (linux/amd64) kubernetes/67a30c0
Response Status: 101 Switching Protocols in 7 milliseconds
Response Headers:
Upgrade: websocket
Connection: Upgrade
Sec-Websocket-Accept: 79Hxe312CXYTd0Nv6Uz1JGEcQ0M=
Sec-Websocket-Protocol: v5.channel.k8s.io
The subprotocol is v5.channel.k8s.io
Write() on stream 4
Write() done on stream 4
kubectl 로그를 조금 정리했다.
그냥 통상적인 websocket 흐름이다.
정상적으로 101 코드와 함께 스위칭 프로토콜 과정이 일어난다.
표시는 안 됐지만, 요청이 날아갈 때 Upgrade: websocket
헤더도 들어갔지 싶다.
요청 실패 시
GET https://192.168.80.11:6443/api/v1/namespaces/default/pods/debug/exec?command=curl&command=google.com&container=debug&stdin=true&stdout=true&tty=true
Request Headers:
Sec-Websocket-Protocol: v5.channel.k8s.io
User-Agent: kubectl/v1.32.2 (linux/amd64) kubernetes/67a30c0
Response Status: in 4 milliseconds
Response Headers:
RemoteCommand fallback: unable to upgrade streaming request: pods "debug" is forbidden: ValidatingAdmissionPolicy 'deny-log-policy' with binding 'default-deny-log' denied request: failed expression: false
POST https://192.168.80.11:6443/api/v1/namespaces/default/pods/debug/exec?command=curl&command=google.com&container=debug&stdin=true&stdout=true&tty=true
Request Headers:
User-Agent: kubectl/v1.32.2 (linux/amd64) kubernetes/67a30c0
X-Stream-Protocol-Version: v5.channel.k8s.io
X-Stream-Protocol-Version: v4.channel.k8s.io
X-Stream-Protocol-Version: v3.channel.k8s.io
X-Stream-Protocol-Version: v2.channel.k8s.io
X-Stream-Protocol-Version: channel.k8s.io
Response Status: 422 Unprocessable Entity in 4 milliseconds
Response Headers:
Cache-Control: no-cache, private
Content-Type: application/json
Date: Mon, 17 Mar 2025 12:40:40 GMT
Content-Length: 440
Audit-Id: 95ce6bd2-8535-4d1b-a8f2-e501de1fee47
실패 시에는 도중 흐름이 다르다.
일단 웹소켓 업그레이드 응답이 안 돌아오고, fallback이 발생한다.
이 이후에 POST가 발생하는데, 이때는 내 kubectl이 되는 대로 가능한 프로토콜 버전을 죄다 쏴버린다.
그리고 돌아오는 응답은 422.[3]
아무튼 서버는 처리할 수 없다라는 코드가 돌아온다.
여기에서 내 생각은 이렇다.
일단 웹소켓 업그레이드 요청이 갔지만 서버는 이에 원하는 응답을 주지 못했다.
원래대로라면 여기에서 422를 뱉었어야 할 것 같은데, 아무튼.
그래서 이후에 버전이 안 맞나, 하면서 kubectl이 다시 요청을 날리는데 이때 POST가 날아가는 것이다.
그리고 이번에는 제대로 에러코드를 내어준다.
결론
처음부터 의도하고 진행한 테스트가 아니라 두서가 많이 정리는 안 됐다.
아무튼 이로부터 한 가지 사실을 알 수 있었다.
일단 exec 이란 놈은 http로는 get이면서도 쿠버 동사로는 클러스터 조작을 일으키는 특이한 놈이 맞다.
구체적으로 exec은 CONNECT 동사에 해당하며 클러스터 조작 동사에 속하기에 승인 제어를 우회할 수 없다.
추가적으로 같이 언급할 만한 사실은 log는 exec과 달리 승인 제어를 우회한다는 것이다.
일반적인 조회 동사이니 어쩌면 당연하다고도 볼 수 있겠다.
관련 문서
이름 | noteType | created |
---|---|---|
Admission Control | knowledge | 2025-01-20 |
Admission Webhook | knowledge | 2025-01-20 |
Validation Admission Policy | knowledge | 2025-03-17 |
Kyverno | knowledge | 2025-03-17 |
6W - api 구조와 보안 1 - 인증 | published | 2025-03-15 |
6W - api 보안 2 - 인가, 어드미션 제어 | published | 2025-03-16 |
E-Kyverno 기본 실습 | topic/explain | 2025-03-17 |
E-검증 승인 정책 실습 | topic/explain | 2025-03-17 |
S-exec 명령어가 승인 제어에 걸리는 이유 | topic/shooting | 2025-03-17 |