Files
document-haness/.run/redis/redis-settings-secrets-credentials.md
T

24 KiB

Redis 설정은 어떻게 실패하는가: 바인딩·검증·Secret·Credential 추적

Redis 코드 상세 시리즈 04/20 · 전체 지도 · 이전: app.redis.enabled에서 capability bean까지: Spring 조립 코드 읽기 · 다음: 하나의 설정에서 세 topology로: RedisTopologyClientFactory 코드 읽기

이 글이 답하는 코드 질문

Redis 설정에는 endpoint, topology, timeout, pool ceiling, TLS, ACL account가 함께 들어갑니다. 이 값들은 언제 binding되고, 어느 단계에서 거절되며, secret://... reference는 어떻게 실제 username/password가 될까요? 이 글은 Spring property에서 RedisURI에 전달될 credential까지의 경로와 현재 environment/secret registry drift를 구분합니다.

코드 지도

코드 입력 출력 실패 위치
RedisSdkSettings app.redis.* typed 설정과 warning 목록 validate()
RedisSdkAutoConfiguration.redisSdkSettings() Spring binder bound settings bean binding failure
RedisCredentialResolver purpose + secret://<source>/<name> optional RedisCredentials malformed/unresolved reference
RedisResolvedCredentials role별 credential immutable role map + Sentinel credential client factory 이전
SecretSourceConfig strategy + environment application SecretSource backend 생성
SecretSourceValidator profile, role selectors, secret source prod secret contract singleton 초기화 종료 시점
env-keys.yaml 환경 키 계약 분류·기본값·required_when registry test/build gate
secrets-classification.yaml secret 이름 source·rotation·masking 계약 registry contract test

Redis-off에서는 binding도 하지 않습니다

