Files
document-haness/.run/redis/redis-command-policy-admission.md
T

26 KiB

YAML 한 줄이 Redis 명령을 거절하기까지: Policy Loader·Catalog·Guard

Redis 코드 상세 시리즈 07/20 · 전체 지도 · 이전: Redis 연결을 여섯 lane으로 나눈 이유: Pool과 RuntimeOwner 생명주기 · 다음: Raw key와 영구 쓰기를 막는 코드: Namespace·Hash Slot·TTL

이 글이 답하는 코드 질문

Redis 명령 하나가 애플리케이션 코드에서 Lettuce 호출로 넘어가기 전에 무엇을 검사합니까?

이 질문은 다음 세 경계를 나눠 읽어야 답할 수 있습니다.

  • YAML은 조직이 명령을 어떻게 분류했는지 기록합니다.
  • catalog는 분류되지 않은 명령을 기본 거절합니다.
  • guard는 서버 능력, permit, namespace, slot, budget, timeout을 순서대로 검사합니다.

기준은 source HEAD 3b5aee50e33c44c02d08c94bb39ad34814482010, 2026-08-13입니다.

정적 조사 결과 정책 파일에는 명령과 subcommand를 합쳐 314개 항목이 있습니다. WAIT 항목은 없습니다. 따라서 현재 WAIT는 허용 명령이 아니라 catalog 조회에서 거절되는 default-deny 대상입니다.

먼저 보는 클래스·리소스 지도

진입점 입력 출력 다음 호출
redis-command-policy.yml 명령별 scalar 필드 조직 정책 314개 RedisCommandPolicyLoader
RedisCommandPolicyLoader.loadDefault classpath YAML Map<CommandId, RedisCommandPolicy> RedisCommandCatalog
RedisCommandCatalog.require CommandId 분류된 policy CommandPolicyGuard
CommandRequest key, 크기, permit, budget, block, 지연된 invocation 실행 전 요청 executor
CommandPolicyGuard.validate CommandRequest CommandAdmission sync/reactive/queueing executor
ConfiguredRedisPermitVerifier permit와 요구 policy 통과 또는 거절 guard/context
RedisCommandDescriptor policy에서 파생된 값 실행 불변식 lane·translator

CommandRequest.invocation은 이미 시작한 future가 아니라 Supplier<CompletionStage<R>>입니다. invocation field 선언 덕분에 guard가 끝나기 전에는 driver call이 시작되지 않습니다.

객체 생성 시점과 request-time을 구분합니다

객체 생성 시점

의도된 조립 순서는 다음과 같습니다.

  1. RedisCommandPolicyLoader/redis-sdk/redis-command-policy.yml을 읽습니다.
  2. loader가 각 block을 RedisCommandPolicy로 바꿉니다.
  3. RedisCommandCatalog가 immutable map을 소유합니다.
  4. deployment 설정으로 ConfiguredRedisPolicyAuthority와 verifier를 만듭니다.
  5. probed RedisCapabilities, RedisNamespace, renderer, slot calculator로 guard를 만듭니다.
  6. guard와 translator를 sync/reactive/queueing executor에 주입합니다.

그러나 이 순서가 production Spring bean으로 완성됐다고 볼 근거는 없습니다. RedisSdkAutoConfiguration은 runtime client와 owner를 만들지만 catalog, authority, verifier, guard, executor bean은 만들지 않습니다.

request-time

sequenceDiagram
    participant O as Typed/Advanced operation
    participant R as CommandRequest
    participant G as CommandPolicyGuard
    participant C as RedisCommandCatalog
    participant E as Executor
    participant L as Lettuce gateway
    O->>R: key·size·optional permit/budget·invocation 구성
    E->>G: validate(request)
    G->>C: require(commandId)
    C-->>G: policy 또는 default-deny
    G->>G: reachability→capability→permit→namespace→slot→budget(if present)→timeout
    G-->>E: CommandAdmission
    E->>L: invocation.get()

여기서 관측한 reply 크기와 예외 번역은 validate 안에 있지 않습니다. guard의 주석은 전체 pipeline을 요약하지만, 실제 validate는 admission까지 담당합니다. requireRequestBudget이 비교하는 값은 request byte와 request builder가 미리 선언한 expectedReplyBytes입니다. typed decoder path에서 서버가 돌려준 byte·element 수를 OperationBudget과 비교하려면 해당 decoder가 RedisOperationContext.requireReplyWithinBudget을 명시적으로 호출해야 합니다.

