본문으로 건너뛰기

"보안" 태그로 연결된 16개 게시물개의 게시물이 있습니다.

모든 태그 보기

CNPG exporter 취약점 점검

· 약 14분

CloudNativePG metrics exporter는 1.29.1과 1.28.3 이전까지 postgres superuser로 접속한 뒤 SET ROLE pg_monitor로 권한을 낮춰 쿼리를 실행했어요. SET ROLE은 current_user만 바꾸고 session_user는 그대로 두기 때문에, scrape 세션 안에서 평가되는 SQL 식이 RESET ROLE로 superuser를 되찾을 수 있었습니다. 이 결함이 CVE-2026-44477(GHSA-423p-g724-fr39)이에요. 이 글은 방어와 점검 관점에서 원인, 수정 내용, 업그레이드 순서, 점검 쿼리를 정리하고 공격 절차는 다루지 않습니다. PostgreSQL 쪽 동작은 순수 PostgreSQL 컨테이너에서 하나씩 측정했고, exploit 체인은 재현하지 않았습니다.

요약​

항목내용
식별자CVE-2026-44477, GHSA-423p-g724-fr39
공개2026-05-08 (저장소 advisory와 패치 릴리스 같은 날)
심각도Critical, CVSS 3.1 9.9 (AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H)
CWECWE-250, CWE-271, CWE-426
영향 버전1.28.3 미만, 1.29.0
해결 버전1.28.3, 1.29.1
필요 권한scrape 대상 database 안에서 search_path에 걸리는 객체를 만들 수 있는 계정
결과PostgreSQL superuser 획득, primary Pod 안에서 postgres OS 사용자로 명령 실행
제보Mehmet Ince

영향 범위와 해결 버전은 GitHub advisory의 패키지 범위(< 1.28.3, >= 1.29.0, < 1.29.1)와 1.29.1 릴리스 노트, 1.28.3 릴리스 노트에서 확인했습니다. 1.27 이하 라인에는 별도 패치 릴리스가 없습니다. 1.27.x는 2026-03-09에 이미 지원이 끝났습니다.

경고

2026-06-29에 같은 계열의 후속 취약점 CVE-2026-55769(GHSA-x8c2-3p4r-v9r6)가 공개됐고 1.28.4, 1.29.2, 1.30.0에서 수정됐습니다. 1.29.1이나 1.28.3에서 멈추면 이 후속 건이 남습니다. 업그레이드 목표는 아래 "업그레이드" 절을 참고하십시오.

원인: SET ROLE 뒤에 남는 superuser 신원​

수정 전 exporter는 Pod 로컬 Unix socket으로 postgres에 접속했고, 쿼리마다 read-only 트랜잭션을 열어 다음을 실행했습니다. 수정 commit의 createMonitoringTx에서 지워진 줄입니다.

// Set the pg_monitor role
_, err = tx.Exec("SET ROLE TO pg_monitor")

advisory는 이 구조를 연쇄의 출발점으로 봅니다. 요지는 세 문장입니다.

  • SET ROLE은 current_user만 바꾸고 session_user는 postgres로 남습니다.
  • scrape 세션에서 평가되는 SQL 식은 무엇이든 RESET ROLE로 superuser를 되찾고, COPY ... TO PROGRAM으로 Pod 안에서 프로세스를 띄울 수 있습니다.
  • READ ONLY 트랜잭션은 데이터베이스 상태에 대한 쓰기를 막을 뿐, 외부 프로세스 실행은 막지 않습니다.

아래 세 절은 이 문장들을 PostgreSQL 18.4 컨테이너에서 하나씩 확인한 결과입니다. 공격 객체와 권한 회복을 한 번에 엮는 코드는 쓰지 않았습니다.

SET ROLE과 session_user​

superuser로 접속해 read-only 트랜잭션 안에서 SET ROLE pg_monitor를 실행한 결과입니다.

BEGIN READ ONLY;
SET ROLE pg_monitor;
SELECT session_user, current_user,
(SELECT rolsuper FROM pg_catalog.pg_roles WHERE rolname = current_user) AS cur_super,
(SELECT rolsuper FROM pg_catalog.pg_roles WHERE rolname = session_user) AS sess_super;
SELECT count(*) FROM pg_catalog.pg_authid;
session_user | current_user | cur_super | sess_super
--------------+--------------+-----------+------------
postgres | pg_monitor | f | t

ERROR: permission denied for table pg_authid

권한 검사는 current_user 기준이라 pg_authid 조회가 막힙니다. 여기까지만 보면 권한이 잘 낮아진 것처럼 보입니다. 같은 트랜잭션에서 RESET ROLE을 실행하면 결과가 달라집니다.

BEGIN READ ONLY;
SET ROLE pg_monitor;
RESET ROLE;
SELECT session_user, current_user;
SELECT count(*) AS authid_rows FROM pg_catalog.pg_authid;
session_user | current_user
--------------+--------------
postgres | postgres

authid_rows
-------------
17

RESET ROLE은 current_user를 session_user로 되돌리는 명령이고, 이 명령에는 별도 권한이 필요 없습니다. session_user가 superuser인 한 SET ROLE은 격리 수단이 되지 못합니다.

SET SESSION AUTHORIZATION도 같은 결과​

advisory는 SQL 수준에서 SET SESSION AUTHORIZATION pg_monitor로 한 단계 더 내려도 부족하다고 적었습니다. SET SESSION AUTHORIZATION의 권한 검사가 현재 session_user가 아니라 처음 인증된 사용자가 superuser인지를 보기 때문입니다. 측정 결과도 같았습니다.

SET SESSION AUTHORIZATION cnpg_metrics_exporter;
SELECT session_user, current_user;
RESET SESSION AUTHORIZATION;
SELECT session_user, current_user;
SET SESSION AUTHORIZATION cnpg_metrics_exporter;
SET SESSION AUTHORIZATION postgres;
SELECT session_user, current_user;
session_user | current_user
-----------------------+-----------------------
cnpg_metrics_exporter | cnpg_metrics_exporter

session_user | current_user
--------------+--------------
postgres | postgres

session_user | current_user
--------------+--------------
postgres | postgres

session_user를 비 superuser로 바꾼 뒤에도 RESET SESSION AUTHORIZATION이나 SET SESSION AUTHORIZATION postgres로 다시 superuser가 됩니다. 인증 단계의 사용자 자체를 바꿔야 한다는 advisory의 결론이 여기서 나옵니다.

READ ONLY가 막는 것과 막지 못하는 것​

superuser 세션에서 BEGIN READ ONLY 뒤에 실행한 결과입니다.

문장결과
CREATE TABLE t2(id int)cannot execute CREATE TABLE in a read-only transaction
INSERT INTO t VALUES (1)cannot execute INSERT in a read-only transaction
SELECT nextval('s')cannot execute nextval() in a read-only transaction
SELECT pg_read_file('postgresql.conf')성공 (32,557바이트)
COPY (SELECT ...) TO '/tmp/ro_copy.txt'성공, 파일 생성 확인
COPY (SELECT 1) TO PROGRAM 'true'성공
SELECT set_config('work_mem', '8MB', true)성공

transaction_read_only는 데이터베이스 안의 테이블, 시퀀스, 카탈로그 변경을 막습니다. 서버 파일 읽기와 쓰기, 외부 프로그램 실행은 트랜잭션이 기록하는 데이터 변경이 아니어서 검사 대상에서 빠집니다. 그래서 이 조합을 막는 것은 트랜잭션 모드가 아니라 role의 권한입니다.

영향 조건​

advisory는 영향 대상을 두 갈래로 정리했습니다. 공격 방법은 이 글에서 다루지 않고 대상 여부를 판단하는 데 필요한 조건만 옮깁니다.

  • 기본 모니터링을 켠 모든 지원 릴리스. 기본 제공 default-monitoring.yaml의 pg_extensions 메트릭이 스키마를 붙이지 않은 current_database()를 호출했고 target_databases: '*'로 모든 사용자 database에서 실행됐기 때문입니다. bootstrap.initdb가 만드는 기본 app role처럼 database를 소유한 비 superuser 계정이 있으면 조건을 채웁니다.
  • 스키마를 붙이지 않은 함수나 relation을 참조하는 custom metric 쿼리를 쓰는 배포. 고객이 metric 쿼리 본문을 넣거나 바꿀 수 있는 멀티테넌트 플랫폼을 advisory는 가장 위험한 경우로 봤습니다.

advisory는 스키마 누락이 가장 직접적인 형태일 뿐이라고 덧붙였습니다. scrape 세션에서 평가되는 식은 모두 superuser 권한으로 실행됐으므로, 사용자 정의 함수, operator, cast, 사용자 소유 테이블이나 view, 읽는 객체에 걸린 index 식과 RLS policy도 같은 범주에 들어갑니다. 쿼리에 pg_catalog.를 붙이는 것만으로는 원인이 없어지지 않는다는 뜻입니다.

PostgreSQL 14 이하는 조건이 더 넓다고 advisory가 따로 적었습니다. PostgreSQL 15부터 public 스키마의 CREATE 권한이 PUBLIC에서 빠졌기 때문입니다. 두 버전에서 아무 권한 없는 login role로 확인한 결과입니다.

버전has_schema_privilege(..., 'public', 'CREATE')public ACLpublic 소유자
14.19t{postgres=UC/postgres,=UC/postgres}postgres
18.4f{pg_database_owner=UC/pg_database_owner,=U/pg_database_owner}pg_database_owner

CloudNativePG 1.29와 1.30은 PostgreSQL 14부터 18까지 지원합니다. 14를 쓰는 클러스터라면 database 소유자가 아닌 계정도 대상에 넣어 판단해야 합니다.

수정 내용​

advisory 본문은 "세 개의 패치"라고 적었지만 2026-09-28에 조회한 Patches 절에는 두 항목만 있습니다. 두 패치 모두 1.29.1과 1.28.3에 들어갔습니다.

Patch 1: 기본 쿼리의 스키마 명시​

PR #10576은 default-monitoring.yaml과 문서 예제의 카탈로그 참조에 pg_catalog.를 붙였습니다. pg_extensions 쿼리의 변경은 이렇습니다.

- current_database() as datname,
+ pg_catalog.current_database() as datname,
...
- WHEN default_version = installed_version THEN 0
+ WHEN default_version OPERATOR(pg_catalog.=) installed_version THEN 0

