LabHub

CGOA — GitOps 인증 어소시에이트 · GitOps 보안과 운영 · 이론

시크릿을 Git 에 넣지 않고 GitOps 하기 — 그리고 GitOps 가 못 지키는 것

LabHub 에서 이어서 보기

한 줄 요약

GitOps 는 "모든 것을 Git 에" 를 요구하는데 시크릿만은 그럴 수 없습니다. 해법은 네 갈래이고 각각 반대급부가 다릅니다. 그리고 GitOps 를 아무리 잘해도 그 아래 계층이 뚫려 있으면 소용없다는 것이 4C 모델 이 말하는 바입니다.

왜 이게 필요했나

평문 시크릿을 커밋하면 무슨 일이 벌어지는지부터 정확히 알아야 합니다. 공개 저장소에 푸시된 자격증명은 수 초에서 수 분 안에 자동 스캐너에 수집됩니다. 그래서 발견했을 때의 올바른 순서는 폐기 → 영향 조사 → 히스토리 정리 입니다. 히스토리 재작성부터 하는 것은 순서가 틀렸습니다 — 포크된 저장소, 이미 클론한 로컬 사본, 플랫폼이 보관하는 PR 참조, 코드 검색 캐시, CI 캐시에 이미 복제된 값은 되돌릴 수 없기 때문입니다. 히스토리 재작성은 유출을 되돌리는 조치가 아니라 재발을 줄이는 위생 작업입니다.

또 하나 반드시 짚어야 할 오해가 있습니다. 쿠버네티스 Secret 의 base64 는 암호화가 아닙니다. 키도 없고 명령 하나로 되돌아갑니다. 게다가 기본 설정에서 Secret 은 etcd 에 평문으로 저장됩니다. etcd 백업 파일이나 스냅숏, 디스크 이미지를 얻은 사람은 모든 Secret 을 읽습니다. 저장 시 암호화(EncryptionConfiguration)를 켜야 하고, 이때 identity 프로바이더는 반드시 목록 맨 마지막 에 두어야 합니다. 앞에 오면 평문 저장으로 되돌아갑니다.

어떻게 동작하나 — 네 갈래

| 방식 | Git 에 들어가는 것 | 복호화 주체 | 장점 | 반대급부 |
| --- | --- | --- | --- | --- |
| Sealed Secrets | 공개키로 암호화된 SealedSecret | 클러스터 안 컨트롤러(비밀키) | 값이 Git 안에 있어 자족적 | 클러스터마다 키가 다름, 키 분실 시 전부 재암호화, 값 교체는 커밋 필요 |
| External Secrets Operator | 외부 저장소 참조만 (ExternalSecret) | 오퍼레이터가 외부 매니저에서 가져와 Secret 생성 | 값이 Git 에 아예 없음, 로테이션이 저장소 쪽 일 | 외부 매니저 의존, 오퍼레이터 자격증명이 새 급소 |
| SOPS | 값만 암호화된 YAML(키는 평문) | CI 또는 GitOps 도구(age/KMS/PGP) | diff 가 읽히고 리뷰 가능 | 복호화 키 배포 문제, Argo CD 는 플러그인 필요(Flux 는 네이티브) |
| Argo CD Vault Plugin | 자리표시자가 든 매니페스트 | repo-server 의 플러그인이 렌더 시점에 치환 | 매니페스트가 깔끔함 | 렌더 결과에 평문이 등장, CMP 사이드카 운영 부담 |

여기서 가장 자주 오해되는 지점을 분명히 해 둡니다. ExternalSecret 매니페스트는 커밋해도 안전합니다 — 값이 없기 때문입니다. 그러나 오퍼레이터가 그 값을 실제 쿠버네티스 Secret 으로 실체화하므로, RBAC 문제와 저장 시 암호화 문제는 그대로 남습니다. 시크릿 매니저를 쓴다고 클러스터 쪽 위생이 면제되지 않습니다. 네임스페이스에 get secrets 권한이 있는 사람은 그 네임스페이스의 모든 Secret 을 읽습니다. 개발자에게 편의로 준 편집 권한이 사실상 프로덕션 자격증명 열람 권한인 경우가 매우 흔합니다.

서명된 커밋과 signatureKeys

Git 이 진실의 원천이라면 "누가 그 진실을 썼는가"가 곧 보안 경계입니다. AppProject 의 signatureKeys 에 GPG 키 ID 를 등록하면, 그 프로젝트의 Application 은 서명이 검증된 커밋(또는 태그)에서만 동기화합니다. 저장소 자격증명이 털려 공격자가 커밋을 밀어 넣어도, 등록된 키로 서명되지 않았으면 에이전트가 적용을 거부합니다. 브랜치 보호가 서버 쪽 통제라면 signatureKeys 는 클러스터 쪽 통제이고, 둘은 서로를 대체하지 않습니다.

