Files
keycloak-pattern/docs/guides/experiments/a5-asymmetric-partition.md
T
DongHyeonkaandClaude Opus 5 6f6ab86345 docs(guides): reproduction guides for all 26 experiments
Written by subagents running under the writing-practitioner-guides skill,
one guide per experiment, 22,566 lines. Each walks a reader from baseline
capture through injection, injection verification, observation and recovery.

Section 3 carries the weight in most of them. Injection failed silently nine
times in this lab, and a failed injection looks exactly like no effect — so
the guides verify the target is actually in the intended state before
reading any result. A-4 makes virsh list the only proof because the node
reads Ready for 40 seconds after the machine is off; A-5 makes the packet
counter the sole go/no-go because a rule on the wrong node produces an empty
result that reads like a finding; A-6 quotes the run where 적용완료 was
printed between four Cannot find device "eth0" lines.

The traps the guides are built around are ones that invert a conclusion
rather than merely annoy:

  A-0   emptying the session table without a restart leaves cache entries
        that get counted as replication arriving
  A-2   dropping -o /dev/null fuses body and status into one string
  A-3   presence of "ready to accept connections" instead of its timestamp
  B-2   row count alone reads an UPDATE as nothing having happened
  B-4   tr ',' '\n' splits ["admin","editor"] so only admin is seen
  B-7   no login screen means the cookie died and SSO re-authenticated
  C-1   counting sessions without joining realm counts your own kcadm one
  D-1   kubectl exec without -i restores nothing and still exits 0
  D-4a  "ran with error output" is what success looks like

Every quoted block is copied from docs/evidence/ and marked 실측; reshaped
commands are marked 미검증 rather than passed off as measured. Where a source
document carries a ★ correction the guides follow the corrected claim — A-7's
REVOKED_TOKEN hypothesis, C-1's session count, B-2's schema attribution.

Two hazards are stated rather than smoothed over: B-6 deletes a key that
cannot be recreated, and D-1/D-4 need host sudo, which asks for a password,
so those steps say a person must type them.

Audit over all 26: 672 interpretation pairs, 486 evidence citations, 117
undo sections, and zero occurrences of the patterns the skill forbids —
no python data processing, no deprecated kubectl get endpoints, no
placeholders, no bare kcadm.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 18:29:00 +09:00

