콘텐츠로 이동

Access Control

AkasicDB vector search의 access control은 PostgreSQL의 access control 모델을 그대로 따릅니다. Vector는 하나의 column type이고, vector column을 가진 테이블은 일반적인 PostgreSQL 테이블입니다. 따라서 table privilege와 Row-Level Security (RLS)가 별도의 메커니즘 없이 다른 쿼리와 동일하게 vector search에 적용됩니다.

이 페이지는 PostgreSQL 15 문서의 Row Security Policies를 기반으로 하며, 예제를 vector search에 맞게 변경했습니다.

Example Setup

이 페이지의 예제는 embedding과 department를 가진 문서를 저장하는 다음 테이블을 사용합니다.

CREATE TABLE documents (
  id         int PRIMARY KEY,
  title      text NOT NULL,
  department text NOT NULL,
  embedding  vector(3)
);

CREATE INDEX documents_embedding_idx
  ON documents
  USING vectoron (embedding vector_l2_ops);

INSERT INTO documents VALUES
  (1, 'Engineering roadmap', 'engineering', '[0.9, 0.1, 0.0]'),
  (2, 'API design guide',    'engineering', '[0.8, 0.2, 0.1]'),
  (3, 'Incident review',     'engineering', '[0.2, 0.9, 0.1]'),
  (4, 'Quarterly budget',    'finance',     '[0.1, 0.8, 0.2]'),
  (5, 'Expense policy',      'finance',     '[0.7, 0.3, 0.2]'),
  (6, 'Audit checklist',     'finance',     '[0.3, 0.2, 0.9]');

Table and Column Privileges

Vector search는 SELECT로 row를 읽으므로, role에 대상 테이블의 SELECT privilege가 필요합니다.

CREATE ROLE analyst;
GRANT SELECT ON documents TO analyst;

Column 단위 privilege도 그대로 적용됩니다. Vector column에 대한 privilege가 없는 role은 해당 column을 ORDER BY에서 참조할 수 없으므로, 다른 column을 읽을 수 있어도 vector search를 실행할 수 없습니다.

CREATE ROLE viewer;
GRANT SELECT (id, title) ON documents TO viewer;

이 경우 vieweridtitle을 읽을 수 있지만, embedding으로 정렬하는 쿼리는 permission 오류로 실패합니다.

Row-Level Security

Privilege는 role이 테이블을 조회할 수 있는지를 결정하고, RLS는 role이 어떤 row를 볼 수 있는지를 결정합니다. RLS는 기본적으로 비활성화되어 있으며 테이블 단위로 활성화합니다.

ALTER TABLE documents ENABLE ROW LEVEL SECURITY;

활성화하면 테이블은 default-deny 상태가 됩니다. Policy가 접근을 허용하기 전까지 role은 어떤 row도 볼 수 없습니다. Policy는 CREATE POLICY로 생성합니다.

  • USING expression은 SELECT, UPDATE, DELETE에서 role이 볼 수 있는 row를 결정합니다.
  • WITH CHECK expression은 INSERT, UPDATE에서 role이 쓸 수 있는 row를 결정합니다. 생략하면 USING expression이 기본값으로 사용됩니다.

RLS의 USING expression은 vector search에도 동일하게 적용됩니다. Policy가 허용하지 않은 row는 검색 결과에 포함되지 않으며, LIMIT은 policy를 통과한 결과를 기준으로 적용됩니다.

다음 설정은 각 role이 자신의 department 문서만 볼 수 있도록 제한합니다. doc_admin은 모든 row를 조회하고 수정할 수 있습니다.

CREATE ROLE doc_admin;
CREATE ROLE engineering;
CREATE ROLE finance;
CREATE ROLE alice;
CREATE ROLE bob;
GRANT engineering TO alice;
GRANT finance TO bob;

GRANT SELECT ON documents TO engineering, finance;
GRANT SELECT, INSERT, UPDATE, DELETE ON documents TO doc_admin;

ALTER TABLE documents ENABLE ROW LEVEL SECURITY;

CREATE POLICY admin_all ON documents TO doc_admin
  USING (true)
  WITH CHECK (true);

CREATE POLICY engineering_read ON documents FOR SELECT TO engineering
  USING (department = 'engineering');

CREATE POLICY finance_read ON documents FOR SELECT TO finance
  USING (department = 'finance');

PostgreSQL은 user와 group을 구분하지 않고 둘 다 role 하나로 통합합니다. 이 예제에서 engineeringfinance는 부서를 나타내는 group 성격의 role이고, alicebobGRANT engineering TO alice;처럼 그 member로 등록된 개별 사용자 role입니다. Privilege와 policy를 부서 role에 부여하므로, 새 사용자는 부서 role의 member로 추가하기만 하면 됩니다.

