기록 84편을 계약 에이전트로 다시 썼다. 기존 71편(kss 25 · virt 46)과, 계약에만 있고 안 쓰여 있던 새 글감 13편이다. 원장 84개를 열어 단계마다 스킬 영수증과 관문 종료 코드를 적었고 verify-pipeline-run.py 가 error 0 으로 닫는다. SSOT 결함 둘을 고쳤다. - kss 의 `약 58일` 이 반입 중 `약 59일` 로 바뀌어 있었다. 원 증거 파일이 「남은 일수: 88일 … 실제 갱신까지 약 58일」로 산수를 직접 적는다. D-4a 쪽 `약 59일` 은 강제 갱신 뒤(`VALID: 89 days`)라 맞는 값이라 그대로 뒀다. - virt §198 의 `11.6GB` 는 §178 의 원 측정 `Mem: 11648`(MiB)과 어긋나는데 원 가이드의 표기 그대로라 고치지 않고 쓰이는 자리에 대조를 적었다. 기록의 수치 오류 셋을 고쳤다 — CASE 요약의 「게스트 셋에 8240MB」(5120+3120 은 둘이다), k3s 편이 같은 것을 여섯·일곱·여덟로 세던 것, no-docker 편의 「셋을 더 든다」(§281 의 표는 네 행이고 디스크 행이 빠져 있었다). 계약을 셋 고쳤다. - kss 의 sourceRepository 리비전이 cdac9b8 이었는데 그 커밋에는 docs/guides/** 28개가 아예 없다. 9465582b 로 바꾸고, 반입한 바이트가 어느 커밋과도 같지 않다는 것을 측정값과 함께 적었다 — 반입은 커밋이 아니라 그 시점의 작업 트리에서 떠 온 것이다(kss 297/306 · virt 12/14 가 작업 트리와 같고, 200 커밋을 거슬러 전수 대조했을 때 가장 가까운 커밋도 28개가 어긋났다). - virt 계약이 「2026-09-11 재배분」이라고 적는데 SSOT 는 재배분 날짜를 적지 않고 재배분 뒤 값은 이미 2026-09-10 측정에 찍혀 있다. - kss 후보 대장이 지나친 절 아홉에 처분을 적었다(warn 9 → 0). 새 글감은 0건이고 넷은 앵커가 h3 슬러그의 접두가 아니라 중간 토막이라 검사기가 못 본 것이었다. style_profile.mjs 의 결함 둘을 고쳤다 — frontmatter 가 문장으로 세어져 (실측 398자짜리 「문장」 하나) 평균 길이를 기준 안으로 밀어 올리고 있었고, engPerSent 의 분자는 목록을 포함한 글에서, 분모는 목록을 걷어낸 글에서 세고 있었다(Question 기록에서 11.94 → 3.86). verify-pipeline.py 전 항목 PASS · error 0 · unittest 334건 OK. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
589 lines
35 KiB
Markdown
589 lines
35 KiB
Markdown
---
|
||
id: 7447ccbf-1800-43a4-a9d2-8ac774965c4b
|
||
kind: SETUP
|
||
slug: reproduce-b6-key-rotation
|
||
title: 서명 키를 더한 뒤 옛 키를 지우고 옛 토큰이 언제 끊기는지 본다
|
||
topic: where-application-state-lives
|
||
topicName: 세션과 토큰을 Redis 와 PostgreSQL 에 나눠 두기
|
||
project: keycloak-session-store
|
||
status: 게시 전
|
||
studio: "https://hyeonworks.com/studio/documents/7447ccbf-1800-43a4-a9d2-8ac774965c4b/edit"
|
||
pinnedVersions:
|
||
- name: curl
|
||
version: 8.5.0
|
||
source:
|
||
- final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-6
|
||
sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af
|
||
---
|
||
|
||
# 서명 키를 더한 뒤 옛 키를 지우고 옛 토큰이 언제 끊기는지 본다
|
||
|
||
realm 에 RSA 서명 키를 하나 더해 겹치는 구간을 만들고, 옛 공급자를 지운 뒤 두 토큰을 같은 두 줄로 다시 치는 절차다. 지운 키는 개인키와 함께 사라져 돌아오지 않으므로 실험대 전용 realm 에서만 한다. 약 15분.
|
||
|
||
## 관계
|
||
|
||
- **볼륨 없는 영속화와 유예 없는 키 회전**
|
||
이 절차가 만드는 `옛 401 · 새 200` 을 그 기록이 결론으로 적는다. 결론이 필요하면 그쪽을 읽는다.
|
||
- **주입이 아홉 번 조용히 실패했고 전부 아무 일도 없는 것처럼 보였다**
|
||
여기서 `-q` 필터가 오류도 종료코드도 없이 빈 결과를 주는 대목이 그 아홉 건 중 하나다.
|
||
- **예측을 먼저 적고, 주입이 걸렸는지 결과와 따로 확인하고, 대조군 없이 귀속하지 않는다**
|
||
이 절차는 주입 검증에서 `old 200` 을 보기 전에는 관찰로 넘어가지 않는다. 그것을 안 보고 지우면 뒤에 나온 401 의 원인을 못 가른다.
|
||
- **cookie secret 을 갈아치우고 로그인해 있던 세션이 어떻게 되는지 본다**
|
||
같은 회전을 식별자가 없는 쪽에서 치는 편이다. 여기서 `kid` 가 겹침을 가능하게 하는 것을 보고 나면 그쪽에서 겹침이 왜 불가능한지가 한 줄로 끝난다.
|
||
|
||
## 본문
|
||
|
||
<!-- body:start -->
|
||
|
||
## 읽기 전에 — 어디서 치는가
|
||
|
||
명령은 전부 `[kc-lab-1]` 에서 친다. Keycloak 이미지에는 `curl` 도 `wget` 도 없어서(`exit 127`) 파드 안에서 HTTP 요청을 보낼 수 없다. JWKS(JSON Web Key Set, 서버가 공개키를 싣는 목록)와 토큰은 호스트에서 공개 이름으로 치고, `kcadm.sh` 만 `kubectl exec` 로 감싸 파드 안에서 돌린다.
|
||
|
||
터미널은 하나면 된다. 붙잡아 두어야 하는 셸이 없고, 대신 `OLD` 과 `NEW` 두 변수를 끝까지 들고 가므로 중간에 터미널을 닫지 않는다.
|
||
|
||
| 무엇 | 값 |
|
||
|---|---|
|
||
| 네임스페이스 | `keycloak-lab` · 리소스 서버는 `header-lab` |
|
||
| realm | `keycloak-patterns` — 클라이언트 `bff-confidential`, 사용자 `labuser` |
|
||
| 주입 수단 | `kcadm.sh create components` — `priority` 가 더 높은 RSA 공급자를 하나 더 만든다 |
|
||
| 판정하는 쪽 | 리소스 서버 `echo`. 이 절차의 401 과 200 은 전부 그 앱이 낸다 |
|
||
| 시간 제약 | access token 수명 60초. 토큰을 받고 1분 안에 그 토큰으로 친다 |
|
||
| 전 구간 | 약 15분. 주입 검증까지는 아무것도 안 깨진다 |
|
||
| 도구 | `jq` 가 이 실험대에 없다. JSON 은 `tr` 과 `grep` 으로 자른다 |
|
||
|
||
## 이 실험이 가르는 것
|
||
|
||
암호화 키를 어디에 두고 어떻게 교체하며, 교체하는 동안 옛 키로 저장된 값을 어떻게 읽는가. B층이 들고 온 이 물음이 두 갈래로 갈린다.
|
||
|
||
| 어느 키인가 | 지금 상태 |
|
||
|---|---|
|
||
| 토큰 **저장소**의 암호화 키 | 존재하지 않는다. B-2 에서 `bytea` 안이 JWT 문자열 그대로였다 |
|
||
| 토큰 **서명** 키 (Keycloak realm) | 존재하고 회전할 수 있다 — 이 절차가 잰다 |
|
||
|
||
앞의 것이 없으므로 교체할 것도 없다. 그래서 이 절차는 뒤의 것만 치고, 거기서 본 모양이 나중에 앞의 것을 설계할 때 쓰인다.
|
||
|
||
원래 실행은 예측이 빗나간 실험이었다. 리소스 서버가 JWKS 를 캐시하니 옛 키를 지워도 한동안은 통할 것이라고 적어 두었는데, 제거 직후 바로 401 이 나왔다. 「교체」라는 한 단어가 성질이 정반대인 두 조작을 가리킨다.
|
||
|
||
```text
|
||
키 추가 → 무중단. JWKS 에 옛 키와 새 키가 함께 남는다
|
||
키 제거 → ★ 즉시 파괴적. 옛 키로 서명된 토큰이 곧바로 401
|
||
```
|
||
|
||
절차를 끝까지 밟으면 RS256 키 수가 `1 → 2 → 1` 로 움직이는 것, 겹치는 구간에서 옛 토큰과 새 토큰이 둘 다 200 인 것, 제거 뒤에 옛 토큰만 401 이 되는 것, 리소스 서버를 재시작해 캐시를 비워도 그 401 이 그대로인 것을 자기 화면에서 보게 된다.
|
||
|
||
## 전제와 되돌리기
|
||
|
||
- `05-keycloak` 이 끝나 있고 realm `keycloak-patterns` 에 클라이언트 `bff-confidential` 과 사용자 `labuser` 가 있다.
|
||
- B-0 이 끝나 BFF 가 떠 있다.
|
||
- 리소스 서버(`echo`, 네임스페이스 `header-lab`)가 떠 있다. 이 절차의 401 과 200 은 전부 그 앱이 판정한다.
|
||
|
||
**이건 되돌릴 수 없는 실험이다.** 지우는 것은 서명 키 공급자이고 그 안의 개인키가 함께 사라진다. 같은 이름으로 공급자를 다시 만들어도 새 키 쌍이 생기고 `kid` 가 달라지므로, 옛 키로 서명된 토큰은 영구히 검증되지 않는다. 실험대에서만 한다.
|
||
|
||
되돌릴 수 있는 것은 주입 하나다. 방금 만든 공급자를 지우면 원래대로 돌아간다. id 는 주입이 화면에 찍어 주는 값이고 그 줄을 그대로 옮겨 친다 — 원래 실행에서는 `7902af43-a0cc-4ebd-ad25-04d563854d16` 이었다. 아래 블록에 박힌 값이 그 원래 실행의 id 라, 8 절을 친 뒤에 그 출력이 찍어 준 자기 id 로 바꿔야 지워진다. 8 절을 치기 전에는 지울 공급자가 없다.
|
||
|
||
```bash label="[kc-lab-1] 관찰 절로 넘어가기 전에 그만둘 때 — id 를 8 절 출력의 자기 값으로 바꾼다"
|
||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||
delete components/7902af43-a0cc-4ebd-ad25-04d563854d16 -r keycloak-patterns
|
||
```
|
||
|
||
## 주입 전에 같은 명령으로 먼저 본다
|
||
|
||
제거 후에 볼 것을 제거 전에 똑같은 명령으로 먼저 봐 둔다. 넓은 것부터 좁혀 간다.
|
||
|
||
```text
|
||
kcadm 로그인 → 키 공급자 목록 → JWKS 원문 → 토큰의 kid → 그 토큰이 통하는가
|
||
```
|
||
|
||
### 1. kcadm 세션을 파드 안에 만든다
|
||
|
||
**목적** — 뒤의 모든 `kcadm.sh` 명령이 관리 API 로 인증되게 한다.
|
||
|
||
**행동** — 관리자 자격증명으로 로그인하고, 값이 넘어갔는지는 길이로만 본다.
|
||
|
||
```bash label="[kc-lab-1] ① kcadm 에 로그인한다"
|
||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||
config credentials --server http://localhost:8080 --realm master --user admin \
|
||
--password "$(kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||
-o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d)"
|
||
```
|
||
|
||
```bash label="[kc-lab-1] ② 비밀번호의 길이만 센다"
|
||
kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||
-o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d | wc -c
|
||
```
|
||
|
||
**예상 결과** — ① 은 아무것도 안 나오면 성공이다. 실패하면 한 줄짜리 오류가 뜬다. ② 는 0 이 아닌 수를 낸다.
|
||
|
||
**왜 필요한가** — `kcadm.sh` 는 파드 안 파일에 세션을 저장한다. 파드가 재시작되면 그 파일이 사라지고 그다음 모든 명령이 `401` 로 떨어지므로 맨 앞에서 한 번 해 둔다. 비밀번호는 명령 치환으로 넘기므로 값이 터미널에도 셸 히스토리에도 남지 않는다.
|
||
|
||
**문제가 생기면** — 중간에 `kcadm` 이 전부 `Unauthorized` 로 바뀌면 파드가 재시작된 것이다. ① 을 다시 친다.
|
||
|
||
### 2. 키 공급자 목록을 통째로 받는다
|
||
|
||
**무엇을 보는가** — 이 realm 에 어떤 키 공급자가 있고 각각의 id 가 무엇인지.
|
||
|
||
```bash label="[kc-lab-1] 공급자 목록을 필드 셋으로 받는다"
|
||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||
get components -r keycloak-patterns --fields id,name,providerId
|
||
```
|
||
|
||
**어디를 보나** — JSON 배열이 여러 줄로 나온다. `providerId` 가 `rsa-generated` 인 항목이 서명 키 공급자이고 `hmac-generated` · `aes-generated` 등이 함께 나온다. `"name" : "rsa-generated"` 인 항목의 `"id"` 를 지금 적어 둔다. 관찰 절에서 지울 대상이다.
|
||
|
||
**이 값이 뜻하는 것** — 여기서 「키 공급자만 걸러 보자」는 시도가 빈 결과를 준다. 가이드가 이 줄을 미검증으로 표시했다(unknown) — 원래 실행에서 이렇게 치고 아무것도 못 받았다.
|
||
|
||
```bash label="[kc-lab-1] 이렇게 치면 조용히 빈 결과다 (unknown)"
|
||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||
get components -r keycloak-patterns -q type=org.keycloak.keys.KeyProvider
|
||
```
|
||
|
||
오류도 종료코드도 없이 비어 있다. 「키 공급자가 하나도 없구나」로 읽으면 이 절차 전체가 무너진다. 빈 출력은 「없다」가 아니라 「이 명령으로는 안 보인다」일 수 있고, `--fields` 로 전체를 받아 눈으로 고른다.
|
||
|
||
### 3. JWKS 원문을 한 번 통째로 본다
|
||
|
||
**무엇을 보는가** — 어떤 필드가 실려 있는지. 다음부터 무엇으로 걸를지가 여기서 정해진다.
|
||
|
||
```bash label="[kc-lab-1] ① JWKS 를 자르지 않고 본다"
|
||
curl -s https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-connect/certs
|
||
```
|
||
|
||
줄바꿈 없이 한 줄로 길게 나온다. 실측의 첫머리는 이렇다(observed, `01-before-rotation.txt`).
|
||
|
||
```text
|
||
{"keys":[{"kid":"gokjn0zFUok8r7JVqW1cxuyojH1bTT87vzfQG9RrFX4"
|
||
```
|
||
|
||
그 뒤로 `kty` · `alg` · `use` · `n` · `e` 가 이어지고 다음 키가 온다. `kid` 마다 `alg` 가 따로 붙는다. 읽을 만하게 자를 때는 `jq` 가 없으므로 `tr` 로 쉼표를 줄바꿈으로 바꾼다.
|
||
|
||
```bash label="[kc-lab-1] ② kid 만 뽑아 본다"
|
||
curl -s https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-connect/certs \
|
||
| tr ',' '\n' | grep kid
|
||
```
|
||
|
||
**어디를 보나** — 실측은 이렇다(observed, `01-before-rotation.txt`).
|
||
|
||
```text
|
||
JWKS kid 목록:
|
||
{"keys":[{"kid":"gokjn0zFUok8r7JVqW1cxuyojH1bTT87vzfQG9RrFX4"
|
||
{"kid":"OY-caYDNGoP4HMAz-Q9UPTU-DM1i896NuzUZu6gfCqM"
|
||
```
|
||
|
||
**이 값이 뜻하는 것** — `kid` 는 둘인데 같은 파일의 윗줄은 RS256 키가 하나라고 적는다(observed).
|
||
|
||
```text
|
||
JWKS 의 RS256 키 수: 1
|
||
```
|
||
|
||
세는 단위가 다르다. JWKS 에는 서명 키만 실리지 않는다. 이 realm 에서는 암호화용 키(`RSA-OAEP` 계열)가 함께 실려 있고 그것도 `kid` 를 갖는다. `grep kid | wc -l` 로 세면 서명 키 수를 과다 계산한다.
|
||
|
||
### 4. RS256 만 세는 두 형태를 알아 둔다
|
||
|
||
**무엇을 보는가** — 알고리즘까지 보고 세는 방법. 아래 두 줄은 가이드가 미검증으로 표시했다(unknown).
|
||
|
||
JWKS 는 키 하나가 `}` 로 끝나므로 `tr '}'` 로 자르면 한 줄이 한 키가 된다.
|
||
|
||
```bash label="[kc-lab-1] ① 키 단위로 잘라 RS256 만 센다 (unknown)"
|
||
curl -s https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-connect/certs \
|
||
| tr '}' '\n' | grep -c RS256
|
||
```
|
||
|
||
Keycloak 자신에게 묻는 쪽이 확실하고 그쪽이 1순위 도구다.
|
||
|
||
```bash label="[kc-lab-1] ② Keycloak 에 직접 묻는다 (unknown)"
|
||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||
get keys -r keycloak-patterns
|
||
```
|
||
|
||
**어디를 보나** — 키마다 붙는 `algorithm` 과 `status` 를 보고, `RS256` 이면서 `ACTIVE` 인 것이 지금 서명에 쓰이는 키다.
|
||
|
||
**이 값이 뜻하는 것** — 이 두 명령은 원래 실행 기록에 출력이 없다. 위에 인용한 「RS256 키 수: 1」만이 실측이다.
|
||
|
||
### 5. 시험체가 될 옛 토큰을 하나 받아 둔다
|
||
|
||
**목적** — 회전 전에 발급된 토큰을 확보한다. 이 토큰 하나가 이 절차의 시험체다.
|
||
|
||
**행동** — 토큰 엔드포인트와 클라이언트 비밀을 변수에 담고 direct grant 로 받는다.
|
||
|
||
```bash label="[kc-lab-1] ① 옛 키로 서명된 토큰을 받고 길이만 본다"
|
||
KC=https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-connect/token
|
||
CS=$(kubectl -n keycloak-lab get secret bff-secrets \
|
||
-o jsonpath='{.data.KEYCLOAK_CLIENT_SECRET}' | base64 -d)
|
||
OLD=$(curl -s -X POST "$KC" \
|
||
-d grant_type=password -d client_id=bff-confidential -d "client_secret=$CS" \
|
||
-d username=labuser -d password=labpass -d scope=openid \
|
||
| sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')
|
||
echo "${#OLD}자"
|
||
```
|
||
|
||
**예상 결과** — 길이만 나온다. 원래 실행의 모양은 이렇다(observed).
|
||
|
||
```text
|
||
2043자
|
||
```
|
||
|
||
**왜 필요한가** — 변수 이름이 `OLD` 인 까닭은 회전이 끝난 뒤에도 이 값이 「옛 키로 서명된 토큰」으로 남아 있어야 하기 때문이다. 중간에 다시 받으면 새 키로 서명되어 실험이 성립하지 않는다. 토큰 값도 클라이언트 비밀도 화면에 찍지 않고 길이만 본다.
|
||
|
||
**문제가 생기면** — `0자` 가 나오면 토큰을 못 받았다. 변수에 담지 말고 같은 `curl` 을 그대로 쳐서 응답 본문을 읽는다.
|
||
|
||
### 6. 그 토큰이 어느 키로 서명됐는지 읽는다
|
||
|
||
**무엇을 보는가** — JWT 의 첫 토막이 헤더이고 거기 `kid` 가 있다.
|
||
|
||
```bash label="[kc-lab-1] 토큰 헤더를 디코드한다"
|
||
echo "$OLD" | cut -d. -f1 | tr '_-' '/+' | base64 -d 2>/dev/null; echo
|
||
```
|
||
|
||
**어디를 보나** — 원래 실행의 모양은 이렇다(observed).
|
||
|
||
```json
|
||
{"alg":"RS256","typ":"JWT","kid":"OY-caYDNGoP4HMAz-Q9UPTU-DM1i896NuzUZu6gfCqM"}
|
||
```
|
||
|
||
증거 파일에는 이렇게 남아 있다(observed, `01-before-rotation.txt`).
|
||
|
||
```text
|
||
발급 토큰의 kid: OY-caYDNGoP4HMAz-Q9UPTU-DM1i896NuzUZu6gfCqM
|
||
```
|
||
|
||
**이 값이 뜻하는 것** — `kid` 는 key ID 이고, 서명한 쪽이 어느 키를 썼는지 토큰 헤더에 적어 준다. 검증하는 쪽은 JWKS 에서 그 `kid` 를 찾아 공개키를 얻는다. `kid` 가 없다면 검증자는 「지금 유효한 키」 하나만 알 수 있고, 키가 바뀌는 순간 옛 토큰이 전부 죽는다. 겹치는 구간을 가능하게 하는 것이 이 `kid` 다. 여기서 본 값이 3 절의 목록에 있는지 대조한다. base64 패딩 때문에 끝이 깨져 보일 수 있고(`2>/dev/null` 이 그 불평을 지운다) 헤더는 짧아서 대개 온전히 보인다.
|
||
|
||
### 7. 그 토큰이 지금 통하는지 본다
|
||
|
||
**무엇을 보는가** — 대조군. 이 확인을 건너뛰면 뒤의 401 이 아무 의미가 없다.
|
||
|
||
```bash label="[kc-lab-1] ① 상태줄과 본문을 함께 본다"
|
||
curl -s -i -H "Authorization: Bearer $OLD" https://app1.hyeonworks.com/api/me
|
||
```
|
||
|
||
200 이면 `subject` 같은 클레임이 돌아오고, 401 이면 `WWW-Authenticate` 헤더에 이유가 붙는다. 이 헤더를 한 번 봐 두면 뒤에서 401 이 났을 때 왜인지 물을 근거가 생긴다. 여러 번 비교할 때부터는 코드만 뽑는다.
|
||
|
||
```bash label="[kc-lab-1] ② 상태 코드만 뽑는다"
|
||
curl -s -o /dev/null -w 'old %{http_code}\n' \
|
||
-H "Authorization: Bearer $OLD" https://app1.hyeonworks.com/api/me
|
||
```
|
||
|
||
**어디를 보나** — 실측은 이렇다(observed, `01-before-rotation.txt`).
|
||
|
||
```text
|
||
=== [2] 그 토큰이 지금 통하는가 (리소스 서버) ===
|
||
/api/me HTTP 200
|
||
```
|
||
|
||
**이 값이 뜻하는 것** — 회전 전에는 통한다. 원래 실행은 클러스터 안에서 `http://echo.header-lab.svc:8081/api/me` 를 쳤고, 이 절차가 공개 이름을 쓰는 까닭은 `kc-lab-1` 에서 클러스터 DNS 가 안 풀리기 때문이다. `app1.hyeonworks.com` 의 `/api` 는 Ingress 가 같은 `echo` 로 보내므로 도달하는 앱은 같다. 그리고 access token 은 60초짜리다(이 realm 은 `accessTokenLifespan=60`). 1분을 넘기면 회전과 무관하게 401 이 나온다.
|
||
|
||
## 주입
|
||
|
||
### 8. 우선순위가 더 높은 RSA 공급자를 추가한다
|
||
|
||
**목적** — 발급은 새 키로 가고 검증은 옛 키와 새 키를 둘 다 받는 상태를 만든다.
|
||
|
||
Keycloak 의 키 회전은 바꾸기가 아니라 더 높은 우선순위로 추가하기다. 기존 공급자는 그대로 두고 `priority` 가 더 큰 공급자를 하나 더 만든다.
|
||
|
||
```text
|
||
t0 키 A 만 있다. 발급: A, 검증: A
|
||
t1 키 B 추가. 발급: B, 검증: A + B ← 겹치는 구간
|
||
t2 키 A 제거. 발급: B, 검증: B
|
||
```
|
||
|
||
**행동** — 공급자를 만들고 시각을 남긴다.
|
||
|
||
```bash label="[kc-lab-1] priority 200 짜리 RSA 공급자를 만든다"
|
||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||
create components -r keycloak-patterns \
|
||
-s name=rsa-rotated -s providerId=rsa-generated \
|
||
-s providerType=org.keycloak.keys.KeyProvider \
|
||
-s 'config.priority=["200"]' -s 'config.algorithm=["RS256"]' -s 'config.keySize=["2048"]'
|
||
date '+%H:%M:%S 추가'
|
||
```
|
||
|
||
**예상 결과** — 실측은 이렇다(observed, `02-rotation.txt`).
|
||
|
||
```text
|
||
=== [3] 키 회전 — 우선순위가 더 높은 RSA 공급자를 추가한다 ===
|
||
Created new component with id '7902af43-a0cc-4ebd-ad25-04d563854d16'
|
||
```
|
||
|
||
돌아온 id 를 적어 둔다. 전제 절의 되돌리기가 그 값을 쓴다.
|
||
|
||
**왜 필요한가** — `config.priority` 가 기존 공급자보다 커야 발급이 새 키로 간다. 기본값은 100 이고 여기서는 200 을 줬다. 낮게 주면 새 키는 만들어지지만 발급에 쓰이지 않아 주입 검증에서 `kid` 가 안 바뀐다. `config.*` 값이 대괄호로 감싼 배열인 것에도 주의한다 — `-s config.priority=200` 처럼 쓰면 형이 안 맞는다. Keycloak 컴포넌트 설정은 값이 전부 문자열 목록이다.
|
||
|
||
**문제가 생기면** — 생성이 거절되면 `config.*` 의 대괄호부터 본다.
|
||
|
||
## 주입 검증
|
||
|
||
결과를 해석하기 전에, 주입이 의도한 것만 건드렸는지 본다. 주입 전과 똑같은 명령을 다시 친다.
|
||
|
||
### 9. JWKS 에 옛 키가 남아 있는가
|
||
|
||
```bash label="[kc-lab-1] 3 절과 똑같은 줄을 다시 친다"
|
||
curl -s https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-connect/certs \
|
||
| tr ',' '\n' | grep kid
|
||
```
|
||
|
||
실측은 이렇다(observed, `02-rotation.txt`).
|
||
|
||
```text
|
||
=== [4] 회전 후 JWKS — 옛 키가 남아 있는가 ===
|
||
RS256 키 수: 2
|
||
kid 목록:
|
||
{"keys":[{"kid":"1B4AQHoxZvFaQi1tc1byz8ifU-nYFB6engD4YB4Fz84"
|
||
{"kid":"gokjn0zFUok8r7JVqW1cxuyojH1bTT87vzfQG9RrFX4"
|
||
{"kid":"OY-caYDNGoP4HMAz-Q9UPTU-DM1i896NuzUZu6gfCqM"
|
||
```
|
||
|
||
옛 `kid`(`OY-caYDN…`)가 목록에서 빠지지 않았다. 새 것이 하나 늘었고 아무것도 사라지지 않았다. JWKS 는 지금 검증에 쓸 수 있는 키 전부를 싣는 목록이고, 추가는 그 목록을 늘린다.
|
||
|
||
### 10. 새 토큰은 어느 키로 서명되는가
|
||
|
||
`OLD` 은 건드리지 않는다.
|
||
|
||
```bash label="[kc-lab-1] 새 토큰을 받고 헤더를 읽는다"
|
||
NEW=$(curl -s -X POST "$KC" \
|
||
-d grant_type=password -d client_id=bff-confidential -d "client_secret=$CS" \
|
||
-d username=labuser -d password=labpass -d scope=openid \
|
||
| sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')
|
||
echo "$NEW" | cut -d. -f1 | tr '_-' '/+' | base64 -d 2>/dev/null; echo
|
||
```
|
||
|
||
실측은 이렇다(observed, `02-rotation.txt`).
|
||
|
||
```text
|
||
=== [5] 새 토큰은 어느 키로 서명되는가 ===
|
||
새 토큰의 kid: 1B4AQHoxZvFaQi1tc1byz8ifU-nYFB6engD4YB4Fz84
|
||
```
|
||
|
||
`kid` 가 우선순위 200 짜리 새 키로 바뀌었다. 발급은 우선순위가 가장 높은 키로 간다. 여기서 `kid` 가 안 바뀌었다면 `priority` 를 낮게 준 것이다.
|
||
|
||
### 11. 둘 다 통해야 겹치는 구간이 무중단이다
|
||
|
||
```bash label="[kc-lab-1] 두 토큰을 같은 두 줄로 친다"
|
||
curl -s -o /dev/null -w 'old %{http_code}\n' \
|
||
-H "Authorization: Bearer $OLD" https://app1.hyeonworks.com/api/me
|
||
curl -s -o /dev/null -w 'new %{http_code}\n' \
|
||
-H "Authorization: Bearer $NEW" https://app1.hyeonworks.com/api/me
|
||
```
|
||
|
||
실측은 이렇다(observed, `02-rotation.txt`).
|
||
|
||
```text
|
||
=== [6] ★ 회전 전에 발급된 토큰은 아직 통하는가 ===
|
||
옛 토큰 /api/me HTTP 200
|
||
새 토큰 /api/me HTTP 200
|
||
```
|
||
|
||
둘 다 200 이므로 키 추가는 무중단이다. 새 토큰은 새 키로 서명되고 옛 토큰은 JWKS 에 아직 있는 옛 키로 검증되며 사용자는 아무것도 못 느낀다.
|
||
|
||
여기서 `old` 가 401 이면 둘 중 하나다. 토큰이 만료됐거나(60초), 추가 말고 다른 것을 건드렸다. 가르는 법은 옛 토큰의 `exp` 를 보는 것이고 JWT 의 가운데 토막이 클레임이다.
|
||
|
||
```bash label="[kc-lab-1] 만료인지 아닌지 가른다"
|
||
echo "$OLD" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null; echo
|
||
date +%s
|
||
```
|
||
|
||
`exp` 가 지금보다 작으면 만료다. 다시 받아야 하는데, 순서 그대로 5 절만 다시 치면 8 절이 만든 `rsa-rotated` 가 이미 `priority` 200 이라 새로 받은 토큰도 새 키로 서명된다. 8 절이 화면에 찍어 준 id 로 그 공급자부터 지우고(전제 절의 되돌리기 한 줄), 5 절로 토큰을 받고, 8 절을 다시 쳐서 추가한 뒤 11 절로 온다.
|
||
|
||
## 관찰
|
||
|
||
**여기서부터 되돌릴 수 없다.** 계속하기 전에 셋을 확인한다 — 이 realm 이 실험대 전용인가, 지금 살아 있는 세션 중에 잃으면 곤란한 것이 있는가, 11 절의 `old 200` 을 실제로 봤는가. 마지막 것을 안 봤다면 401 이 나와도 원인을 못 가른다.
|
||
|
||
**넷째로 `$OLD` 에 시간이 얼마나 남았는지 본다.** access token 이 60초짜리라, 12 절에서 목록을 받아 눈으로 id 를 고르고 13 절에서 그 id 를 손으로 옮겨 적는 동안 만료된다. 그러면 15 절의 `old 401` 이 키 제거 때문인지 만료 때문인지 안 갈린다 — 이 절차의 결론이 바로 그 401 이라, 여기서 못 가르면 아무것도 못 잰다. 11 절의 만료 확인 두 줄을 지금 한 번 쳐서 `exp` 와 `date +%s` 의 차이를 보고, 12·13 절을 칠 만큼 안 남았으면 11 절이 안내한 대로 `$OLD` 를 다시 받고 온다.
|
||
|
||
### 12. 지울 대상을 정확히 고른다
|
||
|
||
**무엇을 보는가** — 남길 것과 지울 것의 id. `-q` 는 여전히 안 먹는다.
|
||
|
||
```bash label="[kc-lab-1] ① 목록을 다시 받는다"
|
||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||
get components -r keycloak-patterns --fields id,name,providerId
|
||
```
|
||
|
||
목록이 길면 그 항목 둘레만 잘라 본다. `"id"` 는 `"name"` 보다 위에 나온다.
|
||
|
||
```bash label="[kc-lab-1] ② 지울 항목 둘레만 본다"
|
||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||
get components -r keycloak-patterns --fields id,name,providerId \
|
||
| grep -B2 '"name" : "rsa-generated"'
|
||
```
|
||
|
||
**어디를 보나** — 남길 것이 `"name" : "rsa-rotated"` 이고 지울 것이 `"name" : "rsa-generated"` 다.
|
||
|
||
**이 값이 뜻하는 것** — 둘을 바꿔 지우면 실험이 뒤집힌다. 원래 실행에서 옛 공급자의 id 는 `980ee9b7…` 로 시작했고 그것이 `OY-caYDN…` 키를 갖고 있었다. 환경마다 id 가 다르고 증거에 남은 것도 앞 8자뿐이니 전체 id 는 위 출력에서 그대로 옮겨 온다.
|
||
|
||
### 13. 옛 공급자를 지운다
|
||
|
||
**목적** — 옛 서명 키를 JWKS 에서 없앤다.
|
||
|
||
**행동** — 위 출력의 id 를 변수에 옮기고 지운다. 아래 블록은 그대로 붙여넣으면 안 된다. 첫 줄의 `980ee9b7-...` 은 원래 실행의 값이므로 12 절 출력에서 읽은 자기 id 로 바꾼다. 안 바꾸고 치면 없는 컴포넌트를 지우라는 요청이 되어 옛 공급자는 살아 있고, 15 절이 `옛 200` 을 내 결론이 뒤집힌다.
|
||
|
||
```bash label="[kc-lab-1] 옛 공급자를 지우고 시각을 남긴다"
|
||
OLDID=980ee9b7-... # ← 위 출력에서 그대로 옮긴다. 환경마다 다르다
|
||
|
||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||
delete components/"$OLDID" -r keycloak-patterns
|
||
date '+%H:%M:%S 제거'
|
||
```
|
||
|
||
**예상 결과** — 조용히 끝나면 성공이다. 증거 파일에는 이렇게 남아 있다(observed, `03-old-key-removed.txt`).
|
||
|
||
```text
|
||
=== [7] 옛 RSA 공급자(980ee9b7 = OY-caYDN 키) 제거 ===
|
||
제거 완료
|
||
```
|
||
|
||
**왜 필요한가** — 시각을 적어 두면 뒤에 나온 401 을 이 조작에 귀속할 수 있다. 그리고 지워진 것은 공급자이므로 그 안의 개인키도 함께 사라진다.
|
||
|
||
**문제가 생기면** — 지운 뒤 새 토큰까지 401 이면 새 공급자를 지운 것이다. 14 절과 15 절을 먼저 치고 `kid` 를 대조한다.
|
||
|
||
### 14. JWKS 에서 사라졌는지 본다
|
||
|
||
```bash label="[kc-lab-1] 또 같은 줄을 친다"
|
||
curl -s https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-connect/certs \
|
||
| tr ',' '\n' | grep kid
|
||
```
|
||
|
||
실측은 이렇다(observed, `03-old-key-removed.txt`).
|
||
|
||
```text
|
||
=== [8] JWKS 에서 사라졌는가 ===
|
||
RS256 키 수: 1
|
||
{"keys":[{"kid":"1B4AQHoxZvFaQi1tc1byz8ifU-nYFB6engD4YB4Fz84"
|
||
{"kid":"gokjn0zFUok8r7JVqW1cxuyojH1bTT87vzfQG9RrFX4"
|
||
```
|
||
|
||
`OY-caYDN…` 이 없고 RS256 은 다시 하나다. `gokjn0…` 은 처음부터 끝까지 목록에 있는데, 서명 키가 아니라 암호화 키이기 때문이다.
|
||
|
||
### 15. 두 토큰을 같은 두 줄로 다시 친다
|
||
|
||
**치기 전에 `$OLD` 가 아직 안 죽었는지 본다.** 11 절의 만료 확인 두 줄을 그대로 다시 친다. `exp` 가 `date +%s` 보다 작으면 아래에서 나올 `old 401` 은 키 제거가 아니라 만료이고, 두 401 은 화면에서 똑같이 보인다.
|
||
|
||
```bash label="[kc-lab-1] 11 절과 똑같은 두 줄"
|
||
curl -s -o /dev/null -w 'old %{http_code}\n' \
|
||
-H "Authorization: Bearer $OLD" https://app1.hyeonworks.com/api/me
|
||
curl -s -o /dev/null -w 'new %{http_code}\n' \
|
||
-H "Authorization: Bearer $NEW" https://app1.hyeonworks.com/api/me
|
||
```
|
||
|
||
실측은 이렇다(observed, `03-old-key-removed.txt`).
|
||
|
||
```text
|
||
=== [9] ★ 옛 키로 서명된 토큰은 이제 어떻게 되는가 ===
|
||
옛 토큰 /api/me HTTP 401 (캐시가 살아 있으면 아직 통할 수 있다)
|
||
새 토큰 /api/me HTTP 200
|
||
```
|
||
|
||
제거는 즉시 반영된다. 괄호 안의 「캐시가 살아 있으면 아직 통할 수 있다」는 측정하기 전에 적어 둔 예상이고, 옆의 401 이 그 예상을 부정한 값이다. 증거 파일에 예상과 결과가 나란히 남아 있다.
|
||
|
||
**`$OLD` 가 만료된 뒤였다면 이 실행은 15·16 절의 판정을 못 낸다.** 옛 키는 13 절에서 개인키와 함께 사라져 옛 키로 서명된 토큰을 새로 만들 방법이 없다. 그때는 401 을 키 제거에 귀속하지 말고, 실험대를 5 절부터 다시 밟되 8 절에서 15 절까지를 토큰 수명 안에 끝낸다. 원래 실행이 그 구간을 얼마 만에 끝냈는지는 원본 가이드에 없다(unknown).
|
||
|
||
### 16. 리소스 서버를 재시작해 캐시를 비운다
|
||
|
||
**목적** — 401 이 캐시 상태 때문인지 가른다.
|
||
|
||
```bash label="[kc-lab-1] echo 를 다시 띄우고 기다린다"
|
||
kubectl -n header-lab rollout restart deploy/echo
|
||
kubectl -n header-lab rollout status deploy/echo --timeout=180s
|
||
```
|
||
|
||
그다음 15 절의 두 줄을 다시 친다. 실측은 이렇다(observed, `03-old-key-removed.txt`).
|
||
|
||
```text
|
||
=== [10] 리소스 서버를 재시작해 JWKS 캐시를 비우면 ===
|
||
deployment "echo" successfully rolled out
|
||
옛 토큰 /api/me HTTP 401
|
||
새 토큰 /api/me HTTP 200
|
||
```
|
||
|
||
**왜 필요한가** — 재시작 전과 후가 같으므로 401 은 캐시 상태와 무관하고 캐시는 유예를 주지 않았다. Spring 의 `NimbusJwtDecoder` 는 모르는 `kid` 를 만나면 JWKS 를 다시 가져온다. 캐시는 이미 아는 키를 다시 안 받으려는 장치이지 옛 키를 붙잡아 두는 장치가 아니다.
|
||
|
||
```text
|
||
옛 토큰 도착
|
||
│
|
||
├─▶ kid = OY-caYDN… → 캐시에 없다
|
||
│ │
|
||
│ └─▶ JWKS 를 다시 가져온다 (여기서 오히려 빨리 갱신된다)
|
||
│
|
||
└─▶ 새로 받은 JWKS 에도 없다 → 401
|
||
```
|
||
|
||
캐시가 오히려 제거를 빨리 반영시킨다. 유예는 캐시로 만드는 것이 아니라 옛 키를 JWKS 에 남겨 두는 기간으로 만든다.
|
||
|
||
**문제가 생기면** — 재시작 뒤 옛 토큰이 200 이면 지운 것이 그 토큰의 키가 아니다. 12 절의 목록과 6 절의 `kid` 를 대조한다.
|
||
|
||
### 겹치는 구간은 얼마나 길어야 하나
|
||
|
||
**이 절차는 그 길이를 재지 않았다.** 추가와 제거가 연달아 일어났고 전 구간이 약 15분이다. 아래 값은 realm 설정에서 따라 나온 추론이다.
|
||
|
||
| 이 실험대에서 | 수명 |
|
||
|---|---|
|
||
| access token | 60초 |
|
||
| refresh token | 1800초 (30분) |
|
||
| 필요한 겹침 | 최소 30분 — 앞 두 값에서 따라 나온 추론이고 측정하지 않았다 |
|
||
|
||
겹침의 최소 길이는 옛 키로 서명된 것 중 가장 오래 사는 것의 수명과 같다. 실제로 재려면 추가와 제거 사이를 30분 이상 벌리고, 그 사이에 받은 refresh token 으로 제거 뒤에 갱신을 시도한다. 이 절차에는 그 단계가 없다.
|
||
|
||
수명 세 값은 realm 설정이므로 직접 볼 수 있다. 가이드가 이 줄을 미검증으로 표시했다(unknown).
|
||
|
||
```bash label="[kc-lab-1] realm 의 수명 세 값을 받는다 (unknown)"
|
||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||
get realms/keycloak-patterns --fields accessTokenLifespan,ssoSessionIdleTimeout,ssoSessionMaxLifespan
|
||
```
|
||
|
||
겹침 길이를 정하는 것은 키가 아니라 그 키로 만든 것의 수명이다. 30분짜리 refresh token 을 발급하면서 겹침을 5분만 두면 25분어치의 토큰을 죽인다. 토큰 저장소의 암호화 키를 나중에 설계할 때도 같은 모양이 된다.
|
||
|
||
```text
|
||
쓰기: 새 key 하나로만
|
||
읽기: 새 key + 옛 key(들) ← key 에도 식별자가 필요하다
|
||
제거: 옛 key 로 암호화된 마지막 항목이 만료된 뒤
|
||
```
|
||
|
||
저장된 값에 `kid` 에 해당하는 표시가 없으면 회전이 불가능하다. 그것이 없을 때 무슨 일이 나는지는 B-7 이 잰다.
|
||
|
||
## 복구와 원상복구 확인표
|
||
|
||
**이 절차에는 원상복구가 없다.** 지운 키 공급자는 개인키와 함께 사라졌고, 같은 이름으로 다시 만들면 새 키 쌍이 생기고 `kid` 가 다르므로 옛 토큰은 그래도 401 이다. 정상 상태는 새 키 하나만 남은 상태이고, 실험 전과 다르지만 깨진 상태가 아니다.
|
||
|
||
| 남은 것 | 어떻게 | 왜 |
|
||
|---|---|---|
|
||
| `rsa-rotated` 공급자 | 그냥 둔다. 지금 유일한 RS256 서명 키다 | 지우면 realm 이 서명할 키를 잃는다 |
|
||
| 셸 변수 `OLD` `NEW` `CS` | 터미널을 닫으면 사라진다 | `unset OLD NEW CS` |
|
||
| 실험 중 발급한 토큰 | 60초 뒤 만료된다 | 별도 조치 없음 |
|
||
|
||
이름이 거슬리면 새 공급자를 하나 더 만들고 `rsa-rotated` 를 지운다. 다만 그것 역시 또 한 번의 회전이고 또 하나의 새 키다.
|
||
|
||
| 항목 | 명령 | 돌아왔을 때 |
|
||
|---|---|---|
|
||
| 서명 키 | `curl -s …/protocol/openid-connect/certs \| tr ',' '\n' \| grep kid` | RS256 이 하나 |
|
||
| 새 토큰 | 토큰 발급 + `/api/me` | `200` |
|
||
| 리소스 서버 | `kubectl -n header-lab get pods` | `echo` 가 `1/1 Running` |
|
||
| Keycloak | `kubectl -n keycloak-lab get pods` | 둘 다 `1/1 Running` |
|
||
| 공급자 목록 | `kcadm get components --fields id,name,providerId` | `rsa-generated` 가 없고 `rsa-rotated` 가 있다 |
|
||
|
||
## 막히면
|
||
|
||
원래 실행이 실제로 겪은 증상이고 지어낸 것은 없다고 가이드가 적는다.
|
||
|
||
| 증상 | 원인 | 확인 |
|
||
|---|---|---|
|
||
| `kcadm get components -q type=...` 가 빈 결과 | `-q` 필터가 안 먹는다. 오류도 없다 | `--fields id,name,providerId` 로 전체를 받는다 |
|
||
| `kcadm` 이 전부 `401`/`Unauthorized` | 파드가 재시작되어 kcadm 세션이 사라졌다 | `config credentials` 를 다시 |
|
||
| `kubectl exec keycloak-0 -- curl` 이 `exit 127` | Keycloak 이미지에 curl 도 wget 도 없다 | JWKS 와 토큰은 `kc-lab-1` 호스트에서 친다 |
|
||
| `jq: command not found` | 이 실험대에는 jq 가 없다 | `tr ',' '\n' \| grep` 로 자른다 |
|
||
| kid 를 세니 2개인데 문서는 1개라고 한다 | RS256 이 아닌 암호화 키가 섞여 있다 | `tr '}' '\n' \| grep -c RS256` 또는 `kcadm get keys` |
|
||
| 공급자를 추가했는데 새 토큰의 kid 가 그대로 | `config.priority` 가 기존보다 낮다 | 값이 `["200"]` 처럼 배열인지 |
|
||
| 추가만 했는데 옛 토큰이 401 | 추가가 아니라 토큰이 만료됐다(60초) | 클레임의 `exp` 와 `date +%s` 비교 |
|
||
| 제거했는데 새 토큰이 401 | 지운 것이 새 공급자다 | `kid` 를 다시 확인하고 남은 공급자 목록을 본다 |
|
||
| 제거했는데 옛 토큰이 200 | 지운 것이 그 토큰의 키가 아니다 | 토큰 헤더의 `kid` 와 지운 공급자의 키를 대조 |
|
||
| 「캐시 때문일 것」이라 재시작을 기다린다 | 캐시는 유예를 주지 않는다 | 재시작 전후가 같다 |
|
||
| 지운 키를 되살리려 한다 | 되살릴 수 없다. 같은 이름과 같은 키는 다르다 | 새 키 하나만 남은 상태가 정상이다 |
|
||
|
||
## 무엇이 관측이고 무엇이 아닌가
|
||
|
||
이 절차의 숫자는 `2026-09-04 14:30–14:32 KST` 에 돈 한 번의 실행에서 나왔다(observed). 해설 문서 머리의 `15:50–16:00 KST` 는 문서를 쓴 시각이고 증거 파일의 mtime 이 앞의 값이라, 실측으로 인용하는 것은 뒤쪽이라고 가이드가 적는다.
|
||
|
||
- (observed) 회전 전 JWKS 의 `kid` 두 개(`gokjn0zFUok8r7JVqW1cxuyojH1bTT87vzfQG9RrFX4` · `OY-caYDNGoP4HMAz-Q9UPTU-DM1i896NuzUZu6gfCqM`)와 RS256 키 수 1, 발급 토큰의 `kid` 가 `OY-caYDN…` 인 것, 그 토큰의 `/api/me HTTP 200`, 추가한 공급자의 id `7902af43-a0cc-4ebd-ad25-04d563854d16`, 회전 후 RS256 키 수 2 와 늘어난 `kid` `1B4AQHoxZvFaQi1tc1byz8ifU-nYFB6engD4YB4Fz84`, 새 토큰의 `kid` 가 그것인 것, 겹치는 구간의 `옛 200 · 새 200`, 제거 후 RS256 키 수 1 과 `옛 401 · 새 200`, `echo` 를 재시작한 뒤에도 `옛 401 · 새 200` 인 것.
|
||
- 비밀은 길이와 존재만 적었다. 클라이언트 비밀은 `CS` 변수에 명령 치환으로만 넘겨 화면에 찍지 않고, admin 비밀번호도 `wc -c` 로 길이만 본다. 토큰은 `2043자` 라는 길이만 옮겼고 값은 증거 파일에 있다. `kid` 와 공급자 id 는 공개 식별자라 그대로 적었다.
|
||
- (unknown) `-q type=org.keycloak.keys.KeyProvider` 로 거르는 줄(조용히 빈 결과를 준다), `tr '}' '\n' | grep -c RS256` 로 RS256 만 세는 줄, `kcadm get keys` 로 알고리즘과 상태를 보는 줄, realm 의 수명 세 값을 한 번에 받는 줄. 가이드가 전부 미검증으로 표시했고 원래 실행 기록에 이 명령들의 출력이 없다.
|
||
- 판 번호는 이 편의 출력에 하나도 안 찍혔다. B층 아홉 편 가운데 판 번호가 남은 것은 다섯 편이고 B-6 은 거기 없다(observed). `curl 8.5.0` 은 같은 실험대의 B-4 가 echo 앱에서 되돌려받은 user-agent 이지 이 편이 잰 값이 아니다(inferred).
|
||
- 추론이지 측정이 아닌 것 — 「겹침은 최소 30분」은 access token 60초와 refresh token 1800초라는 설정에서 따라 나온 값이다. 겹침을 실제로 30분 유지하며 그 사이에 발급된 refresh token 이 제거 뒤에 어떻게 되는지는 측정하지 않았다.
|
||
|
||
<!-- body:end -->
|