본문으로 건너뛰기

8.1 Security

CloudNativePG의 보안은 데이터베이스 한 겹이 아니라 여러 겹으로 이루어진다. 코드가 컨테이너 이미지가 되고, 그 이미지가 Pod로 떠서, Kubernetes 클러스터 안에서 네트워크로 묶이고, 그 안에서 PostgreSQL이 계정과 권한을 관리한다. 각 겹마다 방어 지점이 다르기 때문에, CloudNativePG는 이를 컨테이너·클러스터(RBAC·네트워크)·PostgreSQL의 계층으로 나눠 설계했다. 이 절에서는 DBA가 실제로 손대게 되는 계층을 중심으로, 기본 방어 자세와 조정 포인트를 정리한다.

보안 계층 개요

    flowchart TD
  CODE["코드·이미지<br/>서명·SBOM·스캔"]
  CONT["컨테이너<br/>securityContext"]
  RBAC["RBAC<br/>ServiceAccount 권한"]
  NET["네트워크<br/>NetworkPolicy·TLS"]
  PG["PostgreSQL<br/>계정·pg_hba·권한"]
  STO["스토리지<br/>encryption at rest"]

  CODE --> CONT --> RBAC --> NET --> PG --> STO

  classDef node fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
  class CODE,CONT,RBAC,NET,PG,STO node
  

아래로 갈수록 DBA의 손이 자주 닿는 계층이다. 이미지 서명·정적 분석 같은 상위 계층은 CloudNativePG 프로젝트가 책임지고, 컨테이너 아래부터는 운영하는 쪽에서 조정하게 된다.

신뢰 모델: Cluster 쓰기 권한의 무게

가장 먼저 이해해야 할 원칙이 있다. Cluster 리소스에 쓰기 권한을 준다는 것은 그 결과로 만들어지는 Pod·Service·PVC 전체에 대한 통제권을 준다는 것과 같다. Cluster 매니페스트에는 spec.securityContext, postInitSQL, superuser 권한을 가진 managed role, 커스텀 pg_hba 규칙까지 담을 수 있기 때문이다.

Cluster 리소스에 대한 create/update 권한은 사실상 데이터베이스 superuser 권한과 맞먹는다. RBAC를 설계할 때 이 권한을 누구에게 줄지는 데이터베이스 계정을 만들 때만큼 신중하게 다뤄야 한다. cnpg.io/validation: disabled 애너테이션은 웹훅 검증을 우회할 뿐 보안 경계가 아니다.

컨테이너 보안: securityContext

CloudNativePG는 PostgreSQL Pod를 최소 권한으로 띄운다. 별도 설정 없이도 다음과 같은 기본 securityContext가 적용된다.

securityContext:
  allowPrivilegeEscalation: false
  capabilities:
    drop:
    - ALL
  privileged: false
  readOnlyRootFilesystem: true
  runAsNonRoot: true
  seccompProfile:
    type: RuntimeDefault

root로 실행하지 않고, 루트 파일시스템은 읽기 전용이며, 모든 Linux capability를 떨어뜨린 상태다. 특정 UID/GID로 실행해야 하는 환경이라면 Cluster 스펙에서 재정의한다.

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: cluster-example
spec:
  instances: 3
  podSecurityContext:
    runAsUser: 26
    runAsGroup: 26
    fsGroup: 26
    fsGroupChangePolicy: "OnRootMismatch"
  storage:
    size: 10Gi
securityContext를 바꾸면 클러스터가 아예 뜨지 못할 수도 있다. 프로덕션에 반영하기 전 반드시 비운영 환경에서 검증한다. OpenShift에서는 CloudNativePG가 securityContext를 명시하지 않고 restricted SCC(Security Context Constraints)를 상속받게 두는 것이 기본이다.

RBAC: 두 종류의 ServiceAccount

CloudNativePG의 RBAC는 역할이 뚜렷이 나뉜 두 ServiceAccount로 돌아간다.

ServiceAccount위치권한 범위
cnpg-manageroperator namespaceClusterRole — 모든 namespace의 Cluster 감시
Cluster별 SACluster namespace자기 Cluster 관련 리소스에 한정

Operator의 cnpg-manager가 ClusterRole을 필요로 하는 이유는, affinity 계산을 위해 nodes를 읽고 ClusterImageCatalog 같은 클러스터 범위 오브젝트에 접근해야 하기 때문이다. 실제로 어떤 권한을 갖는지는 다음으로 확인한다.

kubectl describe clusterrole cnpg-manager