TO role로 생성한 policy는 해당 role의 member에게도 적용됩니다. 따라서 alice에게는 engineering_read가, bob에게는 finance_read가 적용됩니다. 실제 환경에서 사람이 접속하는 role은 CREATE ROLE alice LOGIN PASSWORD '...';처럼 LOGIN 속성이 필요합니다. 이 예제는 관리자 세션에서 SET ROLE로 전환해 시연하므로 LOGIN 없이 동작합니다.

Top-k Results per Role

모든 role이 같은 top-3 쿼리를 실행합니다. 세션에서 role을 바꾸려면 SET ROLE을, 되돌리려면 RESET ROLE을 사용합니다.

Note

Superuser가 아닌 세션에서 SET ROLE을 실행하려면 로그인 role이 대상 role의 member여야 합니다. 그렇지 않으면 permission denied to set role 오류가 발생합니다. 관리자 세션에서 GRANT alice TO <login role>;처럼 membership을 먼저 부여합니다.

SELECT id, title, embedding <-> '[1,0,0]'::vector AS distance
  FROM documents
 ORDER BY embedding <-> '[1,0,0]'::vector
 LIMIT 3;

doc_admin으로 위 질의를 실행하면 다음 결과가 반환됩니다.

SET ROLE doc_admin;
 id |        title        | distance
----+---------------------+----------
  1 | Engineering roadmap |     0.02
  2 | API design guide    |     0.09
  5 | Expense policy      |     0.22

alice로 같은 질의를 실행하면 engineering 문서만 반환됩니다.

RESET ROLE;
SET ROLE alice;
 id |        title        | distance
----+---------------------+----------
  1 | Engineering roadmap |     0.02
  2 | API design guide    |     0.09
  3 | Incident review     |     1.46

bob으로 같은 질의를 실행하면 finance 문서만 반환됩니다.

RESET ROLE;
SET ROLE bob;
 id |       title       | distance
----+-------------------+----------
  5 | Expense policy    |     0.22
  6 | Audit checklist   |     1.34
  4 | Quarterly budget  |     1.49

각 role의 검색 결과에는 policy가 허용하는 row만 포함됩니다. Policy가 허용하지 않은 row는 결과에 포함되지 않습니다. LIMIT k는 반환 결과의 상한을 설정합니다. 접근 가능한 row가 k개보다 적거나 검색 과정에서 충분한 결과를 찾지 못하면 k개보다 적은 결과가 반환될 수 있습니다. RLS는 접근 제어를 보장하지만 검색 결과의 개수나 recall을 보장하지는 않습니다.

RLS and Vector Indexes

RLS가 적용된 vector search에서는 policy를 통과한 row만 반환됩니다. Policy를 통과하는 row가 적을수록 LIMIT보다 적은 결과가 반환되거나 검색 시간이 증가할 수 있습니다. 따라서 실제 policy와 데이터 분포를 사용해 반환 건수, recall 및 latency를 검증하는 것이 좋습니다.

Policy expression은 가능한 한 현재 row의 column을 비교하는 단순한 조건으로 작성합니다. 다른 테이블을 조회하는 subquery나 실행 비용이 큰 함수는 검색을 느리게 할 수 있습니다.

Application Integration Patterns

위 예제는 동작을 보여주기 위해 SET ROLE로 role을 전환했습니다. 실제 서비스에서는 접근 경계를 어느 레이어가 소유하느냐에 따라 database role에 매핑하는 대상이 달라집니다.

Service별 role. 여러 서비스가 하나의 database를 공유할 때, 서비스마다 전용 role을 만들고 최소한의 privilege만 부여하는 것은 흔한 관행입니다. 최소 권한 원칙을 적용할 수 있고, monitoring과 감사 로그에서 서비스별 활동을 식별할 수 있으며, credential이 유출됐을 때 피해 범위를 제한합니다. 접근 경계가 서비스 단위라면 table/column privilege로 충분하며 RLS까지는 필요하지 않은 경우가 많습니다.

최종 사용자 또는 tenant별 제어. 최종 사용자마다 database role을 만드는 방식은 흔하지 않습니다. 확장성이 나쁘고 connection pooling과의 상성도 좋지 않기 때문입니다. 표준적인 패턴은 공용 service role 하나로 접속하고, transaction마다 session variable을 설정한 뒤, policy가 current_setting으로 그 값을 읽는 것입니다. 아래의 app.department는 예약된 설정이 아니라 애플리케이션이 이름을 정한 임의의 세션 변수입니다. PostgreSQL은 점이 들어간 이름의 변수를 선언 없이 자유롭게 만들고 읽을 수 있게 허용합니다.