함수와 함께 = 같은 operator도 OPERATOR(pg_catalog.=)로 바꿨습니다. v1.29.0과 v1.29.1의 파일을 받아 스키마 없는 호출을 세어 보면 count( 3곳, now( 1곳, current_database( 1곳이 사라지고 OPERATOR( 13곳이 생깁니다. 파일 길이는 둘 다 491줄입니다. target_databases에 '*'를 쓰는 쿼리는 두 버전 모두 pg_extensions 하나입니다.

이 패치는 오퍼레이터가 배포하는 설정만 고칩니다. default-monitoring.yaml을 복사해 만든 custom ConfigMap은 사용자가 직접 고쳐야 합니다.

Patch 2: 전용 cnpg_metrics_exporter role​

commit 04e8c19가 근본 원인을 없앤 변경입니다. 18개 파일을 바꿨고, 요지는 인증 단계의 사용자 교체입니다.

  • cnpg_metrics_exporter role을 LOGIN PASSWORD NULL로 만들고 pg_monitor만 부여
  • NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS INHERIT LOGIN 속성과 pg_monitor 멤버십, 비밀번호 없음을 reconcile마다 primary에서 다시 맞춤
  • exporter 전용 connection pool이 user=cnpg_metrics_exporter application_name=cnpg_metrics_exporter로 Unix socket에 접속
  • pg_hba.conf에 local all cnpg_metrics_exporter peer map=cnpg_metrics_exporter, pg_ident.conf에 매핑 한 줄 추가
  • SET ROLE TO pg_monitor 제거, 대신 트랜잭션마다 search_path 앞에 pg_catalog를 붙임
  • target_databases: '*' 확장 시 has_database_privilege(datname, 'CONNECT')가 false인 database는 Debug 로그만 남기고 제외
  • bootstrap.initdb.import의 role 가져오기 대상에서 제외

수정 후 구조를 같은 PostgreSQL 18.4 컨테이너에서 재현했습니다. role을 commit과 같은 문장으로 만들고, 전용 계정으로 접속해 앞 절의 문장을 다시 실행한 결과입니다.

session_user | current_user
-----------------------+-----------------------
cnpg_metrics_exporter | cnpg_metrics_exporter

RESET -- 결과는 그대로 cnpg_metrics_exporter
ERROR: permission denied to set role "postgres"
ERROR: permission denied to set session authorization "postgres"
ERROR: permission denied for function pg_read_file
ERROR: permission denied to COPY to or from an external program
DETAIL: Only roles with privileges of the "pg_execute_server_program" role may COPY to or from an external program.
ERROR: permission denied to COPY to a file
ERROR: permission denied for table pg_authid

pg_stat_activity와 pg_settings는 그대로 조회됩니다. session_user가 처음부터 비 superuser이므로 RESET ROLE이 돌아갈 곳도 비 superuser입니다.

peer 매핑도 확인했습니다. commit의 두 줄을 컨테이너 설정에 넣고({{.Username}} 자리는 컨테이너 OS 사용자 postgres) reload한 뒤 pg_hba_file_rules, pg_ident_file_mappings로 읽힌 규칙을 보고 접속해 봤습니다.

접속 시도결과
OS 사용자 postgres, socket, -U cnpg_metrics_exporter성공, session_user는 cnpg_metrics_exporter
OS 사용자 root, socket, -U cnpg_metrics_exporterPeer authentication failed
TCP 127.0.0.1, 비밀번호 입력password authentication failed

비밀번호가 NULL이라 TCP로는 접속할 수 없습니다. socket에서는 Pod 안의 postgres OS 사용자만 이 role로 접속합니다.

노트

수정 후 exporter 권한은 pg_monitor 범위로 줄었습니다. 사용자 소유 테이블이나 pg_authid, pg_subscription 같은 superuser 전용 카탈로그를 읽던 custom 쿼리는 실패합니다. 이런 쿼리를 계속 쓰려면 GRANT USAGE ON SCHEMA, GRANT SELECT를 cnpg_metrics_exporter에 따로 부여해야 합니다. PUBLIC의 CONNECT를 회수한 database도 GRANT CONNECT가 필요합니다. 공식 문서는 이 role에 pg_monitor와 필요한 테이블 권한 외의 멤버십을 주지 말라고 경고합니다.

후속 취약점 CVE-2026-55769​

1.30.0 릴리스 날인 2026-06-29에 GHSA-x8c2-3p4r-v9r6이 공개됐습니다. 이번에는 exporter 대신 instance manager가 superuser로 여는 connection에 search_path가 고정돼 있지 않았던 문제입니다. 역시 CWE-426이고 critical입니다. 수정은 PR #10774이며, 오퍼레이터가 여는 모든 pooled connection의 startup message에 search_path = pg_catalog, public, pg_temp를 넣었습니다.

라인CVE-2026-44477 수정CVE-2026-55769 수정
1.281.28.31.28.4
1.291.29.11.29.2
1.301.30.0부터 포함1.30.0부터 포함

advisory는 오퍼레이터 connection에 고정 search_path를 주거나 신뢰할 수 없는 사용자에게 database 소유권을 주지 않은 클러스터는 해당되지 않는다고 적었습니다. database 소유자 계정이 이번 두 건에서 공통 조건이라는 점은 운영 관점에서 기억해 둘 만합니다.

업그레이드​

지원 정책 문서 기준으로 1.28.x는 2026-06-30에 지원이 끝났고, 1.29.x는 2026-09-29가 EOL입니다. 2026-09-23에 나온 1.30.1과 1.29.3이 현재 최신입니다. 두 CVE를 함께 정리하려면 1.30.1로 올리는 것이 가장 단순합니다. 1.29에 남는다면 최소 1.29.2가 필요하지만 EOL이 바로 다음 날입니다.

공식 문서와 릴리스 노트에서 확인한 순서와 주의 사항입니다. CloudNativePG 업그레이드 자체는 이 글에서 실행해 보지 않았습니다.

  1. 현재 오퍼레이터 버전과 대상 클러스터의 PostgreSQL major 버전 확인
  2. custom monitoring ConfigMap, Secret에서 사용자 테이블이나 superuser 전용 카탈로그를 읽는 쿼리 목록 작성
  3. 그 쿼리에 필요한 GRANT를 cnpg_metrics_exporter 기준으로 준비 (role은 업그레이드 후 생김)
  4. replica cluster가 있으면 source primary cluster를 먼저 업그레이드. role은 source primary에서 만들어져 streaming replication으로 내려가므로, replica cluster를 먼저 올리면 role이 없는 상태로 scrape합니다
  5. 업그레이드 후 instance manager 로그에서 exporter의 권한 오류, role 누락 오류 확인
  6. role이 없으면 공식 문서의 수동 생성 문장을 writable primary에서 superuser로 실행
CREATE ROLE cnpg_metrics_exporter WITH LOGIN NOSUPERUSER NOCREATEDB
NOCREATEROLE NOREPLICATION NOBYPASSRLS INHERIT;
GRANT pg_monitor TO cnpg_metrics_exporter;

바로 올릴 수 없을 때 advisory가 제시한 임시 조치는 네 가지입니다. custom 쿼리의 모든 카탈로그 참조에 pg_catalog.를 붙이기, 신뢰하는 role만 database를 소유하게 하기, target_databases: '*' 범위 줄이기, 신뢰할 수 없는 사용자에게 metric 쿼리 본문을 노출하지 않기입니다. advisory는 첫 번째가 부분 완화일 뿐이라고 명시합니다.

점검 항목​

아래 SQL은 PostgreSQL 18.4 컨테이너에서 실행해 결과 형태를 확인했습니다. CloudNativePG Pod 안에서는 실행해 보지 않았습니다. primary에 superuser로 접속해 실행하는 것을 전제로 합니다. superuser로 사용자 database에 접속할 때는 PGOPTIONS="-c search_path=pg_catalog"를 붙여 세션 search_path를 고정하십시오.

exporter가 어떤 계정으로 접속해 있는지 확인합니다. 수정 전 버전이면 usename이 postgres, rolsuper가 t로 나옵니다.

SELECT a.datname, a.usename, r.rolsuper, a.application_name
FROM pg_catalog.pg_stat_activity a
JOIN pg_catalog.pg_roles r ON r.oid = a.usesysid
WHERE a.application_name = 'cnpg_metrics_exporter';
datname | usename | rolsuper | application_name
----------+-----------------------+----------+-----------------------
app | cnpg_metrics_exporter | f | cnpg_metrics_exporter
postgres | postgres | t | cnpg_metrics_exporter

위 출력은 수정 전과 수정 후 연결을 컨테이너에 동시에 띄워 만든 예시입니다. 실제 클러스터에서는 한 종류만 나와야 합니다.

role 속성과 멤버십이 commit이 강제하는 상태와 같은지 봅니다. member_of에 pg_monitor 외의 role이 있으면 누가 왜 줬는지 확인해야 합니다.

SELECT r.rolname, r.rolsuper, r.rolinherit, r.rolcanlogin,
a.rolpassword IS NULL AS no_password,
ARRAY(SELECT b.rolname FROM pg_catalog.pg_auth_members m
JOIN pg_catalog.pg_roles b ON b.oid = m.roleid
WHERE m.member = r.oid) AS member_of
FROM pg_catalog.pg_roles r
JOIN pg_catalog.pg_authid a ON a.oid = r.oid
WHERE r.rolname = 'cnpg_metrics_exporter';
rolname | rolsuper | rolinherit | rolcanlogin | no_password | member_of
-----------------------+----------+------------+-------------+-------------+--------------
cnpg_metrics_exporter | f | t | t | t | {pg_monitor}

database 소유자가 누구인지 봅니다. 두 CVE 모두 database 소유자 계정이 조건에 들어가므로, 애플리케이션 계정이 소유자인지 확인해 둡니다.

SELECT d.datname, pg_catalog.pg_get_userbyid(d.datdba) AS owner, r.rolsuper AS owner_is_super
FROM pg_catalog.pg_database d
JOIN pg_catalog.pg_roles r ON r.oid = d.datdba
WHERE NOT d.datistemplate
ORDER BY 1;

database나 role 수준에서 search_path를 바꿔 둔 설정이 있는지 봅니다. 의도하지 않은 항목이 있으면 설정한 사람과 이유를 확인합니다.

SELECT d.datname, r.rolname, s.setconfig
FROM pg_catalog.pg_db_role_setting s
LEFT JOIN pg_catalog.pg_database d ON d.oid = s.setdatabase
LEFT JOIN pg_catalog.pg_roles r ON r.oid = s.setrole
WHERE pg_catalog.array_to_string(s.setconfig, ',') LIKE '%search_path%';

pg_catalog 밖에 pg_catalog 함수와 같은 이름의 함수, 또는 사용자 operator가 있는지 database마다 확인합니다. 확장 소속 객체는 pg_depend의 deptype = 'e'로 제외했습니다. 이 조건이 없으면 citext 하나만 설치해도 동명 함수 25개가 나오고, 조건을 넣으면 확장이 아닌 객체만 남습니다.

SELECT n.nspname AS schema, p.proname AS name, 'function' AS kind,
pg_catalog.pg_get_userbyid(p.proowner) AS owner
FROM pg_catalog.pg_proc p
JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
AND EXISTS (SELECT 1 FROM pg_catalog.pg_proc c
WHERE c.pronamespace = 'pg_catalog'::pg_catalog.regnamespace
AND c.proname OPERATOR(pg_catalog.=) p.proname)
AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d
WHERE d.classid = 'pg_catalog.pg_proc'::pg_catalog.regclass
AND d.objid = p.oid AND d.deptype = 'e')
UNION ALL
SELECT n.nspname, o.oprname, 'operator', pg_catalog.pg_get_userbyid(o.oprowner)
FROM pg_catalog.pg_operator o
JOIN pg_catalog.pg_namespace n ON n.oid = o.oprnamespace
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d
WHERE d.classid = 'pg_catalog.pg_operator'::pg_catalog.regclass
AND d.objid = o.oid AND d.deptype = 'e')
ORDER BY 1, 2;

결과가 있다고 해서 공격 흔적이라는 뜻은 아닙니다. 애플리케이션이 일부러 만든 함수일 수도 있으므로 소유자와 생성 시점을 확인하는 출발점으로 쓰십시오.

custom 쿼리 본문은 Kubernetes 쪽에서 확인합니다. 아래 명령은 이 글에서 실행하지 않았고, 클러스터 spec의 monitoring.customQueriesConfigMap 경로를 따른 것입니다.

kubectl get cluster -A -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}{"\t"}{.spec.monitoring.customQueriesConfigMap}{"\n"}{end}'
kubectl get configmap <name> -n <ns> -o yaml | grep -nE "target_databases|current_database\(|now\("

측정 환경과 한계​

  • macOS 호스트의 Colima VM (Docker 28.3.3, 2 CPU, 약 12GB, Linux 6.8, arm64), 다른 작업과 공유
  • postgres:18 (PostgreSQL 18.4), postgres:14 (PostgreSQL 14.19) 컨테이너
  • CloudNativePG 오퍼레이터와 Kubernetes 클러스터는 띄우지 않았습니다. CNPG 쪽 사실은 advisory, commit diff, 릴리스 노트, 지원 정책 문서, default-monitoring.yaml 원본으로만 확인했습니다
  • exploit 체인은 재현하지 않았고, PostgreSQL 동작을 하나씩 따로 확인했습니다

관련 글​

  • pg_maintain 권한 위임 정리: 유지보수 명령 중 search_path를 고정하는 PostgreSQL 17의 방어 장치를 다룹니다. 권한을 위임받은 세션이 무엇을 실행하게 되는지를 본다는 점에서 이 글과 같은 질문입니다
  • CloudNativePG 10.1 모니터링: exporter 구조, 9187 포트, custom 쿼리 ConfigMap 작성법

참고​

SET ROLE로 권한을 낮춘 연결은 처음 인증한 신원을 계속 기억해요. 모니터링처럼 주기적으로 도는 연결일수록 처음부터 낮은 권한으로 인증하는 편이 맞다는 것이 이번 수정에서 제가 얻은 기준이에요. 아직 1.29.1 이전이라면 1.30.1로 올리고 위 점검 쿼리를 한 번 실행해 보시길 권해요.

pg_maintain 권한 위임 정리

· 약 8분

PostgreSQL 17은 테이블 수준 권한에 MAINTAIN을, predefined role에 pg_maintain을 추가했습니다. VACUUM과 ANALYZE 같은 유지보수 명령을 소유자나 superuser가 아닌 계정에 위임하기 위한 권한입니다.

이 글의 모든 동작은 Docker 컨테이너(PostgreSQL 16.14 / 17.11 / 18.4, aarch64)에서 직접 실행해 확인한 결과입니다.

배경: PostgreSQL 16까지의 권한 모델​

PostgreSQL 16까지 유지보수 명령의 실행 조건은 테이블 소유자, 데이터베이스 소유자, superuser 셋 중 하나였습니다. 중간 단계가 없었습니다.

소유자가 아닌 ops 계정으로 실행한 결과입니다.

SET ROLE ops;
VACUUM orders;
ANALYZE orders;
REINDEX TABLE orders;
WARNING: permission denied to vacuum "orders", skipping it
WARNING: permission denied to analyze "orders", skipping it
ERROR: must be owner of table orders

REINDEX는 에러로 멈추는데 VACUUM과 ANALYZE는 경고만 남기고 넘어갑니다. 대상이 여러 개일 때 하나가 막혔다고 전체를 중단시키지 않으려는 설계입니다. 문제는 권한 자체가 없는 계정으로 데이터베이스 전체를 대상으로 실행할 때입니다.

$ vacuumdb -U ops -d postgres -z
$ echo $?
0

경고 70건이 나왔는데 종료코드는 0입니다. 이 동작은 PostgreSQL 18.4까지 동일합니다.

그래서 PostgreSQL 16까지는 유지보수 배치 계정에 소유권을 넘기거나, superuser를 주거나, SECURITY DEFINER 함수로 감싸는 세 가지 우회책 중 하나를 골라야 했습니다. 셋 다 유지보수 하나 때문에 필요 이상의 권한이 따라옵니다.

MAINTAIN은 원래 PostgreSQL 16을 겨냥해 커밋됐다가 릴리스 전에 되돌려졌습니다. 되돌린 이유는 아래 search_path 절에서 따로 다룹니다.

기능 1: MAINTAIN 테이블 권한​

ACL 약어는 m이고 대상 객체 타입은 테이블 계열입니다.

GRANT MAINTAIN ON TABLE orders TO ops;
relname | relacl
---------+------------------------------
orders | {app=arwdDxtm/app,ops=m/app}

이 권한으로 열리는 명령은 여섯 개입니다.

명령비고
VACUUMFULL 포함
ANALYZE통계 갱신
CLUSTER
REINDEX
REFRESH MATERIALIZED VIEWmaterialized view 자체에 부여해야 함
LOCK TABLE모드 제한 없음

GRANT ALL ON TABLE에도 포함됩니다. PostgreSQL 16의 arwdDxt가 17부터 arwdDxtm이 됩니다.

-- 16.14
grant_all_test16 | {app=arwdDxt/app,ops=arwdDxt/app}
-- 17.11
grant_all_test | {app=arwdDxtm/app,ops=arwdDxtm/app}

일괄 부여와 기본 권한 지정도 지원합니다.

GRANT MAINTAIN ON ALL TABLES IN SCHEMA public TO ops;
ALTER DEFAULT PRIVILEGES FOR ROLE app IN SCHEMA public
GRANT MAINTAIN ON TABLES TO ops;

기능 2: pg_maintain predefined role​

테이블마다 부여하는 대신 role 하나로 클러스터 전체를 덮습니다.

GRANT pg_maintain TO ops2;

모든 relation에 MAINTAIN이 있는 것처럼 동작하고, 범위에 시스템 카탈로그도 들어갑니다.

SET ROLE ops2;
REINDEX TABLE pg_catalog.pg_class; -- 성공

pg_maintain은 다른 predefined role의 멤버가 아닙니다. 이 role을 부여해도 pg_monitor나 pg_read_all_stats가 함께 붙지 않습니다.

기능 3: 유지보수 중 search_path 고정​

PostgreSQL 16에서 되돌린 이유가 여기 있습니다. Nathan Bossart(네이선 보사트)의 revert 커밋 메시지입니다.

A role with the MAINTAIN privilege may be able to use search_path tricks to escalate privileges to the table owner.

소유자 app이 index expression에 함수를 걸어 두고, ops가 MAINTAIN으로 REINDEX를 실행할 때 그 함수가 누구로 실행되는지 확인했습니다.

-- 소유자 app
CREATE FUNCTION trap2(i int) RETURNS int LANGUAGE plpgsql IMMUTABLE AS $$
BEGIN
RAISE NOTICE '[trap] current_user=% / search_path=%',
current_user, current_setting('search_path');
RETURN i;
END $$;
CREATE INDEX small_trap_idx ON small (trap2(amt));

-- 유지보수 담당 ops
SET ROLE ops;
SET search_path TO evil, public;
REINDEX INDEX public.small_trap_idx;

PostgreSQL 17.11의 출력입니다.

NOTICE: [trap] current_user=app / search_path=pg_catalog, pg_temp

index expression은 명령을 실행한 ops가 아니라 테이블 소유자 app의 권한으로 평가됩니다. 그리고 ops가 세션에 지정한 evil, public은 무시되고 search_path가 pg_catalog, pg_temp로 고정됩니다.

앞쪽 절반은 PostgreSQL 16도 같습니다. 뒤쪽 고정이 없으면 경로가 이렇게 열립니다.

ops는 자기가 만든 스키마를 search_path 앞에 두기만 하면 됩니다. 소유자의 함수가 스키마를 한정하지 않고 다른 함수를 호출하면 그 호출은 ops가 준비한 동명 함수로 연결되고, 결과적으로 app의 권한으로 실행됩니다. PostgreSQL 16 개발 주기가 늦어 기능 전체를 되돌렸고, Jeff Davis(제프 데이비스)가 search_path 고정을 구현한 뒤 PostgreSQL 17에서 함께 들어왔습니다.

장점​

읽기 권한과 유지보수 권한이 분리됩니다. MAINTAIN만 가진 계정으로 실행한 결과입니다.

VACUUM orders; -- 성공
ANALYZE orders; -- 성공
SELECT count(*) FROM orders; -- ERROR: permission denied for table orders
DELETE FROM orders WHERE id=1; -- ERROR: permission denied for table orders
TRUNCATE orders; -- ERROR: permission denied for table orders
ALTER TABLE orders ADD COLUMN x int; -- ERROR: must be owner of table orders
DROP TABLE orders; -- ERROR: must be owner of table orders

데이터를 한 줄도 읽지 못하는 계정이 그 테이블을 vacuum하고 통계를 갱신합니다.

통계도 새어 나가지 않습니다. pg_stats는 컬럼 단위 SELECT 권한을 확인하므로 MAINTAIN만 가진 계정에는 0행이 돌아옵니다. 히스토그램과 최빈값은 사실상 데이터 샘플이라 이 구분이 맞습니다.

표준 GRANT 체계에 들어와 있어서 ALTER DEFAULT PRIVILEGES로 신규 테이블까지 자동 적용되고, 권한 감사도 relacl의 m 하나로 끝납니다. 소유권을 옮기거나 superuser를 내주던 방식은 감사 추적이 어려웠습니다.

단점과 제약​

파티션 테이블의 부모에 부여해도 자식 파티션으로 내려가지 않습니다. 그리고 VACUUM이라 에러도 나지 않습니다.

GRANT MAINTAIN ON TABLE evt TO ops; -- evt 는 파티션 부모

SET ROLE ops;
VACUUM evt;
-- WARNING: permission denied to vacuum "evt_2026_01", skipping it
-- WARNING: permission denied to vacuum "evt_2026_02", skipping it

파티션이 많으면 ALL TABLES IN SCHEMA로 덮거나 pg_maintain role을 쓰는 쪽이 현실적입니다.

materialized view도 별도입니다. 기반 테이블에 부여해도 REFRESH MATERIALIZED VIEW는 막히고, materialized view 자체에 다시 부여해야 통과합니다.

SET ROLE ops;
REFRESH MATERIALIZED VIEW mv_orders;
-- ERROR: permission denied for materialized view mv_orders

RESET ROLE;
GRANT MAINTAIN ON TABLE mv_orders TO ops; -- TABLE 키워드로 부여한다

LOCK TABLE이 포함되는데 모드 제한이 없습니다. 읽기 권한도 없는 계정이 운영 테이블을 ACCESS EXCLUSIVE로 잠글 수 있으므로, 유지보수 위임과 가용성 위험을 같이 저울질해야 합니다.

권한을 제대로 부여한 뒤에도 VACUUM의 조용한 skip 동작은 그대로입니다. 배치 쪽에서 종료코드 대신 경고 건수를 세는 편이 안전합니다.

search_path 고정은 MAINTAIN 권한을 쓰지 않아도 적용됩니다. ANALYZE, CLUSTER, CREATE INDEX, CREATE MATERIALIZED VIEW, REFRESH MATERIALIZED VIEW, REINDEX, VACUUM 전부가 대상이고 소유자 본인이 실행해도 같습니다. 그래서 이런 코드가 PostgreSQL 17에서 깨집니다.

CREATE FUNCTION helper(i int) RETURNS int LANGUAGE sql IMMUTABLE
AS 'SELECT i * 2';

CREATE FUNCTION expr(i int) RETURNS int LANGUAGE plpgsql IMMUTABLE AS $$
BEGIN
RETURN helper(i); -- 스키마를 한정하지 않았다
END $$;

CREATE INDEX t2_expr_idx ON t2 (expr(amt));
버전CREATE INDEX 결과
16.14성공, indisvalid = t
17.11ERROR: function helper(integer) does not exist
18.4ERROR: function helper(integer) does not exist

helper와 expr이 같은 public 스키마에 있고 소유자가 직접 실행했는데도 실패합니다. search_path가 pg_catalog, pg_temp라서 public이 검색 경로에 없기 때문입니다. 기존 인덱스를 재생성하거나 REINDEX를 실행하는 시점에 드러나므로, 업그레이드 직후가 아니라 한참 뒤 정기 유지보수 중에 발견되기 쉽습니다.

고치는 방법은 두 가지이고 둘 다 확인했습니다. 함수 정의에 검색 경로를 지정하거나,

CREATE FUNCTION expr_fixed(i int) RETURNS int LANGUAGE plpgsql IMMUTABLE
SET search_path = public, pg_catalog
AS $$ BEGIN RETURN helper(i); END $$;

호출 쪽을 스키마로 한정합니다.

CREATE FUNCTION expr_qual(i int) RETURNS int LANGUAGE plpgsql IMMUTABLE
AS $$ BEGIN RETURN public.helper(i); END $$;
경고

PostgreSQL 17로 올리기 전에 index expression과 materialized view 정의에 쓰인 사용자 정의 함수를 먼저 확인하는 편이 좋습니다. pg_proc에서 proconfig가 비어 있으면서 본문에 스키마 한정 없는 호출이 있는 함수가 후보입니다.

마지막으로, autovacuum은 이 권한 체계와 무관하게 동작합니다. pg_maintain을 부여한다고 autovacuum 쪽이 달라지지 않습니다.

17과 18의 차이​

PostgreSQL 18에서 MAINTAIN의 범위가 넓어졌습니다. 통계 조작 함수가 이 권한으로 묶였습니다.

버전MAINTAIN 설명에 포함된 범위
17VACUUM, ANALYZE, CLUSTER, REFRESH MATERIALIZED VIEW, REINDEX, LOCK TABLE
18위 전부 + database object statistics manipulation functions
SET ROLE ops2; -- pg_maintain
SELECT pg_clear_relation_stats('public', 'orders'); -- 성공
SELECT pg_restore_relation_stats(
'schemaname', 'public', 'relname', 'orders',
'version', 180000, 'relpages', 42::integer); -- t

SET ROLE nobody;
SELECT pg_clear_relation_stats('public', 'orders');
-- ERROR: permission denied for table orders

읽지 못하는 테이블의 통계를 지우거나 원하는 값으로 덮어쓸 수 있다는 뜻입니다. 통계가 틀어지면 실행 계획이 틀어지므로, PostgreSQL 18에서 pg_maintain을 부여하는 판단은 17에서보다 무겁게 봐야 합니다.

같은 흐름에서 PostgreSQL 18은 pg_signal_autovacuum_worker role도 추가했습니다. 16과 17 컨테이너에는 없고 18.4에만 존재하는 것을 확인했습니다. autovacuum worker를 취소하거나 세션을 종료하는 권한이라, 유지보수 위임을 설계할 때 함께 검토할 대상입니다.

정리​

MAINTAIN은 vacuum과 통계 갱신을 데이터 접근과 분리한 권한입니다. 유지보수 하나 때문에 소유권을 넘기거나 superuser를 내주던 구간이 없어졌습니다.

도입 전에 두 가지는 확인하고 가시는 게 좋아요. 하나는 search_path 고정 때문에 기존 index expression 함수가 깨지지 않는지, 다른 하나는 파티션과 materialized view처럼 권한이 상속되지 않는 곳이 배치에 섞여 있지 않은지입니다.

참고​

Oracle RU 즉시 적용

· 약 5분

벤더가 "패치를 빨리 적용하세요"라고 말하는 건 새롭지 않아요. 그런데 그 이유가 "AI가 우리 패치를 분석해서 공격 방법을 알아낼 수 있어서"라면 성격이 다릅니다.

2026년 8월 18일 오라클이 Prepare Now: Apply Oracle Database Release Update Immediately Upon Availability를 냈습니다. 지원 중인 모든 릴리스, 19c와 Oracle AI Database 26ai를 포함해, 데이터베이스 자산 전체에 최신 분기 Release Update를 테스트하고 배포할 준비를 지금 하라는 내용입니다.

권고의 근거가 달라졌다​

주목할 부분은 요구 사항이 아니라 근거입니다. 오라클이 든 이유는 frontier AI 모델이 소프트웨어 취약점을 찾고 악용하는 장벽을 크게 낮추고 있다는 것입니다. 그 모델들이 하는 일로 네 가지를 꼽았습니다. 약점 식별, 소프트웨어 변경 분석, 보안 패치의 리버스 엔지니어링, 그리고 잠재적 공격 경로 개발입니다. 속도와 규모가 전례 없다고 표현했습니다.

이 목록에서 세 번째가 핵심입니다. 보안 패치의 리버스 엔지니어링입니다.

패치는 무엇이 잘못됐는지를 알려 줍니다. 코드의 어느 줄이 어떻게 바뀌었는지 보면 그 전에 무엇이 가능했는지 역산할 수 있습니다. 이걸 patch diffing이라고 부르고, 오래된 기법입니다. 새로운 건 그 작업의 비용입니다. 숙련된 분석가가 며칠 걸리던 일을 모델이 훨씬 빨리 해내면, 패치 공개와 실제 적용 사이의 구간이 그만큼 위험해집니다.

패치를 늦게 적용하는 쪽이 더 위험해지는 구조는 원래 있었습니다. 달라진 것은 그 위험이 커지는 속도입니다.

8월 CSPU의 물량​

같은 주에 나온 숫자가 이 권고의 배경을 보여 줍니다. Qualys 분석에 따르면 8월 Critical Security Patch Update는 943개 취약점을 다뤘습니다. Oracle 8월 CSPU advisory도 이 릴리스를 분기 CPU가 아닌 CSPU로 분류하고, 신규 보안 패치 943개를 담았다고 밝힙니다.

무인증 원격 악용이 가능한 것들이 제품군별로 이렇게 나왔습니다.

제품군무인증 원격 악용 가능
Oracle Fusion Middleware182
Oracle Hyperion107
Oracle Commerce47
Oracle E-Business Suite27
Oracle Siebel CRM21

E-Business Suite에서 CVSS 9.8인 CVE-2026-60782와 CVE-2026-70926이 나왔습니다.

DBA 관점에서 궁금한 것은 Database 쪽 물량입니다. 943개 중 데이터베이스 제품군은 17개였습니다.

구성요소패치 수최고 CVSS
Oracle Database Server69.6
Oracle Autonomous Health Framework78.8
Oracle Essbase49.8

전체 943개에 비해 17개는 적어 보입니다. 하지만 Database Server의 최고 점수가 9.6이라는 게 중요합니다. 개수가 아니라 그 6개가 무엇인지가 판단 근거입니다.

월간 체제와 즉시 적용이 만나면​

오라클이 분기 CPU에 월간 CSPU를 더한 이야기를 다룬 적이 있습니다. 이번 권고를 그 흐름 위에 놓으면 실무 부담이 선명해집니다.

분기 체제에서는 1년에 네 번 패치 시기가 왔습니다. 한 번마다 테스트에 몇 주를 쓸 여유가 있었습니다. 월간 체제에서는 열두 번입니다. 여기에 "나오는 즉시 적용하라"가 붙으면, 테스트 기간을 얼마로 잡을지가 실질적인 문제가 됩니다.

권고와 현실 사이의 간격을 어떻게 좁힐지가 결국 각 조직의 판단입니다. 몇 가지 방향은 이렇습니다.

RU 종류를 구분하는 것이 먼저입니다. 오라클의 Release Update(RU)와 Monthly Recommended Patch(MRP)는 성격이 다릅니다. 과거의 Release Update Revision(RUR)은 19c 기준 2023년 초에 제공이 끝났고, 그 자리를 MRP가 대신합니다. 전체를 같은 절차로 다루면 열두 번을 감당하지 못합니다.

테스트 자동화의 범위를 다시 볼 필요도 있습니다. 회귀 검증을 사람이 도는 구조라면 월간 주기를 못 따라갑니다. 이 부분은 패치 정책 이전에 검증 파이프라인 문제입니다.

그리고 적용 순서를 노출도로 정하는 방법이 있습니다. 무인증 원격 악용이 가능한 구성요소를 앞에 두는 식입니다. 위 표에서 Fusion Middleware의 182개가 왜 먼저인지는 숫자가 설명합니다.

AI가 양쪽에서 일한다​

Anthropic의 Project Glasswing을 다룰 때는 AI가 핵심 인프라의 취약점을 찾아 주는 쪽 이야기였습니다. 이번 오라클 공지는 같은 능력의 반대쪽입니다.

두 사례를 나란히 놓으면 구도가 정리됩니다. 방어 측은 AI로 코드를 감사해 취약점을 미리 찾습니다. 공격 측은 AI로 패치를 분석해 이미 고쳐진 취약점을 역산합니다. 전자는 패치 이전, 후자는 패치 이후입니다. 그래서 패치 공개 시점이 양쪽의 경계선이 됩니다.

벤더가 이 판단을 공식 문서로 낸 것이 이번 공지의 의미라고 봅니다. "AI 때문에 패치 정책을 바꿉니다"를 제품 블로그에 적은 사례가 아직 흔하지 않습니다. 오라클이 자사 데이터베이스 고객 전체를 향해 그렇게 적었습니다.

실무에서 확인할 것​

Oracle Database Appliance를 쓰는 환경이라면 릴리스 19.32에 7월 Database Release Update와 19c, 26ai 클론 파일이 들어 있습니다. 계획, 테스트, 배포 일정을 지금 시작하라는 것이 공지의 요구입니다.

정리하면 확인 항목은 셋입니다. 현재 운영 중인 데이터베이스의 RU 수준이 어디인지, 다음 RU가 나올 때 테스트에 며칠을 쓸 계획인지, 그리고 그 며칠이 이번 공지의 논리에 비추어 감당할 만한 구간인지입니다.

마지막 항목이 답하기 어렵습니다. 패치 공개 후 며칠이 안전한지 아무도 숫자로 말해 주지 않으니까요. 저는 이 공지가 그 숫자가 줄어들고 있다는 신호로 읽혔어요.

참고​

PMM ClickHouse 권한 취약점

· 약 6분

DB를 지키려고 붙인 모니터링 도구가 DB로 들어가는 문이 되면 곤란하겠죠. 2026년 8월 19일 Percona가 자사 모니터링 제품에서 그런 경로를 공개했습니다.

Percona 보안 공지의 대상은 Percona Monitoring and Management(PMM) 3.9.0 이하입니다. CVSS는 8.7, CVE 번호는 공지 시점에 아직 배정되지 않았고 배정되면 공지를 갱신한다고 적혀 있습니다.

문제의 모양​

PMM은 Grafana를 화면으로 쓰고, 쿼리 분석 데이터를 ClickHouse에 담습니다. 문제는 그 둘을 잇는 데이터소스 설정에 있었습니다.

두 가지가 겹칩니다. 첫째, Grafana는 로그인한 사용자가 raw data source API에 접근하는 것을 허용합니다. Viewer 역할도 포함입니다. 대시보드만 볼 수 있는 계정이 데이터소스에 직접 쿼리를 보낼 수 있다는 뜻입니다. 둘째, ClickHouse 데이터소스가 연결할 때 쓰는 기본 신원이 전역 DDL, DML, SOURCES 권한을 가지고 있었습니다.

Grafana에서 anonymous access를 켜 두면 로그인하지 않은 사용자까지 이 경로에 닿습니다. 기본값은 꺼져 있습니다. 이 조건은 명확히 해 두는 게 좋습니다. 뒤에 나오는 전체 공격 체인은 anonymous access가 켜져 있고 AWS EC2에 올라간 배포에서 성립합니다.

SOURCES 권한이 실제로 무엇을 여는가​

공지가 url() 함수를 언급하는데, ClickHouse를 자주 다루지 않으면 이게 왜 위험한지 감이 안 옵니다. 직접 확인해 봤습니다. 기본 설정으로 ClickHouse 컨테이너를 띄웠습니다.

$ docker run -d --name chtest -e CLICKHOUSE_PASSWORD=pw \
clickhouse/clickhouse-server:latest
$ docker exec chtest clickhouse-client --password pw -q "select version()"
26.7.5.10

default 사용자가 가진 권한을 그대로 뽑아 봤습니다.

show grants for default;
GRANT SOURCES ON *.* TO default
GRANT TABLE ENGINE ON * TO default
GRANT CHECK, SHOW, SELECT, INSERT, ALTER, CREATE, DROP, UNDROP TABLE,
TRUNCATE, OPTIMIZE, BACKUP, KILL QUERY, KILL TRANSACTION,
MOVE PARTITION BETWEEN SHARDS, SYSTEM, dictGet,
displaySecretsInShowAndSelect, INTROSPECTION, CLUSTER,
FILE, URL, REMOTE, MONGO, REDIS, MYSQL, POSTGRES, SQLITE,
ODBC, JDBC, HDFS, S3, HIVE, AZURE, KAFKA, NATS, RABBITMQ,
YTSAURUS, ARROW FLIGHT, SOURCES ON *.* TO default

목록을 보면 성격이 드러납니다. FILE, URL, REMOTE, S3, MONGO, MYSQL, POSTGRES, HDFS가 전부 들어 있습니다. SOURCES는 이들을 묶은 상위 권한입니다.

이게 뜻하는 바는 ClickHouse의 쿼리가 데이터베이스 안에서 끝나지 않는다는 것입니다. url() 테이블 함수는 SELECT 문 안에서 HTTP 요청을 보냅니다. s3()는 S3 버킷을 읽습니다. file()은 서버 로컬 파일을 테이블처럼 읽습니다. SELECT 권한만 있는 계정이 아니라, HTTP 클라이언트를 겸하는 계정이 되는 셈입니다.

file()에는 별도 울타리가 하나 더 있습니다. 임의 경로는 막습니다.

$ clickhouse-client -q "select count() from file('/etc/hostname','LineAsString')"
Code: 291. DB::Exception: File `/etc/hostname` is not inside
`/var/lib/clickhouse/user_files`. (DATABASE_ACCESS_DENIED)

user_files 디렉터리 밖은 거부됩니다. 다만 그 안이라면 그대로 읽힙니다.

$ docker exec chtest sh -c 'echo secret-token-abc > /var/lib/clickhouse/user_files/t.txt'
$ clickhouse-client -q "select * from file('t.txt','LineAsString')"
secret-token-abc

네트워크 쪽은 울타리가 없습니다. 권한이 실제로 통과하는지 확인하려고 닫힌 포트를 겨냥해 봤습니다.

$ clickhouse-client -q "select * from url('http://127.0.0.1:1/x','LineAsString')"
Code: 1000. DB::Exception: Connection refused. (POCO_EXCEPTION)

돌아온 것이 권한 거부가 아니라 연결 거부입니다. 권한 검사를 통과해 실제로 소켓을 열려고 시도했다는 뜻입니다. 겨냥한 주소가 열려 있었다면 요청이 나갔을 것입니다. SELECT 문 하나가 DB 서버에서 출발하는 HTTP 요청이 되는 지점입니다.

공격 체인​

공지가 설명하는 흐름은 네 단계입니다.

url() 함수로 AWS IMDSv1에 요청을 보냅니다. 인스턴스 메타데이터 서비스의 v1은 토큰 없이 GET 하나로 응답합니다. 여기서 EC2 인스턴스에 붙은 role의 임시 자격증명을 얻습니다.

그 자격증명으로 S3에 접근해 Terraform state 파일을 읽습니다. state 파일에는 인프라 구성이 그대로 들어 있고, 관리자 비밀번호 같은 값이 평문으로 남아 있는 경우가 흔합니다.

마지막으로 그 값으로 PMM 관리자로 인증합니다. 모니터링 시스템의 관리자가 되면 감시 대상 DB의 접속 정보를 그 안에서 찾을 수 있습니다.

단계마다 그 자체로는 알려진 문제입니다. IMDSv1이 토큰 없이 응답하는 것도, Terraform state에 비밀이 남는 것도 오래된 이야기입니다. 이 공지가 보여 주는 건 그 알려진 약점들이 SELECT 권한 하나로 연결된다는 점입니다.

Grafana 데이터소스에서 감시 대상 DB 정보까지 이어지는 공격 체인

수정과 완화​

수정 버전은 PMM 3.9.1이고 2026년 8월 19일 나왔습니다. 근본 수정은 이쪽입니다.

바로 올릴 수 없는 환경을 위해 공지가 완화 스크립트를 함께 제공합니다. 방향은 단순합니다. 데이터소스가 쓰는 계정을 읽기 전용으로 새로 만들고, SOURCES 계열을 주지 않는 것입니다.

CREATE USER IF NOT EXISTS grafana_ro
IDENTIFIED WITH plaintext_password BY '<password>'
SETTINGS readonly = 1;
GRANT SELECT ON pmm.* TO grafana_ro;

readonly = 1을 걸고 pmm 데이터베이스에 SELECT만 줍니다. SOURCES를 주지 않으면 url(), s3(), mongodb(), remote(), file() 호출이 막힙니다. 대시보드가 필요한 것은 SELECT뿐이니 기능은 그대로 돕니다.

참고로 위 CREATE USER를 기본 default 계정으로 실행하면 거부될 수 있습니다. 제 컨테이너에서는 이렇게 나왔습니다.

Code: 497. DB::Exception: default: Not enough privileges.
To execute this query, it's necessary to have the grant
CREATE USER ON grafana_ro. (ACCESS_DENIED)

SOURCES는 넉넉히 주면서 사용자 관리 권한은 안 주는 기본 구성입니다. 이 스크립트를 돌리려면 ACCESS MANAGEMENT 권한이 있는 계정으로 접속해야 합니다.

관측 스택 계정 권한을 점검하는 자리​

공지가 다루지 않는 부분을 하나 짚고 싶습니다. 이번 문제의 뼈대는 PMM에 국한되지 않습니다. "대시보드가 DB에 붙을 때 어떤 권한으로 붙는가"라는 질문입니다. 이 질문은 어느 관측 스택에나 유효합니다.

점검할 지점을 정리하면 이렇습니다.

확인할 것왜
데이터소스 계정의 권한 목록SELECT 외에 무엇이 붙어 있는지. ClickHouse면 SOURCES 유무
Grafana anonymous access 설정켜져 있으면 비인증 사용자가 데이터소스 API에 닿는다
Viewer 역할의 실제 범위대시보드 열람과 raw query 실행이 같은 등급인지
DB 서버에서 나가는 네트워크DB가 외부로 HTTP를 보낼 수 있는지
IMDS 버전v1이 열려 있으면 SSRF 한 방이 자격증명이 된다
Terraform state 저장소평문 비밀이 남아 있는지, 접근 주체가 누구인지

DB 계정 권한부터 보는 게 순서지만, 네트워크 쪽에서도 한 겹 막을 수 있습니다. DB 서버가 외부로 HTTP를 보낼 이유가 대개 없습니다. IMDSv2를 강제하는 것도 이 체인 두 번째 단계를 끊습니다.

남는 생각​

Copy Fail이나 NGINX Rift 같은 사례는 코드 한 줄의 결함이었습니다. 이번 건은 성격이 다릅니다. ClickHouse가 url()을 제공하는 것도, Grafana가 데이터소스 API를 여는 것도 각각은 의도된 기능입니다. 두 기능이 만나는 자리에서 권한 설정이 넉넉했던 것이 문제였습니다.

그래서 패치만으로 끝나지 않습니다. 3.9.1로 올린 뒤에도 데이터소스 계정에 무엇이 붙어 있는지 한 번 확인해 볼 만합니다. 저는 이 공지를 읽고 나서 ClickHouse의 SOURCES 권한이 그렇게 많은 것을 묶고 있는 줄 몰랐다는 걸 알았어요.

참고​

Workers Spectre 재현

· 약 5분

Spectre는 2018년 이야기라고 생각하고 있었는데, 그게 아니었어요. 멀티테넌트 서버리스에서는 아직 진행 중인 문제입니다.

Cloudflare가 2026년 8월 19일 자사 Workers 환경에서 원격 Spectre 공격을 재현한 연구를 공개했습니다. 에든버러 대학교와 공동 연구입니다. 결과 문장이 짧고 셉니다. 프로덕션 환경에서 초당 최대 12비트, 정확도 99%로 안정적으로 유출했습니다.

무엇을 어떻게 빼냈나​

피해자 Worker 안에 JWT 토큰을 두고 비트 단위로 읽어 냈습니다. 첫 바이트가 문자 e, 이진값 0b01100101로 정확히 나왔습니다.

초당 12비트는 느리게 들립니다. JWT 하나가 수백 바이트라면 몇 분이 걸립니다. 하지만 공격이 성립하는지 여부에서는 속도가 본질이 아닙니다. 정확도 99%로 안정적이라는 쪽이 문제입니다. 오류율이 높으면 재시도로 시간이 곱해지는데, 그게 아니라는 뜻입니다.

원격 타이머라는 전제가 무너졌다​

Spectre 계열 공격에는 정밀한 시간 측정이 필요합니다. 캐시에 있는 값을 읽는 시간과 없는 값을 읽는 시간의 차이가 신호이기 때문입니다. 그 차이가 나노초 단위입니다.

2018년 이후 브라우저와 런타임이 취한 대응 중 하나가 타이머를 무디게 만드는 것이었습니다. Cloudflare Workers도 타이머를 동결(frozen timer)했습니다. Worker 안에서 시간을 재도 값이 변하지 않게 만든 것입니다. 멀티스레딩과 공유 메모리도 껐습니다.

이 연구가 그 전제를 치웁니다. 외부 서버로 나가는 WebSocket 연결 하나로 충분했습니다. 그쪽에서 고해상도 타임스탬프를 받아 오면 됩니다. 몇 번의 표본만으로 중앙값 기준 밀리초 미만 해상도를 얻었습니다.

여기에 증폭 기법을 붙였습니다. L1 캐시의 tree-based PLRU 교체 정책을 이용해 단일 캐시 사건에서 나온 시간 차이를 임의로 키웠습니다. 나노초 차이를 밀리초 해상도로 읽을 수 있게 만드는 단계입니다.

isolate가 프로세스가 아니라서​

Cloudflare Workers는 수만 테넌트를 같은 OS 프로세스 안에서 돌립니다. 분리 단위가 프로세스가 아니라 V8 isolate입니다. 이 선택이 효율을 만듭니다. 프로세스를 띄우는 비용 없이 요청이 들어오면 즉시 실행되고, cold start가 사실상 사라집니다.

대가는 분리 강도입니다. 같은 프로세스 안이면 주소 공간이 이어져 있습니다. Worker 프로세스 안에서 임의 읽기가 한 번 성립하면 다른 테넌트의 데이터에 닿습니다.

공격자가 피해자와 같은 프로세스에 들어가는 방법도 단순했습니다.

fetch("https://victim.example")

피해자 스크립트를 호출하면 스케줄러가 바로 그 프로세스 안에 피해자 Worker 인스턴스를 띄웁니다. Durable Objects와 지속 WebSocket 연결로 isolate를 계속 살려 뒀습니다.

gadget은 두 개를 썼습니다. 첫 번째는 압축된 힙 포인터를 유출합니다. isolate의 힙 기준 주소를 얻는 단계입니다. 두 번째는 TypedArray의 backing store를 이용한 speculative type confusion으로, 공격자가 만든 임의의 64비트 포인터에서 값을 읽습니다. 기준 주소를 알아낸 뒤 임의 주소로 넘어가는 구성입니다.

2021년 방어는 왜 놓쳤나​

Cloudflare가 2021년에 내놓은 방어가 Dynamic Process Isolation(DyPrIs)입니다. 의심스러워 보이는 스크립트를 찾아 별도 프로세스로 격리합니다. 하드웨어 성능 카운터로 분기 예측 실패 같은 신호를 감시합니다.

이 방어가 통하지 않은 이유가 둘입니다.

첫째, 시점입니다. DyPrIs는 스크립트 실행이 끝난 뒤에 격리했습니다. 그런데 이 공격은 WebSocket keep-alive로 실행을 몇 시간에서 하루까지 이어 갑니다. 격리가 적용될 시점이 오지 않습니다.

둘째, 지표가 희석됐습니다. DyPrIs는 분기 예측 실패 횟수를 iTLB 접근 횟수로 정규화합니다. 그런데 WebSocket 트래픽 자체가 iTLB 활동을 늘립니다. 분모가 커지니 비율이 탐지 문턱 아래로 내려갑니다.

방어를 우회한 방식이 취약점을 새로 찾은 게 아니라, 정상 기능인 지속 연결을 쓴 것이라는 점이 인상적입니다. 탐지 지표를 정할 때 그 지표가 정상 트래픽으로 흔들릴 수 있는지가 중요하다는 사례입니다.

새로 붙인 방어​

이후 적용한 것이 셋입니다.

V8 샌드박스입니다. JavaScript 힙의 상당 부분에서 원시 64비트 포인터를 제거합니다. 이 연구가 쓴 speculative type confusion gadget은 임의의 64비트 포인터를 만들어 넣는 방식이었으니, 그 재료를 없애는 접근입니다. Cloudflare는 해당 gadget을 "재사용하기 더 어렵게" 만든다고 표현했습니다. 불가능하다고 하지 않았습니다.

Memory Protection Keys(MPK)입니다. 2025년 9월에 배포했습니다. isolate별 힙이 하드웨어로 강제되는 접근 경계 뒤에 놓입니다. 접근은 CPU가 막습니다. 같은 프로세스 안이라는 구조를 유지하면서 분리를 강화하려면 이 방향이 남습니다.

같은 프로세스의 V8 isolate에 MPK 하드웨어 경계를 추가한 전후 비교

DyPrIs 개선입니다. 장시간 실행과 I/O 위주 워크로드를 다루도록 바꿨고, 실행이 끝난 뒤가 아니라 실행 중에 탐지합니다. 위 두 회피 경로를 각각 겨냥한 수정입니다.

회피 경로대응
실행 종료 후 격리실행 중 탐지로 전환
iTLB 정규화 희석장시간, I/O 위주 워크로드 처리
임의 64비트 포인터 조립V8 샌드박스
같은 프로세스 내 힙 접근MPK 하드웨어 경계

실제 악용 흔적은 지난 3년간 발견되지 않았다고 밝혔습니다.

남는 질문​

이 글에서 가장 오래 남는 대목은 구조 쪽입니다. isolate 기반 멀티테넌시는 cold start를 없애려는 선택이었고, 그 선택이 만든 공격면을 하드웨어 기능(MPK)으로 메우는 단계에 와 있습니다. 소프트웨어 격리를 얇게 만든 대가를 CPU 기능으로 지불하는 구조입니다.

NGINX Rift처럼 코드 결함을 고치는 사례와 다른 종류의 문제입니다. 고쳐야 할 한 줄이 없습니다. 아키텍처 선택의 청구서라서, 방어도 계층을 더 쌓는 형태가 됩니다.

DB를 운영하는 쪽에서 읽으면 결론이 좀 밋밋합니다. 서버리스 런타임 위에 올려 둔 코드가 다루는 비밀은 언제든 같은 프로세스 이웃에게 노출될 수 있다고 가정하는 편이 낫습니다. DB 접속 자격증명을 Worker 환경 변수에 두고 오래 살려 두는 구성이라면, 자격증명 수명을 짧게 잡는 것이 이 종류의 공격에 대한 가장 현실적인 대응입니다.

참고​

PostgreSQL 18.6 보안

· 약 5분

이번 분기 마이너 릴리스는 조용히 지나갈 수 없는 규모예요. 8월 13일 공지로 PostgreSQL 18.6, 17.11, 16.15, 15.19, 14.24가 한꺼번에 나왔는데, 보안 취약점 수정이 28건입니다. 통상 마이너 릴리스의 보안 수정이 손에 꼽는 수준인 걸 생각하면 이례적인 숫자입니다.

버전 번호부터 눈에 걸립니다. 18 계열의 직전 버전은 18.4인데 이번이 18.6입니다. 18.5는 릴리스 준비 중 회귀(regression)가 발견되어 배포 없이 결번 처리됐습니다. 18.4에서 바로 18.6으로 올라가면 되고, 중간에 놓친 버전은 없습니다.

CVSS 8점대만 12건​

28건 전체 중 CVSS 8.0 이상이 12건입니다. 성격별로 묶으면 그림이 보입니다.

계열대표 CVECVSS내용
클라이언트 도구CVE-2026-64648.1psql COPY FROM STDIN, 데이터 줄을 psql 명령으로 처리
클라이언트 도구CVE-2026-184088.8psql \unrestrict, 조작된 덤프 원본에서 임의 코드 실행
클라이언트 도구CVE-2026-193858.8pg_dump heap buffer overflow
heap overflowCVE-2026-146648.8regexp 처리, 임의 코드 실행 가능
heap overflowCVE-2026-146698.8to_char()
heap overflowCVE-2026-146768.8pg_stat_statements
type confusionCVE-2026-146718.8refint plan cache
type confusionCVE-2026-162388.8pg_restore_attribute_stats()
type confusionCVE-2026-162398.8cursor CLOSE + DECLARE
type confusionCVE-2026-146808.8"internal" 인자 처리
기타CVE-2026-146628.8tsvector/tsquery integer wraparound
기타CVE-2026-157428.8fuzzystrmatch, 임의 주소 기록

이 중 DBA가 특히 무겁게 볼 것은 psql 계열입니다. 나머지는 대체로 "DB에 로그인한 공격자가 권한을 넘어서는" 유형이라 접속 통제가 1차 방어선이 되지만, psql 취약점은 방향이 반대입니다. 신뢰할 수 없는 덤프나 SQL 파일을 psql로 읽어들이는 쪽이 피해자가 됩니다.

CVE-2026-6464는 COPY FROM STDIN 처리 중 초기 실패가 발생하면 뒤따르는 데이터 줄을 psql 명령으로 해석하는 문제입니다. 누군가 건네준 덤프 파일을 복원하는 일상 작업이 곧 공격 경로입니다. CVE-2026-18408도 같은 결로, pg_dump 출력에 포함되는 \unrestrict 처리를 악용하면 덤프를 만든 쪽(superuser 권한으로 조작된 서버)이 복원하는 쪽 클라이언트에서 임의 코드를 실행할 수 있습니다. 외부에서 받은 덤프를 복원할 일이 있는 조직이라면 클라이언트 패키지 업데이트를 서버만큼 서둘러야 합니다.

일반 서버 취약점과 psql 취약점의 공격 경로 비교

낮은 점수 중에도 운영에 직접 닿는 항목이 있습니다. CVE-2026-14663은 pgcrypto에서 비활성화된 cipher로 암복호화를 요청하면 조용히 평문으로 처리하던 문제입니다. 점수는 6.5지만 "암호화됐다고 믿었는데 평문이었다"는 유형이라 감사 관점에서는 확인해 볼 가치가 있습니다. CVE-2026-14672는 SCRAM 인증에서 scram_iterations 응답 차이로 계정 존재 여부가 노출되는 문제입니다.

업데이트만으로 끝나지 않는 세 가지​

이번 릴리스의 함정은 바이너리 교체 후에도 남는 숙제입니다. 릴리스 노트가 세 가지 후속 조치를 명시합니다.

첫째, parallel GIN index build를 쓴 적이 있다면 reltuples 오염을 확인합니다. PostgreSQL 14, 15, 16, 18에서 parallel worker가 초기화되지 않은 row count를 보고해 pg_class.reltuples가 Infinity나 NaN이 되는 버그가 있었습니다. 이 상태가 되면 autovacuum과 autoanalyze가 해당 테이블을 건너뛰고, 자연적으로는 복구되지 않습니다. GIN 인덱스를 가진 테이블을 이 쿼리로 확인합니다.

SELECT DISTINCT t.oid::regclass, t.reltuples
FROM pg_class t
JOIN pg_index i ON t.oid = i.indrelid
JOIN pg_class ic ON i.indexrelid = ic.oid
WHERE t.relhasindex AND ic.relam = 2742; -- 2742 = gin

reltuples가 Infinity, NaN, 혹은 말이 안 되는 값이면 해당 테이블에 ANALYZE를 실행합니다. vacuum이 안 도는 테이블은 wraparound 위험까지 이어지니, GIN을 쓰는 클러스터라면 이 확인을 빼먹지 않는 게 좋습니다.

둘째, btree_gist 인덱스의 REINDEX입니다. float4/float8 컬럼에서 NaN 처리, bit/bit varying 컬럼에서 정렬이 잘못되던 문제가 고쳐졌습니다. 해당 타입 컬럼에 btree_gist 인덱스가 있다면 REINDEX가 필요합니다.

셋째, 큰 ltree 값의 인덱스도 REINDEX 대상입니다. 약 14,653개 이상의 label을 가진 ltree 값에서 비교 연산이 틀려 B-tree 인덱스가 손상될 수 있었습니다. 그 정도 깊이의 ltree를 쓰는 곳은 드물겠지만, 해당된다면 REINDEX 대상입니다.

이 밖에 버그 수정 중에는 standby가 구버전 마이너의 WAL을 재생하다 멈추는 deadlock 수정(14~16), DEFAULT partition이 pruning에서 잘못 제외되던 문제, REINDEX CONCURRENTLY와 deferred unique constraint 조합 오류 같은 굵직한 것들이 포함됐습니다. 전체 목록은 릴리스 노트에 있습니다.

19 Beta 3와 14 EOL​

같은 날 PostgreSQL 19 Beta 3도 나왔습니다. 눈에 띄는 변화는 GROUP BY ALL 문법이 revert 된 것입니다. Beta 1에 들어왔던 편의 문법인데 최종 릴리스 전에 빠졌습니다. 그 외 FOR PORTION OF(temporal 문법) 수정 다수, logical replication의 sequence 동기화 race condition 수정 등이 반영됐습니다. 19 신기능을 검증 중이라면 Beta 3 기준으로 다시 확인하는 게 안전합니다.

그리고 PostgreSQL 14의 지원 종료가 2026년 11월 12일로 예고됐습니다. 이번 14.24를 포함해 앞으로 한 번의 릴리스만 남았습니다. 아직 14로 운영 중인 클러스터는 마이그레이션 일정을 지금 세워야 합니다.

지금 해야 할 일​

우선순위로 정리하면 이렇습니다.

  1. 서버 바이너리 업데이트 (마이너 업데이트라 dump/reload 불필요, 재시작만)
  2. 클라이언트 패키지(psql, pg_dump)도 같이 업데이트 - 이번 릴리스에서는 클라이언트가 서버만큼 급합니다
  3. GIN 인덱스 테이블의 reltuples 확인, 이상 시 ANALYZE
  4. btree_gist(float/bit 컬럼), 깊은 ltree 인덱스 REINDEX
  5. PostgreSQL 14 사용 중이면 업그레이드 계획 수립

28건이라는 숫자에 놀랐지만, 뒤집어 보면 fuzzing과 코드 감사가 코어에 촘촘히 들어가고 있다는 뜻이기도 합니다. 저는 클라이언트 도구가 공격면이 되는 흐름이 이번 릴리스의 진짜 교훈이라고 봐요. 서버는 잘 잠가 두는데 psql은 아무 파일이나 여는 습관, 이번 기회에 같이 고치면 좋겠습니다.

참고 자료​

Claude Code 자체 호스팅

· 약 3분

코딩 에이전트 도입 논의가 보안 검토에서 멈추는 조직, 많죠. 코드는 사내망 밖으로 못 나간다는 원칙과, 에이전트 세션은 벤더 클라우드에서 돈다는 현실이 부딪히는 지점이에요.

Anthropic이 8월 6일 퍼블릭 베타로 공개한 self-hosted environments는 정확히 그 지점을 겨냥합니다. 웹, 모바일, 데스크톱에서 시작하는 Claude Code 클라우드 세션의 실행 환경을 조직이 제공한 머신으로 바꿉니다. 저장소 체크아웃, 빌드 산출물, 시크릿, 세션이 만들거나 수정하는 모든 파일이 조직 인프라 안에 남습니다.

구조: runner라는 상주 프로세스​

동작 단위는 runner입니다. claude self-hosted-runner로 띄우는 장수(long-lived) 프로세스로, 세션 하나가 시작되면 runner 하나가 그 세션의 Claude Code 실행을 맡습니다. 운영 모드는 두 가지입니다.

모드동작
Fixed정해진 수의 runner를 상시 유지, 세션을 분배
On-demandorchestrator가 대기 중인 세션 수에 맞춰 runner 수명 관리

CI 러너를 운영해 본 조직이라면 구조가 낯설지 않을 겁니다. GitHub Actions의 self-hosted runner와 개념이 같고, 이름도 같습니다. 사내 Kubernetes에 runner 풀을 두고 세션을 받는 그림이 자연스럽게 그려집니다.

Fixed와 On-demand runner 운영 방식 비교

기존 Remote Control과의 차이도 여기 있습니다. Remote Control은 특정 개인의 머신에 세션을 묶는 기능입니다. self-hosted environments는 조직 공용 인프라에 세션을 올리고, 권한 있는 누구든 쓸 수 있습니다. 개인의 원격 제어와 조직의 실행 기반이라는 층위 차이입니다.

무엇이 남고, 무엇이 나가나​

이 기능을 검토할 보안 담당자가 볼 핵심은 경계선입니다.

사내에 남는 것: 저장소 체크아웃, 빌드 산출물, 시크릿, 세션이 만들고 수정하는 파일 전부. 코드가 Anthropic의 실행 환경에 복제되지 않습니다.

여전히 Anthropic으로 가는 것: 대화 데이터입니다. 프롬프트, 응답, 도구 실행 결과는 모델 추론을 위해 전송되고, 세션 transcript는 여러 기기에서 이어 쓰기 위해 저장됩니다. 도구 실행 결과에는 코드 조각이 포함될 수 있으니, "코드가 한 줄도 안 나간다"는 이해는 정확하지 않습니다. 나가는 것은 모델이 보는 문맥이고, 남는 것은 파일시스템과 실행 환경입니다.

조건도 명확합니다. Team과 Enterprise 플랜 전용이고, ZDR(zero data retention) 계약 조직은 현재 쓸 수 없습니다. transcript 저장이 기능의 전제라서 생기는 제약으로 보입니다. 그리고 공식 문서가 셋업과 유지보수에 전담 엔지니어링 인력이 필요하다고 미리 말해 둡니다. 켜면 되는 토글이 아니라 운영해야 하는 인프라입니다.

어떤 조직에 의미가 있나​

이 기능이 풀어 주는 매듭은 두 종류입니다.

첫째, 컴플라이언스 매듭입니다. 소스코드의 물리적 위치를 통제해야 하는 조직(금융, 공공, 계약상 제약이 있는 곳)은 지금까지 클라우드 세션 자체가 선택지 밖이었습니다. 실행 환경이 사내로 들어오면 검토의 성격이 "코드를 외부에 두어도 되는가"에서 "대화 데이터 전송을 허용하는가"로 좁혀집니다. 후자는 이미 API 사용 승인과 같은 범주라 통과 경로가 있는 조직이 많습니다.

둘째, 환경 매듭입니다. 빌드에 사내 아티팩트 저장소, 내부 DNS, VPN 안쪽 서비스가 필요한 프로젝트는 벤더가 제공하는 격리 환경에서 애초에 빌드가 안 됩니다. runner가 사내망에 있으면 이 문제가 사라집니다.

베타답게 다듬는 중인 흔적도 보입니다. 이 글을 게시한 뒤 나온 2.1.233(2026-08-14)에서 runner의 세션 시작 시간을 줄이는 개선(working tree 재작성 없는 브랜치 생성, 서버 왕복 2회 제거)이 들어왔습니다.

개인 사용자에게는 당장 해당 없는 기능이지만, 방향은 기억해 둘 만해요. 에이전트가 개인 도구에서 조직 인프라로 올라가는 단계마다 이런 부품이 하나씩 채워지고 있고, self-hosted runner는 그중 꽤 큰 조각입니다.

참고 자료​

AlloyDB AI 에이전트 연동

· 약 6분

Google Cloud가 2026년 6월 2일 AlloyDB Remote MCP Server를 정식 출시(GA)했어요. AI 에이전트가 AlloyDB(PostgreSQL 호환) 데이터베이스에 표준 프로토콜로 직접 붙어 SQL을 실행하고 인스턴스를 관리할 수 있게 됐어요.

여기서 눈여겨볼 단어는 GA입니다. 프리뷰 단계의 실험 기능이 아니라 production에 올려도 된다는 신호이며, 곧 실무 환경에서 에이전트가 운영 데이터베이스에 접근하는 구성이 현실이 된다는 뜻입니다. 운영자와 DBA 입장에서는 "AI가 우리 DB에 직접 붙는다"는 상황을 이제 설계 대상으로 다뤄야 합니다.

표준 프로토콜 하나로 붙는다​

MCP(Model Context Protocol)는 LLM이 외부 데이터/도구에 일관된 방식으로 연결되도록 만든 오픈 표준입니다. 모델마다, 도구마다 제각각이던 연동 방식을 하나의 규약으로 묶는다고 보면 됩니다. 에이전트는 MCP 서버가 노출하는 도구(tool) 목록을 받아 보고, 그중 필요한 것을 골라 호출합니다. "데이터베이스에 질의해줘"라는 요청이 들어오면, 에이전트가 MCP 서버의 SQL 실행 도구를 호출하는 식입니다.

AlloyDB Remote MCP Server는 이 규약을 AlloyDB에 입힌 것입니다. Google Cloud의 관리형 인프라 위에서 동작하고, HTTP 엔드포인트 하나를 노출합니다.

https://alloydb.googleapis.com/mcp

"Remote"라는 이름이 붙은 이유가 여기 있습니다. 예전 MCP 연동은 보통 로컬에서 stdio 스트림으로 도는 서버를 띄워 쓰는 방식이었습니다. 이번 GA는 그 서버를 Google Cloud가 직접 관리형으로 호스팅합니다. 운영자가 별도 서버를 배포하거나 패치할 필요 없이, 인증된 에이전트가 원격 엔드포인트로 붙습니다.

노출하는 도구는 크게 두 갈래입니다.

갈래대표 도구하는 일
데이터베이스execute_sql, execute_sql_read_onlySQL 실행 (쓰기/읽기 전용)
데이터베이스export_data, import_data데이터 반출/반입
사용자 관리create_user, list_users, get_user데이터베이스 사용자 관리
인스턴스 관리create_instance, update_instance, get_instance인스턴스 생성, 변경, 조회
클러스터 관리create_cluster, restore_cluster, create_backup클러스터/백업 관리

주목할 점은 도구가 단순 조회에 그치지 않는다는 것입니다. SQL 실행은 물론이고 인스턴스 변경, 데이터 반출입, 백업 생성과 클러스터 복원까지 포함합니다. 에이전트가 마음만 먹으면 운영 작업 상당수를 직접 수행할 수 있는 범위입니다. 그만큼 권한 경계가 중요해집니다.

읽기 전용 도구가 별도로 있다는 점도 눈여겨볼 만합니다. execute_sql은 쓰기까지 가능하지만, execute_sql_read_only는 수정/삭제를 막습니다. 에이전트가 의도치 않게 데이터를 바꾸는 사고를 도구 수준에서 차단하는 장치입니다.

인증/권한 모델​

에이전트가 어떻게 인증하고 무엇까지 할 수 있는지가 운영의 핵심입니다. AlloyDB Remote MCP Server는 공유 비밀번호나 API 키를 쓰지 않습니다. 실제로 공식 문서는 "이 서버는 API 키를 받지 않는다"고 못박습니다.

인증은 OAuth 2.0 bearer 토큰으로 합니다. 에이전트는 Google Cloud IAM 신원으로 발급받은 토큰을 HTTP Authorization 헤더에 실어 보냅니다. 모든 Google Cloud 신원이 인증 주체가 될 수 있습니다.

권한은 IAM 역할로 갈립니다. 도구가 요구하는 역할이 작업 종류에 따라 다릅니다.

작업필요 역할
인스턴스/사용자 생성roles/alloydb.admin
SQL 실행 (쓰기 포함)roles/alloydb.admin 또는 roles/alloydb.databaseUser
읽기 전용 SQLroles/alloydb.viewer, roles/alloydb.databaseUser, roles/alloydb.admin
인스턴스/사용자 목록 조회roles/alloydb.viewer

OAuth 스코프는 https://www.googleapis.com/auth/alloydb가 기본입니다. 즉 에이전트가 할 수 있는 일의 상한은 그 에이전트에 붙은 IAM 역할이 결정합니다. 비밀번호를 공유하던 시절처럼 "DB에 붙을 수 있으면 전부 다 된다"가 아니라, 신원별로 역할을 좁혀 부여하는 구조입니다.

여기서 DBA가 정확히 이해해야 할 분리가 하나 있습니다. 에이전트가 들고 오는 IAM 신원과 실제 데이터베이스 내부의 사용자 권한은 별개 층위입니다. IAM 역할은 "이 에이전트가 AlloyDB API를 호출할 수 있는가"를 가릅니다. 그 호출이 데이터베이스 안에서 어떤 테이블/스키마까지 읽고 쓰는지는 데이터베이스 자체의 GRANT/REVOKE로 통제합니다. Google Cloud는 IAM으로 에이전트를 특정 테이블, 스키마, 뷰에 한정할 수 있다고 안내하는데, 실제 운영에서는 IAM 역할 부여와 데이터베이스 권한을 양쪽 다 좁혀야 의미가 살아납니다.

통제 지점을 어디에 두나​

AI 에이전트가 production 데이터베이스에 직접 붙는다는 건, 사람 손을 거치지 않은 호출이 운영 작업을 일으킬 수 있다는 뜻입니다. 통제 지점을 정리합니다.

에이전트는 사람과 다르게 행동합니다. 잘못된 판단으로 DELETE나 DROP을 내릴 수 있고, 프롬프트 조작에 넘어가 의도하지 않은 질의를 던질 수도 있습니다. 권한, 감사, 입력 검증을 사람 DBA보다 더 촘촘하게 설정해야 하는 이유입니다.

에이전트마다 별도 IAM 신원을 부여하고 작업에 꼭 필요한 역할만 붙입니다. 조회만 하는 에이전트에 roles/alloydb.admin을 주는 일은 없어야 합니다. 최소 권한의 기본값은 roles/alloydb.viewer 수준에서 시작해 필요할 때 올립니다.

분석/조회가 목적이라면 execute_sql_read_only만 쓰도록 제한합니다. 쓰기가 가능한 execute_sql은 명확히 필요한 경우에만, 권한을 좁힌 별도 신원에 한정합니다. update_instance, restore_cluster, import_data 같은 도구는 데이터베이스의 형상 자체를 바꾸므로, 에이전트가 이런 도구에 닿을 이유가 없다면 IAM 역할에서 해당 권한을 빼 원천 차단합니다.

모든 질의, 동작, 도구 호출은 Cloud Audit Logs에 남습니다. 어떤 신원이 언제 무슨 도구를 호출했는지 추적할 수 있습니다. 단순히 켜두는 데서 끝내지 말고 비정상 패턴(대량 반출, 권한 밖 시도)에 대한 알림까지 엮어야 통제가 완성됩니다.

Google Cloud는 Model Armor라는 선택적 보호 계층도 제공합니다. 프롬프트 injection을 거르고 데이터 반출을 막는 용도입니다. 에이전트가 외부 입력을 받는 구성이라면 이 계층을 검토할 가치가 있습니다.

앞서 짚었듯 IAM만으로는 부족합니다. 데이터베이스 안에서도 에이전트용 role을 만들어 접근 가능한 스키마/테이블을 GRANT로 한정합니다. IAM과 데이터베이스 권한이 모두 좁아야 실효가 있습니다.

결국 권한 모델은 데이터베이스가 더 이상 사람만 붙는 곳이 아니라는 전제에서 다시 짜야 합니다. 사람은 실수해도 속도가 느리지만, 에이전트는 잘못된 동작을 빠르게 반복합니다.

흐름과 권한 경계​

에이전트 요청이 데이터베이스에 닿기까지의 경로와, 그 위에 놓인 두 겹의 권한 경계를 도식으로 정리합니다.

위쪽 IAM 검사는 "이 신원이 이 도구를 호출할 자격이 있는가"를 가르는 1차 경계입니다. 통과해도 데이터베이스 안에서 GRANT로 정해진 범위를 벗어나면 2차 경계에서 막힙니다. 두 경계를 모두 통과한 작업만 실행되고, 그 흔적은 빠짐없이 감사 로그에 남습니다. DBA가 손볼 지점은 결국 이 두 경계의 폭과 마지막 로그를 읽어내는 체계입니다.

마치며​

AlloyDB Remote MCP Server GA는 "AI 에이전트가 데이터베이스에 직접 붙는" 구성이 실험을 넘어 production 선택지가 됐다는 신호입니다. 편의는 분명합니다. 자연어로 질의하고, 스키마를 자동으로 파악하고, 운영 작업까지 위임할 수 있습니다.

그만큼 DBA가 쥐어야 할 통제도 늘었어요. 비밀번호 공유가 IAM 신원으로 바뀌고 사람의 클릭이 에이전트의 도구 호출로 바뀌는 동안, 권한은 더 좁게, 감사는 더 촘촘하게 가져가야 해요. 데이터베이스에 새 문이 하나 열렸으니, 그 문의 자물쇠와 출입 기록을 챙기는 일이 다음 차례예요.


출처:

Project Glasswing

· 약 5분

Anthropic이 자사 비공개 모델 Claude Mythos를 약 150개 조직에 추가로 풀어, 전력, 수도, 의료, 통신 같은 핵심 인프라의 소프트웨어 취약점을 찾는 Project Glasswing을 15개국 이상으로 확대했어요. 6월 2일자 발표 기준이고, 4월에 미국 정부를 포함한 50여 개 파트너로 시작한 프로그램을 한 단계 키운 것이에요.

제가 이 블로그에서 Copy Fail, NGINX Rift, DirtyDecrypt 같은 CVE를 다룰 때마다 깔려 있던 전제가 하나 있었어요. 취약점은 사람이 찾는다는 것입니다. 그 전제가 지금 흔들리고 있습니다.

Project Glasswing과 Claude Mythos가 무엇인가​

Claude Mythos는 Anthropic이 일반에 공개하지 않은 프리뷰 모델입니다. Anthropic은 이 모델이 코드에서 취약점을 찾고 익스플로잇을 짜는 능력에서 극소수 최상위 인간 보안 연구자를 제외한 거의 모두를 앞선다고 설명합니다. 몇 주 단위로 수천 개의 제로데이를 찾아낼 수 있다는 것이 핵심 주장입니다.

Project Glasswing은 이 모델을 통제된 형태로 파트너 조직에 제공하는 프로그램입니다. 파트너는 Mythos를 받아 자기 코드베이스를 스캔하고, 취약점을 식별하고, 패치를 작성하고, 릴리스 전 점검까지 돌립니다. 발표에 따르면 실제 활용 범위는 다음과 같습니다.

  • 취약점 패치 작성과 테스트
  • 릴리스 전 소프트웨어 검증
  • 침투 테스트
  • 위협 탐지
  • 메모리 안전 언어로의 코드 번역

흥미로운 대목은 Anthropic이 이 모델을 일반에 공개하지 않겠다고 못 박았다는 점입니다. 이런 사이버 능력이 악용되는 것을 막을 안전장치가 아직 충분히 갖춰지지 않았고, 자사를 포함한 어떤 AI 개발사도 그 수준에 이르지 못했다는 이유입니다. 취약점을 잘 찾는 능력은 방어에도 쓰이지만 공격에도 그대로 쓰입니다. 이중 용도 문제를 인정한 것입니다.

어디에 적용됐고, 무엇을 찾아냈나​

확대된 명단에는 전력, 수도, 의료, 통신, 그리고 하드웨어 벤더와 오픈소스 메인테이너가 들어갑니다. 초기 50개 파트너 그룹에서는 상대적으로 빠져 있던 영역이라는 게 Anthropic의 설명입니다. 국가는 미국 외에 호주, 캐나다, 프랑스, 독일, 이탈리아, 스위스, 네덜란드, 스페인, 벨기에, 스웨덴, 인도, 일본, 뉴질랜드, 그리고 한국이 포함됐습니다. 접근 권한이 확인된 조직으로는 Okta, Samsung, SK Hynix, SK Telecom, NATO, EU 사이버보안 기관 ENISA 등이 거론됐습니다.

Anthropic은 확대 우선순위를 정한 기준을 이렇게 밝혔습니다.

파트너 대부분은 한 번의 대규모 공격으로 1억 명 이상이 피해를 입을 수 있다고 본다. 세계 안보와 각국 안보 양쪽에 중대한 영향을 미친다.

규모 수치는 출처에 나온 것만 정리하면 다음과 같습니다.

집계 범위항목수치
파트너 전체발견된 고위험/심각 취약점10,000건 이상
오픈소스 스캔스캔한 오픈소스 프로젝트1,000개 이상
오픈소스 스캔전체 발견 이슈23,019건
오픈소스 스캔그중 고위험/심각 등급6,202건
오픈소스 스캔Anthropic + 외부 6개 보안업체가 검증한 건1,752건
오픈소스 스캔검증된 건의 진짜 양성(true positive) 비율90% 초과

프로그램은 2026년 4월 초에 시작했습니다. 구체적 사례로 Cloudflare와 Mozilla가 자사 코드베이스에서 각각 수백 건의 취약점을 발견했다고 언급됐습니다. 또 수십억 대 기기가 쓰는 암호 라이브러리 wolfSSL에서 인증서 위조 공격으로 이어질 수 있는 결함을 Mythos가 짚어낸 사례도 공개됐습니다.

방어자에게 이것은 무엇을 의미하나​

겉으로 드러난 그림은 방어자에게 유리합니다. 취약점을 찾는 비용이 급격히 떨어지면, 자기 코드를 먼저 점검하는 쪽이 더 많은 취약점을 더 빨리 고칠 수 있습니다. Anthropic이 내세우는 목표 자체가 "방어자에게 영구적 우위"를 만드는 것입니다. 더 싼 AI 모델이 공격자 손에 들어가기 전에 방어 쪽이 앞서가야 한다는 논리입니다.

하지만 이 그림에는 균형추가 필요합니다.

첫째, 찾는 것과 고치는 것은 다른 문제입니다. Anthropic 스스로 인정한 병목이 바로 여기에 있습니다. 취약점을 찾기는 쉬워졌는데, 검증하고 공개하고 패치하는 일은 여전히 사람의 몫이고 느립니다. 23,019건을 찾아냈지만 검증된 건은 1,752건입니다. 나머지는 누군가 일일이 분류하고 확인해야 합니다. 진짜 양성 비율 90% 초과라는 수치도 1,752건을 Anthropic과 외부 보안업체 여섯 곳이 직접 검증해서 얻은 결과입니다. 자동 탐지가 뱉어낸 결과는 곧바로 믿을 수 있는 결론이라기보다, 사람이 처리해야 할 대기열에 가깝습니다.

둘째, 부담이 특정 지점에 쏠립니다. 특히 오픈소스 메인테이너가 그렇습니다. 많은 프로젝트가 소수의 자원봉사자로 굴러갑니다. 여기에 AI가 쏟아내는 취약점 리포트가 밀려들면, 그 자체가 새로운 운영 부하가 됩니다. Anthropic도 이 긴장을 알고 있어서, 메인테이너가 리포트를 더 빨리 분류할 수 있도록 Mythos를 활용하는 방안과 오픈소스 취약점 보고 모범 사례 공개를 검토 중이라고 밝혔습니다. 도구가 만든 문제를 같은 도구로 풀겠다는 접근인데, 효과는 지켜봐야 합니다.

셋째, 이중 용도 위험은 사라지지 않고 미뤄졌을 뿐입니다. Anthropic은 Mythos급 모델을 일반 공개하지 않겠다고 했지만, 동시에 "앞으로 몇 주 안에" 모든 고객에게 Mythos급 모델을 제공할 것으로 본다고 말합니다. 경쟁사도 비슷한 방향입니다. 같은 기간 OpenAI는 사이버보안 특화 모델 GPT-5.5-Cyber를 다수 테스트 파트너에 배포했습니다. 방어자가 먼저 쓰는 동안 공격자도 비슷한 능력에 다가가고 있다는 뜻이고, "영구적 우위"라는 표현은 마케팅 문구에 가깝게 들립니다.

정리하면, 운영/보안 관점에서 실질적으로 바뀌는 건 탐지 단계의 처리량입니다. 그 뒤의 검증, 분류, 패치, 배포는 여전히 사람과 조직의 역량에 묶여 있고, 오히려 탐지가 빨라질수록 이쪽 병목이 더 도드라집니다. AI 취약점 탐지를 도입한다면 모델 성능보다 우리 조직이 늘어난 발견 건수를 소화할 분류/패치 파이프라인을 갖췄는지를 먼저 점검해야 합니다.

탐지에서 패치까지, 병목은 뒤쪽에 있다​

탐지는 싸지고 빨라졌고, 그래서 무게중심이 도식의 아래쪽, 사람이 붙어야 하는 검증과 패치/배포로 옮겨갔어요. Project Glasswing이 던지는 진짜 질문은 "AI가 취약점을 찾을 수 있는가"보다 "찾아낸 것을 우리가 감당할 수 있는가"에 가까워요.


참고:

Oracle 월간 CSPU 전환

· 약 8분

Oracle이 20년 넘게 유지해 온 분기 Critical Patch Update(CPU) 체제 위에 매달 발행하는 Critical Security Patch Update(CSPU)를 얹었습니다. 그 첫 릴리스가 2026년 5월 28일에 나왔습니다.

분기 CPU는 그대로 1, 4, 7, 10월에 누적본으로 나옵니다. CSPU는 그 사이 달에 셋째 화요일마다 끼어드는 소규모/고우선 전용 패치입니다. 첫 회인 5월 CSPU만 이 일정의 예외로 28일(목)에 나왔고, 6월부터 셋째 화요일(6월 16일)에 맞춰 나옵니다(Oracle 5월 CSPU advisory). 첫 CSPU는 CVE 35건을 담았고, 그중 하나는 CVSS 10.0이었습니다. Oracle이 분기 리듬을 깨고 빠른 차선을 새로 낸 이유를 Oracle Security Blog의 공지와 DBA 관점에서 정리합니다.

20년간 이어진 분기 CPU​

Oracle의 분기 CPU는 DBA에게 익숙한 행사입니다. 매년 1, 4, 7, 10월 셋째 화요일에, Oracle의 전 제품군에 걸친 보안 수정이 한 묶음으로 떨어집니다. 한 번에 수백 건이 쏟아지는 게 특징입니다. 바로 직전인 2026년 4월 CPU만 해도 450건의 취약점을 한꺼번에 고쳤습니다.

이 모델의 핵심은 누적(cumulative)이라는 점입니다. 가장 최신 CPU 하나에 그 이전의 모든 수정이 포함되므로, DBA는 사실상 최신 CPU 한 개만 적용하면 됩니다. 관리가 단순하다는 장점은 분명합니다.

문제는 주기입니다. 1월에 발표된 취약점을 표준 절차로 막으려면 4월 CPU까지 최대 석 달을 기다려야 합니다. 그 석 달 동안 취약점은 공격자에게 노출된 구간으로 남습니다.

사이를 메우는 월간 CSPU​

CSPU는 이 구간을 줄이는 장치입니다. 분기 CPU를 대체하지 않고 사이를 메웁니다.

  • 발행 주기: 분기 CPU가 없는 달의 셋째 화요일 — 2, 3, 5, 6, 8, 9, 11, 12월
  • 성격: 전 제품군을 대상으로 하는 게 아니라, 즉시 대응이 필요하다고 Oracle이 판단한 소수 항목만 추린 묶음
  • 사전 예고: 각 CSPU 발행 직전 목요일(T-5)에 미리 공지

분기 CPU 4회와 CSPU 8회를 합치면, DBA는 1년에 12번의 예측 가능한 패치 이벤트를 갖게 됩니다(Oracle). 사실상 월 1회 리듬입니다.

분기 CPU vs 월간 CSPU​

둘은 목적이 다릅니다. 나란히 놓으면 역할이 분명해집니다.

항목분기 CPU월간 CSPU
발행 시점1, 4, 7, 10월 셋째 화요일2, 3, 5, 6, 8, 9, 11, 12월 셋째 화요일
연간 횟수4회8회
범위전 제품군 전수고우선 항목만 선별
규모수백 건 (4월 450건)소규모 (5월 35건)
누적 여부누적 (최신본 하나면 됨)비누적 (개별 적용)
사전 예고발행 전 목요일발행 전 목요일

여기서 DBA가 놓치면 안 되는 한 가지가 있습니다. CSPU는 비누적이지만, 다음 분기 CPU가 그동안의 CSPU 수정을 모두 빨아들인다는 점입니다. 즉 CSPU를 적용하지 않고 넘어갔더라도 다음 분기 CPU를 적용하면 그 사이 CSPU 수정이 따라옵니다 — 다만 그때까지 노출 구간이 길어질 뿐입니다.

5월 28일, 첫 CSPU의 내용​

첫 CSPU는 새 CVE 35건을 담았습니다(Oracle Security Blog). 분기 CPU의 수백 건과 비교하면 의도적으로 작은 묶음입니다. 제품군별 분포는 다음과 같습니다.

제품군패치 수
Oracle E-Business Suite12
Oracle REST Data Services (ORDS)11
Oracle Communications Unified Assurance8
Oracle Database Server3
Oracle Hospitality OPERA 51

심각도가 가볍지 않습니다. 가장 눈에 띄는 건 ORDS의 CVE-2026-46840으로, CVSS 10.0 만점입니다. 기밀성, 무결성, 가용성이 모두 완전히 무너지는 등급입니다. E-Business Suite에도 CVSS 9.8~9.9급이 여러 건 있었고, Hospitality OPERA 5의 CVE-2026-34311은 9.8이었습니다.

집계 기준도 짚어 둘 필요가 있습니다. 새 CVE는 35건이지만, Communications 제품군에 들어간 서드파티 컴포넌트 CVE까지 합치면 이번 CSPU가 처리한 취약점은 모두 77건입니다(SecurityWeek). 그중 상당수는 인증 없이 원격으로 악용할 수 있었습니다. ORDS 7건, Communications 4건, E-Business Suite/Database Server 각 3건이 인증 없는 원격 공격에 그대로 열려 있었습니다.

묶음이 작다고 가벼운 것은 아닙니다. 오히려 분기까지 기다릴 수 없다고 Oracle이 판단한 항목만 골라 담았기 때문에, 평균 심각도는 분기 CPU보다 높다고 봐야 합니다.

20년 리듬을 깬 배경​

이 블로그가 주목하는 지점이 여기입니다. Oracle이 20년 넘게 지켜 온 분기 리듬을 굳이 깬 배경에는, 취약점이 발견되는 속도 자체가 달라졌다는 인식이 깔려 있습니다.

Oracle Database 업그레이드를 총괄하는 Mike Dietrich(마이크 디트리히)는 이 변화를 AI 기반 위협에 대한 대응으로 명시했습니다. 그는 AI 모델이 이전과 비교가 안 되는 속도로 취약점을 찾아내고 있으며, Oracle 자체도 Anthropic과 OpenAI의 모델을 취약점 탐지에 활용하고 있다고 밝혔습니다. 발견 속도가 빨라지면 노출 구간을 그만큼 줄여야 한다는 게 그의 논리입니다.

이건 지난달 Copy Fail 글에서 다룬 흐름과 정확히 같은 줄기입니다. AI 코드 감사기가 10년 묵은 커널 버그를 한 시간 만에 찾아낸 사건은, 방어자에게 한 가지를 분명히 일러 줬습니다. 오늘 멀쩡해 보이는 코드가 내일도 멀쩡하리라고 가정할 수 없다는 것입니다. 취약점이 더 빨리, 더 많이 드러나는 시대에는 패치 사이클도 그만큼 짧아져야 합니다.

분기에서 월간으로의 전환은 그 압력에 벤더가 내놓은 응답입니다. 발견과 패치 사이의 간격을 구조적으로 좁히려는 시도입니다.

[과거] 분기 모델
취약점 발견 ───────── 최대 3개월 ─────────▶ CPU 적용
(노출 구간)

[현재] 월간 + 분기 모델
취약점 발견 ─── 최대 1개월 ───▶ CSPU 적용
(좁힌 구간)

DBA가 지금 해야 할 일​

패치 이벤트가 연 4회에서 12회로 3배 늘었다는 건, 운영 부담도 그만큼 늘었다는 뜻입니다. 12번을 모두 같은 속도로 따라가면 검증과 변경 관리가 무너집니다. 그래서 차선을 나눠 대응해야 합니다.

1) 패치 차선을 risk로 나눈다​