그 호출은 MGET, bounded range, collection page 같은 일부 typed decoder에는 있지만 모든 경로에 있지는 않습니다. 기본 GET requestexpectedReplyBytes가 0이고 decode에도 관측 reply 검사가 없습니다. script, function, raw, admin은 budget을 request에 붙이지만 결과 decoder 앞에서 관측 크기를 검사하지 않습니다. extension은 더 나뉩니다. policy name이 있으면 collection budget을 붙이고 null이면 budget을 비우며, 어느 분기도 관측 reply 크기를 검사하지 않습니다.

batch는 이 typed helper를 쓰지 않는 별도 경로입니다. preflight에서는 item의 declared expectedReplyBytes 합계를 검사하고, 응답 뒤에는 decoded result shape의 근사치를 누적합니다. 이 값은 exact wire bytes가 아닙니다. 따라서 admission을 통과했다는 사실만으로 실제 reply byte ceiling까지 집행됐다고 말할 수 없습니다.

YAML parser가 fail-closed인 방식

loader는 범용 YAML parser를 사용하지 않습니다. 허용하는 문법은 commands: root 하나, 명령 block, scalar field뿐입니다.

readBlocks는 다음 입력을 거절합니다.

  • tab이 들어간 문서
  • 두 번째 root 또는 commands:가 아닌 root
  • root보다 먼저 나온 command block
  • 0·2·4칸 외 indentation
  • 중복 command
  • 알 수 없는 field
  • 값이 비어 있는 field
  • 중복 field

허용 field 집합은 FIELDS에 고정되어 있습니다. risksupport는 필수입니다. boolean은 정확히 true 또는 false여야 합니다.

기본값도 정책입니다

toPolicy는 생략한 값을 다음처럼 채웁니다.

  • minimum-version: 7.2
  • read-only: false
  • blocking: false
  • retry-safe: read-only
  • may-be-ambiguous: !read-only
  • key-spec: 1 1 1
  • access: support class에서 파생
  • timeout-profile: risk와 blocking에서 파생

key-specnone, movable, 또는 <first> <last> <step>만 읽습니다. keySpec parser가 다른 표기를 거절합니다.

R1~R4와 support class는 다른 축입니다

RedisRiskLevel은 비용과 위험을 분류합니다.

risk 코드상 의미
R1 bounded single-key ordinary command
R2 O(N), 큰 reply, blocking, multi-key, 큰 payload 등; permit와 budget 필요
R3 server·client·ACL·topology 작업; application path 거절
R4 destructive; SDK 전체 차단

CommandSupport은 어떤 surface로 노출하는지 정합니다.

  • TYPED
  • ADVANCED_TYPED
  • RAW_ONLY
  • ADMIN_ONLY
  • VERSION_GATED
  • BLOCKED

두 축의 조합은 자유롭지 않습니다. RedisCommandDescriptor constructorR4BLOCKED가 아니거나 R3ADMIN_ONLY/BLOCKED가 아니면 실패합니다. ambiguous write를 retry-safe로 표시하는 조합도 거절합니다.

Guard의 실제 검사 순서

validate의 순서는 다음과 같습니다.

  1. catalog에서 policy를 찾습니다.
  2. BLOCKED, NONE, application에서 도달할 수 없는 risk를 거절합니다.
  3. probed server version이 minimumVersion을 만족하는지 봅니다.
  4. R2이면 permit와 budget을 요구합니다.
  5. 모든 key가 process namespace에 속하는지 확인하고 render합니다.
  6. key의 slot을 계산하고 Cluster에서 여러 slot이면 거절합니다.
  7. request byte와 예상 reply byte가 budget 이내인지 확인합니다.
  8. effective timeout을 계산합니다.
  9. descriptor로 connection lane을 정해 CommandAdmission을 반환합니다.

이 순서에서 실패하면 invocation supplier는 평가되지 않습니다. 즉 namespace 위반이나 budget 초과는 Redis 서버 오류가 아니라 전송 전 SDK 거절입니다.

Permit은 marker interface가 아닙니다

R2 요청에 AdvancedOperationPermit 구현체를 넣었다고 통과하지 않습니다. ConfiguredRedisPermitVerifier.check는 네 가지를 확인합니다.

  1. concrete granted type인가
  2. 현재 authority의 issuer id인가
  3. HMAC signature가 맞는가
  4. command가 요구한 policy name과 같은가