argocd cluster add 의 함정

외부 클러스터를 등록할 때 쓰는 명령입니다.

argocd cluster add my-cluster-context --name production

이 한 줄이 대상 클러스터에 argocd-manager ServiceAccount 를 만들고 cluster-admin ClusterRole 을 바인딩 합니다. 편의를 위한 기본값이고 문서에도 그렇게 적혀 있지만, 실무에서는 이것이 "Argo CD 를 뚫으면 모든 클러스터가 뚫린다"를 뜻합니다. Argo CD 는 여러 클러스터의 열쇠를 한 곳에 모아 두는 시스템이기 때문에 침해 시 폭발 반경이 가장 큰 컴포넌트 중 하나입니다.

대응은 두 가지입니다. 첫째, 대상 클러스터의 ClusterRole 을 최소 권한으로 교체합니다 — 읽기는 전체(get/list/watch)로 두되, 쓰기는 실제로 배포하는 리소스 종류로 좁힙니다. 둘째, 배포 대상 네임스페이스가 정해져 있다면 클러스터 스코프 대신 네임스페이스 스코프 권한만 주고 AppProject 의 destinations 로 한 번 더 조입니다.

4C 안에서 GitOps 의 자리

클라우드 네이티브 보안의 4C 는 바깥에서 안으로 Cloud → Cluster → Container → Code 입니다. 핵심 명제는 하나입니다.

> 바깥 계층이 취약하면 안쪽 계층으로는 구할 수 없다.

컨테이너를 아무리 잘 굳혀도 클러스터 API 가 인터넷에 열려 있으면 소용없고, 코드에 취약점이 없어도 클라우드 계정의 IAM 이 헐거우면 소용없습니다.

이 지도 위에서 GitOps 가 담당하는 층은 주로 Cluster 입니다. 클러스터에 무엇이 들어가는지를 선언으로 고정하고, 변경 경로를 Git 하나로 좁히고, 드리프트를 되돌립니다. 부수적으로 Code 계층에도 기여합니다 — 모든 변경이 리뷰와 서명을 거치므로 공급망의 마지막 구간이 감사됩니다. 반면 GitOps 가 하지 않는 일도 분명합니다. 이미지 안의 취약점을 찾아 주지 않고(Container 계층, 스캐너의 일), 클라우드 계정의 권한 설계를 대신해 주지 않으며(Cloud 계층), 런타임에 벌어지는 침입을 탐지하지 않습니다. 시험에서 "GitOps 를 도입하면 컨테이너 이미지 취약점이 해결되는가?" 류의 문항이 나오면 답은 아니오입니다.

현장에서 만나는 모습

저자의 홈랩은 Harbor 를 10.0.0.202, Gitea 를 10.0.0.200, Argo CD 를 10.0.0.201 에 두고 MetalLB L2 풀 10.0.0.200-215 로 노출합니다. 네트워크는 Cilium 1.20 eBPF 가 kube-proxy 없이 처리하고, hostFirewall 과 WireGuard 노드 간 암호화까지 켜져 있습니다. 여기서 4C 의 계층 감각이 실제로 드러납니다 — Argo CD UI 가 사설망 안에만 열려 있다는 사실(Cluster/네트워크 계층)이, Argo CD 자체의 RBAC 설정 실수(Cluster 계층 안쪽)를 상당 부분 막아 주고 있습니다. 반대로 그 네트워크 경계가 사라지면 안쪽 설정만으로 버텨야 합니다.

또 하나. 이 클러스터는 컨트롤 플레인 3 대로 etcd 쿼럼을 갖췄지만 controlPlaneEndpoint 가 첫 노드의 물리 IP 라, 그 노드가 죽으면 데이터는 살아 있는데 아무도 API 에 접속하지 못합니다. 가용성과 접근성이 별개 이듯, 시크릿을 암호화했다는 것과 그 시크릿을 읽을 수 있는 사람이 적다는 것도 별개 입니다. 암호화는 저장 매체를 얻은 공격자를 막고, RBAC 은 API 를 통과하는 사람을 막습니다. 둘 다 필요합니다.

다음 퀴즈에서 확인할 것

이 모듈은 퀴즈로 마무리합니다. 실습 환경에는 Sealed Secrets 컨트롤러도 Vault 도 없고 인터넷도 없으므로, 시크릿 도구의 선택 기준과 반대급부는 문제로 익히는 편이 정확합니다. 앞 모듈에서 만든 AppProject 파일에 signatureKeys 와 좁은 destinations 를 얹어 보면 이 리딩의 내용이 실물로 연결됩니다.