920 lines
39 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# A-5 재현 가이드 — 한 방향만 끊어 보고, 왜 안 갈라지는지 직접 본다
해설 문서: [`docs/experiment-a5-asymmetric-partition.md`](../../experiment-a5-asymmetric-partition.md) ·
증거 원문: [`docs/evidence/a5-asymmetric-partition/`](../../evidence/a5-asymmetric-partition/)
## 이 가이드가 끝나면
당신 터미널에서 이것들을 **직접 본다.**
| 보게 되는 것 | 어디서 |
|---|---|
| 규칙을 넣었는데 **0 패킷**인 상태 | `iptables -L -n -v` 의 카운터 |
| kube-router 가 내 규칙을 **아래로 밀어내는** 것 | `FORWARD` 체인의 줄 번호 |
| JGroups 연결 방향이 **A-1 때와 반대**인 것 | `conntrack -L` |
| 단방향 차단이 **스스로 낫는** 것 | 연결이 뒤집혀 재연결 |
| `coord = t` 가 둘인 split brain | PostgreSQL `JGROUPS_PING` |
| 그런데 **한쪽만 DOWN 이고 외부는 200** 인 것 | `health/ready` · `endpointslice` |
| `MergeView` 로 50초 만에 합쳐지는 것 | Keycloak 로그 |
## 전제
- [`05-keycloak`](../05-keycloak/) · [`06-observability`](../06-observability/) 가 끝나 있다.
- [`A-1`](a1-jgroups-transport-block.md) 을 먼저 해 두면 훨씬 이해가 빠르다.
**이 실험은 A-1 이 실패한 자리에서 시작한다.**
- 명령은 **`kc-lab-1` 에서** 친다. `kubectl``sudo` 로 쓴다.
- `kc-lab-2` 에는 `ssh kc-lab-2` 로 붙는다. **iptables 는 두 노드에 각각
넣어야 하고, 어느 노드에 넣느냐가 이 실험의 핵심이다.**
- 터미널 **두 개**를 열어 두면 편하다. 하나는 상주 탐침 파드용, 하나는 관찰용.
## 주의 — 이건 상태를 부수는 실험이다
Keycloak 클러스터를 실제로 분단시킨다. **실험대에서만 한다.**
전 구간 약 30분이고, 되돌리는 방법은 매 단계에 적어 두었다.
중간에 그만두려면 두 줄이면 된다.
```bash
sudo iptables -t raw -F PREROUTING ; sudo iptables -F FORWARD
ssh kc-lab-2 'sudo iptables -t raw -F PREROUTING ; sudo iptables -F FORWARD'
```
> **`-F FORWARD` 는 그 체인 전체를 비운다.** 이 실험대의 `FORWARD` 정책은
> `ACCEPT` 이고 실제 규칙은 kube-router·kube-proxy 가 **자기 체인에** 두므로
> 잠시 뒤 스스로 복구된다. 그래도 지우기 전에 **`sudo iptables -S FORWARD` 로
> 무엇이 있었는지 한 번 보고** 지운다.
## 표시 규약
| 표시 | 뜻 |
|---|---|
| **실측** | 2026-09-04 12:2812:46 KST 실행 기록의 **출력 원문**. 증거 파일에 그대로 있다 |
| **형태** | 값이 매번 달라지는 출력. 모양만 보이고 숫자는 당신 것과 다르다 |
| **미검증** | 손으로 치기 좋게 이 가이드에서 고친 형태. 원래 실행은 스크립트로 했다 |
IP·파드 이름·포트 번호는 **당신 환경에서 다르다.** 이 문서는 자리표시자
(`<...>`)를 쓰지 않는 대신, 그 값을 뽑는 명령을 먼저 적는다. 예시로 실린 값은
전부 위 실행 기록의 실제 값이다.
---
# 0. 왜 이 실험을 하는가
A-1 이 열어 둔 질문이 하나 있었다.
> *「`keycloak-1` 은 멤버가 하나 줄어든 정상적인 사건이라 Ready 를 유지했고,
> `keycloak-0` 은 합류 자체를 못 해 DOWN 이 됐다. **양쪽이 동시에 DOWN 이 되는
> 경로가 있다면 전면 장애다.**」*
그 경로를 찾는 것이 이 실험이다. 그리고 A-1 은 도구도 하나 남겼다.
| | A-1 이 배운 것 |
|---|---|
| NetworkPolicy | **기존 연결을 못 끊는다.** conntrack 의 `ESTABLISHED` 가 먼저 통과시킨다 |
| 그래서 | 이번엔 iptables 로 직접 간다 |
**그런데 iptables 에도 벽이 세 개 있었다.** 이 가이드의 절반은 그 세 번의
실패를 **일부러 다시 밟는 것**이다. 셋 다 화면에는 **「아무 일도 없었다」**로
보이기 때문에, 겪어 보지 않으면 다음에도 똑같이 속는다.
```
실패 ① filter FORWARD 최상단에 넣었는데 → CNI 가 밀어낸다
실패 ② raw 로 옮겼는데도 0 패킷 → 연결 방향을 잘못 짚었다
성공 수신측 노드의 raw PREROUTING → 19 패킷
그런데 그래도 안 갈라진다 → 반대 방향으로 재연결한다
```
---
# 1. 기준선 — 아무것도 넣기 전에
**시험군만 재는 측정은 측정이 아니다.** 특히 이 실험은 **주입이 걸리기 전과
후가 화면상 똑같이 보이므로**, 기준선이 없으면 실패를 성공으로 읽는다.
## 1-1. 파드 IP 와 노드 — 이 값이 곧 규칙의 인자다
**확인**
```bash
sudo kubectl -n keycloak-lab get pods -o wide
```
**실측** — [`01-injection.txt`](../../evidence/a5-asymmetric-partition/01-injection.txt)
```
keycloak-0=10.42.1.77 (kc-lab-2) keycloak-1=10.42.0.42 (kc-lab-1)
keycloak-0 1/1 Running 0 11m
keycloak-1 1/1 Running 1 (2m48s ago) 155m
postgres-7b474b88c8-9cmsv 1/1 Running 0 14m
```
**어디를 봐야 하는가**
- **`NODE` 와 파드 번호가 어긋난다** — `keycloak-0``kc-lab-2` 에 있다.
iptables 를 **어느 노드에** 넣을지 정할 때 이걸 헷갈리면 규칙은 걸리는데
패킷은 안 걸린다
- **IP 가 A-1 때와 다르다** (`10.42.1.43``10.42.1.77`). 파드가 재시작되면
바뀐다. 여기 적힌 값을 그대로 쓰지 말고 **지금 뽑는다**
변수로 잡아 둔다. **파드가 재시작되면 다시 잡는다.**
```bash
K0=$(sudo kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
K1=$(sudo kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
echo "K0=$K0 K1=$K1"
```
> `keycloak-1` 의 `RESTARTS` 가 **1** 인 것도 보인다. A-4 에서 노드를 껐다
> 켠 흔적이다. **직전 실험의 잔재가 남아 있는지 확인하는 자리**이기도 하다.
## 1-2. 클러스터가 지금 하나인가
**확인**
```bash
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "select name, ip, coord from jgroups_ping order by name"
```
**형태**
```
name | ip | coord
------------------+-----------------+-------
keycloak-0-24309 | 10.42.1.77:7800 | f
keycloak-1-45480 | 10.42.0.42:7800 | t
(2 rows)
```
**어디를 봐야 하는가****`coord` 열에 `t` 가 정확히 하나.**
둘이면 이미 갈라져 있는 것이고, 그 상태에서 주입해 봐야 아무것도 판정 못 한다.
**확인** — 로그가 말하는 뷰
```bash
sudo kubectl -n keycloak-lab logs keycloak-0 | grep ISPN000094 | tail -1
sudo kubectl -n keycloak-lab logs keycloak-1 | grep ISPN000094 | tail -1
```
**형태**
```
ISPN000094: [keycloak-0-24309(v=16.0.12)|13] (2) [keycloak-0-24309(v=16.0.12), keycloak-1-45480(v=16.0.12)]
```
**뷰 ID(`|13`)를 적어 둔다.** 이 실험의 판정 기준이 이 숫자의 변화다.
## 1-3. 지표 — 그리고 이 자리에서 원 실행이 넘어졌다
Keycloak 컨테이너에는 `curl``wget` 도 없다(`exit 127`). 밖에서 Prometheus 에
묻는 것이 가장 짧다.
**확인**
```bash
sudo kubectl -n observability exec deploy/prometheus -- \
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size'
```
**형태** — 한 줄 JSON 이 통째로 나온다. 처음 한 번은 그대로 본다
```json
{"status":"success","data":{"resultType":"vector","result":[
{"metric":{"__name__":"vendor_cluster_size","pod":"keycloak-1","node":"kc-lab-1"},"value":[1757037600.123,"2"]},
{"metric":{"__name__":"vendor_cluster_size","pod":"keycloak-0","node":"kc-lab-2"},"value":[1757037600.123,"2"]}]}}
```
라벨을 보고 나면 읽기 좋게 자른다 (`jq` 는 이 실험대에 없다). **미검증**
```bash
sudo kubectl -n observability exec deploy/prometheus -- \
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size' \
| tr ',' '\n' | grep -E '"pod":|^"[0-9]'
```
**어디를 봐야 하는가** — 두 줄이고 값이 둘 다 `2`.
> **★ 원 실행의 기준선은 남지 않았다.** 값을 뽑으려고 붙인 파이썬 한 줄이
> 죽었기 때문이다. — [`01-injection.txt`](../../evidence/a5-asymmetric-partition/01-injection.txt)
> ```
> Traceback (most recent call last):
> File "<string>", line 3, in <module>
> for r in json.load(sys.stdin)["data"]["result"]: print(f" cluster_size {r["metric"].get("pod"):12} = {r["value"][1]}")
> json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
> ```
> **입력이 비어 있었다.** 그런데 파서가 죽으면서 **원본도 같이 사라졌다** —
> 화면에 남은 것은 파이썬 스택트레이스뿐이고, Prometheus 가 무엇을 돌려줬는지는
> 아무도 모른다. **원본을 먼저 보고 나중에 자르면** 이런 일이 없다.
> 이 가이드가 `wget` 원문을 먼저 보여 주는 이유다.
## 1-4. ★ 연결 방향 — 이 실험에서 가장 중요한 기준선
**어느 쪽이 클라이언트이고 어느 쪽이 서버인가.** 이걸 모르면 규칙을 엉뚱한
노드에 넣게 된다.
**확인** — 두 노드 모두에서 본다
```bash
sudo conntrack -L 2>/dev/null | grep 7800
ssh kc-lab-2 'sudo conntrack -L 2>/dev/null | grep 7800'
```
**실측** — 해설 문서 1절 (실패 ② 에서 인용된 원 실행의 연결)
```
ESTABLISHED src=10.42.1.77 dst=10.42.0.42 sport=60485 dport=7800
──────────── ────────────────────
keycloak-0 가 클라이언트 keycloak-1 이 서버
```
**어디를 봐야 하는가**`dport=7800` 인 쪽이 **서버**다. `src` 가 클라이언트.
**이 결과가 의미하는 것****A-1 때와 방향이 반대다.** A-1 에서는
`10.42.0.35:40023 → 10.42.1.43:7800`, 즉 `keycloak-1` 이 걸었다. 지금은
`keycloak-0` 이 건다.
> **JGroups 의 TCP 연결 방향은 고정이 아니다.** 먼저 뜬 쪽, 먼저 JOIN 을 건
> 쪽에 따라 달라진다. 파드가 재시작될 때마다 바뀔 수 있다.
> **가정하지 말고 매번 `conntrack -L` 로 본다.**
`2>/dev/null``conntrack` 이 stderr 로 찍는 「N flow entries have been shown」
요약을 지우려는 것이다. 처음에는 빼고 쳐서 그 줄도 한번 본다.
## 1-5. 밖에서 보이는 상태
**확인**
```bash
curl -I --max-time 8 https://auth.hyeonworks.com/realms/master
```
여러 번 재서 비교할 것이므로 이제부터는 코드만 뽑는다.
```bash
curl -s -o /dev/null -w '%{http_code}\n' --max-time 8 https://auth.hyeonworks.com/realms/master
```
`200` 이어야 한다.
---
# 2. 주입 시도 ① — `filter` 테이블 최상단 (실패한다)
**일부러 실패하는 단계다.** 건너뛰지 않는 편이 좋다. 이 실패의 모양을 봐 둬야
다음에 자기 규칙을 의심할 수 있다.
**되돌리기** — 먼저 읽어 둔다
```bash
ssh kc-lab-2 'sudo iptables -F FORWARD'
```
## 2-1. 넣는다
A-1 의 NetworkPolicy 는 conntrack 에 막혔다. **`FORWARD` 최상단에 넣으면
conntrack 승인보다 먼저 평가될 것**이라는 게 이 시도의 가설이다.
**하기**`keycloak-0`(수신측이라고 **가정한** 쪽) 이 있는 노드에
```bash
ssh kc-lab-2 "sudo iptables -I FORWARD 1 -p tcp -d $K0 --dport 7800 -j DROP"
ssh kc-lab-2 "sudo iptables -I FORWARD 1 -p tcp -d $K0 --dport 57800 -j DROP"
date '+%H:%M:%S 주입'
```
> **57800 도 같이 막는다.** FD_SOCK2(장애 감지 채널)는 `bind_port + 50000` 을
> 쓴다. 7800 만 막으면 **장애 감지는 계속 통해서** 분단이 어정쩡해진다.
**확인** — 방금 넣은 것이 실제로 1번인가
```bash
ssh kc-lab-2 'sudo iptables -L FORWARD -n -v --line-numbers'
```
**실측** — [`01-injection.txt`](../../evidence/a5-asymmetric-partition/01-injection.txt)
```
Chain FORWARD (policy ACCEPT)
num target prot opt source destination
1 DROP 6 -- 0.0.0.0/0 10.42.1.77 tcp dpt:57800
2 DROP 6 -- 0.0.0.0/0 10.42.1.77 tcp dpt:7800
주입 시각: 12:28:23
```
**넣은 직후에는 맞게 보인다.** 여기서 만족하고 넘어가면 속는다.
## 2-2. 잠시 뒤 다시 본다 — ★ 밀려나 있다
**확인** — 1~2분 뒤 같은 명령을 다시
```bash
ssh kc-lab-2 'sudo iptables -L FORWARD -n -v --line-numbers'
```
**실측** — 해설 문서 1절 (실패 ①)
```
num pkts bytes target
1 232 377K KUBE-ROUTER-FORWARD /* kube-router netpol */ ← 다시 1번이 되었다
2 0 0 DROP tcp dpt:57800
3 0 0 DROP tcp dpt:7800 ← 0 패킷
```
**어디를 봐야 하는가** — 두 가지를 동시에 본다.
| 열 | 무엇을 말하는가 |
|---|---|
| `num` | 내 규칙이 **1번이 아니다.** kube-router 체인이 위로 돌아왔다 |
| **`pkts`** | **0.** 이 규칙에는 패킷이 단 한 개도 도달하지 않았다 |
**이 결과가 의미하는 것** — **kube-router 가 주기적으로 자기 체인을 `FORWARD`
최상단에 다시 삽입한다.** 내가 1번에 넣어도 곧 2번, 3번으로 밀려나고,
kube-router 체인이 패킷을 먼저 처리해 버린다.
> **직접 넣은 iptables 규칙은 CNI 가 관리하는 체인과 경쟁한다.**
> **넣는 것으로 끝이 아니다. 패킷 카운터로 확인해야 한다.**
> **정직하게** — 이 확인을 담았어야 할 증거 파일
> [`02-injection-verify.txt`](../../evidence/a5-asymmetric-partition/02-injection-verify.txt)
> 는 **원 실험 시점에 0바이트로 저장됐다.** 리다이렉션이 stdout 만 받았는데
> 출력이 stderr 로 갔던 것으로 보인다. 지금 그 파일에 들어 있는 것은 **사후에
> 다시 수집한 것**이며, 원 시점의 DROP 규칙은 이미 없어서 재현되지 않는다.
> 남아 있는 것은 구조적 사실 하나 — kube-router 체인이 `FORWARD` 1번을
> 차지하고 있다는 것뿐이다.
> **당신은 지금 실제 카운터를 볼 수 있다.** 이 단계를 건너뛰지 않는 이유다.
## 2-3. 그래서 아무 일도 안 일어난다
**확인** — 25초 간격으로 몇 번 본다
```bash
sudo kubectl -n keycloak-lab get pods | grep keycloak
curl -s -o /dev/null -w '%{http_code}\n' --max-time 8 https://auth.hyeonworks.com/realms/master
```
**실측** — [`01-injection.txt`](../../evidence/a5-asymmetric-partition/01-injection.txt)
```
+25초 | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
+50초 | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
...
+200초 | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
```
**★ 여기서 「비대칭 차단은 클러스터를 안 가른다」고 결론 내리면 틀린다.**
결론이 우연히 맞더라도 **근거가 없다.** 규칙에 패킷이 0 개 왔으니
**이 관찰은 아무것도 측정하지 않았다.**
## 2-4. 치운다
**하기**
```bash
ssh kc-lab-2 "sudo iptables -D FORWARD -p tcp -d $K0 --dport 7800 -j DROP"
ssh kc-lab-2 "sudo iptables -D FORWARD -p tcp -d $K0 --dport 57800 -j DROP"
ssh kc-lab-2 'sudo iptables -L FORWARD -n --line-numbers | head -5'
```
`-D` 는 **넣을 때와 똑같은 인자**를 줘야 지워진다. 안 지워지면 줄 번호로:
`sudo iptables -D FORWARD 3`.
---
# 3. 주입 시도 ② — `raw` 테이블로 옮긴다 (그래도 0 패킷)
## 3-1. 개념 — netfilter 처리 순서
```
패킷 도착
├─▶ raw PREROUTING ← conntrack 보다 먼저. NOTRACK·DROP 용
├─▶ conntrack 조회/생성 ← 여기서 ESTABLISHED 가 결정된다
├─▶ mangle PREROUTING
├─▶ nat PREROUTING
├─▶ filter FORWARD ← NetworkPolicy·kube-router 가 여기 있다
└─▶ 목적지 파드
```
| 어디에 넣는가 | 기존 연결을 끊는가 | CNI 와 경쟁하는가 |
|---|---|---|
| NetworkPolicy (filter) | **못 끊는다** — conntrack 이 먼저 통과시킨다 (A-1) | 없음 |
| filter FORWARD 직접 | 순서에 따라 | **경쟁한다** (kube-router 가 밀어낸다) — 2절 |
| **raw PREROUTING** | **끊는다** | **없다** — CNI 가 안 쓰는 테이블 |
**진짜 네트워크 분단을 흉내내려면 `raw` 테이블이 맞다.**
## 3-2. 넣는다 — 아직 같은 노드, 같은 목적지
**되돌리기**
```bash
ssh kc-lab-2 'sudo iptables -t raw -F PREROUTING'
```
**하기**
```bash
ssh kc-lab-2 "sudo iptables -t raw -I PREROUTING 1 -p tcp -d $K0 --dport 7800 -j DROP"
ssh kc-lab-2 "sudo iptables -t raw -I PREROUTING 1 -p tcp -d $K0 --dport 57800 -j DROP"
date '+%H:%M:%S 주입'
```
**확인** — 카운터
```bash
ssh kc-lab-2 'sudo iptables -t raw -L PREROUTING -n -v'
```
**실측** — [`03-raw-table-injection.txt`](../../evidence/a5-asymmetric-partition/03-raw-table-injection.txt)
```
=== [검증] 이번엔 패킷이 걸렸는가 ===
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 DROP 6 -- * * 0.0.0.0/0 10.42.1.77 tcp dpt:57800
0 0 DROP 6 -- * * 0.0.0.0/0 10.42.1.77 tcp dpt:7800
```
**어디를 봐야 하는가****`pkts` 가 여전히 0.** 이번에는 CNI 와 경쟁하지도
않는데 0 이다.
## 3-3. 왜 0 인가 — 1-4 를 다시 본다
**확인**
```bash
ssh kc-lab-2 'sudo conntrack -L 2>/dev/null | grep 7800'
```
1-4 에서 본 것이 답이다.
```
ESTABLISHED src=10.42.1.77 dst=10.42.0.42 sport=60485 dport=7800
└── keycloak-0 ──┘ └── keycloak-1 ──┘
(클라이언트) (서버, 7800 을 듣는 쪽)
```
**이 결과가 의미하는 것**`10.42.1.77`(keycloak-0)은 이 연결의 **출발지**다.
`-d 10.42.1.77 --dport 7800` 은 **존재하지 않는 패킷**을 노린 규칙이었다.
7800 으로 **들어가는** 패킷은 `10.42.0.42`(keycloak-1) 쪽으로 간다.
```
내가 막은 것: → 10.42.1.77:7800 (그런 패킷이 없다)
실제 흐름: → 10.42.0.42:7800 (여기를 막아야 한다)
```
**규칙을 넣은 노드도 틀렸다.** 목적지 파드가 있는 노드에서 잡아야 한다.
## 3-4. 치운다
```bash
ssh kc-lab-2 'sudo iptables -t raw -L PREROUTING -n --line-numbers'
ssh kc-lab-2 'sudo iptables -t raw -F PREROUTING'
```
**지우기 전에 `-L` 로 무엇이 있는지 본다.** `-F` 는 체인 전체를 비운다.
---
# 4. 주입 성공 — 수신측 노드의 `raw PREROUTING`
## 4-1. 넣는다
**되돌리기**
```bash
sudo iptables -t raw -F PREROUTING
```
**하기** — 이번에는 **`kc-lab-1`(keycloak-1 이 있는 노드)** 에, `keycloak-1`
IP 를 목적지로
```bash
sudo iptables -t raw -I PREROUTING 1 -p tcp -d $K1 --dport 7800 -j DROP
sudo iptables -t raw -I PREROUTING 1 -p tcp -d $K1 --dport 57800 -j DROP
date '+%H:%M:%S 주입'
```
**실측** — [`04-correct-direction.txt`](../../evidence/a5-asymmetric-partition/04-correct-direction.txt)
```
=== keycloak-1(수신측)으로 들어가는 7800/57800 만 DROP — kc-lab-1 에 넣는다 ===
주입: 12:33:58
```
## 4-2. 이번엔 걸리는가 — 카운터가 유일한 판정 기준이다
**확인**
```bash
sudo iptables -t raw -L PREROUTING -n -v
```
**실측** — 같은 파일
```
pkts bytes target prot opt in out source destination
0 0 DROP 6 -- * * 0.0.0.0/0 10.42.0.42 tcp dpt:57800
19 2938 DROP 6 -- * * 0.0.0.0/0 10.42.0.42 tcp dpt:7800
```
**어디를 봐야 하는가****7800 규칙의 `pkts` 가 19.** 드디어 걸린다.
**57800 은 아직 0 인 것도 정보다.** FD_SOCK2 는 이미 붙어 있는 연결을 쓰고
있어서 새 연결을 시도하지 않았다. 조금 지나면 이쪽에도 숫자가 올라간다 —
**실측** — [`05-reconnect-observed.txt`](../../evidence/a5-asymmetric-partition/05-reconnect-observed.txt)
```
=== 차단 규칙 누적 카운터 ===
19 1096 DROP 6 -- * * 0.0.0.0/0 10.42.0.42 tcp dpt:57800
21 3058 DROP 6 -- * * 0.0.0.0/0 10.42.0.42 tcp dpt:7800
```
> **카운터 판정표**
>
> | `pkts` | 뜻 | 할 일 |
> |---|---|---|
> | `0` | **아무것도 측정하지 않았다** | 해석 금지. 방향과 테이블을 다시 본다 |
> | 조금씩 는다 | 재연결 시도가 막히고 있다 | 관찰로 넘어간다 |
> | 폭증한다 | 대상이 너무 넓다 | `-d`·`--dport` 를 좁힌다 |
---
# 5. 효과를 관찰한다 — 단방향은 클러스터를 못 가른다
## 5-1. 파드와 외부
**확인** — 25초 간격
```bash
sudo kubectl -n keycloak-lab get pods | grep keycloak
curl -s -o /dev/null -w '%{http_code}\n' --max-time 8 https://auth.hyeonworks.com/realms/master
```
**실측** — [`04-correct-direction.txt`](../../evidence/a5-asymmetric-partition/04-correct-direction.txt)
```
+25초 - | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
+50초 - | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
+75초 - | keycloak-0:1/1 keycloak-1:0/1 | 외부 200
+100초 - | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
+125초 - | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
```
**어디를 봐야 하는가**`+75초``keycloak-1` 이 **한 번 `0/1`
흔들렸다가 `+100초` 에 돌아온다.**
**이 결과가 의미하는 것** — 주입이 **닿기는 했다**(2절의 아무 일 없음과 다르다).
그런데 **스스로 나았다.**
## 5-2. 뷰가 변했나 — 그리고 로그 시각의 함정
**확인**
```bash
sudo kubectl -n keycloak-lab logs keycloak-0 --since=20m | grep ISPN000094
sudo kubectl -n keycloak-lab logs keycloak-1 --since=20m | grep ISPN000094
```
**여기서 시각을 비교하려다 대부분 한 번은 틀린다.**
```
당신 셸의 date 12:33:58 KST
컨테이너 로그의 시각 03:33:58 ← 같은 순간이다. UTC 다
```
**Keycloak 컨테이너는 UTC 로 찍는다.** KST 는 UTC+9 이므로 **9시간을 빼서**
맞춰 본다. 이걸 모르면 「주입 전 로그」와 「주입 후 로그」를 정반대로 가른다.
**실측** — [`06-view-history-and-cleanup.txt`](../../evidence/a5-asymmetric-partition/06-view-history-and-cleanup.txt)
```
2026-09-04 03:33:49 | MergeView::[keycloak-0-24309(v=16.0.12)|13] (2) [keycloak-0-24309(v=16.0.12), keycloak-1-45480(v=16.0.12)],
```
**어디를 봐야 하는가** — 뷰 `13`, 멤버 `(2)`, 그리고 **`MergeView`**.
**이 결과가 의미하는 것 — ★ 여기가 이 실험의 가장 미묘한 자리다.**
해설 문서는 처음에 「주입 이후 뷰 변화가 하나도 없었다」고 썼다. 맞는 말이다.
그런데 그 **「주입 전부터 그대로」의 「전」이 9초였다.**
```
03:33:49 MergeView 로 뷰 13 이 만들어짐 ← 그 직전에는 |12] (1), 즉 분단 상태였다
03:33:58 내 주입 ← 9초 뒤
```
앞선 실패한 주입 시도들이 만든 흔들림이 막 봉합된 직후였던 것이다.
> **로그 한 줄만 보고 「변화 없음」이라고 말하면 안 된다.**
> **그 줄이 언제 생겼는지**를 함께 본다. `grep` 에 시각이 같이 나오는 형태를
> 쓰는 이유가 이것이다.
>
> 결론 자체(주입 이후 뷰가 변하지 않았다)는 유지된다. 다만 **기준선이 9초짜리
> 였다**는 사실은 함께 적어야 정직하다.
## 5-3. ★ 왜 안 갈라졌나 — 연결이 뒤집혔다
**확인** — 1-4 와 **똑같은 명령**을 다시 친다. 그게 대조의 방법이다
```bash
sudo conntrack -L 2>/dev/null | grep 7800
```
**실측** — [`05-reconnect-observed.txt`](../../evidence/a5-asymmetric-partition/05-reconnect-observed.txt)
```
tcp 6 299 ESTABLISHED src=10.42.0.42 dst=10.42.1.77 sport=48473 dport=7800 src=10.42.1.77 dst=10.42.0.42 sport=7800 dport=48473
tcp 6 86232 ESTABLISHED src=10.42.0.42 dst=10.42.1.77 sport=44205 dport=57800 src=10.42.1.77 dst=10.42.0.42 sport=57800 dport=44205
```
**어디를 봐야 하는가**`src``dst` 를 1-4 와 나란히 놓는다.
```
차단 전: src=10.42.1.77 → dst=10.42.0.42:7800 ← 내가 막은 방향
차단 후: src=10.42.0.42 → dst=10.42.1.77:7800 ← 열린 방향으로 다시 붙었다
```
**이 결과가 의미하는 것** — **JGroups 는 막힌 연결이 죽자 반대 방향으로 새로
연결했다.** 그리고 FD_SOCK2 가 상대를 의심하기 전에 복구가 끝났다.
**확인** — 의심 카운터로 뒷받침한다
```bash
sudo kubectl -n observability exec deploy/prometheus -- \
wget -qO- 'localhost:9090/api/v1/query?query=vendor_jgroups_fd_sock2_get_num_suspected_members'
sudo kubectl -n observability exec deploy/prometheus -- \
wget -qO- 'localhost:9090/api/v1/query?query=vendor_jgroups_merge3_get_num_merge_events'
```
**실측** — [`06-view-history-and-cleanup.txt`](../../evidence/a5-asymmetric-partition/06-view-history-and-cleanup.txt)
```
keycloak-0 merge_events=1.0 suspected=0.0
keycloak-1 merge_events=1.0 suspected=0.0
```
```
keycloak-0 cluster_size=2.0
keycloak-1 cluster_size=2.0
```
**`suspected = 0`.** 아무도 상대를 의심하지 않았다 — **끊긴 적이 없는 것과
같다.** (`merge_events = 1` 은 5-2 의 9초 전 병합의 것이다.)
> **한 방향만 막는 것으로는 JGroups 를 가를 수 없다.**
> 두 노드는 서로에게 연결을 걸 수 있으므로, **한쪽 길이 막히면 다른 길로 간다.**
>
> 운영적으로는 좋은 소식이다 — **단방향 방화벽 오설정은 자가 치유된다.**
> 반대로 **분단을 재현하려는 실험자에게는 함정**이다.
---
# 6. 양방향 차단 — 갈라지지만 전면 장애는 아니다
## 6-1. 반대 노드에도 넣는다
**되돌리기** — 두 줄이다. 이제 양쪽에 있다
```bash
sudo iptables -t raw -F PREROUTING
ssh kc-lab-2 'sudo iptables -t raw -F PREROUTING'
```
**하기**`kc-lab-1` 의 규칙은 그대로 두고, `kc-lab-2` 에 반대 방향을 추가
```bash
ssh kc-lab-2 "sudo iptables -t raw -I PREROUTING 1 -p tcp -d $K0 --dport 7800 -j DROP"
ssh kc-lab-2 "sudo iptables -t raw -I PREROUTING 1 -p tcp -d $K0 --dport 57800 -j DROP"
date '+%H:%M:%S 주입'
```
**확인** — 양쪽 카운터를 다 본다. **한쪽만 걸리면 그건 여전히 단방향이다**
```bash
sudo iptables -t raw -L PREROUTING -n -v
ssh kc-lab-2 'sudo iptables -t raw -L PREROUTING -n -v'
```
**실측** — [`07-bidirectional-block.txt`](../../evidence/a5-asymmetric-partition/07-bidirectional-block.txt)
```
=== 양방향 차단 — 두 노드 모두에 raw DROP ===
주입: 12:40:25
```
## 6-2. 이번에는 갈라진다
**확인** — 25초 간격
```bash
sudo kubectl -n keycloak-lab get pods | grep keycloak
sudo kubectl -n keycloak-lab get endpointslice -l kubernetes.io/service-name=keycloak \
-o custom-columns=ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready
curl -s -o /dev/null -w '%{http_code}\n' --max-time 8 https://auth.hyeonworks.com/realms/master
```
**실측** — 같은 파일
```
+75초 keycloak-0:1/1 keycloak-1:1/1 | ready=[10.42.0.42 10.42.1.77] 외부 200
+100초 keycloak-0:1/1 keycloak-1:0/1 | ready=[10.42.1.77] 외부 200
+125초 keycloak-0:1/1 keycloak-1:0/1 | ready=[10.42.1.77] 외부 200
...
+225초 keycloak-0:1/1 keycloak-1:0/1 | ready=[10.42.1.77] 외부 200
```
**어디를 봐야 하는가** — 세 가지가 한 줄에 있다.
- `keycloak-1`**`0/1` 로 내려가서 안 돌아온다** (5-1 과 다르다)
- ready 주소가 **둘에서 하나로** 줄었다
- **외부는 계속 `200`**
> `kubectl get endpoints` 는 v1.33 부터 deprecated 라 경고가 뜬다.
> `endpointslice` 를 본다.
**확인** — 뷰
```bash
sudo kubectl -n keycloak-lab logs keycloak-0 | grep ISPN000094 | tail -1
sudo kubectl -n keycloak-lab logs keycloak-1 | grep ISPN000094 | tail -1
```
**실측** — 같은 파일
```
keycloak-0 [keycloak-0-24309(v=16.0.12)|14] (1) [keycloak-0-24309(v=16.0.12)]
keycloak-1 [keycloak-1-45480(v=16.0.12)|14] (1) [keycloak-1-45480(v=16.0.12)]
```
**뷰 ID 는 둘 다 14 인데 멤버는 각자 1 명이다.** 같은 번호의 다른 세계다.
## 6-3. split brain 을 DB 한 줄로 확인한다
**확인**
```bash
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "select name, ip, coord from jgroups_ping order by name"
```
**실측** — [`08-coordinator-and-recovery.txt`](../../evidence/a5-asymmetric-partition/08-coordinator-and-recovery.txt)
```
name | ip | coord
------------------+-----------------+-------
keycloak-0-24309 | 10.42.1.77:7800 | t
keycloak-1-45480 | 10.42.0.42:7800 | t
(2 rows)
```
**어디를 봐야 하는가****`coord = t` 가 둘.** 1-2 에서 하나였던 것과 대조한다.
**분단을 확인하는 가장 짧은 명령이 이것이다.** 로그를 두 번 긁는 것보다 빠르고,
지표보다 정확하다.
## 6-4. ★ 그런데 한쪽만 DOWN 이다
Keycloak 컨테이너에 `curl` 이 없으므로 **임시 파드에서 묻는다.**
**하기** — 상주 파드를 띄운다
```bash
sudo kubectl -n keycloak-lab run a5-probe --image=curlimages/curl:8.11.1 \
--restart=Never --command -- sleep 1800
sudo kubectl -n keycloak-lab wait --for=condition=Ready pod/a5-probe --timeout=120s
```
**되돌리기**
```bash
sudo kubectl -n keycloak-lab delete pod a5-probe --ignore-not-found
```
> **왜 `--rm -it` 짜리 일회용 파드를 안 쓰나.** 원 실행이 그렇게 했다가
> 붙지 못했다. — [`08-coordinator-and-recovery.txt`](../../evidence/a5-asymmetric-partition/08-coordinator-and-recovery.txt)
> ```
> warning: couldn't attach to pod/a5-h, falling back to streaming logs: Internal error occurred: error attaching to container: container is in CONTAINER_EXITED state
> ```
> 파드가 만들어지고 **명령이 끝나 버리기 전에** 붙어야 하는 경주가 된다.
> 관찰을 여러 번 반복할 것이라면 **상주 파드가 항상 낫다.**
**확인** — 양쪽 헬스체크
```bash
sudo kubectl -n keycloak-lab exec a5-probe -- curl -s "http://$K0:9000/health/ready"
sudo kubectl -n keycloak-lab exec a5-probe -- curl -s "http://$K1:9000/health/ready"
```
**실측** — 같은 파일
```
--- keycloak-0 ---
{"status":"UP","checks":[{"name":"GracefulShutdown","status":"UP"}
{"name":"KeycloakInitialized","status":"UP"}
{"name":"Keycloakclusterhealthcheck","status":"UP"}
--- keycloak-1 ---
{"status":"DOWN","checks":[{"name":"GracefulShutdown","status":"UP"}
{"name":"Keycloakdatabaseconnectionsasynchealthcheck","status":"UP"}
{"name":"KeycloakInitialized","status":"UP"}
```
**어디를 봐야 하는가** — 맨 앞의 `"status"`. **`keycloak-0` 은 UP, `keycloak-1`
은 DOWN.** 그리고 `keycloak-1` 쪽에서 **DB 체크는 UP** 인 것도 본다 —
DB 때문이 아니라 클러스터 때문이다.
**이 결과가 의미하는 것 — A-1 의 열린 질문에 대한 답이다.**
| | keycloak-0 | keycloak-1 |
|---|---|---|
| 분단 전 역할 | **코디네이터** (뷰 13 의 발행자) | 일반 멤버 |
| 분단 후 자기 인식 | 「멤버가 하나 나갔다」 — **정상 사건** | 「코디네이터를 잃었다」 — **비정상** |
| 헬스체크 | **UP** | **DOWN** |
| Service 엔드포인트 | **남는다** | 빠진다 |
**Keycloak 의 클러스터 헬스체크는 비대칭이다.** 코디네이터였던 쪽은 자기가
정상이라고 보고, 잃은 쪽만 DOWN 이 된다. 그래서 **완전 분단조차 용량 저하로
끝나고 전면 장애가 되지 않는다.**
> **A-2(DB 상실)에서는 양쪽이 동시에 DOWN 이었다.** 차이는 이것이다 —
> **DB 는 모두가 의존하는 하나지만, 클러스터 멤버십은 서로 상대적이다.**
Grafana 에서 같은 것을 그림으로 본다 —
[`a5-cluster-size-bidirectional-block.png`](../../evidence/a5-asymmetric-partition/a5-cluster-size-bidirectional-block.png).
---
# 7. 복구
## 7-1. 지운다
**확인** — 지우기 전에 무엇이 있는지 본다
```bash
sudo iptables -t raw -L PREROUTING -n -v --line-numbers
ssh kc-lab-2 'sudo iptables -t raw -L PREROUTING -n -v --line-numbers'
```
**하기**
```bash
date '+%H:%M:%S 해제'
sudo iptables -t raw -F PREROUTING
ssh kc-lab-2 'sudo iptables -t raw -F PREROUTING'
```
**실측** — [`08-coordinator-and-recovery.txt`](../../evidence/a5-asymmetric-partition/08-coordinator-and-recovery.txt)
```
=== 차단 해제 ===
해제: 12:44:37
```
## 7-2. 자동으로 다시 붙는지 본다
**확인** — 25초 간격
```bash
sudo kubectl -n keycloak-lab get pods | grep keycloak
```
**실측** — 같은 파일
```
+25초 keycloak-0:1/1 keycloak-1:0/1
+50초 keycloak-0:1/1 keycloak-1:1/1
→ 복구 완료
```
**50초. 사람 개입 없음.**
## 7-3. 누가 붙였나 — `MergeView`
**확인**
```bash
sudo kubectl -n keycloak-lab logs keycloak-0 | grep MergeView | tail -1
sudo kubectl -n keycloak-lab logs keycloak-1 | grep MergeView | tail -1
```
**실측** — 같은 파일
```
keycloak-0 MergeView::[keycloak-0-24309(v=16.0.12)|15] (2) [keycloak-0-24309(v=16.0.12), keycloak-1-45480(
keycloak-1 MergeView::[keycloak-0-24309(v=16.0.12)|15] (2) [keycloak-0-24309(v=16.0.12), keycloak-1-45480(
```
**어디를 봐야 하는가** — 뷰 ID 가 **15**, 멤버 `(2)`, **양쪽이 같은 줄.**
```
[keycloak-0-24309|13] (2) ← 정상
[keycloak-0-24309|14] (1) ← 분단. 양쪽이 각자 14 를 발행
MergeView::[...|15] (2) ← 병합. 뷰 ID 는 계속 증가한다
```
**뷰 ID 는 단조 증가**하므로 「언제 몇 번 갈라졌는지」를 로그만으로 셀 수 있다.
## 7-4. 캐시별 재분배 로그
**확인**
```bash
sudo kubectl -n keycloak-lab logs keycloak-0 | grep ISPN100007 | tail -6
```
**실측** — 해설 문서 4절 ·
원문은 [`06-view-history-and-cleanup.txt`](../../evidence/a5-asymmetric-partition/06-view-history-and-cleanup.txt)
```
[Context=work] ISPN100007: After merge (or coordinator change) ...
[Context=clientSessions] ISPN100007: After merge ...
[Context=offlineSessions] ISPN100007: After merge ...
[Context=loginFailures] ISPN100007: After merge ...
[Context=actionTokens] ISPN100007: After merge ...
```
증거 파일의 원문은 한 줄이 길어 잘려 있다. 그 형태도 한 번 본다.
```
2026-09-04 03:32:59,874 INFO [org.infinispan.CLUSTER] (non-blocking-thread--p2-t2) [Context=work] ISPN100007: After merge (or coo
```
**`ISPN100007` 은 병합(또는 코디네이터 변경) 후 캐시별 토폴로지 재계산**이다.
**캐시가 여럿이므로 로그도 캐시 수만큼 나온다.** 한 줄만 보고 「한 번
재분배됐다」고 세면 틀린다 — `work`·`clientSessions`·`offlineSessions`·
`loginFailures`·`actionTokens` 가 각각 찍는다.
## 7-5. 원상복구 확인표
| 항목 | 명령 | 돌아왔을 때 |
|---|---|---|
| raw 규칙 | `sudo iptables -t raw -S PREROUTING` | `-P PREROUTING ACCEPT` 만 |
| filter 규칙 | `sudo iptables -S FORWARD \| head -5` | 내가 넣은 `DROP` 이 없음 |
| (반대 노드) | `ssh kc-lab-2 'sudo iptables -t raw -S PREROUTING'` | 같음 |
| 파드 | `sudo kubectl -n keycloak-lab get pods` | `keycloak` 둘 다 `1/1 Running` |
| 디스커버리 | 6-3 의 psql | **`coord = t` 가 하나** |
| 뷰 | `logs keycloak-0 \| grep ISPN000094 \| tail -1` | 멤버 `(2)`, 양쪽 동일 |
| 지표 | `vendor_cluster_size` | 양쪽 `2` |
| Service | `get endpointslice -l kubernetes.io/service-name=keycloak` | ready 주소 **둘** |
| 탐침 파드 | `sudo kubectl -n keycloak-lab get pod a5-probe` | 지웠으면 `NotFound` |
| 밖 | `curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master` | `200` |
conntrack 은 건드리지 않아도 된다. **차단이 풀리면 새 연결이 스스로 성립한다.**
> **이 실험이 재지 않은 것** — 분단 중에 **세션이 어떻게 되는지**는 재지 않았다
> (그건 A-1 4-5 의 주제다). 여기서는 **누가 살아남는가**만 봤다.
---
# 막히면
전부 이 실험대가 **실제로 겪은** 증상이다. 지어낸 것은 없다.
| 증상 | 원인 | 확인 |
|---|---|---|
| 규칙을 넣었는데 아무 일도 없다 | **카운터가 0 이면 아무것도 측정 안 된 것** | `iptables -L -n -v``pkts` — 4-2 |
| 내 규칙이 1번이 아니다 | **kube-router 가 자기 체인을 재삽입한다** | `--line-numbers` 로 순서 — 2-2 |
| `raw` 인데도 0 패킷 | **연결 방향을 잘못 짚었다** | `conntrack -L \| grep 7800` — 3-3 |
| conntrack 에 아무것도 안 보인다 | 반대 노드에서 봤다 | **두 노드 모두에서** 본다 — 1-4 |
| 단방향인데 안 갈라진다 | **정상이다. 열린 방향으로 재연결한다** | `conntrack``src`/`dst` 뒤집힘 — 5-3 |
| 로그에 변화가 없어 보인다 | **컨테이너 로그는 UTC.** KST 와 9시간 차 | `logs` 의 시각에서 9를 뺀다 — 5-2 |
| 「주입 전부터 그대로」인데 미심쩍다 | 그 「전」이 9초일 수 있다 | 앞 뷰가 **언제** 생겼는지 본다 — 5-2 |
| `-D` 로 규칙이 안 지워진다 | 넣을 때와 인자가 다르다 | 줄 번호로 지운다: `-D FORWARD 3` |
| 임시 파드에 attach 실패 | `--rm -it` 는 경주가 된다 | **상주 파드**를 쓴다 — 6-4 |
| `kubectl exec keycloak-0 -- curl``exit 127` | **이미지에 curl 도 wget 도 없다** | 탐침 파드나 Prometheus |
| 지표를 파이썬으로 자르다 죽었다 | **원본까지 같이 사라진다** | `wget` 원문을 먼저 본다 — 1-3 |
| `get endpoints` 가 경고를 찍는다 | v1.33 부터 deprecated | `get endpointslice -l kubernetes.io/service-name=...` |
| 57800 카운터만 0 이다 | FD_SOCK2 가 아직 재연결을 안 했다 | 조금 기다렸다 다시 본다 — 4-2 |
| 해제했는데 2~3분째 안 붙는다 | 반대 노드 규칙이 남아 있다 | **두 노드 모두** `-t raw -S PREROUTING` |
---
# 실험자를 위한 한 장 요약
| 상황 | 확인 방법 |
|---|---|
| 규칙을 넣었는데 안 걸린다 | `iptables -L -n -v`**패킷 카운터** |
| 방향을 모르겠다 | `sudo conntrack -L 2>/dev/null \| grep 7800``dport` 쪽이 서버 |
| CNI 가 밀어낸다 | **`raw` 테이블**을 쓴다 |
| 갈라졌는지 알고 싶다 | `JGROUPS_PING.coord`, `ISPN000094`, `vendor_cluster_size` |
| 누가 살아남을지 알고 싶다 | **분단 전 코디네이터가 누구였는가** |
---
# 다음
| 실험 | A-5 가 남긴 질문 |
|---|---|
| [A-6](../../experiment-a6-latency-injection.md) 지연 주입 | **`tc netem` 도 똑같은 함정.** 인터페이스를 잘못 고르면 카운터가 0 이다 |
| [A-7](../../experiment-a7-volatile-comparison.md) volatile 비교 | 여기서는 분단에도 서비스가 계속됐다. volatile 이면 **세션이 갈라진다** |
| [A-1](a1-jgroups-transport-block.md) 로 되돌아가서 | 분단 중 **로그아웃이 전파되지 않는다.** 그 상태를 여기서 다시 만들 수 있다 |
| 운영 | **단방향 방화벽 오설정은 자가 치유된다.** 양방향이어야 사고가 된다 |