각 Cluster에는 Cluster와 같은 이름의 ServiceAccount가 따로 붙는다. 이 SA는 자기 Cluster에 연관된 Secret·ConfigMap만 읽고, 자기 Cluster의 status만 갱신할 수 있는 최소 권한이다. 클라우드 IAM 역할(예: AWS IRSA)을 붙이려면 미리 만든 SA를 지정한다.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: postgres-cloud-sa
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/PostgresRole
---
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: cluster-prod
spec:
  serviceAccountName: postgres-cloud-sa
  instances: 3
  storage:
    size: 100Gi

serviceAccountName은 한번 설정하면 바꿀 수 없고(immutable), serviceAccountTemplate과 함께 쓸 수 없다.

네트워크: 포트와 NetworkPolicy

NetworkPolicy로 트래픽을 잠글 때 반드시 열어 둬야 하는 통로가 있다. Operator는 각 인스턴스의 TCP 8000 포트로 접속해 PostgreSQL 상태를 읽어 온다. 이 통로를 막으면 Operator가 인스턴스 상태를 파악하지 못해 failover 판단이 어긋난다.

컴포넌트포트용도TLS
Operator9443webhook있음
Operator8080metrics없음
Instance Manager9187metrics선택
Instance Manager8000status있음
PostgreSQL5432DB 접속선택

Operator와 인스턴스 사이의 8000 포트 통신은 자체 서명 ECDSA 인증서로 인증한다. Operator는 시작할 때 메모리에 인증서를 만들고 그 SHA-256 fingerprint를 .status.operatorCertificateFingerprint에 게시하며, instance manager는 이 fingerprint를 고정해 두고 일치하지 않는 호출을 거부한다.

status 포트(8000)가 TLS로 서빙되지 않으면 instance manager는 Operator를 인증할 수 없어, 보호된 엔드포인트(backup·controldata·WAL)로 오는 모든 호출을 영구히 거부한다. TLS를 끄는 설정은 피한다.

PostgreSQL 계층: 기본 방어 자세

데이터베이스 자체는 다음과 같은 안전한 기본값으로 부트스트랩된다.

  • superuser 비활성화enableSuperuserAccess는 기본 false. postgres 계정의 네트워크 접속이 막혀 있다. 켰다가 다시 끄면 Operator가 비밀번호를 NULL로 되돌린다.
  • 강한 비밀번호 해시 — PostgreSQL 14 이상은 scram-sha-256, 그 이하는 md5.
  • 암호화된 복제 — replica는 physical async streaming replication으로 붙되, 노드 간 연결은 암호화되고 TLS client certificate로 인증한다. 기본 TLS 버전은 1.3.
  • 비밀번호 로그 차단 — 비밀번호가 딸린 CREATE/ALTER ROLE 실행 중에는 statement 로깅을 일시 억제해 로그로 비밀번호가 새지 않게 한다.

search_path 고정으로 권한 상승 차단

CloudNativePG가 여는 모든 연결은 search_pathpg_catalog, public, pg_temp로 고정한다. 신뢰할 수 없는 스키마가 내장 오브젝트를 가리는 CVE-2018-1058 계열의 권한 상승 공격을 막기 위해서다. 이 고정은 CloudNativePG가 여는 연결, PgBouncer의 lookup 함수, 모니터링 쿼리에 적용된다.

다만 사용자가 직접 작성한 postInitSQL·postInitApplicationSQL·postInitTemplateSQL은 예외로, 표준 "$user", public 규칙으로 실행된다. 부트스트랩 SQL을 작성할 때는 스키마 해석 경로를 스스로 챙겨야 한다.

스토리지: 저장 시 암호화

CloudNativePG는 저장 데이터 암호화(encryption at rest)를 직접 하지 않고 하위 storage class에 위임한다. 프로덕션에서는 암호화를 지원하는 storage class를 선택하는 것을 강하게 권장한다.

namespace 격리

Operator는 관리하는 모든 리소스를 부모 Cluster와 같은 namespace 안에 가둔다. 인스턴스 Pod는 언제나 Cluster와 동일한 namespace에 생성되며, Kubernetes owner reference도 namespace를 넘지 못한다. 따라서 namespace 자체가 하나의 자연스러운 격리 경계가 된다.

정리

  • 보안은 컨테이너·RBAC·네트워크·PostgreSQL·스토리지의 계층 방어로 설계된다
  • Cluster 쓰기 권한 = Pod·데이터베이스 통제권. RBAC 설계 시 superuser급으로 취급
  • Pod는 non-root·read-only·capability drop이 기본. 재정의는 비운영에서 검증
  • Operator↔인스턴스의 8000 포트 TLS 통로는 반드시 열어 두고 TLS를 끄지 않는다
  • superuser 비활성·scram-sha-256·암호화 복제·search_path 고정이 데이터베이스 기본값

다음 절(8.2)에서는 이 방어의 핵심 재료인 TLS 인증서 체계를 본다.