CRD 와 오퍼레이터 · 오퍼레이터 운영 · 이론
설치 순서·권한 경계·업그레이드
한 줄 요약
오퍼레이터는 클러스터 상태를 대신 조작하는 자동화된 관리자다. 그래서 오퍼레이터의 서비스 어카운트가 탈취되면 공격자는 그 오퍼레이터의 모든 권한을 그대로 갖는다.
왜 이게 필요했나
대부분의 워크로드는 자기 네임스페이스 안의 일을 한다. 웹 애플리케이션이 침해되면 피해는 대체로 그 애플리케이션과 그 데이터에 한정된다. 오퍼레이터는 다르다. 여러 네임스페이스의 CR 을 watch 하고, Deployment 를 만들고, Secret 을 읽고, 때로는 RBAC 오브젝트까지 만든다. 컨트롤러 파드 하나에서 발견된 취약점이 곧 클러스터 전체의 장악으로 이어질 수 있다.
그래서 오퍼레이터 운영에서 가장 중요한 두 가지는 화려한 기능이 아니라 설치 순서와 권한 경계 다.
어떻게 동작하나
설치 순서는 의존성 그래프다. 순서를 지키지 않으면 조용히 이상하게 동작한다.
1. CustomResourceDefinition — 새 타입을 API 에 먼저 등록2. ServiceAccount / ClusterRole / ClusterRoleBinding — 권한 부여3. Deployment — 컨트롤러 기동CRD 가 먼저여야 하는 이유는 컨트롤러가 뜨자마자 그 타입을 watch 하기 때문이다. 타입이 없으면 watch 설정이 실패하고 컨트롤러가 크래시 루프에 빠진다. 권한이 먼저여야 하는 이유도 같다 — 권한 없이 뜬 컨트롤러는 목록 조회부터 403 을 맞는다. GitOps 로 배포한다면 이 순서를 동기화 웨이브나 의존성으로 명시해야 한다.
권한은 동사를 줄이는 것이 핵심이다. 초보 오퍼레이터에서 가장 흔한 실수가 verbs: ["*"] 다. 실제로 필요한 것은 훨씬 좁다.
| 컨트롤러가 하는 일 | 필요한 동사 | 불필요하게 넓은 권한 |
| --- | --- | --- |
| CR 을 읽고 감시 | get, list, watch | create, delete |
| 자식 리소스 생성/갱신 | get, list, watch, create, update, patch | delete (소유자 참조로 대체 가능) |
| 상태 보고 | update, patch (status 서브리소스) | 메인 리소스 전체 update |
| 이벤트 기록 | create, patch | get, list |
특히 delete 를 조심해야 한다. 자식 정리는 대부분 소유자 참조와 가비지 컬렉터에 맡길 수 있으므로, 컨트롤러에 삭제 권한이 없어도 된다. 없으면 침해된 컨트롤러가 리소스를 대량 삭제하는 시나리오가 원천 차단된다.
또 하나 자주 빠뜨리는 것이 서브리소스 권한 이다. webservices 에 대한 update 권한이 있어도 webservices/status 는 별개의 리소스로 취급되므로 따로 줘야 한다. 파이널라이저를 다루면 webservices/finalizers 도 필요하다. 이 두 개가 빠져서 "권한은 준 것 같은데 컨트롤러가 status 를 못 쓴다"는 증상이 자주 나온다.
리더 선출은 중복 실행을 막는다. 컨트롤러를 두 벌 띄우면 같은 오브젝트를 둘이 동시에 조정해 충돌한다. 리더 선출은 여러 복제본 중 하나만 실제로 일하게 하고, 리더는 coordination.k8s.io 의 Lease 오브젝트에 자기 신원을 적고 주기적으로 갱신한다. 리더가 죽으면 임차가 만료되고 다른 복제본이 인계받는다. 여기서도 최소권한 감각이 필요하다 — Lease 권한은 resourceNames 로 그 하나에만 한정할 수 있다.
업그레이드에서 CRD 를 건드리지 마라. 컨트롤러 이미지를 올리는 것은 롤링 업데이트로 안전하지만, CRD 의 옛 버전을 함께 지우는 것은 위험하다. status.storedVersions 에 그 버전이 남아 있다면 이미 저장된 오브젝트를 읽을 수 없게 된다. 업그레이드 절차는 항상 "CRD 는 더하기만, 빼기는 재저장을 마친 뒤"다.
이 실습 환경에 대한 솔직한 설명
이 랩에는 실제 컨트롤러 바이너리가 없고, 파드 안을 들여다볼 수단(kubectl exec, 진짜 로그)도 없다. 그래서 오퍼레이터 설치 실습은 매니페스트의 정확성을 다룬다. 네임스페이스와 라벨, 서비스 어카운트와 ClusterRole 의 동사 집합, Deployment 의 어카운트·리더 선출 인자·프로브·자원 제한·보안 컨텍스트, 그리고 리더 선출 Lease 를 만들고 검증한다. 권한이 정말 의도대로 걸렸는지는 로그가 아니라 kubectl auth can-i --as= 로 확인한다. 이 방식은 실제 현장에서도 가장 빠르고 확실한 RBAC 디버깅 수단이다.
현장에서 만나는 모습
첫째, 한 글자 차이의 바인딩 사고. ClusterRoleBinding 의 subject 이름이 실제 서비스 어카운트 이름과 한 글자라도 다르면 아무 오류 없이 바인딩이 만들어지고, 컨트롤러만 403 을 맞는다. 존재하지 않는 주체를 가리키는 바인딩은 완벽히 유효한 오브젝트이기 때문이다.
둘째, CR 을 통한 권한 상승. 오퍼레이터가 사용자의 CR 을 믿고 RBAC 오브젝트를 만들어 준다면, 사용자는 자기가 직접 만들 수 없는 권한을 오퍼레이터를 우회로 삼아 얻을 수 있다. 방어는 여러 겹이다 — 오퍼레이터가 RBAC 를 동적으로 만들지 않게 설계하고, 꼭 필요하면 요청 가능한 권한을 화이트리스트로 제한한다.
셋째, 권한은 문서가 아니라 검증 대상이다. "이 오퍼레이터는 Secret 을 못 읽는다"는 문장은 위키에 적어 둘 것이 아니라 kubectl auth can-i get secrets --as=... 로 확인해 no 를 받아 두는 것이다. 되는 것뿐 아니라 되면 안 되는 것 을 함께 검증표에 넣어야 권한 경계가 실제로 지켜지고 있다는 증거가 된다.
다음 실습에서 할 것
오퍼레이터 전용 네임스페이스를 라벨과 함께 만들고, 설치 순서를 문서화하고, 와일드카드 없는 최소권한 ClusterRole 과 바인딩을 만든다. 컨트롤러 Deployment 에 전용 어카운트·리더 선출·프로브·메모리 제한·비루트 실행을 채우고, 리더 선출 Lease 를 만든다. 그다음 이미지를 올리며 CRD 버전이 보존되는지 확인하고, 고장난 오퍼레이터 매니페스트 세 군데를 진단해 고친 뒤, 마지막에 "되는 것과 되면 안 되는 것"을 모두 담은 권한 검증표를 만들어 실제 응답과 대조한다.