보안
보안 (Security)
CloudNativePG는 스택의 모든 계층에서 "최소 권한(Least Privilege)" 원칙을 따라 기본적으로 안전하도록 설계됐어요. 이 문서는 보안 거버넌스, 공급망 무결성, 그리고 Code(코드)·Container(컨테이너)·Cluster(클러스터)라는 세 가지 별개의 계층에서 분석된 기술적 통제를 설명해요.
:::warning 이 페이지에 포함된 정보가 쿠버네티스 클러스터에 대한 정기적인 InfoSec 의무를 면제하지는 않아요. 쿠버네티스 문서의 "Overview of Cloud Native Security" 페이지를 숙지하세요. :::
:::note[4C's 보안 모델에 관해] EDB가 CloudNativePG의 보안에 취한 접근 방식에 대한 더 나은 이해와 맥락은 "The 4C's Security Model in Kubernetes" 블로그 기사를 참조하세요. :::
출처: 문서
본문
거버넌스와 신뢰 (Governance and Trust)
취약점 보고 (Vulnerability Reporting)
:::important 공개 GitHub 이슈를 통해 보안 취약점을 보고하지 마세요. :::
CloudNativePG는 조정 공개(coordinated disclosure) 모델을 따르며, 취약점을 비공개로 보고하는 방법에 대한 지침은 GitHub의 Security Policy를 참조하세요.
릴리스 무결성과 공급망 (Release Integrity and Supply Chain)
CloudNativePG는 OpenSSF Software Producer Security (OSPS) Baseline을 따르며, 쿠버네티스 매니페스트와 컨테이너 이미지를 포함한 모든 공식 릴리스 자산은 암호학적으로 서명되고 검증 가능해요.
설치 중 이러한 자산을 검증하는 빠른 시작 지침은 Installation Guide를 참조하세요. Signatures(서명)과 Attestations(증명)에 대한 자세한 정보는 아래에서 찾을 수 있어요.
코드 (Code)
CloudNativePG의 소스 코드는 CI/CD 파이프라인에 직접 통합된 인기 있는 Go용 오픈소스 린터인 GolangCI-Lint를 사용하여 보안 취약점 확인을 포함한 체계적인 정적 분석을 거쳐요. GolangCI-Lint는 같은 소스 코드에서 여러 린터를 실행할 수 있어요.
보안 문제를 식별하는 데 사용되는 도구는 다음과 같아요:
-
Golang Security Checker (
gosec): 하드코딩된 자격 증명, 정수 오버플로, SQL 주입 같은 알려진 취약점, 위협, 약점을 감지하도록 설계된 규칙 집합에 대해 소스 코드의 추상 구문 트리를 스캔하는 린터예요. GolangCI-Lint는 그 스위트의 일부로gosec를 실행해요. -
govulncheck: CI/CD 파이프라인에서 실행되며 Go 코드나 컴파일러에 영향을 주는 알려진 취약점을 보고해요. 오퍼레이터가 알려진 취약점을 포함한 버전의 Go 컴파일러로 빌드되면
govulncheck가 감지해요. -
CodeQL: GitHub가 제공하며 보안 문제를 스캔하고 감지된 취약점이 있는 모든 풀 리퀘스트를 차단해요. CodeQL은 Go 코드만 검토하도록 구성되며, Python이나 Bash 같은 저장소의 다른 언어는 제외해요.
-
Snyk: 예약된 작업에서 야간 코드 스캔을 수행하고 코드 보안 및 라이선스 문제와 관련된 새로운 발견 사항을 강조한 주간 보고서를 생성해요.
CloudNativePG 저장소는 Security 섹션에서 "Private vulnerability reporting" 옵션이 활성화되어 있어요. 이 기능을 통해 사용자는 공개적으로 공개되기 전에 신중한 처리가 필요한 보안 문제를 안전하게 보고할 수 있어요. 보안 버그를 발견하면 이 경로로 보고하세요.
:::info[Important] CI/CD 파이프라인의 정적 코드 분석 단계에서 실패하면 CloudNativePG의 전체 전달 프로세스가 차단돼요. 모든 커밋은 GolangCI-Lint가 정의한 모든 린터를 통과해야 해요. :::
컨테이너 (Container)
CloudNativePG의 모든 컨테이너 이미지는 매 커밋 후 CI/CD 파이프라인을 통해 자동으로 빌드돼요. 이 이미지들에는 오퍼레이터의 이미지뿐만 아니라 오퍼랜드(operand)의 이미지 — 구체적으로 지원되는 모든 PostgreSQL 버전용 — 도 포함돼요.
:::info[Important] 모든 오퍼랜드 이미지는 베이스 이미지와 패키지 수준 모두에서 최신 보안 업데이트를 통합하기 위해 우리 파이프라인에 의해 자동으로 정기적으로 재빌드돼요. 이를 통해 커뮤니티에 배포되는 컨테이너 이미지가 정기적으로 patch-level 업데이트를 받도록 보장해요. :::
CI/CD 과정에서 이미지들은 다음 도구로 스캔돼요:
이미지 서명 (Image Signatures)
오퍼레이터와 오퍼랜드 이미지는 sigstore의 서명 도구인 cosign을 사용하여 암호학적으로 서명돼요. 이 과정은 GitHub Actions를 통해 자동화되며 OpenID Connect를 통해 발급된 단기 토큰을 활용해요.
토큰 발급자는 https://token.actions.githubusercontent.com이며, 서명 신원은 cloudnative-pg 저장소에서 실행되는 GitHub 워크플로에 해당해요. 이 워크플로는 서명 과정을 간소화하기 위해 cosign-installer 액션을 사용해요.
오퍼레이터 이미지의 진위를 검증하려면 이미지 다이제스트와 함께 다음 cosign 명령을 사용해요:
cosign verify ghcr.io/cloudnative-pg/cloudnative-pg@sha256:<DIGEST> \
--certificate-identity-regexp="^https://github.com/cloudnative-pg/cloudnative-pg/.github/workflows/release-publish.yml@refs/tags/v" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com"
증명 (Attestations)
컨테이너 이미지는 투명성과 추적성을 위해 다음 증명을 포함해요:
- Software Bill of Materials (SBOM): 이미지에 포함되거나 빌드 과정에서 사용된 소프트웨어 아티팩트의 포괄적인 목록으로, in-toto SPDX predicate 표준으로 형식화돼요.
- Provenance(출처): SLSA GitHub Generator Github action으로 생성된, 빌드 과정을 상세히 설명하는 메타데이터예요. 이는 아티팩트가 프로젝트 소스에서 직접 신뢰할 수 있는 격리된 GitHub Actions 러너에서 빌드되었음을 제공하는 SLSA Level 3 보증이에요.
특정 이미지와 플랫폼의 SBOM을 다음 명령으로 검색할 수 있어요:
docker buildx imagetools inspect <IMAGE> \
--format '{{ json (index .SBOM "<PLATFORM>").SPDX }}'
이 명령은 SBOM을 JSON 형식으로 출력해 소프트웨어 구성 요소와 빌드 의존성의 상세 뷰를 제공해요.
빌드 수준 출처(provenance)는 다음을 사용해요:
docker buildx imagetools inspect <IMAGE> \
--format '{{ json (index .Provenance "<PLATFORM>").SLSA }}'
SLSA 출처 검증
slsa-verifier로 SLSA Level 3 출처를 검증할 수 있어요.
컨테이너 이미지를 검증하려면 다이제스트 참조를 전달하세요:
slsa-verifier verify-image \
ghcr.io/cloudnative-pg/cloudnative-pg@sha256:<DIGEST> \
--source-uri github.com/cloudnative-pg/cloudnative-pg
컨테이너 보안을 위한 지침과 프레임워크
컨테이너 수준 보안을 보장하기 위해 다음 지침과 프레임워크가 고려됐어요:
- "Container Image Creation and Deployment Guide": 미국 국방부(DoD)의 국방정보시스템국(DISA)이 개발.
- "CIS Benchmark for Docker": 인터넷 보안 센터(CIS)가 개발.
:::note[컨테이너 수준 보안에 관해] CloudNativePG에서 EDB가 컨테이너 수준 보안에 대해 취한 접근 방식에 대한 더 많은 정보는 "Security and Containers in CloudNativePG" 블로그 기사를 참조하세요. :::
클러스터 (Cluster)
클러스터 수준 보안은 컨트롤 플레인과 노드를 형성하는 모든 쿠버네티스 구성 요소와 클러스터에서 실행되는 애플리케이션(PostgreSQL 포함)을 고려해요. 먼저 이 섹션의 나머지를 규정하는 신뢰 모델과 보안 경계를 정의하는 것으로 시작해요.
신뢰 모델과 보안 경계 (Trust Model and Security Boundaries)
CloudNativePG는 보안 정책을 시행하기 위해 오퍼레이터와 쿠버네티스 플랫폼 사이의 명확한 책임 분리를 따르며, 아래 하위 섹션은 PostgreSQL Pod에 영향을 줄 수 있는 사람이 누구인지, 시행 경계가 실제로 어디에 있는지, 오퍼레이터가 관리하는 리소스를 어떻게 한정하는지, 그리고 이 모든 것에 대한 접근이 RBAC로 어떻게 통제되는지 설명해요.
신뢰된 클러스터 리소스 작성자 (Trusted Cluster Resource Writers)
Cluster 커스텀 리소스에 대한 생성 또는 업데이트 권한이 있는 모든 사용자는 오퍼레이터가 관리하는 PostgreSQL Pod의 완전한 사양을 정의하도록 신뢰돼요. 여기에는 spec.securityContext, spec.podSecurityContext, spec.serviceAccountName 같은 보안에 민감한 필드가 포함돼요. 오퍼레이터는 지시된 대로 이 설정을 적용해요. 클러스터 작성자가 그것들을 재정의하지 않으면 오퍼레이터는 강화된(hardened) 기본 Pod 및 컨테이너 보안 컨텍스트를 적용해요 ("Pod and Container Security Contexts" 참조).
이것은 모든 쿠버네티스 오퍼레이터가 동작하는 방식과 일치해요: 오퍼레이터는 높은 수준의 의도를 낮은 수준의 쿠버네티스 리소스로 변환하는 권한 있는 에이전트로 동작해요. 누군가에게 Cluster 리소스에 대한 쓰기 접근을 부여하는 것은 결과적인 Pod, Service, PVC 및 기타 관리 리소스에 대한 제어를 부여하는 것과 동등해요.
같은 원칙이 CNPG-I 플러그인에도 적용돼요. 플러그인은 Pod 수명 주기에 연결되며 Pod가 생성되기 전에 Pod 사양의 어떤 필드든 수정할 수 있어요. 따라서 플러그인 설치는 Cluster 리소스에 대한 쓰기 접근 부여와 동등한, 클러스터 관리자가 명시적으로 내리는 신뢰 결정이에요.
같은 권한이 데이터베이스 자체로 확장돼요: 오퍼레이터가 PostgreSQL을 슈퍼유저로 조정하기 때문에, 클러스터 작성자는 문서화된 의도된 필드를 통해 결과적인 클러스터에 대해 슈퍼유저 수준의 영향력을 얻게 돼요. 여기에는 다음이 포함돼요:
- 초기화 시
postgres슈퍼유저로 실행되는 부트스트랩 SQL 훅postInitSQL,postInitApplicationSQL,postInitTemplateSQL("Executing Queries After Initialization" 참조); superuser: true를 설정하거나 기존 역할의 멤버십을 부여할 수 있고, PostgreSQL의COPY ... FROM PROGRAM같은 기능을 사용해 오퍼랜드 Pod 안에서 명령을 실행할 수 있는 인라인 관리 역할 (spec.managed.roles);pg_hba규칙(예:trust인증)과password_encryption,fsync, 메모리 관련 설정처럼 클러스터의 보안 자세와 내구성을 형성하는 파라미터를 포함한 커스텀 PostgreSQL 구성.
이것은 권한 상승이 아니라 의도된 신뢰 모델이에요.
오퍼랜드 Pod에서 실행되는 코드도 마운트된 ServiceAccount 아래에서 실행되며, 이 계정은 자신의 클러스터와 연결된 Secrets를 읽을 수 있어요 ("Calls to the API server made by the instance manager"과 "Secrets" 참조). 따라서 클러스터 작성자는 Namespace Isolation(네임스페이스 격리)에 의해 클러스터 자신의 네임스페이스로 한정된 그 Secrets를 읽을 수 있어요.
:::note 이러한 신뢰 경계는 프로젝트의 자체 보안 평가에서도 다뤄져요. :::
쿠버네티스 입소 통제 (Kubernetes Admission Control)
오퍼레이터는 표준 쿠버네티스 API를 통해 Pod를 생성하므로, 모든 Pod는 클러스터에 구성된 전체 입소 통제 체인을 거쳐요. 여기에는 네임스페이스 수준에서 Pod Security Standards를 시행하는 쿠버네티스 내장 메커니즘인 Pod Security Admission(PSA)과 사용 중인 모든 타사 정책 엔진이 포함돼요. 네임스페이스가 PSA restricted 또는 baseline 프로필을 시행하면, 쿠버네티스 API 서버는 정책을 위반하는 모든 Pod(오퍼레이터가 생성한 Pod 포함)를 거부해요.
위에서 설명한 신뢰된 클러스터 작성자가 어떤 Pod 필드든 설정할 수 있기 때문에, 이 입소 통제 체인(오퍼레이터가 아님)이 환경에서 Pod 보안 정책을 시행하는 경계예요.
네임스페이스 격리 (Namespace Isolation)
오퍼레이터는 관리하는 모든 리소스를 부모 Cluster의 네임스페이스로 한정해요. 인스턴스 Pod는 항상 자신의 Cluster와 같은 네임스페이스에 생성되며, 다른 어떤 관계에서도 추론하지 않고 명시적이고 결정적으로 설정해요. 쿠버네티스 소유자 참조(owner references)는 단일 네임스페이스로 한정되므로, 오퍼레이터는 다른 네임스페이스의 리소스에 대한 소유권을 설정할 수도 없어요.
커스텀 리소스에 대한 RBAC
CloudNativePG 커스텀 리소스에 대한 접근은 표준 쿠버네티스 RBAC로 통제돼요. Cluster 리소스에 대한 쓰기 접근이 결과적인 Pod에 대한 제어를 의미하므로, cnpg.io/podPatch(관리되는 Pod에 사용자 제공 패치를 적용), cnpg.io/validation, cnpg.io/reconciliationLoop 같은 주석도 Cluster 리소스 자체와 같은 RBAC 규칙을 받아요.
특히 cnpg.io/validation을 disabled로 설정하면 해당 Cluster에 대한 오퍼레이터 자신의 validating admission webhook을 우회하며, 여기에는 cnpg.io/podPatch에 수행되는 구문 및 적용 가능성 검사도 포함돼요. 따라서 CloudNativePG의 webhook 검증은 조언적(advisory)이에요: Cluster를 쓸 수 있는 동일한 주체가 끌 수 있으므로 보안 경계가 아니에요. 쿠버네티스 입소 통제는 이 주석과 무관하게 결과적인 Pod에 여전히 적용돼요.
같은 추론이 DatabaseRole 리소스에도 적용돼요: 그 사양은 superuser: true와 inRoles를 통한 기존 역할(포스트그레스 역할 자체 포함)의 멤버십을 허용해요. 따라서 네임스페이스의 DatabaseRole 객체에 대한 쓰기 접근은 그 네임스페이스의 PostgreSQL 클러스터에 대한 슈퍼유저 접근과 동등하며, Cluster 리소스에 대한 쓰기 접근과 같은 주의가 필요해요.
비슷한 고려가 Pooler 리소스에도 적용돼요: 그 authQuery 필드는 PgBouncer가 클라이언트 연결을 인증하기 위해 postgres 데이터베이스에서 실행하는 임의의 SQL을 받아들이고, auth_user 파라미터("Connection Pooling" 참조)는 그 쿼리가 실행되는 역할을 선택해요. 따라서 Pooler에 대한 쓰기 접근은 이미 PostgreSQL 인증이 성공하는 모든 역할로서 임의의 SQL을 실행할 능력을 의미하며, Cluster 리소스에 대한 쓰기 접근과 같은 주의가 필요해요.
이 리소스들에 대한 쓰기 접근을 고권한 부여로 취급하세요: 멀티 테넌트 배포에서는 테넌트를 별도 네임스페이스로 격리하고 쿠버네티스 RBAC로 네임스페이스별로 범위를 지정하되, Namespace Isolation(네임스페이스 격리)에 의존해 영향을 한정하세요.
여기서 논의된 RBAC는 CloudNativePG 자체 커스텀 리소스에 대한 접근을 통제해요. 다음 섹션은 상보적인 측면을 다뤄요: 오퍼레이터가 여러분을 대신해 쿠버네티스 리소스를 관리하기 위해 의존하는 RBAC를요.
역할 기반 접근 제어 (RBAC)
이 섹션은 위의 RBAC on Custom Resources에서 논의된 사용자의 커스텀 리소스 접근과 대비되는, 오퍼레이터 자신의 RBAC(여러분을 대신해 쿠버네티스 리소스를 관리하기 위해 사용하는 서비스 계정과 역할)를 다뤄요.
오퍼레이터는 cnpg-manager라는 전용 서비스 계정을 사용해 쿠버네티스 API 서버와 상호작용해요. 이 서비스 계정은 일반적으로 오퍼레이터 네임스페이스(보통 cnpg-system)에 설치돼요. 하지만 네임스페이스는 배포 방법에 따라 달라질 수 있어요 (아래 하위 섹션 참조).
같은 네임스페이스에 cnpg-manager 서비스 계정과 역할 사이의 바인딩이 있어요. 이 역할의 구체적인 이름과 유형(Role 또는 ClusterRole)도 배포 방법에 따라 달라져요. 이 역할은 오퍼레이터가 올바르게 기능하기 위해 필요로 하는 권한을 정의해요. 이 역할에 대해 더 알아보려면 배포 방법에 따라 kubectl describe clusterrole 또는 kubectl describe role 명령을 사용할 수 있어요.
:::info[Important]
위 권한들은 오퍼레이터의 서비스 계정이 쿠버네티스 API 서버와 상호작용하기 위해 전용으로 예약된 것이에요. Cluster, Pooler, Backup, ScheduledBackup, Database, Publication, Subscription, ImageCatalog, ClusterImageCatalog 리소스와만 상호작용하는 오퍼레이터의 사용자들은 직접 접근할 수 없어요.
:::
아래에 몇 가지 예시와, 가장 중요하게는 CloudNativePG가 표준 쿠버네티스 네임스페이스 또는 비네임스페이스 리소스의 전체 또는 일부 관리를 요구하는 이유를 제공해요.
configmaps
: 오퍼레이터는 Prometheus exporter 모니터링 메트릭용 기본 config map을 생성하고 관리해야 해요.
deployments
: 오퍼레이터는 표준 쿠버네티스 Deployment 리소스를 사용해 PgBouncer 연결 풀러를 관리해야 해요.
jobs
: 오퍼레이터는 서로 다른 Cluster 단계를 관리하기 위한 작업(jobs)을 처리해야 해요.
persistentvolumeclaims
: PGDATA가 있는 볼륨은 PostgreSQL Cluster 리소스의 중심 요소예요; 오퍼레이터는 정의된 스케줄링 정책에 따라 요청된 볼륨을 동적으로 프로비저닝하기 위해 선택된 스토리지 클래스와 상호작용해야 해요.
pods
: 오퍼레이터는 Cluster의 인스턴스를 관리해야 해요.
secrets
: Cluster 객체에 인증서와 비밀번호를 제공하지 않으면, 오퍼레이터는 무작위 생성된 비밀번호와 TLS 인증서를 자체 프로비저닝하고 시크릿에 저장함으로써 "관례 위에 구성(convention over configuration)" 패러다임을 채택해요.
serviceaccounts
: 오퍼레이터는 인스턴스 매니저(PostgreSQL 서버를 제어하는 컨테이너의 PID 1 프로세스)가 쿠버네티스 API 서버와 안전하게 통신하고 작업을 조정하며 Cluster의 신뢰할 수 있는 상태를 지속적으로 제공할 수 있게 하는 서비스 계정을 만들어야 해요.
services
: 오퍼레이터는 애플리케이션의 PostgreSQL 클러스터(또는 연결 풀러)에 대한 네트워크 접근을 제어하고, 장애 조치/스위치오버 작업을 자동화된 방식으로 제대로 관리해야 해요 (예를 들어 서비스의 올바른 엔드포인트를 적절한 프라이머리 PostgreSQL 인스턴스에 할당).
validatingwebhookconfigurations와 mutatingwebhookconfigurations
: 오퍼레이터는 자체 서명된 webhook CA를 두 webhook 구성 모두에 주입하며, 이는 관리하는 모든 리소스를 검증하고 변형하는 데 필요해요. 자세한 내용은 쿠버네티스 문서를 참조하세요.
volumesnapshots
: 오퍼레이터는 PostgreSQL 서버의 백업을 가져오기 위해 VolumeSnapshots 객체를 생성해야 해요. 복원 과정을 시작하기 전에 검증하기 위해 VolumeSnapshots도 읽어요.
nodes
: 오퍼레이터는 Affinity와 AntiAffinity용 레이블을 얻어 Pod가 어느 노드에 스케줄링될 수 있는지 결정해야 해요. 예를 들어 레플리카가 같은 노드에 스케줄링되는 것을 방지하는 데 유용해요 — 노드가 서로 다른 가용 영역에 있으면 특히 중요해요. 이 권한은 노드가 스케줄링되었는지 결정해 스케줄링되지 않은 노드에 Pod를 만드는 것을 방지하거나, 프라이머리가 스케줄링되지 않은 노드에 있으면 스위치오버를 트리거하는 데에도 사용돼요.
배포와 ClusterRole 리소스
위에서 언급했듯이 각 배포 방법은 서비스 계정의 네임스페이스 위치와 역할 바인딩 및 각 역할의 이름과 유형에서 차이가 있을 수 있어요.
쿠버네티스 매니페스트를 통한 설치
쿠버네티스 매니페스트로 CloudNativePG를 설치할 때 권한은 기본적으로 ClusterRoleBinding으로 설정돼요. 다음을 실행해 오퍼레이터가 요구하는 권한을 검사할 수 있어요:
kubectl describe clusterrole cnpg-manager
OLM을 통한 설치
보안 관점에서 Operator Lifecycle Manager(OLM)는 더 유연한 배포 방법을 제공해요. 오퍼레이터가 모든 네임스페이스 또는 특정 네임스페이스를 감시하도록 구성할 수 있어, 더 세분화된 권한 관리를 가능하게 해요.
:::info OLM을 사용하면 오퍼레이터를 자신의 네임스페이스에 배포하고 CloudNativePG 클러스터에 사용되는 특정 네임스페이스를 감시하도록 구성할 수 있어요. 이 설정은 권한을 포함하고 접근을 더 효과적으로 제한하는 데 도움을 줘요. :::
ClusterRole 권한이 필요한 이유
오퍼레이터는 현재 nodes와 ClusterImageCatalog 객체를 읽으려면 ClusterRole 권한이 필요해요. 다른 모든 권한은 네임스페이스 범위(즉 Role) 또는 클러스터 전체(즉 ClusterRole)일 수 있어요.
이러한 권한이 있어도 누군가 ServiceAccount에 접근하게 되면 오직 get, list, watch 권한만 가지게 되며, 이는 리소스를 보는 것으로 제한돼요. 하지만 권한이 없는 사용자가 ServiceAccount에 접근한다면 그것은 더 심각한 보안 문제를 나타내요.
따라서 사용자가 오퍼레이터의 ServiceAccount와 권한이 상승된 다른 ServiceAccount에 접근하는 것을 방지하는 것이 중요해요.
인스턴스 매니저가 만드는 API 서버 호출
오퍼랜드 컨테이너의 진입점인 인스턴스 매니저는 일부 리소스의 상태가 올바르게 업데이트되도록 하고 그 Postgres 클러스터와 연결된 config map과 시크릿에 접근하기 위해 쿠버네티스 API 서버에 몇 가지 호출을 해야 해요. 그러한 호출은 같은 PostgreSQL Cluster 리소스 이름을 공유하는 오퍼레이터가 만든 전용 ServiceAccount를 통해 수행돼요.
:::info[Important] 오퍼랜드는 API 서버를 통해 특정하고 제한적인 리소스 하위 집합에만 접근할 수 있어요. 서비스 계정은 Pod 안에서 API 서버에 접근하는 권장 방법이에요. :::
ServiceAccount 토큰의 자동 마운트 비활성화
일부 강화된 환경은 모든 Pod와 ServiceAccount 자체에 automountServiceAccountToken: false를 요구하는 입소 정책을 시행해요. 클러스터 사양에서 automountServiceAccountToken 필드를 설정해 그러한 정책을 준수할 수 있어요:
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: cluster-example
spec:
automountServiceAccountToken: false
instances: 3
storage:
size: 1Gi
값은 인스턴스 Pod의 사양, 오퍼레이터가 실행하는 Jobs의 pod 템플릿, 오퍼레이터가 생성한 ServiceAccount에 그대로 복사돼요.
인스턴스 매니저는 쿠버네티스 API 서버에 대한 접근이 필요하므로, 자동 마운트가 비활성화되면 오퍼레이터는 쿠버네티스가 자동 마운트했을 볼륨과 동등한 kube-api-access라는 명시적 projected volume을 그 Pod와 Jobs에 추가하고, /var/run/secrets/kubernetes.io/serviceaccount에서 모든 컨테이너에 마운트해요. 결과적으로 워크로드는 기본 설정에서 가지는 것과 같은 API 서버 접근을 유지하면서 automountServiceAccountToken 필드를 확인하는 정책을 충족해요.
필드를 변경하면 클러스터의 롤링 업데이트가 트리거돼요.
:::note
공유 ServiceAccount가 serviceAccountName를 통해 참조될 때(아래 참조), 오퍼레이터는 그것을 수정하지 않아요: 그 경우 ServiceAccount 자체에 automountServiceAccountToken: false를 설정하는 것은 여러분의 책임이에요.
:::
공유 ServiceAccount 사용
기본적으로 CloudNativePG는 각 클러스터에 대해 클러스터 이름을 딴 전용 ServiceAccount를 만들어요. 하지만 IAM 역할을 사용하는 클라우드 환경(예: AWS IRSA, GCP Workload Identity, Azure Workload Identity)에서는 각 클러스터가 자신의 ServiceAccount를 만들 때마다 새 클러스터마다 개별 IAM 구성이 필요해서, 여러 클러스터를 관리할 때 확장하기 어려워져요.
CloudNativePG는 클러스터 사양에서 serviceAccountName 필드를 지정해 여러 클러스터가 단일 ServiceAccount를 공유할 수 있게 해줘요. 이를 통해 그 ServiceAccount를 사용하는 모든 클러스터에서 동작하는 일회성 IAM 구성을 가능하게 해줘요.
:::important
공유 ServiceAccount를 사용할 때 ServiceAccount를 직접 만들고 관리하는 것은 여러분의 책임이에요. 오퍼레이터는 지정된 ServiceAccount가 존재하는지 검증하지만 생성하거나 수정하지는 않아요.
:::
다음은 공유 ServiceAccount를 사용하는 예시예요:
# Create the shared ServiceAccount once
apiVersion: v1
kind: ServiceAccount
metadata:
name: postgres-cloud-sa
annotations:
# AWS IRSA annotation
eks.amazonaws.com/role-arn: arn:aws:iam:us-east-1:123456789012:role/PostgresRole
---
# Reference it from multiple clusters
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: cluster-prod
spec:
serviceAccountName: postgres-cloud-sa
instances: 3
storage:
size: 100Gi
:::note
serviceAccountName 필드는 serviceAccountTemplate와 상호 배타적이에요. 오퍼레이터가 ServiceAccount를 관리하게 하거나(선택적으로 serviceAccountTemplate로 커스터마이즈), serviceAccountName으로 기존 것을 참조할 수 있지만 둘 다는 안 돼요.
:::
:::warning
serviceAccountName 필드는 한번 설정하면 불변(immutable)이에요. ServiceAccount를 변경해야 한다면 클러스터를 다시 만들어야 해요.
:::
:::note
여러 클러스터가 ServiceAccount를 공유할 때, 오퍼레이터는 각 클러스터에 대해 별도의 Role과 RoleBinding을 만들어요. 공유 ServiceAccount는 그것을 사용하는 모든 클러스터의 쿠버네티스 RBAC 권한을 축적해요. 각 Role은 특정 클러스터의 리소스로 좁게 범위가 지정되므로 이것은 권한 상승을 구성하지 않아요.
:::
Pooler와 함께 공유 ServiceAccount 사용
같은 공유 ServiceAccount 기능이 Pooler 리소스에도 사용 가능해요. IAM 통합이 있는 클라우드 환경에 PgBouncer 풀러를 배포할 때, 오퍼레이터가 풀러마다 하나를 만들게 하는 대신 Pooler 스펙의 serviceAccountName 필드를 지정해 기존 ServiceAccount를 참조할 수 있어요.
apiVersion: postgresql.cnpg.io/v1
kind: Pooler
metadata:
name: pooler-prod
spec:
cluster:
name: cluster-prod
serviceAccountName: pgbouncer-cloud-sa
instances: 3
pgbouncer:
poolMode: session
:::warning
클러스터처럼 풀러의 serviceAccountName 필드도 한번 설정하면 불변이에요. ServiceAccount를 변경해야 한다면 풀러를 다시 만들어야 해요.
:::
투명성을 위해 서비스 계정과 관련된 권한은 roles.go 파일에 정의되어 있어요. 예를 들어 myns 네임스페이스의 일반적인 mypg 클러스터의 권한을 검색하려면 다음 명령을 입력할 수 있어요:
kubectl get role -n myns mypg -o yaml
그런 다음 역할이 서비스 계정에 바인딩되었는지 확인하세요:
kubectl get rolebinding -n myns mypg -o yaml
:::info[Important] 역할은 주어진 네임스페이스로 제한된다는 것을 기억하세요. :::
아래에 일반적인 쿠버네티스 리소스에 대한 서비스 계정과 관련된 권한의 빠른 요약을 제공해요.
configmaps
: 인스턴스 매니저는 커스텀 모니터링 쿼리처럼 같은 클러스터와 관련된 config map만 읽을 수 있어요
secrets
: 인스턴스 매니저는 같은 클러스터와 관련된 시크릿만 읽을 수 있어요, 즉: 스트리밍 복제 사용자, 애플리케이션 사용자, 슈퍼 사용자, LDAP 인증 사용자, 클라이언트 CA, 서버 CA, 서버 인증서, 백업 자격 증명, 커스텀 모니터링 쿼리
events
: 인스턴스 매니저는 클러스터에 대한 이벤트를 만들어 PostgreSQL 인스턴스 수명 주기의 특정 측면을 API 서버에 알릴 수 있어요
여기서는 CloudNativePG 고유 리소스에 대한 같은 요약을 제공해요.
clusters
: 인스턴스 매니저는 자신의 Cluster 리소스에 대해서만 get, list, watch라는 읽기 전용 권한이 필요해요
clusters/status
: 인스턴스 매니저는 자신의 Cluster 리소스의 상태만 update와 patch해야 해요
backups
: 인스턴스 매니저는 네임스페이스의 모든 Backup 리소스를 읽으려면 get과 list 권한이 필요해요. 추가로 오브젝트 스토어에 대응하는 것이 없는 Backup 객체(보통 보존 정책 때문에)를 제거해 쿠버네티스 클러스터를 정리하려면 delete 권한이 필요해요
backups/status
: 인스턴스 매니저는 네임스페이스의 모든 Backup 리소스의 상태를 update와 patch해야 해요
Pod와 컨테이너 보안 컨텍스트 (Pod and Container Security Contexts)
보안 컨텍스트는 Pod 또는 컨테이너에 대한 권한과 접근 제어 설정을 정의해요.
CloudNativePG는 컨테이너 실행에 privileged 모드를 요구하지 않아요. PostgreSQL 컨테이너는 postgres 시스템 사용자로 실행돼요. 어떤 구성 요소도 root로 실행될 것을 요구하지 않아요.
마찬가지로 볼륨 접근도 privileged 모드나 root 권한을 요구하지 않아요. 적절한 권한은 쿠버네티스 플랫폼 및/또는 관리자가 할당해야 해요. PostgreSQL 컨테이너는 읽기 전용 루트 파일시스템(즉 쓰기 가능한 계층 없음)으로 실행돼요.
오퍼레이터는 PostgreSQL 클러스터의 모든 Pod와 컨테이너의 보안 컨텍스트 설정을 관리해요. PostgreSQL 컨테이너에 사용될 Seccomp Profile은 Cluster 리소스의 spec.seccompProfile 섹션으로 구성할 수 있어요. 이 섹션이 비어 있으면 컨테이너는 seccompProfile Type이 RuntimeDefault(즉 컨테이너 런타임 기본값)를 사용해요.
기본 seccompProfile을 사용하는 PostgreSQL 컨테이너의 보안 컨텍스트는 다음과 같아요:
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
privileged: false
readOnlyRootFilesystem: true
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
보안 컨텍스트 커스터마이즈
CloudNativePG는 각각 spec.podSecurityContext와 spec.securityContext 필드를 통해 Pod와 컨테이너에 대한 보안 컨텍스트의 세밀한 제어를 제공해요.
:::info[Important] 보안 컨텍스트를 변경하면 PostgreSQL 클러스터의 보안 자세에 크게 영향을 줄 수 있고 Pod가 시작되거나 올바르게 동작하지 못하게 할 수 있어요. 변경하기 전에 어떤 필드를 재정의할지와 오퍼레이터 기본값과 어떻게 병합되는지 검토하고, 비프로덕션 환경에서 테스트하며, 필요한 최소한의 잘 문서화된 수정만 적용하세요. :::
Pod 보안 컨텍스트 (spec.podSecurityContext):
이를 통해 모든 PostgreSQL 클러스터 Pod에 적용되는 기본 PodSecurityContext를 재정의할 수 있어요. 지정되면 오퍼레이터의 기본 설정과 병합되며, 명시적으로 설정된 필드에 대해 여러분의 값이 우선해요.
예시:
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: cluster-example
spec:
instances: 3
podSecurityContext:
runAsUser: 26
runAsGroup: 26
fsGroup: 26
supplementalGroups: [2000, 3000]
fsGroupChangePolicy: "OnRootMismatch"
컨테이너 보안 컨텍스트 (spec.securityContext):
이를 통해 PostgreSQL 클러스터 Pod 내의 모든 컨테이너에 적용되는 기본 SecurityContext를 재정의할 수 있어요. podSecurityContext처럼 오퍼레이터의 기본값과 병합돼요.
예시:
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: cluster-example
spec:
instances: 3
securityContext:
allowPrivilegeEscalation: false
# Note: capabilities are not merged with operator defaults.
# If specified, they fully replace any defaults.
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
readOnlyRootFilesystem: true
runAsNonRoot: true
:::info[Important] 명시적으로 설정하지 않은 필드에 대해서는 오퍼레이터가 안전한 기본값을 적용해요. 이는 부분 구성도 보안 모범 사례를 유지하도록 보장해요. :::
:::note
이 필드들은 pod와 container 보안 컨텍스트에 엄격한 요구사항을 가진 Pod Security Standards의 restricted 프로필로 작업할 때 특히 유용해요. Kubernetes Admission Control에서 설명했듯이 결과적인 Pod에 그러한 프로필을 시행하는 것은 오퍼레이터가 아니라 이 입소 체인이에요.
:::
보안 컨텍스트 제약 (Security Context Constraints)
Security Context Constraints (SCC)를 활용하는 환경에서 실행할 때 오퍼레이터는 PostgreSQL 클러스터 Pod의 보안 컨텍스트를 명시적으로 설정하지 않고, Pod가 이미 정의된 제한된 Security Context Constraints를 상속하도록 허용해요.
AppArmor로 Pod 접근 제한
container.apparmor.security.beta.kubernetes.io 주석을 통해 모든 Cluster Pod 안의 postgres, initdb, join, full-recovery, bootstrap-controller 컨테이너에 AppArmor 프로필을 할당할 수 있어요. 예를 들어:
kind: Cluster
metadata:
name: cluster-apparmor
annotations:
container.apparmor.security.beta.kubernetes.io/postgres: runtime/default
container.apparmor.security.beta.kubernetes.io/initdb: runtime/default
container.apparmor.security.beta.kubernetes.io/join: runtime/default
:::warning
이런 종류의 주석을 사용하면 클러스터가 동작을 멈출 수 있어요. 그런 경우 Cluster에서 주석을 안전하게 제거할 수 있어요.
:::
AppArmor 구성은 쿠버네티스 노드 수준이어야 해요. 즉 기본 운영 체제가 이 옵션을 활성화하고 제대로 구성해야 해요.
그렇지 않은 상황에서 Cluster 생성 시점에 주석이 추가되었다면 Pod가 생성되지 않을 거예요. 반면 Cluster 생성 후 주석을 추가하면 클러스터의 Pod가 시작할 수 없게 되고 다음과 같은 오류를 보게 돼요:
metadata.annotations[container.apparmor.security.beta.kubernetes.io/postgres]: Forbidden: may not add AppArmor annotations]
이런 경우 쿠버네티스 관리자에게 연락해 사용할 적절한 AppArmor 프로필을 요청하세요.
네트워크 정책 (Network Policies)
Cluster 리소스가 생성한 Pod는 쿠버네티스 네트워크 정책으로 제어되어 IP 및 TCP 수준에서 인바운드/아웃바운드 네트워크 접근을 활성화/비활성화할 수 있어요. networking 문서에서 더 많은 정보를 찾을 수 있어요.
:::info[Important] 오퍼레이터는 PostgreSQL 서버의 상태에 대한 정보를 얻기 위해 TCP 포트 8000에서 각 인스턴스와 통신해야 해요. 네트워크 정책을 추가하는 경우 이것을 염두에 두고, 더 세밀한 제어를 위해 CloudNativePG가 사용하는 포트 목록은 아래 "Exposed Ports" 섹션을 참조하세요. :::
네트워크 정책은 이 문서의 범위를 벗어나요. 더 자세한 정보는 쿠버네티스 문서의 "Network policies" 섹션을 참조하세요.
노출된 포트 (Exposed Ports)
CloudNativePG는 아래 표에 나열된 대로 오퍼레이터, 인스턴스 매니저, 오퍼랜드 수준에서 포트를 노출해요:
| System | Port number | Exposing | Name | TLS | Authentication |
|---|---|---|---|---|---|
| operator | 9443 | webhook server | webhook-server |
Yes | Yes |
| operator | 8080 | metrics | metrics |
No | No |
| instance manager | 9187 | metrics | metrics |
Optional | No |
| instance manager | 8000 | status | status |
Yes | Partial (1) |
| operand | 5432 | PostgreSQL instance | postgresql |
Optional | Yes |
| 시스템 | 포트 번호 | 노출 | 이름 | TLS | 인증 |
|---|---|---|---|---|---|
| 오퍼레이터 | 9443 | webhook 서버 | webhook-server |
예 | 예 |
| 오퍼레이터 | 8080 | metrics | metrics |
아니요 | 아니요 |
| 인스턴스 매니저 | 9187 | metrics | metrics |
선택 | 아니요 |
| 인스턴스 매니저 | 8000 | status | status |
예 | 부분 (1) |
| 오퍼랜드 | 5432 | PostgreSQL 인스턴스 | postgresql |
선택 | 예 |
(1) Status, health, probe 엔드포인트는 인증이 없어요. 민감한 엔드포인트(backup, pg_controldata, 부분 WAL 아카이브, 인스턴스 매니저 업그레이드)는 아래 설명된 Operator-to-instance authentication처럼 오퍼레이터의 클라이언트 인증서를 요구해요.
오퍼레이터-인스턴스 인증
오퍼레이터는 시작 시 메모리에서 자체 서명된 ECDSA 클라이언트 인증서를 생성하고 그 SHA-256 공개 키 지문을 클러스터의 .status.operatorCertificateFingerprint에 게시해요. 인스턴스 매니저는 그 지문을 고정하고, 일치하는 인증서를 제시하지 않는 민감한 엔드포인트(backup, pg_controldata, 부분 WAL 아카이브, 인스턴스 매니저 업그레이드)에 대한 호출을 거부해요. Status, health, probe 엔드포인트는 인증이 없이 유지돼요.
인증서는 절대 디스크에 기록되지 않으며 오퍼레이터가 재시작할 때마다 재생성되므로, 신뢰는 CA 검증보다 지문 고정에서 비롯돼요.
클라이언트 인증서는 TLS 연결을 통해서만 제시될 수 있고, 인스턴스 매니저는 항상 TLS로 status 포트를 서빙하므로 이 보호는 무조건적이에요.
PostgreSQL
CloudNativePG의 현재 구현은 데이터베이스 소유자에 대한 비밀번호와 .pgpass 파일을 자동으로 만들고, enableSuperuserAccess를 true로 설정해 요청한 경우에만 postgres 슈퍼유저에 대한 것도 만들어요.
:::warning
enableSuperuserAccess는 안전 우선(security-by-default) 자세를 개선하기 위해 기본적으로 false로 설정되어, 마이크로서비스 방식을 촉진하면서 Cluster 리소스의 spec을 통해 PostgreSQL 변경을 선언적으로 수행하고, 데이터베이스 소유자 사용자를 통해 개발자에게 데이터베이스 안의 완전한 권한을 제공해요.
:::
비밀번호 암호화에 관해서 CloudNativePG는 PostgreSQL의 기본 동작을 따르며, PostgreSQL 14부터 password_encryption은 기본적으로 scram-sha-256으로 설정되고, 이전 버전에서는 md5로 설정돼요.
:::info[Important] 자세한 내용은 PostgreSQL 문서의 "Password authentication" 섹션을 참조하세요. :::
:::note
오퍼레이터는 enableSuperuserAccess 옵션 토글을 지원해요. 실행 중인 클러스터에서 비활성화하면 오퍼레이터는 시크릿의 내용을 무시하고, (이전에 오퍼레이터가 생성했다면) 그것을 제거하며, postgres 사용자의 비밀번호를 NULL로 설정해(사실상 비밀번호 인증을 통한 원격 접근을 비활성화)요.
:::
:::info[Important]
PostgreSQL 문서에서 언급했듯이 PostgreSQL은 일부 역할 작업에 대해 로그에 평문 역할 비밀번호를 포함할 수 있어요. CloudNativePG는 비밀번호가 있는 CREATE/ALTER ROLE 작업 동안 postgres 로깅(문장 로깅과 오류 문장 로깅 모두)을 일시적으로 억제해서 비밀번호 누출을 방지해요.
:::
더 많은 정보는 "Connecting from an application" 페이지의 "Secrets" 섹션을 참조하세요.
이 파일들을 사용해 애플리케이션의 데이터베이스 접근을 구성할 수 있어요.
기본적으로 모든 레플리카는 streaming_replica라는 특수 사용자로 현재 프라이머리 인스턴스와 물리적 비동기 스트리밍 복제로 연결하도록 자동 구성돼요. 노드 사이의 연결은 암호화되며 인증은 TLS 클라이언트 인증서를 통해 이뤄져요 (자세한 내용은 "Client TLS/SSL Connections" 페이지 참조). 기본적으로 오퍼레이터는 TLS v1.3 연결을 요구해요.
현재 오퍼레이터는 관리자가 postgresql 구성의 pg_hba 섹션의 일부로 매니페스트에 pg_hba.conf 줄을 직접 추가할 수 있게 해줘요. 매니페스트에 정의된 줄은 기본 pg_hba.conf에 추가돼요.
오퍼레이터가 pg_hba.conf를 어떻게 관리하는지에 대한 자세한 내용은 문서의 "PostgreSQL Configuration" 페이지를 참조하세요.
관리자는 기본적으로 로컬 postgres 사용자만 데이터베이스의 postgres 사용자에게 매핑하는 pg_ident.conf 파일의 내용도 커스터마이즈할 수 있어요.
오퍼레이터가 pg_ident.conf를 어떻게 관리하는지에 대한 자세한 내용은 문서의 "PostgreSQL Configuration" 페이지를 참조하세요.
:::info[Important] 예시들은 쿠버네티스 클러스터가 사설이고 안전한 네트워크에서 실행된다고 가정해요. :::
스키마 해석과 search_path 강화
데이터베이스에 대한 권한이 있는 사용자는 public 같은 쓰기 가능한 스키마에 객체(함수, 연산자, 테이블, 유형)를 심고, 데이터베이스 또는 역할 수준의 search_path를 바꿀 수 있어요 (예: ALTER DATABASE ... SET search_path 또는 ALTER ROLE ... SET search_path). 나중에 그 데이터베이스에 연결하는 권한 있는 세션은 테넌트가 통제하는 search_path를 상속하므로, 그 쿼리 중 하나의 비정규화된 참조가 의도된 객체 대신 심어진 객체로 해석될 수 있어요. 이것은 CWE-426 (Untrusted Search Path)과 유사한 권한 상승 벡터이며, 잘 알려진 CVE-2018-1058과 같은 종류의 문제예요.
이를 방지하기 위해 CloudNativePG는 PostgreSQL에 여는 모든 연결에서 search_path를 데이터베이스나 연결 역할에 구성된 search_path와 무관하게 pg_catalog, public, pg_temp의 고정 값으로 고정해요:
pg_catalog가 먼저 검색되므로 내장 객체는 항상 다른 스키마에 심어진 같은 이름의 객체보다 우선해요;- 세션 전용 임시 스키마인
pg_temp는 처음이 아니라 마지막에 검색되므로 관계나 데이터 타입을 가릴 수 없어요.
추가로 PgBouncer 통합에 사용되는 SECURITY DEFINER 조회 함수는 자체 고정된 search_path로 생성되며, 모니터링 쿼리는 "Monitoring" 페이지에 설명된 대로 search_path가 고정된 트랜잭션 안에서 실행돼요.
:::note
사용자 작성 SQL — 부트스트랩 중의 postInitSQL/postInitApplicationSQL/postInitTemplateSQL와 논리 임포트의 post-import 쿼리 — 은 표준 "$user", public 해석으로 실행되므로 일반 PostgreSQL 세션에서처럼 동작해요. 그 스크립트에서 객체 참조가 search_path와 무관하도록 해야 한다면 스키마로 정규화하세요.
:::
스토리지 (Storage)
CloudNativePG는 저장 데이터 암호화(encryption at rest)를 기본 스토리지 클래스에 위임해요. 프로덕션 환경의 데이터 보호를 위해 암호화를 지원하는 스토리지 클래스를 선택하는 것을 강력히 권장해요.