모든 CSPU를 같은 속도로 처리할 필요는 없습니다. 인터넷에 노출된 ORDS/E-Business Suite처럼 공격 표면이 넓은 자산은 긴급 차선, 내부망 전용 DB는 표준 차선으로 분류합니다. 5월 CSPU의 ORDS CVSS 10.0 같은 항목은 자산 분류와 무관하게 즉시 차선입니다.

2) T-5 목요일 예고에 계획을 건다​

Oracle이 발행 5일 전 목요일에 미리 알려 주므로, 화요일 발행을 기다리지 말고 목요일 예고로 영향 자산을 먼저 추립니다. 발행 당일에 시작하면 이미 늦습니다.

3) 회귀 테스트를 가볍고 결정론적으로 만든다​

월 1회 리듬에서는 매번 광범위한 수동 검증을 붙일 여유가 없습니다. 핵심 쿼리, 배치, 연동 지점만 자동으로 돌려 확인하는 최소 세트를 미리 갖춰 둬야 패치 주기를 따라갈 수 있습니다.

4) 우회책에 기대지 않는다​

Dietrich의 표현을 빌리면, virtual patching처럼 임시방편에 의존하는 건 보안이 아니라 심리적 위안에 가깝습니다. 결국 정식 패치를 빠르게 적용할 수 있는 체계를 만드는 게 유일한 답입니다.