RedisSdkSettings에는 일부 유효한 local default가 있지만 class 자체에는 @ConfigurationProperties가 없습니다. 이유는 `class 설명에 적혀 있습니다. application-wide scan이 이 type을 발견하면 Redis를 쓰지 않는 deployment도 값을 binding하고 검증하게 됩니다.

실제 등록은 app.redis.enabled=true 조건 아래의 redisSdkSettings()만 합니다. switch가 없거나 false이면 다음 모두 생략됩니다.

  • app.redis.* binding
  • cross-field validation
  • credential reference resolution
  • raw allowlist와 TLS material 읽기
  • client/event loop/runtime owner 생성

disabledIgnoresMalformedRedisConfiguration()은 switch-off 상태에서 Cluster non-zero database, 빈 nodes, zero timeout 같은 값도 context에 영향을 주지 않는다고 고정합니다.

bind → validate → resolve 순서

Spring은 factory method가 settings 객체를 반환한 다음 configuration property를 채웁니다. 그래서 factory method 안에서 validate()를 부르면 아직 default만 검사하게 됩니다. 별도 validation bean이 settings에 의존하는 이유입니다.

sequenceDiagram
    participant B as Spring Binder
    participant S as RedisSdkSettings
    participant V as SettingsValidation bean
    participant R as RedisCredentialResolver
    participant SS as RedisSecretSource
    participant F as TopologyClientFactory
    B->>S: app.redis.* binding
    V->>S: validate()
    S-->>V: warnings 또는 IllegalStateException
    V->>V: raw policy resource probe
    R->>SS: reference의 name resolve
    SS-->>R: secret 또는 empty
    R-->>F: role별 username/password

redisSdkSettingsValidation()은 warning을 log하고 raw gateway가 켜졌다면 allowlist resource가 실제로 읽히는지 확인합니다. redisResolvedCredentials()는 이 validation bean을 parameter로 받아 순서를 강제합니다.

RedisSdkSettings.validate()가 거절하는 것

핵심 cross-field rule은 validate()에 모여 있습니다.

topology와 namespace

  • Cluster에서 database가 0이 아니면 거절합니다.
  • 음수 database와 빈 node 목록을 거절합니다.
  • Sentinel이면 app.redis.sentinel.master-name이 필요합니다.
  • namespace의 environment, service, domainRedisKeyRules.requireToken()을 통과해야 합니다.

Standalone node가 정확히 하나인지, host:port 문법인지 여부는 settings가 아니라 topology factory가 검사합니다. settings validation이 성공해도 client factory 단계에서 실패할 수 있습니다.

timeout과 limit

fast, collection, script, batch, admin timeout은 모두 양수이며 30초 이하여야 합니다. fast timeout이 5초를 넘으면 failure가 아니라 warning입니다. 기본값은 Timeouts field에서 각각 500ms, 2s, 1s, 2s, 3s입니다.

blocking maxBlock은 양수여야 하고 blocking/transaction connection ceiling도 1 이상이어야 합니다. key/value/stream/hash/batch/scan/offline queue/bitmap limit은 모두 양수이며 key byte limit은 RedisKeyRules.MAX_KEY_BYTES를 넘을 수 없습니다. capacity의 in-flight command/byte/reply ceiling도 양수여야 합니다.

여기서 양수 검증과 runtime 적용을 구분해야 합니다. limits.offlineQueueCommands는 기본값이 1,000이고 1 미만이면 거절되지만, production main source에서 getOfflineQueueCommands()를 호출하는 코드는 없습니다. Lettuce의 실제 requestQueueSize는 이 값이 아니라 capacity.maximumInFlightCommands를 사용합니다. 두 기본값도 각각 1,000과 64로 다릅니다.

TLS와 lifecycle

mTLS client certificate를 지정했는데 client key reference가 없으면 실패합니다. TLS가 켜졌지만 hostname verification을 끄면 warning입니다. lifecycle은 nonblank client name, positive connect/TLS-handshake/acquire/shutdown/drain timeout, nonnegative quiet period, quietPeriod <= shutdownTimeout을 요구합니다. 이 규칙은 Lifecycle.validate()에 있습니다.

현재 tlsHandshakeTimeoutacquireTimeout도 binding과 validation은 되지만 production 사용처가 getter 외에는 확인되지 않습니다. owner는 pool 포화 시 즉시 거절하며 acquire timeout 동안 대기하지 않습니다. 이들 setting과 offlineQueueCommands를 runtime에 적용된 값으로 설명하면 안 됩니다.

raw, admin, advanced

raw gateway가 켜지면 nonblank policy resource와 raw 전용 credential reference가 필요합니다. admin plane이 켜지면 admin credential reference가 필요합니다. advanced operation이 꺼진 상태에서 advanced policies를 설정하면 실패합니다.

raw resource의 nonblank 검사는 settings가 하고, 존재/가독성 검사는 requireRawPolicyResource()가 합니다. 기본 raw path는 classpath:redis-sdk/raw-command-allowlist.yml이지만 해당 이름의 resource를 leaf가 제공하지 않습니다. raw를 실제로 켤 때는 존재하는 resource로 명시해야 합니다.

authentication

application credential reference가 없으면 기본적으로 startup failure입니다. local anonymous Redis를 쓰려면 app.redis.authentication.anonymous-access-accepted=true를 명시해야 하고, 이 경우 warning을 남깁니다. advanced credential이 없으면 script가 application account로 fallback한다는 warning을 남깁니다. Authentication.validate()가 이 두 trade-off를 구분합니다.

credential reference 해석

허용 문법은 secret://<source>/<name>입니다. named ACL user를 지정하려면 source segment를 <user>@<source>로 씁니다.

예를 들어 secret://ca-skeleton-application@environment/APP_REDIS_PASSWORD는 다음으로 분해됩니다.

  • scheme: secret://
  • ACL username: ca-skeleton-application
  • source label: environment
  • secret name: APP_REDIS_PASSWORD

RedisCredentialResolver.resolve()는 reference가 blank이면 Optional.empty()를 반환합니다. scheme이 다르거나 source/name separator가 없으면 configuration error입니다. secret source가 null/blank 값을 반환하면 connection 생성 전 startup failure입니다.

source segment에 @가 없으면 username은 default입니다. 이 동작은 usernameOf()에 있습니다. RedisCredentials.toString()은 password를 ***로 바꿔 출력합니다.

source 문자열은 현재 backend routing에 쓰이지 않습니다. resolver는 마지막 path name만 secretSource.apply(name)에 넘깁니다. 즉 secret://vault/NAME이라고 써도 vault backend를 자동 선택하지 않습니다. 실제 backend는 application의 SecretSourceConfig가 선택합니다.

역할별 credential과 fallback

redisResolvedCredentials()은 다음 순서로 account를 해석합니다.

  1. APPLICATION
  2. ADVANCED
  3. PUBSUB
  4. admin enabled일 때 ADMIN
  5. raw enabled일 때 RAW
  6. Sentinel mode일 때 별도 Sentinel control credential

설정되지 않은 advanced/pubsub role은 map에 들어가지 않습니다. topology factory의 role router가 해당 lane을 application client로 보냅니다. admin과 raw는 enabled 상태에서 reference가 필수이므로 암묵적으로 application account에 내려가지 않습니다.

Sentinel credential은 data primary account와 다릅니다. Sentinel control plane이 primary 위치를 조회할 때 쓸 credential이고 application credential은 발견된 primary에 명령을 보낼 때 씁니다.

application SecretSource와 prod validator

기본 application backend는 EnvironmentSecretSource입니다. Spring Environment에서 key를 읽고 null/blank를 empty로 바꿉니다. SecretSourceFactory의 enum switch에는 현재 ENVIRONMENT만 있습니다.

RedisCapabilityConfig.redisSdkSecretSource()가 application SecretSource를 SDK interface에 연결합니다. 따라서 정상적인 app-bootstrap 실행에서는 SDK의 System.getenv() fallback 대신 configured backend를 사용합니다.

SecretSourceValidator.afterSingletonsInstantiated()은 prod profile에서 두 검사를 합니다.

  • property source에 __LOCAL_DEV_ prefix 값이 있으면 거절합니다.
  • REQUIRED_PROD_SECRETS 중 현재 Redis role에 필요한 secret이 없으면 거절합니다.

Redis secret은 global switch와 role selector가 모두 맞을 때만 요구됩니다. cache, rate-limit, session, idempotency, lease prefix를 따로 판정하며 알 수 없는 Redis role은 Redis-on일 때 fail-closed로 요구합니다.

environment/secret registry drift

현재 production code와 registry 사이에는 중요한 불일치가 있습니다.

첫째, SDK와 topology tests는 application credential 예시로 APP_REDIS_PASSWORD를 사용합니다. 그러나 env-keys.yamlsecrets-classification.yaml에는 APP_REDIS_PASSWORD entry가 확인되지 않습니다. 대신 classification registry는 APP_CACHE_REDIS_PASSWORD, APP_RATE_LIMIT_REDIS_PASSWORD, APP_SESSION_REDIS_PASSWORD 같은 이전 role별 이름을 유지합니다.

둘째, SecretSourceValidator.REQUIRED_PROD_SECRETS도 이 role별 legacy secret 이름을 요구합니다. 반면 SDK는 app.redis.authentication.credential-reference에 적힌 임의의 <name>을 해석합니다. validator는 실제 reference target을 읽지 않습니다.

그 결과 prod deployment가 APP_REDIS_PASSWORD를 올바르게 주입하고 reference를 그 이름으로 설정해도, 선택한 role에 따라 APP_CACHE_REDIS_PASSWORDAPP_RATE_LIMIT_REDIS_PASSWORD가 없다는 별도 startup failure를 만날 수 있습니다. 반대로 registry가 요구한 role별 password가 있어도 SDK reference가 다른 이름을 가리키면 SDK resolver에서 실패합니다.

셋째, env-keys.yamlapp.redis.* typed settings가 application.yml에 없고 generated configuration metadata와 대조된다고 설명합니다. 이 구조는 intentional입니다. 따라서 application.ymlAPP_REDIS_NODES placeholder가 없다는 사실 자체는 drift가 아닙니다. 문제는 credential material의 실제 reference target과 prod required-secret 목록이 서로 다른 SSOT를 가진다는 점입니다.

넷째, APP_REDIS_LIFECYCLE_ACQUIRE_TIMEOUTAPP_REDIS_LIFECYCLE_TLS_HANDSHAKE_TIMEOUTenv-keys.yaml runtime 설정 구간에 등록되어 있지만 현행 runtime 적용 코드를 찾지 못했습니다. APP_REDIS_LIMITS_OFFLINE_QUEUE_COMMANDS도 public configuration으로 등록되어 binding·validation되지만, 값을 바꿔도 현행 Lettuce requestQueueSize는 바뀌지 않습니다. 실제 queue ceiling의 입력은 capacity.maximumInFlightCommands입니다.

정상·실패 분기 요약

단계 정상 실패
switch 조건 off이면 완전 생략 off + Redis role은 activation validator failure
binding typed value로 변환 duration/enum/type binding 오류
settings validation warning 또는 validated settings cross-field IllegalStateException
resource validation raw/TLS resource 읽기 가능 startup failure
credential parse optional role 또는 parsed reference literal/malformed reference 거절
secret resolve nonblank secret connection 전에 resolved to nothing
prod secret contract selected role secret 존재 legacy required list와 실제 reference drift 가능

이 구간의 failure는 command가 전송되기 전이므로 execution certainty는 NOT_SENT 성격입니다. 실제 authentication 실패는 connection이 lazy하게 열릴 때 발생할 수 있습니다. reference resolution 성공은 server가 password를 받아들였다는 증명이 아닙니다.

테스트가 고정하는 계약

RedisSdkSettingsTest는 topology/database, timeout, lane ceiling, TLS, raw/admin, authentication warning과 failure를 직접 고정합니다.

RedisSdkAutoConfigurationTest는 configured role별 secret source 호출 횟수와 unresolved/malformed reference의 startup failure를 확인합니다. configuredAccountsAreResolvedPerRole()는 application/advanced/pubsub account map을 고정합니다.

RequiredWhenIsEnforcedTest는 env registry의 required_when 조건을 context failure와 대조합니다. SecretsClassificationRegistryTest는 validator의 required secret list와 classification registry를 1:1로 맞춥니다. 이 테스트들은 두 registry가 서로 일치함을 보이지만 SDK reference target과의 일치까지 보이지는 않습니다.

SecretSourceValidatorTest는 prod/local sentinel과 role별 조건을 고정합니다.

현재 한계와 다음 source 순서

  • credential rotation은 restart-only입니다. runtime refresh/dual credential handover가 조립되지 않았습니다.
  • secret://의 source segment는 현재 backend selector가 아니라 문법·username carrier입니다.
  • RedisStartupProbe production 조립이 없어 reference resolution 뒤 실제 authentication과 server fact 확인은 lazy connection/request에 남습니다.
  • prod required secret validator와 실제 SDK credential reference target은 정렬되지 않았습니다.
  • lifecycle acquire/TLS handshake timeout과 limits.offlineQueueCommands는 registry와 settings에는 있으나 runtime 적용이 확인되지 않습니다. 현행 Lettuce requestQueueSize는 별도 capacity setting을 사용합니다.

다음에는 RedisSdkSettings.validate(), RedisCredentialResolver.resolve(), redisResolvedCredentials(), SecretSourceValidator 순서로 읽으면 됩니다.

관련 시리즈 주제는 topology별 URI/client 생성과 role별 lane routing입니다.

시리즈에서 이어 읽기