여러 key를 건드리는 R2 요청은 advanced permit과 별개로 multi-key permit이 필요합니다. 별도 검사는 한 permit이 비싼 연산 승인과 fan-out 승인을 동시에 뜻하지 않게 합니다.

정상·거절·timeout 분기

정상 분기

GET처럼 catalog의 R1/TYPED 명령은 server version과 namespace를 통과하면 기본 FAST timeout 500ms와 REGULAR lane을 받습니다.

R2 명령은 올바른 policy로 발급된 permit, 필요한 multi-key permit, 양수 budget을 갖춰야 admission을 받습니다. non-blocking 명령과 server block을 선언하지 않은 optional-blocking 명령에서는 budget timeout이 policy 기본 timeout을 덮습니다.

거절 분기

  • catalog에 없는 명령: RedisCommandRejectedException, not sent
  • BLOCKED/R4: SDK 전체 거절
  • server version 미달: RedisCapabilityUnavailableException
  • permit 없음·위조·다른 policy: RedisCommandRejectedException
  • namespace 이탈: RedisCommandRejectedException
  • Cluster cross-slot: RedisCrossSlotException
  • request/예상 reply budget 초과: RedisCommandRejectedException
  • bounded block 누락·0·음수·상한 초과: RedisCommandRejectedException

blocking 명령이 bounded server block을 선언한 경우에는 budget timeout을 쓰지 않습니다. effectiveTimeout은 block 상한을 검사한 뒤 serverBlock + BLOCKING_MARGIN(2초)를 반환합니다. BLPOP처럼 block이 필수인 명령은 선언이 없으면 거절하고, XREAD처럼 optional인 명령은 block을 생략했을 때만 budget 또는 profile timeout으로 돌아갑니다.

WAIT는 현재 사용할 수 없습니다

정책 YAML의 314개 block을 정적으로 세었지만 WAIT block은 찾지 못했습니다. catalog는 unknown command에 permissive fallback을 두지 않습니다. default-deny require 때문에 WAIT를 typed, raw, semantic surface에서 실행할 수 있다고 읽으면 안 됩니다.

기존 operations.mdWAIT 설명은 실행 가능한 현행 surface의 근거가 아닙니다.

테스트가 고정하는 계약

policy loader 테스트는 계약마다 시작점을 나눠 읽을 수 있습니다.

guard 테스트도 한 링크에 여러 사례를 묶지 않습니다.

permit provenance는 authority가 발급한 permit 허용, caller 구현체 거절, 다른 policy permit 거절, 다른 authority permit 거절로 각각 고정됩니다.

이 테스트들은 이번 문서 작업에서 실행하지 않았습니다. source를 정적으로 조사했습니다. 공유 검증 기록에 따르면 기본 module test는 이전 root 세션에서 성공했지만, 이것을 이번 실행 결과로 표현하지 않습니다.

현재 구현 공백과 잘못 읽기 쉬운 지점

  1. 314개 policy와 guard 구현은 존재하지만 production bean 조립은 확인되지 않습니다.
  2. aggregate facade와 executor까지 조립되지 않았으므로 “애플리케이션의 모든 Redis 명령이 현재 이 guard를 지난다”고 단정할 수 없습니다.
  3. admission guard는 request 크기와 예상 reply 크기만 budget과 비교합니다. typed decoder의 actual-size 집행은 일부 경로에만 있습니다. 기본 GET, script, function, raw, admin에는 그 호출이 없고, extension은 policy name이 있을 때만 budget을 갖지만 어느 분기도 관측 reply를 검사하지 않습니다. batch는 decoded shape를 별도로 근사 측정하므로 exact wire-byte 집행이 아닙니다.
  4. permit은 Redis ACL을 넓히지 않습니다. process 내부 provenance 증명이며 실제 보안 경계는 계정 ACL입니다.
  5. WAIT는 policy에 없으므로 현재 default-deny입니다.
  6. server metadata drift gate용 코드와 테스트가 있어도 이 조사에서는 real-server metadata 비교를 실행하지 않았습니다.

다음에 source를 열 때는 policy YAML, loader, catalog, guard, CommandRequest, 각 executor 순으로 보면 됩니다.

시리즈의 관련 문서

관련 범위는 keyspace·expiration, typed operations, advanced surfaces, execution failure certainty입니다.

시리즈에서 이어 읽기