이 변화가 남기는 신호​

이번 변화의 의미는 단순히 "Oracle 패치가 잦아졌다"가 아닙니다. 보안 패치의 기본 단위가 분기에서 월로 바뀌는 흐름이 메이저 DB 벤더에서 시작됐다는 신호입니다.

Microsoft는 오래전부터 매달 둘째 화요일 Patch Tuesday를 돌려 왔습니다. 리눅스 배포판들은 CVE가 뜨면 며칠 안에 안정 커널을 내놓습니다. 분기라는 긴 호흡을 고수하던 Oracle마저 월간 차선을 열었다는 건, AI가 취약점 발견 속도를 끌어올린 환경에서 분기 주기로는 더 이상 버틸 수 없다는 업계의 공통 인식에 합류했다는 뜻입니다.

DBA에게 이 흐름의 결론은 명확합니다. 패치를 분기에 한 번 몰아서 하는 행사가 아니라 상시로 도는 프로세스로 다시 설계해야 합니다. CSPU는 그 전환을 강제하는 첫 신호예요.

정리​

  • Oracle이 분기 CPU에 더해 매달 셋째 화요일 CSPU를 발행하기 시작했습니다. 첫 릴리스는 예외적으로 2026년 5월 28일(목)에 나왔습니다.
  • CSPU는 CPU를 대체하지 않고 사이 달을 메우는 소규모/고우선 패치입니다. 분기 CPU는 누적, CSPU는 비누적입니다.
  • 첫 CSPU는 CVE 35건, ORDS의 CVSS 10.0(CVE-2026-46840)을 포함한 고심각도 항목 중심이었습니다.
  • 전환의 배경은 AI가 끌어올린 취약점 발견 속도 — 발견과 패치 사이 노출 구간을 구조적으로 좁히려는 응답입니다.
  • DBA는 패치를 분기 행사가 아니라 상시 프로세스로 재설계하고, risk 기준으로 차선을 나눠야 합니다.

참고​