app.department는 로그인한 사용자를 확인하는 수단이 아닙니다. 이 값은 app_service로 SQL을 실행할 수 있으면 누구나 변경할 수 있습니다. 서비스는 로그인한 사용자 계정에 저장된 department를 조회해 이 값을 설정해야 합니다. API 요청에 포함된 department를 그대로 설정하거나 사용자에게 app_service 접속 권한을 주면, 사용자가 다른 department의 row에 접근할 수 있습니다.

CREATE ROLE app_service;
GRANT SELECT ON documents TO app_service;

CREATE POLICY app_department_scope ON documents FOR SELECT TO app_service
  USING (department = current_setting('app.department', true));

애플리케이션은 모든 요청을 transaction 안에서 실행하고 SET LOCAL로 변수를 설정합니다.

BEGIN;
SET LOCAL app.department = 'engineering';

SELECT id, title
  FROM documents
 ORDER BY embedding <-> '[1,0,0]'::vector
 LIMIT 3;
COMMIT;

각 transaction에서는 session variable로 지정한 범위에서 policy가 허용하는 row만 반환됩니다. 이를 이용해 하나의 공용 connection pool에서 tenant별 접근 제어를 적용할 수 있습니다.

이 패턴을 안전하게 쓰려면 다음을 지켜야 합니다.

  • SET 대신 transaction 안의 SET LOCAL을 사용합니다. Pooled connection을 재사용하는 다음 요청으로 값이 새어 나가지 않습니다.
  • current_setting의 두 번째 인자 true는 변수가 설정되지 않았을 때 오류 대신 NULL을 반환하게 합니다. NULL 비교는 어떤 row와도 일치하지 않으므로, 변수 설정을 빠뜨린 요청은 모든 row가 아니라 아무 row도 보지 못합니다.
  • 공용 connection에서 SET ROLE을 쓸 수도 있지만, RESET ROLE을 빠뜨리면 다음 요청으로 role이 새어 나갑니다. SET LOCAL 기반의 session variable 패턴은 이런 종류의 실수를 원천적으로 피합니다.

Combining Policies

Policy는 기본적으로 permissive입니다. 한 쿼리에 여러 policy가 적용되면 그중 하나라도 허용하는 row는 보입니다(OR). AS RESTRICTIVE로 생성한 restrictive policy는 모든 policy를 만족해야 row가 보이며(AND), permissive policy 위에 제약을 더하는 용도로 사용합니다. 예를 들어 다음 policy는 doc_admin의 접근을 local connection으로 제한합니다.

CREATE POLICY admin_local_only ON documents AS RESTRICTIVE TO doc_admin
  USING (pg_catalog.inet_client_addr() IS NULL);

Row가 보이려면 최소 하나의 permissive policy가 허용해야 합니다. Restrictive policy만으로는 어떤 접근도 허용되지 않습니다.

Bypassing RLS

다음 경우에는 RLS가 적용되지 않습니다. Policy 설정을 검증할 때 특히 주의해야 합니다.

  • 테이블 owner는 기본적으로 RLS를 우회합니다. Owner에게도 policy를 적용하려면 ALTER TABLE documents FORCE ROW LEVEL SECURITY;를 사용합니다.
  • Superuser와 BYPASSRLS attribute를 가진 role은 항상 RLS를 우회합니다.
  • SET row_security = off;는 일반 사용자의 필터링을 끄지 않습니다. 대신 policy로 필터링될 쿼리를 오류로 실패시킵니다. Row가 조용히 누락되면 안 되는 backup 도구에 유용합니다.

Superuser 세션에서 policy를 테스트할 때는 먼저 SET ROLE로 role을 전환합니다. 그렇지 않으면 모든 row가 계속 보이므로 policy가 동작하지 않는 것처럼 보입니다.

Notes

  • Foreign key constraint 같은 referential integrity 검사는 RLS를 우회합니다. 의도적으로 구성한 insert로 가려진 key 값의 존재 여부를 알아낼 수 있으므로, row의 존재 자체가 민감한 정보라면 이 채널을 고려해야 합니다.
  • TRUNCATEREFERENCES privilege는 RLS의 적용을 받지 않습니다.
  • Vector index의 유무는 RLS가 허용하는 row의 범위를 바꾸지 않습니다. 다만 index 종류와 검색 설정에 따라 반환되는 결과의 개수, recall 및 성능은 달라질 수 있습니다.