Compare commits

..
Author SHA1 Message Date
DongHyeonkaandClaude Opus 5 9dbee18a42 docs(b4): item 3 is no longer unmeasured — link it to the follow-up result
B-4 left role propagation open because oauth2-proxy was not deployed yet.
B-7 deployed it and the follow-up measured it: the value does not change
with request count, only when a new session is created.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 18:10:43 +09:00
DongHyeonkaandClaude Opus 5 905392947d docs: close the three untested items — all three are now measured
D-2 forward upgrade and B-4 role propagation were filled earlier. D-4 forced
renewal was the third, and it needed a person at the keyboard because the
host asks for a sudo password.

It turned out to hold the largest finding of the three: the renewal
succeeded and went unserved for 36 minutes 39 seconds, because nothing in
the setup reloads nginx — no ExecStartPost, three empty hook directories,
no nginx plugin. And the half of the plan's question that was still an
expectation is now a measurement: the reload is graceful, 8856 connections
with zero failures and an in-flight request surviving intact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 18:10:04 +09:00
DongHyeonkaandClaude Opus 5 faf55dc1be docs(d4): the renewal succeeded and was not served for 36 minutes 39 seconds
The forced renewal did work. What did not work was anything making nginx
notice.

  disk     cert2.pem written 2026-09-04 17:22:13 KST
  network  old serial through 08:58:47 UTC, new serial from 08:58:52 UTC
  gap      2199 seconds, and the old certificate was observed 428 times in it

The gap closed only because a person ran `nginx -s reload`. Nothing else
would have: certbot-renew.service is just `ExecStart=/usr/bin/certbot -q
renew` with no ExecStartPost, all three of renewal-hooks/{deploy,post,pre}
are empty, and certbot 5.7.0 here has no nginx plugin (dns-cloudflare,
manual, null, standalone, webroot). Three paths, all empty. Any one of them
would have been enough.

nginx holds the certificate in memory from startup, and certbot swaps the
live/ symlink rather than the path — so the config still looks right while
the served certificate is stale. Master 585 and worker 586 sharing a start
time 22.4 hours old is what proved no reload had happened.

This is invisible for 88 days. The timer ran twice today and exited
0/SUCCESS both times, because a renewal is not due yet. The day it becomes
due, the symptom is an expired certificate and the log still says SUCCESS.

The other half of the plan's question now has a measurement rather than an
expectation. The reload is genuinely graceful: 8856 polled connections, zero
non-200, p95 205.7ms before against 204.3ms after. And an in-flight request
— 845KB pulled at 20k/s, so still transferring 12 seconds in when the reload
landed — completed with all 845361 bytes over a single connection.

The earlier note that nginx reload "is graceful by design but must not be
written that way without checking" was right to hold back. The neighbouring
belief, that renewing gets the new certificate served, was the one that was
false, and there was no way to tell in advance which it would be.

Adds diagrams/d4-renewal-gap.svg and evidence 09-13.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 18:04:44 +09:00
DongHyeonkaandClaude Opus 5 0d84921a56 docs(d4): the renewal unit has no reload, and nginx has not reloaded in 22 hours
The forced renewal did not change the certificate being served — 161 serial
samples over 13 minutes, all identical, notBefore still Sep 3.

Two things were readable without sudo and both matter.

nginx has never been reloaded: master 585 and worker 586 share a start time
and both are 80529 seconds old. A reload keeps the master and replaces the
workers, so the original fork still being there means no reload has happened
since Sep 3 19:00. nginx reads the certificate at startup and holds it in
memory; without a reload it serves the old one no matter what is on disk.

And the unit that is supposed to renew has nothing that would reload it:

  [Service]
  Type=oneshot
  ExecStart=/usr/bin/certbot -q renew
  PrivateTmp=true

No ExecStartPost, no --deploy-hook. The timer runs twice a day and exited
0/SUCCESS both times today, which is precisely why this stays invisible —
the renewal is not due for 88 days, so nothing has failed yet.

What remains undetermined is whether the user's --force-renewal failed, or
succeeded and left new files that nginx never picked up. Telling those apart
needs /etc/letsencrypt, which is root-only. If it is the latter, that is the
most valuable thing this lab has produced: "renewal succeeded" and "the new
certificate is being served" are different events.

CT logs looked like a way around the permission problem and were not: the
served certificate carries two valid SCTs, yet crt.sh returns zero rows for
auth.hyeonworks.com. Same shape as A-2's `up` metric — the observing tool
sees a subset of the truth.

Also recorded: 76 in-flight failures at 08:15:04 are a local artifact, not a
server event. The concurrent 0.2s poll was clean across the same second,
num_connects was 0, and time_total was 50 microseconds. Not reproducible in
100 retries. The monitor now records curl's exit code so a recurrence is
diagnosable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 17:28:01 +09:00
DongHyeonkaandClaude Opus 5 716e62524a docs(d4): measure the controls before the injection that needs a password
The forced renewal needs sudo on the host, and the host asks for a password.
That blocks the injection, not the experiment — the part worth doing first
was the control anyway.

Control 1 — new connections, 0.2s x 900 over 180s: 900/900 = 200, zero
failures. The noise floor is 0, so a single non-200 during the renewal can
be attributed to the renewal. Without this the observation would have been
uninterpretable, which is the mistake A-6 made calling a -41% control
"no effect" and A-8 made claiming zero-downtime from 9 samples.

Control 2 — the poll cannot answer the question the plan actually asked.
TLS handshakes were 900/900, meaning every request is a fresh connection,
so it measures "are new connections accepted", not "what happens to a
request already in flight". A separate device: the 845KB admin console
bundle pulled at --limit-rate 20k, holding one request open for 42 seconds.
Baseline 200 / 845361 bytes / 1 connection.

Monitors are running under setsid, and stop three minutes after the
certificate serial changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 17:12:31 +09:00
DongHyeonkaandClaude Opus 5 74c9b3cea7 docs: replace prose placeholders in reproduction steps with executable commands
The audit found ~80 placeholders, and the damaging ones were where the
measuring apparatus itself was prose rather than a command:

  a6  "( curl ... ) & 를 20개 띄우고 wait"  — the 22.2s headline came from this
  a3  "<로그인 반복, sid 를 /tmp/sids 에>"  — the whole RPO measurement
  a3  "<sid 목록>"                          — the control it is compared against
  a5  "<수신 파드IP>"                       — the injection
  a8  writes /tmp/tok, reads /tmp/rt        — self-inconsistent, sent an empty token
  b3  $KC / $RT / $NEW never assigned
  c2  bare kcadm.sh with no kubectl exec
  a1  conntrack tuples written by hand, though the direction flips per restart

Each is now a shell-expandable form: pod IPs from jsonpath, the admin password
from the secret, ids from kcadm --format csv, conntrack tuples derived from
"conntrack -L" with awk rather than transcribed.

Then the rewritten commands were executed against the live cluster, and one
of them failed — the 20-way load generator, written as "kubectl run --rm -i",
lost its output stream twice in a row. That is a trap this series already hit
once, and the rewrite reintroduced it. A-6 now uses a resident probe pod that
collects into a file and is cat-ed once; verified 20/20 lines.

Evidence: docs/evidence/followup/05-command-reproducibility.txt

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 17:01:54 +09:00
DongHyeonkaandClaude Opus 5 f3f3a8da46 docs: re-measure B-4 role propagation and record it as evidence
The twelve-request result and the post-reauth value were printed in the document but never written to a file; the audit flagged it as a conclusion standing without evidence. Measured again with timestamps into 03-b4-role-propagation.txt, which also surfaced a 107-second clock skew between the browser host and the lab host that the file now records so the two timelines can be compared.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 16:54:05 +09:00
DongHyeonkaandClaude Opus 5 e0d27d47ce docs: correct the places where documents contradicted their own evidence
An independent audit found ten documents printing values their evidence files do not contain. C-1 printed a session count of 0 where the evidence says 4, C-2 printed a success readback for a command that exited 1, and A-1 credited the conntrack flush with a split that the timestamps attribute to a pod restart four seconds earlier.

Also measured wal_writer_delay, which A-3 had asserted as matching without ever querying it, relabelled the A-6 control that moved 41 percent, noted A-8's nine-sample resolution, corrected D-1's RTO to the 41 seconds its own timeline shows, and added a correction banner to D-2. Every experiment document now links its evidence files with their real collection times, and the duplicate screenshots are documented as duplicates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 16:35:49 +09:00
DongHyeonkaandClaude Opus 5 78b270559c docs: add SVG diagrams, explicit concept sections and the diagram convention
Twelve SVG architecture diagrams cover the experiments whose documents had little or no structure drawing, embedded under a 구조 heading with a shared convention file. Seven documents carried their concepts under narrative headings and now have an explicit 개념 section so they can be found.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 16:20:05 +09:00
DongHyeonkaandClaude Opus 5 98a74e90a5 docs: fill the untested items and record why the B layer has no graphs
The forward upgrade to 26.7.3 was zero downtime across 87 samples, and since databasechangelog stayed at 210 the rollback to 26.7.0 also succeeded, which narrows D-2's conclusion: rolling back fails when the schema moved, not because of the version number. The row count is the check.

Role changes never reach the upstream through request repetition; the session is a snapshot taken at login and only a new session picks up the new claim. Auditing the docs also surfaced that Prometheus scrapes only keycloak, kubelet, node-exporter and itself, so the B-layer experiments have no metrics to screenshot rather than missing screenshots.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 16:18:33 +09:00
DongHyeonkaandClaude Opus 5 b5528fae87 docs: index all 23 experiments with what each measured
One table per experiment with its branch and result, plus the nine injections that silently did nothing and the five predictions that turned out wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 15:11:32 +09:00
DongHyeonkaandClaude Opus 5 4864d837f1 docs: D-4 — the certificate is fine and the renewal itself went untested
Three SAN entries and no wildcard is the constraint that cost something real in B-7, where oauth2-proxy had to borrow Grafana's app2 hostname because a fourth name was not available. The served chain is four deep and verifies, so fullchain.pem is configured rather than the cert.pem mistake that only breaks clients without a cached intermediate.

The forced renewal and the reload behaviour could not be measured because sudo on the host asks for a password, the same silent failure first noticed in B-7. nginx reload is graceful by design, but this lab has repeatedly shown that by design is not the same as measured, so it is recorded as untested rather than assumed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 15:09:36 +09:00
82 changed files with 4511 additions and 71 deletions
@@ -0,0 +1,2 @@
[ 19340ms] [ERROR] Failed to load resource: the server responded with a status of 403 () @ https://app2.hyeonworks.com/oauth2/callback?state=rKipZCUv8W5a-xgYheJbjBsInoD5Il1AaF1RlM_RB2s%3A%2Fapi%2Fecho&session_state=Mw52KcQijFB9Bq4rN-C4SF5Y&iss=https%3A%2F%2Fauth.hyeonworks.com%2Frealms%2Fkeycloak-patterns&code=f9a4835a-2af3-b886-bd04-10b5347ee8d2.Mw52KcQijFB9Bq4rN-C4SF5Y.80431dbc-af81-4673-9790-ad06d1570b2e:0
[ 20374ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://app2.hyeonworks.com/favicon.ico:0
@@ -0,0 +1 @@
[ 210ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://app2.hyeonworks.com/favicon.ico:0
@@ -0,0 +1 @@
[ 423ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://app2.hyeonworks.com/favicon.ico:0
@@ -0,0 +1,8 @@
[ 1127ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 2377ms] [WARNING] <meta name="apple-mobile-web-app-capable" content="yes"> is deprecated. Please include <meta name="mobile-web-app-capable" content="yes"> @ https://app2.hyeonworks.com/explore?schemaVersion=1&panes=%7B%22h4a%22%3A%7B%22datasource%22%3A%22PBFA97CFB590B2093%22%2C%22queries%22%3A%5B%7B%22refId%22%3A%22A%22%2C%22expr%22%3A%22vendor_cluster_size%22%2C%22range%22%3Atrue%2C%22instant%22%3Afalse%2C%22editorMode%22%3A%22code%22%2C%22legendFormat%22%3A%22cluster_size+%7B%7Bpod%7D%7D%22%2C%22datasource%22%3A%7B%22type%22%3A%22prometheus%22%2C%22uid%22%3A%22PBFA97CFB590B2093%22%7D%7D%2C%7B%22refId%22%3A%22B%22%2C%22expr%22%3A%22up%7Bjob%3D%5C%22keycloak%5C%22%7D%22%2C%22range%22%3Atrue%2C%22instant%22%3Afalse%2C%22editorMode%22%3A%22code%22%2C%22legendFormat%22%3A%22up+%7B%7Bpod%7D%7D%22%2C%22datasource%22%3A%7B%22type%22%3A%22prometheus%22%2C%22uid%22%3A%22PBFA97CFB590B2093%22%7D%7D%5D%2C%22range%22%3A%7B%22from%22%3A%22now-30m%22%2C%22to%22%3A%22now%22%7D%7D%7D&orgId=1:0
[ 2472ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 3501ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 5119ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 8511ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 14956ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 28065ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
@@ -0,0 +1,10 @@
[ 1362ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1874ms] [WARNING] <meta name="apple-mobile-web-app-capable" content="yes"> is deprecated. Please include <meta name="mobile-web-app-capable" content="yes"> @ https://app2.hyeonworks.com/explore?schemaVersion=1&orgId=1&panes=%7B%22a%22%3A%7B%22datasource%22%3A%22PBFA97CFB590B2093%22%2C%22queries%22%3A%5B%7B%22refId%22%3A%22A%22%2C%22expr%22%3A%22vendor_cluster_size%22%2C%22range%22%3Atrue%2C%22instant%22%3Afalse%2C%22editorMode%22%3A%22code%22%2C%22legendFormat%22%3A%22cluster_size%20%7B%7Bpod%7D%7D%22%2C%22datasource%22%3A%7B%22type%22%3A%22prometheus%22%2C%22uid%22%3A%22PBFA97CFB590B2093%22%7D%7D%2C%7B%22refId%22%3A%22B%22%2C%22expr%22%3A%22up%7Bjob%3D%5C%22keycloak%5C%22%7D%22%2C%22range%22%3Atrue%2C%22instant%22%3Afalse%2C%22editorMode%22%3A%22code%22%2C%22legendFormat%22%3A%22up%20%7B%7Bpod%7D%7D%22%2C%22datasource%22%3A%7B%22type%22%3A%22prometheus%22%2C%22uid%22%3A%22PBFA97CFB590B2093%22%7D%7D%5D%2C%22range%22%3A%7B%22from%22%3A%221788502680000%22%2C%22to%22%3A%221788503520000%22%7D%7D%7D:0
[ 2907ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 3998ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 6253ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 9426ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 12495ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 24486ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 31338ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 46196ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
@@ -0,0 +1,198 @@
[ 1319ms] [WARNING] <meta name="apple-mobile-web-app-capable" content="yes"> is deprecated. Please include <meta name="mobile-web-app-capable" content="yes"> @ https://app2.hyeonworks.com/explore?schemaVersion=1&orgId=1&panes=%7B%22a%22%3A%7B%22datasource%22%3A%22PBFA97CFB590B2093%22%2C%22queries%22%3A%5B%7B%22refId%22%3A%22A%22%2C%22expr%22%3A%22vendor_cluster_size%22%2C%22range%22%3Atrue%2C%22instant%22%3Afalse%2C%22editorMode%22%3A%22code%22%2C%22legendFormat%22%3A%22cluster_size%20%7B%7Bpod%7D%7D%22%2C%22datasource%22%3A%7B%22type%22%3A%22prometheus%22%2C%22uid%22%3A%22PBFA97CFB590B2093%22%7D%7D%2C%7B%22refId%22%3A%22B%22%2C%22expr%22%3A%22vendor_statistics_approximate_entries_unique%7Bcache%3D%5C%22sessions%5C%22%7D%22%2C%22range%22%3Atrue%2C%22instant%22%3Afalse%2C%22editorMode%22%3A%22code%22%2C%22legendFormat%22%3A%22sessions%20%5Cuce90%5Cuc2dc%20%7B%7Bpod%7D%7D%22%2C%22datasource%22%3A%7B%22type%22%3A%22prometheus%22%2C%22uid%22%3A%22PBFA97CFB590B2093%22%7D%7D%5D%2C%22range%22%3A%7B%22from%22%3A%221788497040000%22%2C%22to%22%3A%221788499080000%22%7D%7D%7D:0
[ 5941ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 11107ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 14234ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 17005ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 23049ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 30565ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 44135ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 54992ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 63653ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 70658ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 90768ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 105475ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 114995ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 125443ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 137321ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 147252ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 167227ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 179100ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 187598ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 207362ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 213255ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 230544ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 248833ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 257549ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 262324ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 274443ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 294124ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 310689ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 312049ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 327025ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 339933ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 342391ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 362305ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 370693ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 378685ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 397930ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 414007ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 419130ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 420355ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 428557ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 442088ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 445041ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 446677ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 460087ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 462505ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 463673ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 482762ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 495123ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 513442ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 516414ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 536581ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 543441ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 563624ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 580510ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 588408ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 594209ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 604166ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 605295ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 621987ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 639705ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 653320ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 660911ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 678105ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 693662ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 703089ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 704712ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 719727ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 725514ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 730429ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 735135ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 736677ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 751626ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 767197ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 782052ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 797775ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 803343ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 822168ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 828683ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 845938ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 865903ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 883116ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 894375ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 898471ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 914886ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 933184ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 953353ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 963902ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 982846ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 998975ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1000721ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1003229ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1009879ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1016334ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1023805ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1043684ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1049467ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1059107ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1075856ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1076956ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1094159ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1102825ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1116077ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1134809ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1136553ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1141191ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1158221ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1173507ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1190834ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1198506ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1217761ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1231342ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1233528ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1243017ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1253381ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1256056ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1258513ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1271317ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1284530ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1301830ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1307562ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1310330ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1317970ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1328668ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1336338ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1340155ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1346989ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1348836ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1352113ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1367871ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1371158ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1390720ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1405259ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1420920ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1433518ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1450513ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1453685ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1460654ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1470589ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1489330ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1503666ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1521587ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1537970ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1540426ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1548105ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1549441ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1567671ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1585493ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1597054ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1602669ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1607866ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1623290ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1631460ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1640086ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1645979ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1650742ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1652863ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1663908ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1666587ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1668427ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1676825ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1687882ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1691062ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1698532ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1709597ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1725669ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1739294ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1744412ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1753319ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1761792ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1765298ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1784381ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1786396ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1798982ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1807180ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1823569ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1837292ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1838720ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1848387ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1856847ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1866742ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1879249ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1884700ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1900774ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1912256ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1931088ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1937844ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1948191ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1951360ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1957512ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1958836ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1979011ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 1993551ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 2011369ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 2024782ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 2040763ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 2044650ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 2061337ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 302 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
@@ -0,0 +1 @@
[ 12202ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://app2.hyeonworks.com/favicon.ico:0
@@ -0,0 +1 @@
[ 451ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://app2.hyeonworks.com/favicon.ico:0
@@ -0,0 +1,16 @@
- generic [ref=f55e3]:
- banner [ref=f55e4]:
- generic [ref=f55e5]: keycloak-patterns
- main [ref=f55e6]:
- heading "Sign in to your account" [level=1] [ref=f55e8]
- generic [ref=f55e12]:
- generic [ref=f55e13]:
- generic [ref=f55e14]: Username or email
- textbox "Username or email" [ref=f55e17]
- generic [ref=f55e18]:
- generic [ref=f55e19]: Password
- generic [ref=f55e21]:
- textbox "Password" [ref=f55e24]
- button "Show password" [ref=f55e26] [cursor=pointer]:
- generic [aria-hidden] [ref=f55e27]:
- button "Sign In" [ref=f55e30] [cursor=pointer]
@@ -0,0 +1,16 @@
- generic [ref=f55e3]:
- banner [ref=f55e4]:
- generic [ref=f55e5]: keycloak-patterns
- main [ref=f55e6]:
- heading "Sign in to your account" [level=1] [ref=f55e8]
- generic [ref=f55e12]:
- generic [ref=f55e13]:
- generic [ref=f55e14]: Username or email
- textbox "Username or email" [ref=f55e17]: labuser
- generic [ref=f55e18]:
- generic [ref=f55e19]: Password
- generic [ref=f55e21]:
- textbox "Password" [ref=f55e24]: labpass
- button "Show password" [ref=f55e26] [cursor=pointer]:
- generic [aria-hidden] [ref=f55e27]:
- button "Sign In" [ref=f55e30] [cursor=pointer]
@@ -0,0 +1,17 @@
- generic [ref=f56e1]:
- generic [ref=f56e3]:
- generic [ref=f56e4]: "403"
- heading "Forbidden" [level=1] [ref=f56e6]
- generic [ref=f56e8]:
- paragraph [ref=f56e9]: More Info
- generic [ref=f56e10] [cursor=pointer]:
- separator [ref=f56e12]
- generic [ref=f56e13]:
- button "Go back" [ref=f56e16] [cursor=pointer]
- button "Sign in" [ref=f56e19] [cursor=pointer]
- contentinfo [ref=f56e20]:
- paragraph [ref=f56e22]:
- text: Secured with
- link "OAuth2 Proxy" [ref=f56e23] [cursor=pointer]:
- /url: https://github.com/oauth2-proxy/oauth2-proxy#oauth2_proxy
- text: version v7.7.1
@@ -0,0 +1 @@
- generic [ref=f57e1]: "{ \"headers\" : { \"host\" : [ \"app2.hyeonworks.com\" ], \"user-agent\" : [ \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36\" ], \"accept\" : [ \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\" ], \"accept-encoding\" : [ \"gzip, deflate, br, zstd\" ], \"accept-language\" : [ \"en-US,en;q=0.9\" ], \"cookie\" : [ \"grafana_session=60c3e7ae41ffc00665f4a2c377def399; grafana_session_expiry=1788497124; _oauth2_proxy=djIuWDI5aGRYUm9NbDl3Y205NGVTMDNZMkZrTUdZM01tRmlZekkwWldFell6a3lOREJpTW1KaE5UZGpOVEJoWWcuZVVmckp5VHRqY1VsXzdiV1hiX3hwdw==|1788503135|yDSo7VdgRSlvoVfj9raHPClKTlQiWDg7FauLfoUayw4=\" ], \"priority\" : [ \"u=0, i\" ], \"sec-ch-ua\" : [ \"\\\"Chromium\\\";v=\\\"152\\\", \\\"Not?A_Brand\\\";v=\\\"24\\\", \\\"Google Chrome\\\";v=\\\"152\\\"\" ], \"sec-ch-ua-mobile\" : [ \"?0\" ], \"sec-ch-ua-platform\" : [ \"\\\"Linux\\\"\" ], \"sec-fetch-dest\" : [ \"document\" ], \"sec-fetch-mode\" : [ \"navigate\" ], \"sec-fetch-site\" : [ \"none\" ], \"sec-fetch-user\" : [ \"?1\" ], \"upgrade-insecure-requests\" : [ \"1\" ], \"x-forwarded-email\" : [ \"labuser@example.com\" ], \"x-forwarded-host\" : [ \"app2.hyeonworks.com\" ], \"x-forwarded-port\" : [ \"443\" ], \"x-forwarded-preferred-username\" : [ \"labuser\" ], \"x-forwarded-proto\" : [ \"https\" ], \"x-forwarded-server\" : [ \"traefik-5d6fcf895-wpfhr\" ], \"x-forwarded-user\" : [ \"27df5ea9-8703-4ec5-badd-d972c583e1ff\" ], \"x-real-ip\" : [ \"100.123.124.30\" ] }, \"remoteAddr\" : \"100.123.124.30\", \"localAddr\" : \"10.42.0.53\", \"scheme\" : \"https\", \"secure\" : true, \"serverName\" : \"app2.hyeonworks.com\", \"serverPort\" : 443, \"requestUrl\" : \"https://app2.hyeonworks.com/api/echo\" }"
@@ -0,0 +1 @@
- generic [ref=f58e1]: "{ \"headers\" : { \"host\" : [ \"app2.hyeonworks.com\" ], \"user-agent\" : [ \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36\" ], \"accept\" : [ \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\" ], \"accept-encoding\" : [ \"gzip, deflate, br, zstd\" ], \"accept-language\" : [ \"en-US,en;q=0.9\" ], \"cookie\" : [ \"grafana_session=60c3e7ae41ffc00665f4a2c377def399; grafana_session_expiry=1788497124; _oauth2_proxy=djIuWDI5aGRYUm9NbDl3Y205NGVTMHpabUV5TVdKaVpEa3lOemRrTlRFMU9USTBaV00wWWpGaE16bGhNak0zT1EuN2tTa3dnWUdISDkwMGFSSTVOSUFFUQ==|1788503202|snWKU5IRfRLoD9-bXEodGjEgfHeAw8PQaoHnecpFH90=\" ], \"priority\" : [ \"u=0, i\" ], \"sec-ch-ua\" : [ \"\\\"Chromium\\\";v=\\\"152\\\", \\\"Not?A_Brand\\\";v=\\\"24\\\", \\\"Google Chrome\\\";v=\\\"152\\\"\" ], \"sec-ch-ua-mobile\" : [ \"?0\" ], \"sec-ch-ua-platform\" : [ \"\\\"Linux\\\"\" ], \"sec-fetch-dest\" : [ \"document\" ], \"sec-fetch-mode\" : [ \"navigate\" ], \"sec-fetch-site\" : [ \"none\" ], \"sec-fetch-user\" : [ \"?1\" ], \"upgrade-insecure-requests\" : [ \"1\" ], \"x-forwarded-email\" : [ \"changed-labuser@example.com\" ], \"x-forwarded-host\" : [ \"app2.hyeonworks.com\" ], \"x-forwarded-port\" : [ \"443\" ], \"x-forwarded-preferred-username\" : [ \"labuser\" ], \"x-forwarded-proto\" : [ \"https\" ], \"x-forwarded-server\" : [ \"traefik-5d6fcf895-wpfhr\" ], \"x-forwarded-user\" : [ \"27df5ea9-8703-4ec5-badd-d972c583e1ff\" ], \"x-real-ip\" : [ \"100.123.124.30\" ] }, \"remoteAddr\" : \"100.123.124.30\", \"localAddr\" : \"10.42.1.132\", \"scheme\" : \"https\", \"secure\" : true, \"serverName\" : \"app2.hyeonworks.com\", \"serverPort\" : 443, \"requestUrl\" : \"https://app2.hyeonworks.com/api/echo\" }"
@@ -0,0 +1,175 @@
- generic [ref=f59e1]:
- generic [ref=f59e4]:
- link "Skip to main content" [ref=f59e5] [cursor=pointer]:
- /url: "#pageContent"
- banner [ref=f59e7]:
- generic [ref=f59e8]:
- link [ref=f59e10] [cursor=pointer]:
- /url: /
- img "Grafana" [ref=f59e11]
- generic [ref=f59e14]:
- button "Search or jump to..." [ref=f59e18] [cursor=pointer]
- generic [ref=f59e19]: ctrl+k
- generic [ref=f59e23]:
- button "New" [ref=f59e24] [cursor=pointer]
- button "Help" [ref=f59e30] [cursor=pointer]
- button "News" [ref=f59e33] [cursor=pointer]
- button "Profile" [ref=f59e36] [cursor=pointer]:
- img "User avatar" [ref=f59e37]
- generic [ref=f59e38]:
- button "Open menu" [ref=f59e40] [cursor=pointer]
- navigation "Breadcrumbs" [ref=f59e43]:
- list [ref=f59e44]:
- listitem [ref=f59e45]:
- link "Home" [ref=f59e46] [cursor=pointer]:
- /url: /
- listitem [ref=f59e50]:
- link "Explore" [ref=f59e51] [cursor=pointer]:
- /url: /explore
- listitem [ref=f59e55]:
- generic "Prometheus" [ref=f59e56]
- generic [ref=f59e57]:
- generic [ref=f59e60]:
- button "Copy shortened URL" [ref=f59e61] [cursor=pointer]
- button "Open copy link options" [ref=f59e64] [cursor=pointer]
- button "Toggle top search bar" [ref=f59e68] [cursor=pointer]
- main [ref=f59e74]:
- generic [ref=f59e76]:
- heading "Explore" [level=1] [ref=f59e77]
- generic [ref=f59e82]:
- navigation "Explore toolbar" [ref=f59e84]:
- navigation "Search links" [ref=f59e86]:
- generic [ref=f59e87]:
- button "Content outline" [expanded] [ref=f59e89] [cursor=pointer]:
- generic [ref=f59e92]: Outline
- generic [ref=f59e97] [cursor=pointer]:
- img "Prometheus logo" [ref=f59e99]
- textbox "Select a data source" [ref=f59e100]:
- /placeholder: Prometheus
- generic [ref=f59e104]:
- button "Split the pane" [ref=f59e106] [cursor=pointer]:
- generic [ref=f59e109]: Split
- button "Add" [ref=f59e111] [cursor=pointer]
- generic [ref=f59e116]:
- 'button "Time range selected: Last 30 minutes" [ref=f59e117] [cursor=pointer]'
- button "Zoom out time range" [ref=f59e122] [cursor=pointer]
- generic [ref=f59e126]:
- button "Cancel" [ref=f59e127] [cursor=pointer]
- button "Auto refresh turned off. Choose refresh time interval" [ref=f59e129] [cursor=pointer]
- generic [ref=f59e133]:
- generic [ref=f59e137]:
- button "Collapse outline" [expanded] [ref=f59e139] [cursor=pointer]:
- img "arrow-from-right" [ref=f59e140]
- generic [ref=f59e142]:
- button "Content outline item collapse button" [ref=f59e143] [cursor=pointer]:
- img "angle-right" [ref=f59e144]
- button "Queries" [ref=f59e146] [cursor=pointer]:
- img "arrow" [ref=f59e147]
- generic [ref=f59e154]:
- generic [ref=f59e156]:
- generic [ref=f59e157]:
- generic "Query editor row" [ref=f59e159]:
- generic [ref=f59e160]:
- generic [ref=f59e162]:
- generic [ref=f59e163]:
- button "Collapse query row" [expanded] [ref=f59e164] [cursor=pointer]
- generic [ref=f59e167]:
- button "Query editor row title A" [ref=f59e168] [cursor=pointer]:
- generic [ref=f59e169]: A
- emphasis [ref=f59e170]: (Prometheus)
- generic [ref=f59e171]:
- button "Show data source help" [ref=f59e173] [cursor=pointer]
- button "Duplicate query" [ref=f59e177] [cursor=pointer]
- button "Hide response" [ref=f59e181] [cursor=pointer]
- button "Remove query" [ref=f59e185] [cursor=pointer]
- button "Drag and drop to reorder" [ref=f59e188]:
- img "Drag and drop to reorder" [ref=f59e189]
- generic [ref=f59e192]:
- generic [ref=f59e193]:
- button "Kick start your query" [ref=f59e194] [cursor=pointer]
- generic [ref=f59e197]:
- generic [ref=f59e198] [cursor=pointer]: Explain
- generic [ref=f59e199]:
- checkbox "Explain Toggle switch" [ref=f59e200]
- generic "Toggle switch" [ref=f59e201] [cursor=pointer]
- radiogroup [ref=f59e206]:
- generic [ref=f59e207]:
- radio "Builder" [ref=f59e208] [cursor=pointer]
- generic [ref=f59e209] [cursor=pointer]: Builder
- generic [ref=f59e210]:
- radio "Code" [checked] [ref=f59e211] [cursor=pointer]
- generic [ref=f59e212] [cursor=pointer]: Code
- generic [ref=f59e214]:
- generic [ref=f59e216]:
- button "Loading metrics..." [disabled] [ref=f59e217] [cursor=pointer]
- code [ref=f59e224]:
- generic [ref=f59e225]:
- generic [ref=f59e230]: vendor_cluster_size
- textbox "Editor content;Press Alt+F1 for Accessibility Options." [ref=f59e235]: vendor_cluster_size
- 'button "Options Legend: cluster_size {{pod}} Format: Time series Step: auto Type: Range Exemplars: false" [ref=f59e241] [cursor=pointer]':
- generic [ref=f59e245]:
- heading "Options" [level=6] [ref=f59e246]
- generic [ref=f59e247]:
- generic [ref=f59e248]: "Legend: cluster_size {{pod}}"
- generic [ref=f59e249]: "Format: Time series"
- generic [ref=f59e250]: "Step: auto"
- generic [ref=f59e251]: "Type: Range"
- generic [ref=f59e252]: "Exemplars: false"
- generic "Query editor row" [ref=f59e254]:
- generic [ref=f59e255]:
- generic [ref=f59e257]:
- generic [ref=f59e258]:
- button "Collapse query row" [expanded] [ref=f59e259] [cursor=pointer]
- generic [ref=f59e262]:
- button "Query editor row title B" [ref=f59e263] [cursor=pointer]:
- generic [ref=f59e264]: B
- emphasis [ref=f59e265]: (Prometheus)
- generic [ref=f59e266]:
- button "Show data source help" [ref=f59e268] [cursor=pointer]
- button "Duplicate query" [ref=f59e272] [cursor=pointer]
- button "Hide response" [ref=f59e276] [cursor=pointer]
- button "Remove query" [ref=f59e280] [cursor=pointer]
- button "Drag and drop to reorder" [ref=f59e283]:
- img "Drag and drop to reorder" [ref=f59e284]
- generic [ref=f59e287]:
- generic [ref=f59e288]:
- button "Kick start your query" [ref=f59e289] [cursor=pointer]
- generic [ref=f59e292]:
- generic [ref=f59e293] [cursor=pointer]: Explain
- generic [ref=f59e294]:
- checkbox "Explain Toggle switch" [ref=f59e295]
- generic "Toggle switch" [ref=f59e296] [cursor=pointer]
- radiogroup [ref=f59e301]:
- generic [ref=f59e302]:
- radio "Builder" [ref=f59e303] [cursor=pointer]
- generic [ref=f59e304] [cursor=pointer]: Builder
- generic [ref=f59e305]:
- radio "Code" [checked] [ref=f59e306] [cursor=pointer]
- generic [ref=f59e307] [cursor=pointer]: Code
- generic [ref=f59e309]:
- generic [ref=f59e311]:
- button "Loading metrics..." [disabled] [ref=f59e312] [cursor=pointer]
- code [ref=f59e319]:
- generic [ref=f59e320]:
- generic [ref=f59e325]: "up{job=\"keycloak\"}"
- textbox "Editor content;Press Alt+F1 for Accessibility Options." [ref=f59e330]: "up{job=\"keycloak\"}"
- 'button "Options Legend: up {{pod}} Format: Time series Step: auto Type: Range Exemplars: false" [ref=f59e336] [cursor=pointer]':
- generic [ref=f59e340]:
- heading "Options" [level=6] [ref=f59e341]
- generic [ref=f59e342]:
- generic [ref=f59e343]: "Legend: up {{pod}}"
- generic [ref=f59e344]: "Format: Time series"
- generic [ref=f59e345]: "Step: auto"
- generic [ref=f59e346]: "Type: Range"
- generic [ref=f59e347]: "Exemplars: false"
- generic [ref=f59e348]:
- button "Add query" [ref=f59e349] [cursor=pointer]
- button "Query history" [ref=f59e353] [cursor=pointer]
- button "Query inspector" [ref=f59e357] [cursor=pointer]
- generic:
- main
- generic [ref=f59e364]:
- alert
- alert
- complementary
- complementary
@@ -0,0 +1,56 @@
- generic [ref=f62e4]:
- link "Skip to main content" [ref=f62e5] [cursor=pointer]:
- /url: "#pageContent"
- banner [ref=f62e7]:
- generic [ref=f62e8]:
- link [ref=f62e10] [cursor=pointer]:
- /url: /
- img "Grafana" [ref=f62e11]
- generic [ref=f62e14]:
- button "Search or jump to..." [ref=f62e18] [cursor=pointer]
- generic [ref=f62e19]: ctrl+k
- generic [ref=f62e23]:
- button "New" [ref=f62e24] [cursor=pointer]
- button "Help" [ref=f62e30] [cursor=pointer]
- button "News" [ref=f62e33] [cursor=pointer]
- button "Profile" [ref=f62e36] [cursor=pointer]:
- img "User avatar" [ref=f62e37]
- generic [ref=f62e38]:
- button "Open menu" [ref=f62e40] [cursor=pointer]
- navigation "Breadcrumbs" [ref=f62e43]:
- list [ref=f62e44]:
- listitem [ref=f62e45]:
- link "Home" [ref=f62e46] [cursor=pointer]:
- /url: /
- listitem [ref=f62e50]:
- link "Explore" [ref=f62e51] [cursor=pointer]:
- /url: /explore
- listitem [ref=f62e55]:
- generic "Prometheus" [ref=f62e56]
- generic [ref=f62e57]:
- button "Show more items" [ref=f62e60] [cursor=pointer]
- button "Toggle top search bar" [ref=f62e64] [cursor=pointer]
- main [ref=f62e70]:
- generic [ref=f62e72]:
- heading "Explore" [level=1] [ref=f62e73]
- generic [ref=f62e78]:
- navigation "Explore toolbar" [ref=f62e80]:
- navigation "Search links" [ref=f62e82]:
- generic [ref=f62e83]:
- button "Content outline" [expanded] [ref=f62e85] [cursor=pointer]:
- generic [ref=f62e88]: Outline
- generic [ref=f62e93] [cursor=pointer]:
- img "Prometheus logo" [ref=f62e95]
- textbox "Select a data source" [ref=f62e96]:
- /placeholder: Prometheus
- button "Show more items" [ref=f62e102] [cursor=pointer]
- generic [ref=f62e106]:
- button "Collapse outline" [expanded] [ref=f62e112] [cursor=pointer]:
- img "arrow-from-right" [ref=f62e113]
- generic [ref=f62e119]:
- generic [ref=f62e122]:
- button "Add query" [ref=f62e123] [cursor=pointer]
- button "Query history" [ref=f62e127] [cursor=pointer]
- button "Query inspector" [ref=f62e131] [cursor=pointer]
- generic:
- main
@@ -0,0 +1,29 @@
- generic [ref=f65e4]:
- link "Skip to main content" [ref=f65e5] [cursor=pointer]:
- /url: "#pageContent"
- banner [ref=f65e7]:
- generic [ref=f65e8]:
- link [ref=f65e10] [cursor=pointer]:
- /url: /
- img "Grafana" [ref=f65e11]
- generic [ref=f65e14]:
- button "Search or jump to..." [ref=f65e18] [cursor=pointer]
- generic [ref=f65e19]: ctrl+k
- generic [ref=f65e23]:
- button "New" [ref=f65e24] [cursor=pointer]
- button "Help" [ref=f65e30] [cursor=pointer]
- button "News" [ref=f65e33] [cursor=pointer]
- button "Profile" [ref=f65e36] [cursor=pointer]:
- img "User avatar" [ref=f65e37]
- generic [ref=f65e38]:
- button "Open menu" [ref=f65e40] [cursor=pointer]
- navigation "Breadcrumbs" [ref=f65e43]:
- list [ref=f65e44]:
- listitem [ref=f65e45]:
- link "Home" [ref=f65e46] [cursor=pointer]:
- /url: /
- listitem [ref=f65e50]:
- generic "Explore" [ref=f65e51]
- button "Toggle top search bar" [ref=f65e53] [cursor=pointer]
- main [ref=f65e59]:
- heading "Explore" [level=1] [ref=f65e62]
@@ -0,0 +1,16 @@
- generic [ref=f68e3]:
- banner [ref=f68e4]:
- generic [ref=f68e5]: keycloak-patterns
- main [ref=f68e6]:
- heading "Sign in to your account" [level=1] [ref=f68e8]
- generic [ref=f68e12]:
- generic [ref=f68e13]:
- generic [ref=f68e14]: Username or email
- textbox "Username or email" [ref=f68e17]
- generic [ref=f68e18]:
- generic [ref=f68e19]: Password
- generic [ref=f68e21]:
- textbox "Password" [ref=f68e24]
- button "Show password" [ref=f68e26] [cursor=pointer]:
- generic [aria-hidden] [ref=f68e27]:
- button "Sign In" [ref=f68e30] [cursor=pointer]
@@ -0,0 +1 @@
- generic [active] [ref=f69e1]: "{ \"headers\" : { \"host\" : [ \"app2.hyeonworks.com\" ], \"user-agent\" : [ \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36\" ], \"accept\" : [ \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\" ], \"accept-encoding\" : [ \"gzip, deflate, br, zstd\" ], \"accept-language\" : [ \"en-US,en;q=0.9\" ], \"cache-control\" : [ \"max-age=0\" ], \"cookie\" : [ \"grafana_session=bfb5f2c2406b3f163ea01063ac1b8b83; grafana_session_expiry=1788508427; _oauth2_proxy=djIuWDI5aGRYUm9NbDl3Y205NGVTMHhNamhoWXprNFpHSmxORGRoT0RObU16UmlNemMxTTJJd016ZGlZakpqTWcuQVVUYldxYkFlejYxNXozUUZBcGh1dw==|1788508274|biz4o9E4vZXX07LnF32LU7tIEMrvfwxwCIBsZkxpwbY=\" ], \"priority\" : [ \"u=0, i\" ], \"sec-ch-ua\" : [ \"\\\"Chromium\\\";v=\\\"152\\\", \\\"Not?A_Brand\\\";v=\\\"24\\\", \\\"Google Chrome\\\";v=\\\"152\\\"\" ], \"sec-ch-ua-mobile\" : [ \"?0\" ], \"sec-ch-ua-platform\" : [ \"\\\"Linux\\\"\" ], \"sec-fetch-dest\" : [ \"document\" ], \"sec-fetch-mode\" : [ \"navigate\" ], \"sec-fetch-site\" : [ \"same-site\" ], \"sec-fetch-user\" : [ \"?1\" ], \"upgrade-insecure-requests\" : [ \"1\" ], \"x-forwarded-email\" : [ \"labuser@example.com\" ], \"x-forwarded-host\" : [ \"app2.hyeonworks.com\" ], \"x-forwarded-port\" : [ \"443\" ], \"x-forwarded-preferred-username\" : [ \"labuser\" ], \"x-forwarded-proto\" : [ \"https\" ], \"x-forwarded-server\" : [ \"traefik-5d6fcf895-wpfhr\" ], \"x-forwarded-user\" : [ \"27df5ea9-8703-4ec5-badd-d972c583e1ff\" ], \"x-real-ip\" : [ \"100.123.124.30\" ] }, \"remoteAddr\" : \"100.123.124.30\", \"localAddr\" : \"10.42.1.132\", \"scheme\" : \"https\", \"secure\" : true, \"serverName\" : \"app2.hyeonworks.com\", \"serverPort\" : 443, \"requestUrl\" : \"https://app2.hyeonworks.com/api/echo\" }"
@@ -0,0 +1 @@
- generic [ref=f70e1]: "{ \"headers\" : { \"host\" : [ \"app2.hyeonworks.com\" ], \"user-agent\" : [ \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36\" ], \"accept\" : [ \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\" ], \"accept-encoding\" : [ \"gzip, deflate, br, zstd\" ], \"accept-language\" : [ \"en-US,en;q=0.9\" ], \"cookie\" : [ \"grafana_session=bfb5f2c2406b3f163ea01063ac1b8b83; grafana_session_expiry=1788508427; _oauth2_proxy=djIuWDI5aGRYUm9NbDl3Y205NGVTMWhOakZqTlRjM1ptSTVORFF5TVdWbU1qQmpaak5pWXpka1pXRTFPR1F6WWcuOXF3TmRyQzJIOFlrSDh6T1pTMl91QQ==|1788508374|Ehcd4hjNk0noEy2MSPI-C5jg84JZmilWcKhuj8OH3uw=\" ], \"priority\" : [ \"u=0, i\" ], \"sec-ch-ua\" : [ \"\\\"Chromium\\\";v=\\\"152\\\", \\\"Not?A_Brand\\\";v=\\\"24\\\", \\\"Google Chrome\\\";v=\\\"152\\\"\" ], \"sec-ch-ua-mobile\" : [ \"?0\" ], \"sec-ch-ua-platform\" : [ \"\\\"Linux\\\"\" ], \"sec-fetch-dest\" : [ \"document\" ], \"sec-fetch-mode\" : [ \"navigate\" ], \"sec-fetch-site\" : [ \"none\" ], \"sec-fetch-user\" : [ \"?1\" ], \"upgrade-insecure-requests\" : [ \"1\" ], \"x-forwarded-email\" : [ \"changed-labuser@example.com\" ], \"x-forwarded-host\" : [ \"app2.hyeonworks.com\" ], \"x-forwarded-port\" : [ \"443\" ], \"x-forwarded-preferred-username\" : [ \"labuser\" ], \"x-forwarded-proto\" : [ \"https\" ], \"x-forwarded-server\" : [ \"traefik-5d6fcf895-wpfhr\" ], \"x-forwarded-user\" : [ \"27df5ea9-8703-4ec5-badd-d972c583e1ff\" ], \"x-real-ip\" : [ \"100.123.124.30\" ] }, \"remoteAddr\" : \"100.123.124.30\", \"localAddr\" : \"10.42.0.53\", \"scheme\" : \"https\", \"secure\" : true, \"serverName\" : \"app2.hyeonworks.com\", \"serverPort\" : 443, \"requestUrl\" : \"https://app2.hyeonworks.com/api/echo\" }"
+12
View File
@@ -0,0 +1,12 @@
# 다이어그램 규약
| 표현 | 뜻 |
|---|---|
| 실선 상자 | 살아 있는 구성 요소 |
| 붉은 점선 상자 | 이 실험에서 죽이거나 막은 것 |
| ✂ 붉은 X | 주입 지점 |
| 실선 화살표 | 정상 경로 |
| 붉은 점선 화살표 | 실험에서 깨진 경로 |
| 회색 글씨 | 측정값 |
SVG 는 GitHub 에서 그대로 렌더링되며 외부 폰트를 쓰지 않는다.
+26
View File
@@ -0,0 +1,26 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 300" width="700" height="300" font-family="system-ui,-apple-system,Segoe UI,Roboto,sans-serif">
<style>
.t{font-size:13px;fill:#1f2328}.s{font-size:11px;fill:#59636e}.h{font-size:14px;font-weight:600;fill:#1f2328}
.r{font-size:12px;fill:#cf222e;font-weight:600}
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.ln{stroke:#59636e;stroke-width:1.4;fill:none;marker-end:url(#a)}
.bad{stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;fill:none;marker-end:url(#b)}
</style>
<defs>
<marker id="a" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#59636e"/></marker>
<marker id="b" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#cf222e"/></marker>
</defs>
<text class="h" x="16" y="24">A-2 · PostgreSQL 정지 — 살아남는 노드가 없다</text>
<rect class="box" x="30" y="46" width="180" height="52"/><text class="t" x="120" y="68" text-anchor="middle">keycloak-1</text><text class="s" x="120" y="86" text-anchor="middle">캐시: 세션 N개</text>
<rect class="box" x="480" y="46" width="180" height="52"/><text class="t" x="570" y="68" text-anchor="middle">keycloak-0</text><text class="s" x="570" y="86" text-anchor="middle">캐시: 세션 M개</text>
<path class="ln" d="M210,72 L480,72"/><text class="s" x="345" y="66" text-anchor="middle">7800 · 살아 있다</text>
<path class="bad" d="M120,100 L300,150"/><path class="bad" d="M570,100 L400,150"/>
<rect class="dead" x="270" y="156" width="160" height="52"/>
<text class="r" x="350" y="178" text-anchor="middle">postgres ✗</text><text class="s" x="350" y="196" text-anchor="middle">replicas=0</text>
<rect class="dead" x="30" y="228" width="290" height="52"/>
<text class="r" x="175" y="250" text-anchor="middle">양쪽 모두 NotReady</text><text class="s" x="175" y="268" text-anchor="middle">ready 주소 = [] · 외부 503</text>
<rect class="box" x="370" y="228" width="290" height="52"/>
<text class="t" x="515" y="250" text-anchor="middle">up{job="keycloak"} = 1</text><text class="s" x="515" y="268" text-anchor="middle">프로세스는 살아 있다 — up 은 못 잡는다</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+36
View File
@@ -0,0 +1,36 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 330" width="720" height="330" font-family="system-ui,-apple-system,Segoe UI,Roboto,sans-serif">
<style>
.t{font-size:13px;fill:#1f2328}.s{font-size:11px;fill:#59636e}.h{font-size:14px;font-weight:600;fill:#1f2328}
.r{font-size:12px;fill:#cf222e;font-weight:600}.g{font-size:12px;fill:#1a7f37;font-weight:600}
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ln{stroke:#59636e;stroke-width:1.4;fill:none;marker-end:url(#a)}
</style>
<defs><marker id="a" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#59636e"/></marker></defs>
<text class="h" x="16" y="24">A-7 · 같은 주입, 같은 관측, 정반대 결과</text>
<text class="h" x="180" y="52" text-anchor="middle">persistent (KC 26 기본)</text>
<rect class="box" x="30" y="62" width="300" height="54"/>
<text class="t" x="180" y="84" text-anchor="middle">keycloak ×2 — 로컬 캐시</text>
<path class="ln" d="M180,116 L180,140"/>
<rect class="ok" x="70" y="142" width="220" height="40"/>
<text class="g" x="180" y="167" text-anchor="middle">PostgreSQL — 진실의 원천</text>
<text class="h" x="540" y="52" text-anchor="middle">volatile (KC 24 이전 방식)</text>
<rect class="box" x="390" y="62" width="300" height="54"/>
<text class="t" x="540" y="84" text-anchor="middle">keycloak ×2 — 캐시가 곧 진실</text>
<path class="ln" d="M470,116 L470,140"/><path class="ln" d="M610,140 L610,116"/>
<rect class="ok" x="430" y="142" width="220" height="40"/>
<text class="g" x="540" y="167" text-anchor="middle">클러스터 복제 (7800)</text>
<rect class="box" x="30" y="202" width="660" height="112"/>
<text class="t" x="360" y="224" text-anchor="middle">뒤집힌 세 결과</text>
<text class="s" x="200" y="248" text-anchor="middle">A-1 7800 차단 후 교차 refresh</text>
<text class="g" x="430" y="248" text-anchor="middle">200</text><text class="r" x="560" y="248" text-anchor="middle">400 Session not active</text>
<text class="s" x="200" y="272" text-anchor="middle">A-8 롤링 재시작 후 refresh</text>
<text class="g" x="430" y="272" text-anchor="middle">200</text><text class="r" x="560" y="272" text-anchor="middle">400 Session not active</text>
<text class="s" x="200" y="296" text-anchor="middle">A-2 DB 정지 중 새 로그인</text>
<text class="r" x="430" y="296" text-anchor="middle">500</text><text class="g" x="560" y="296" text-anchor="middle">200</text>
<text class="s" x="430" y="230" text-anchor="middle">persistent</text><text class="s" x="560" y="230" text-anchor="middle">volatile</text>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

+33
View File
@@ -0,0 +1,33 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 330" width="700" height="330" font-family="system-ui,-apple-system,Segoe UI,Roboto,sans-serif">
<style>
.t{font-size:13px;fill:#1f2328}.s{font-size:11px;fill:#59636e}.h{font-size:14px;font-weight:600;fill:#1f2328}
.r{font-size:12px;fill:#cf222e;font-weight:600}
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.ln{stroke:#59636e;stroke-width:1.4;fill:none;marker-end:url(#a)}
.bad{stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;fill:none;marker-end:url(#b)}
</style>
<defs>
<marker id="a" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#59636e"/></marker>
<marker id="b" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#cf222e"/></marker>
</defs>
<text class="h" x="16" y="24">B-1 · Redis 는 세션만 옮기고 토큰은 두고 간다</text>
<rect class="box" x="30" y="46" width="140" height="46"/><text class="t" x="100" y="66" text-anchor="middle">bff-0</text><text class="s" x="100" y="82" text-anchor="middle">kc-lab-1</text>
<rect class="box" x="30" y="104" width="140" height="46"/><text class="t" x="100" y="124" text-anchor="middle">bff-1</text><text class="s" x="100" y="140" text-anchor="middle">kc-lab-2</text>
<rect class="ok" x="330" y="46" width="330" height="60"/>
<text class="t" x="495" y="68" text-anchor="middle">Redis — Application Session</text>
<text class="s" x="495" y="86" text-anchor="middle">sessionRepository → RedisSessionRepository ✔ 옮겨졌다</text>
<text class="s" x="495" y="100" text-anchor="middle">필드: SPRING_SECURITY_CONTEXT · TTL 1772초</text>
<rect class="dead" x="330" y="122" width="330" height="60"/>
<text class="r" x="495" y="144" text-anchor="middle">프로세스 메모리 — OAuth2AuthorizedClient</text>
<text class="s" x="495" y="162" text-anchor="middle">InMemoryOAuth2AuthorizedClientService ✗ 그대로</text>
<text class="s" x="495" y="176" text-anchor="middle">access token · refresh token 이 여기 있다</text>
<path class="ln" d="M170,69 L330,69"/><path class="ln" d="M170,127 L330,80"/>
<path class="bad" d="M170,140 L330,150"/>
<rect class="box" x="30" y="210" width="630" height="90"/>
<text class="t" x="345" y="234" text-anchor="middle">그 결과 사용자에게 보이는 것</text>
<text class="s" x="345" y="256" text-anchor="middle">principal: labuser ← 로그인은 되어 있다</text>
<text class="s" x="345" y="272" text-anchor="middle">accessTokenStoredOnServer: false ← 토큰이 없다</text>
<text class="r" x="345" y="292" text-anchor="middle">완전히 로그아웃되는 편이 차라리 낫다</text>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

+29
View File
@@ -0,0 +1,29 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 300" width="700" height="300" font-family="system-ui,-apple-system,Segoe UI,Roboto,sans-serif">
<style>
.t{font-size:13px;fill:#1f2328}.s{font-size:11px;fill:#59636e}.h{font-size:14px;font-weight:600;fill:#1f2328}
.r{font-size:12px;fill:#cf222e;font-weight:600}
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.ln{stroke:#59636e;stroke-width:1.4;fill:none;marker-end:url(#a)}
.bad{stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;fill:none;marker-end:url(#b)}
</style>
<defs>
<marker id="a" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#59636e"/></marker>
<marker id="b" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#cf222e"/></marker>
</defs>
<text class="h" x="16" y="24">B-3 · 동시 refresh — 경쟁이 아니라 세션 파괴</text>
<rect class="box" x="30" y="46" width="150" height="40"/><text class="t" x="105" y="71" text-anchor="middle">같은 refresh token ×5</text>
<path class="ln" d="M180,66 L280,66"/>
<rect class="box" x="280" y="42" width="180" height="48"/><text class="t" x="370" y="62" text-anchor="middle">Keycloak</text>
<text class="s" x="370" y="80" text-anchor="middle">rotation ON · maxReuse=0</text>
<path class="ln" d="M460,58 L560,58"/><rect class="box" x="560" y="42" width="110" height="24"/><text class="s" x="615" y="58" text-anchor="middle">1× HTTP 200</text>
<path class="bad" d="M460,78 L560,78"/><rect class="dead" x="560" y="66" width="110" height="24"/><text class="s" x="615" y="82" text-anchor="middle">4× HTTP 400</text>
<rect class="dead" x="120" y="120" width="460" height="70"/>
<text class="r" x="350" y="144" text-anchor="middle">재사용 탐지가 client session 을 제거한다</text>
<text class="s" x="350" y="164" text-anchor="middle">user_session 은 남고 client_session = 0 (정상 세션은 1)</text>
<text class="s" x="350" y="180" text-anchor="middle">그래서 오류가 "Session doesn't have required client"</text>
<rect class="dead" x="120" y="210" width="460" height="60"/>
<text class="r" x="350" y="234" text-anchor="middle">★ 이긴 요청의 새 토큰도 곧바로 400</text>
<text class="s" x="350" y="254" text-anchor="middle">재시도로 회복 불가 → Q2 의 판정은 lock</text>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+37
View File
@@ -0,0 +1,37 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 290" width="700" height="290" font-family="system-ui,-apple-system,Segoe UI,Roboto,sans-serif">
<style>
.t{font-size:13px;fill:#1f2328}.s{font-size:11px;fill:#59636e}.h{font-size:14px;font-weight:600;fill:#1f2328}
.r{font-size:12px;fill:#cf222e;font-weight:600}.g{font-size:12px;fill:#1a7f37;font-weight:600}
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.ln{stroke:#59636e;stroke-width:1.4;fill:none;marker-end:url(#a)}
.bad{stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;fill:none;marker-end:url(#b)}
</style>
<defs>
<marker id="a" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#59636e"/></marker>
<marker id="b" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#cf222e"/></marker>
</defs>
<text class="h" x="16" y="24">B-4 · edge 가 설정하지 않은 헤더는 그대로 통과한다</text>
<rect class="box" x="30" y="46" width="130" height="60"/><text class="t" x="95" y="68" text-anchor="middle">공격자</text>
<text class="s" x="95" y="86" text-anchor="middle">X-Auth-Request-</text><text class="s" x="95" y="100" text-anchor="middle">Roles: admin</text>
<path class="ln" d="M160,76 L250,76"/>
<rect class="box" x="250" y="40" width="180" height="72"/><text class="t" x="340" y="62" text-anchor="middle">nginx</text>
<text class="s" x="340" y="82" text-anchor="middle">proxy_set_header 한 것만 덮어쓴다</text>
<text class="r" x="340" y="100" text-anchor="middle">X-Auth-Request-* 는 설정이 없다</text>
<path class="bad" d="M430,76 L520,76"/>
<rect class="dead" x="520" y="46" width="150" height="60"/>
<text class="r" x="595" y="68" text-anchor="middle">upstream</text>
<text class="s" x="595" y="86" text-anchor="middle">['viewer','admin']</text><text class="s" x="595" y="100" text-anchor="middle">둘 다 도착 · 검증 없음</text>
<rect class="box" x="30" y="134" width="310" height="66"/>
<text class="t" x="185" y="156" text-anchor="middle">구분자 문제</text>
<text class="s" x="185" y="176" text-anchor="middle">"admin,editor" 와 "role-with,comma" 가</text>
<text class="s" x="185" y="192" text-anchor="middle">도착 시점에 구별되지 않는다</text>
<rect class="box" x="360" y="134" width="310" height="66"/>
<text class="t" x="515" y="156" text-anchor="middle">크기는 절벽이다</text>
<text class="s" x="515" y="176" text-anchor="middle">4KB 통과 · 8KB → Tomcat 400</text>
<text class="s" x="515" y="192" text-anchor="middle">16KB → 연결 끊김 (nginx)</text>
<rect class="dead" x="30" y="222" width="640" height="50"/>
<text class="r" x="350" y="244" text-anchor="middle">헤더가 인가 근거가 되면 위조 가능성이 곧 권한 상승이다</text>
<text class="s" x="350" y="262" text-anchor="middle">Q4 의 5문항 중 2·4번 해당 → Q4 자신의 기준으로 BFF 구조</text>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

+35
View File
@@ -0,0 +1,35 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 300" width="700" height="300" font-family="system-ui,-apple-system,Segoe UI,Roboto,sans-serif">
<style>
.t{font-size:13px;fill:#1f2328}.s{font-size:11px;fill:#59636e}.h{font-size:14px;font-weight:600;fill:#1f2328}
.r{font-size:12px;fill:#cf222e;font-weight:600}.g{font-size:12px;fill:#1a7f37;font-weight:600}
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.ln{stroke:#59636e;stroke-width:1.4;fill:none;marker-end:url(#a)}
.bad{stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;fill:none;marker-end:url(#b)}
</style>
<defs>
<marker id="a" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#59636e"/></marker>
<marker id="b" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#cf222e"/></marker>
</defs>
<text class="h" x="16" y="24">B-5 · 파드가 Ready 인 채로 계속 실패한다</text>
<rect class="box" x="30" y="46" width="180" height="76"/>
<text class="t" x="120" y="68" text-anchor="middle">bff ×2</text>
<text class="g" x="120" y="90" text-anchor="middle">Ready = true</text>
<text class="s" x="120" y="110" text-anchor="middle">Service 에 그대로 남는다</text>
<path class="bad" d="M210,84 L330,84"/>
<rect class="dead" x="330" y="58" width="160" height="52"/>
<text class="r" x="410" y="80" text-anchor="middle">redis ✗</text><text class="s" x="410" y="98" text-anchor="middle">replicas=0</text>
<rect class="box" x="30" y="146" width="310" height="110"/>
<text class="t" x="185" y="168" text-anchor="middle">health group 이 갈랐다</text>
<text class="r" x="185" y="192" text-anchor="middle">/actuator/health → 503</text>
<text class="g" x="185" y="214" text-anchor="middle">/actuator/health/readiness → 200 UP</text>
<text class="s" x="185" y="238" text-anchor="middle">redis 지표가 readiness 그룹에 없다</text>
<rect class="box" x="360" y="146" width="310" height="110"/>
<text class="t" x="515" y="168" text-anchor="middle">A-2 와 정반대</text>
<text class="s" x="515" y="192" text-anchor="middle">A-2 Keycloak: DB 검사가 readiness 에</text>
<text class="s" x="515" y="208" text-anchor="middle">→ NotReady → 503 (명확)</text>
<text class="s" x="515" y="230" text-anchor="middle">B-5 BFF: 없음 → Ready 유지</text>
<text class="r" x="515" y="248" text-anchor="middle">→ HTTP 000 (멈춤)</text>
<text class="s" x="16" y="284">영속화: 볼륨 없이 AOF 만 켜면 appendonlydir 은 생기지만 파드 삭제로 전부 사라진다 — 볼륨이 먼저다</text>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

+34
View File
@@ -0,0 +1,34 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 260" width="700" height="260" font-family="system-ui,-apple-system,Segoe UI,Roboto,sans-serif">
<style>
.t{font-size:13px;fill:#1f2328}.s{font-size:11px;fill:#59636e}.h{font-size:14px;font-weight:600;fill:#1f2328}
.r{font-size:12px;fill:#cf222e;font-weight:600}.g{font-size:12px;fill:#1a7f37;font-weight:600}
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.ln{stroke:#59636e;stroke-width:1.4;fill:none;marker-end:url(#a)}
.bad{stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;fill:none;marker-end:url(#b)}
</style>
<defs>
<marker id="a" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#59636e"/></marker>
<marker id="b" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#cf222e"/></marker>
</defs>
<text class="h" x="16" y="24">B-6 · 회전은 안전하고 옛 키를 버리는 순간이 위험하다</text>
<rect class="ok" x="30" y="46" width="190" height="76"/>
<text class="t" x="125" y="68" text-anchor="middle">t0 — 키 A 만</text>
<text class="s" x="125" y="88" text-anchor="middle">발급 A · 검증 A</text>
<text class="s" x="125" y="108" text-anchor="middle">JWKS RS256 1개</text>
<path class="ln" d="M220,84 L255,84"/>
<rect class="ok" x="255" y="46" width="190" height="76"/>
<text class="t" x="350" y="68" text-anchor="middle">t1 — B 추가 (priority 200)</text>
<text class="g" x="350" y="88" text-anchor="middle">발급 B · 검증 A+B</text>
<text class="s" x="350" y="108" text-anchor="middle">옛 토큰 200 · 새 토큰 200</text>
<path class="bad" d="M445,84 L480,84"/>
<rect class="dead" x="480" y="46" width="190" height="76"/>
<text class="t" x="575" y="68" text-anchor="middle">t2 — A 제거</text>
<text class="r" x="575" y="88" text-anchor="middle">옛 토큰 즉시 401</text>
<text class="s" x="575" y="108" text-anchor="middle">캐시가 유예를 주지 않는다</text>
<rect class="box" x="30" y="146" width="640" height="60"/>
<text class="t" x="350" y="168" text-anchor="middle">겹침 구간(t1~t2)의 최소 길이 = 옛 키로 서명된 것 중 가장 오래 사는 것의 수명</text>
<text class="s" x="350" y="190" text-anchor="middle">access token 60초 · refresh token 1800초 → 최소 30분</text>
<text class="s" x="16" y="234">모르는 kid 를 만나면 JWKS 를 다시 받으므로 제거가 즉시 반영된다. 유예는 옛 키를 남겨두는 기간으로 만든다.</text>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

+31
View File
@@ -0,0 +1,31 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 300" width="700" height="300" font-family="system-ui,-apple-system,Segoe UI,Roboto,sans-serif">
<style>
.t{font-size:13px;fill:#1f2328}.s{font-size:11px;fill:#59636e}.h{font-size:14px;font-weight:600;fill:#1f2328}
.r{font-size:12px;fill:#cf222e;font-weight:600}.g{font-size:12px;fill:#1a7f37;font-weight:600}
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.ln{stroke:#59636e;stroke-width:1.4;fill:none;marker-end:url(#a)}
.bad{stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;fill:none;marker-end:url(#b)}
</style>
<defs>
<marker id="a" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#59636e"/></marker>
<marker id="b" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#cf222e"/></marker>
</defs>
<text class="h" x="16" y="24">C-1 · SSO 의 구조와 IdP 로그아웃의 한계</text>
<rect class="box" x="220" y="42" width="260" height="52"/>
<text class="t" x="350" y="64" text-anchor="middle">Keycloak user session ×1</text>
<text class="s" x="350" y="82" text-anchor="middle">oqOjHekin4JU-BZjgQLjUByW</text>
<path class="ln" d="M300,96 L180,130"/><path class="ln" d="M400,96 L520,130"/>
<rect class="box" x="60" y="134" width="240" height="52"/>
<text class="t" x="180" y="156" text-anchor="middle">client session — bff-confidential</text>
<text class="s" x="180" y="174" text-anchor="middle">app1 · Redis 세션 + PostgreSQL 토큰</text>
<rect class="box" x="400" y="134" width="240" height="52"/>
<text class="t" x="520" y="156" text-anchor="middle">client session — oauth2-proxy</text>
<text class="s" x="520" y="174" text-anchor="middle">app2 · 쿠키 티켓 + Redis 세션</text>
<rect class="dead" x="220" y="206" width="260" height="40"/>
<text class="r" x="350" y="231" text-anchor="middle">IdP 세션 삭제 ✗</text>
<path class="bad" d="M300,246 L200,262"/><path class="bad" d="M400,246 L500,262"/>
<rect class="ok" x="60" y="256" width="240" height="34"/><text class="g" x="180" y="278" text-anchor="middle">app1 그대로 동작</text>
<rect class="ok" x="400" y="256" width="240" height="34"/><text class="g" x="520" y="278" text-anchor="middle">app2 그대로 동작</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+30
View File
@@ -0,0 +1,30 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 270" width="700" height="270" font-family="system-ui,-apple-system,Segoe UI,Roboto,sans-serif">
<style>
.t{font-size:13px;fill:#1f2328}.s{font-size:11px;fill:#59636e}.h{font-size:14px;font-weight:600;fill:#1f2328}
.r{font-size:12px;fill:#cf222e;font-weight:600}.g{font-size:12px;fill:#1a7f37;font-weight:600}
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.ln{stroke:#59636e;stroke-width:1.4;fill:none;marker-end:url(#a)}
.bad{stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;fill:none;marker-end:url(#b)}
</style>
<defs>
<marker id="a" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#59636e"/></marker>
<marker id="b" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#cf222e"/></marker>
</defs>
<text class="h" x="16" y="24">D-1 · 빈 데이터베이스가 200 을 냈다</text>
<rect class="box" x="30" y="46" width="180" height="46"/><text class="t" x="120" y="66" text-anchor="middle">pg_dump</text><text class="s" x="120" y="82" text-anchor="middle">395KB · 101 테이블 · 세션 포함</text>
<path class="ln" d="M210,69 L270,69"/>
<rect class="dead" x="270" y="42" width="180" height="54"/>
<text class="r" x="360" y="64" text-anchor="middle">DROP SCHEMA CASCADE</text><text class="s" x="360" y="84" text-anchor="middle">남은 테이블 0</text>
<path class="ln" d="M450,69 L510,69"/>
<rect class="ok" x="510" y="42" width="160" height="54"/>
<text class="g" x="590" y="64" text-anchor="middle">복구 1초</text><text class="s" x="590" y="84" text-anchor="middle">오류 0건 · 재시작 0회</text>
<rect class="box" x="30" y="118" width="640" height="76"/>
<text class="t" x="350" y="140" text-anchor="middle">테이블이 0개일 때 무엇이 깨졌는가 — 전부가 아니다</text>
<text class="g" x="350" y="162" text-anchor="middle">/protocol/openid-connect/certs → 200 (realm 키가 캐시에 있다)</text>
<text class="r" x="350" y="182" text-anchor="middle">/.well-known → 500 토큰 발급 → 400</text>
<rect class="dead" x="30" y="210" width="640" height="46"/>
<text class="r" x="350" y="232" text-anchor="middle">헬스체크는 "DB 가 살아 있다"만 보고 "데이터가 있다"는 안 본다</text>
<text class="s" x="350" y="250" text-anchor="middle">RPO = 백업 주기 + A-3 의 synchronous_commit 손실 · 덤프는 같은 호스트 /tmp 에 있었다</text>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

+31
View File
@@ -0,0 +1,31 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 280" width="700" height="280" font-family="system-ui,-apple-system,Segoe UI,Roboto,sans-serif">
<style>
.t{font-size:13px;fill:#1f2328}.s{font-size:11px;fill:#59636e}.h{font-size:14px;font-weight:600;fill:#1f2328}
.r{font-size:12px;fill:#cf222e;font-weight:600}.g{font-size:12px;fill:#1a7f37;font-weight:600}
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.ln{stroke:#59636e;stroke-width:1.4;fill:none;marker-end:url(#a)}
.bad{stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;fill:none;marker-end:url(#b)}
</style>
<defs>
<marker id="a" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#59636e"/></marker>
<marker id="b" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#cf222e"/></marker>
</defs>
<text class="h" x="16" y="24">D-3 · 네 경로 중 RBAC 만 실제로 감춘다</text>
<rect class="dead" x="30" y="46" width="310" height="60"/>
<text class="r" x="185" y="68" text-anchor="middle">kubectl get -o jsonpath | base64 -d</text>
<text class="s" x="185" y="88" text-anchor="middle">POSTGRES_PASSWORD = lab-postgres-change-me</text>
<rect class="dead" x="360" y="46" width="310" height="60"/>
<text class="r" x="515" y="68" text-anchor="middle">저장소 (at rest)</text>
<text class="s" x="515" y="88" text-anchor="middle">Encryption Disabled · state.db 에 평문</text>
<rect class="dead" x="30" y="120" width="310" height="60"/>
<text class="r" x="185" y="142" text-anchor="middle">파드 안</text>
<text class="s" x="185" y="162" text-anchor="middle">KEYCLOAK_CLIENT_SECRET=... 환경변수</text>
<rect class="ok" x="360" y="120" width="310" height="60"/>
<text class="g" x="515" y="142" text-anchor="middle">RBAC</text>
<text class="s" x="515" y="162" text-anchor="middle">default SA 는 get secrets 불가</text>
<rect class="box" x="30" y="200" width="640" height="60"/>
<text class="t" x="350" y="222" text-anchor="middle">describe 는 "14 bytes" 만 보여줘 감춰졌다는 착각을 준다</text>
<text class="s" x="350" y="244" text-anchor="middle">base64 는 감추기 위한 것이 아니라 YAML 에 임의 바이트를 담기 위한 인코딩이다</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+30
View File
@@ -0,0 +1,30 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 250" width="700" height="250" font-family="system-ui,-apple-system,Segoe UI,Roboto,sans-serif">
<style>
.t{font-size:13px;fill:#1f2328}.s{font-size:11px;fill:#59636e}.h{font-size:14px;font-weight:600;fill:#1f2328}
.r{font-size:12px;fill:#cf222e;font-weight:600}.g{font-size:12px;fill:#1a7f37;font-weight:600}
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.ln{stroke:#59636e;stroke-width:1.4;fill:none;marker-end:url(#a)}
.bad{stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;fill:none;marker-end:url(#b)}
</style>
<defs>
<marker id="a" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#59636e"/></marker>
<marker id="b" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#cf222e"/></marker>
</defs>
<text class="h" x="16" y="24">D-4 · 인증서 체인과 SAN 제약</text>
<rect class="ok" x="30" y="46" width="300" height="104"/>
<text class="t" x="180" y="68" text-anchor="middle">체인 4단계 · Verify return code: 0</text>
<text class="s" x="180" y="90" text-anchor="middle">0 CN=auth.hyeonworks.com</text>
<text class="s" x="180" y="106" text-anchor="middle">1 Let's Encrypt YE2</text>
<text class="s" x="180" y="122" text-anchor="middle">2 ISRG Root YE</text>
<text class="s" x="180" y="138" text-anchor="middle">3 ISRG Root X2</text>
<rect class="box" x="360" y="46" width="310" height="104"/>
<text class="t" x="515" y="68" text-anchor="middle">SAN 3개 · 와일드카드 아님</text>
<text class="s" x="515" y="90" text-anchor="middle">auth · app1 · app2</text>
<text class="r" x="515" y="114" text-anchor="middle">네 번째 이름이 없다</text>
<text class="s" x="515" y="134" text-anchor="middle">B-7 에서 Grafana 의 app2 를 빌려야 했다</text>
<rect class="box" x="30" y="168" width="640" height="60"/>
<text class="t" x="350" y="190" text-anchor="middle">단계가 1개면 cert.pem, 2개 이상이면 fullchain.pem 이다</text>
<text class="s" x="350" y="212" text-anchor="middle">브라우저는 중간 인증서를 캐시하므로 cert.pem 실수는 캐시 없는 클라이언트에서만 드러난다</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+94
View File
@@ -0,0 +1,94 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 860 520" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,sans-serif">
<style>
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.t{font-size:12px;fill:#1f2328}
.tb{font-size:12.5px;fill:#1f2328;font-weight:600}
.s{font-size:10.5px;fill:#656d76}
.ttl{font-size:14px;fill:#1f2328;font-weight:600}
.r{stroke:#cf222e;stroke-width:1.6;fill:none}
.g{stroke:#1a7f37;stroke-width:1.6;fill:none}
.k{stroke:#8c959f;stroke-width:1.3;fill:none}
</style>
<defs>
<marker id="a" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#8c959f"/></marker>
<marker id="ar" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#cf222e"/></marker>
<marker id="ag" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#1a7f37"/></marker>
</defs>
<text x="20" y="26" class="ttl">D-4 — 「갱신 성공」과 「새 인증서 서빙」 사이의 36분 39초</text>
<text x="20" y="44" class="s">측정: 2026-09-04 · 일련번호 5초 간격 564표본 · 새 연결 0.2초 간격 8856건</text>
<!-- 좌: 디스크 -->
<rect x="20" y="70" width="250" height="196" class="box"/>
<text x="34" y="92" class="tb">디스크 — certbot</text>
<rect x="36" y="104" width="218" height="46" class="box"/>
<text x="48" y="122" class="t">cert1.pem</text>
<text x="48" y="138" class="s">09-03 10:47:40 · notAfter Dec 2</text>
<rect x="36" y="158" width="218" height="46" class="ok"/>
<text x="48" y="176" class="t">cert2.pem</text>
<text x="48" y="192" class="s">09-04 17:22:13 · notAfter Dec 3</text>
<text x="36" y="224" class="s">certbot renew --force-renewal</text>
<text x="36" y="240" class="s">→ 성공. live/ 링크가 2번을 가리킨다</text>
<text x="36" y="256" class="s">경로는 그대로, 내용만 바뀐다</text>
<!-- 중: 끊긴 고리 -->
<rect x="300" y="96" width="260" height="144" class="dead"/>
<text x="316" y="118" class="tb">★ 잇는 것이 없다</text>
<text x="316" y="140" class="t">1. ExecStartPost</text>
<text x="470" y="140" class="t" fill="#cf222e">없음</text>
<text x="316" y="160" class="t">2. renewal-hooks/deploy/</text>
<text x="470" y="160" class="t" fill="#cf222e">비었음</text>
<text x="316" y="180" class="t">3. certbot nginx 플러그인</text>
<text x="470" y="180" class="t" fill="#cf222e">없음</text>
<text x="316" y="206" class="s">post/ · pre/ 도 비어 있다 (total 8)</text>
<text x="316" y="222" class="s">셋 다 비면 아무도 reload 를 부르지 않는다</text>
<path d="M270 168 L296 168" class="r" marker-end="url(#ar)"/>
<path d="M300 230 L560 108" stroke="#cf222e" stroke-width="1.2" stroke-dasharray="4 3"/>
<path d="M300 108 L560 230" stroke="#cf222e" stroke-width="1.2" stroke-dasharray="4 3"/>
<!-- 우: nginx -->
<rect x="590" y="70" width="250" height="196" class="box"/>
<text x="604" y="92" class="tb">nginx — 메모리</text>
<rect x="606" y="104" width="218" height="60" class="dead"/>
<text x="618" y="124" class="t">기동 시 읽은 cert1</text>
<text x="618" y="140" class="s">master 585 / worker 586</text>
<text x="618" y="156" class="s">둘 다 09-03 19:00:39, 22.4시간째</text>
<text x="606" y="184" class="s">reload 없이는 디스크를 다시 안 본다.</text>
<text x="606" y="200" class="s">reload 하면 마스터는 남고</text>
<text x="606" y="216" class="s">워커만 새로 뜬다 → 워커 PID 로 확인</text>
<text x="606" y="240" class="s">ssl_certificate .../fullchain.pem</text>
<path d="M564 168 L586 168" class="k" marker-end="url(#a)"/>
<!-- 타임라인 -->
<text x="20" y="306" class="tb">밖에서 본 것 — 일련번호 (5초 간격)</text>
<line x1="60" y1="356" x2="800" y2="356" class="k"/>
<line x1="60" y1="350" x2="60" y2="362" class="k"/>
<line x1="300" y1="350" x2="300" y2="362" class="k"/>
<line x1="660" y1="344" x2="660" y2="368" stroke="#1a7f37" stroke-width="2"/>
<line x1="800" y1="350" x2="800" y2="362" class="k"/>
<text x="46" y="382" class="s">08:10:51</text>
<text x="278" y="382" class="s">08:22:13</text>
<text x="620" y="382" class="s">08:58:52</text>
<text x="770" y="382" class="s">09:02</text>
<text x="278" y="398" class="s">디스크 기록</text>
<text x="618" y="398" class="s">nginx -s reload</text>
<rect x="60" y="330" width="600" height="16" class="dead"/>
<text x="300" y="342" class="s" text-anchor="middle" fill="#cf222e">0520BB…B853 (옛 인증서) — 이 구간에서 428회 관측</text>
<rect x="660" y="330" width="140" height="16" class="ok"/>
<text x="730" y="342" class="s" text-anchor="middle" fill="#1a7f37">06C7CB…EA1D</text>
<path d="M300 416 L660 416" class="r" marker-end="url(#ar)"/>
<path d="M660 416 L300 416" class="r" marker-end="url(#ar)"/>
<text x="480" y="410" class="tb" text-anchor="middle" fill="#cf222e">36분 39초</text>
<text x="480" y="434" class="s" text-anchor="middle">사람이 reload 했기에 36분이다. 아무도 안 했다면 다음 재시작까지 — 무기한</text>
<!-- reload 판정 -->
<rect x="20" y="452" width="400" height="52" class="ok"/>
<text x="34" y="472" class="tb">reload 는 무중단이었다</text>
<text x="34" y="490" class="s">새 연결 8856건 전부 200 · p95 205.7 → 204.3ms (변화 없음)</text>
<rect x="440" y="452" width="400" height="52" class="ok"/>
<text x="454" y="472" class="tb">진행 중이던 요청도 살아남았다</text>
<text x="454" y="490" class="s">전송 12초째에 reload · 845361바이트 전량 · 연결수 1</text>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

+57
View File
@@ -0,0 +1,57 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 470" width="760" height="470" font-family="system-ui,-apple-system,Segoe UI,Roboto,sans-serif">
<style>
.t{font-size:13px;fill:#1f2328}.s{font-size:11px;fill:#59636e}.h{font-size:14px;font-weight:600;fill:#1f2328}
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.host{fill:#f6f8fa;stroke:#59636e;stroke-width:1.6;rx:8}
.node{fill:#fff;stroke:#0969da;stroke-width:1.6;rx:8}
.ln{stroke:#59636e;stroke-width:1.4;fill:none;marker-end:url(#a)}
</style>
<defs><marker id="a" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#59636e"/></marker></defs>
<rect class="box" x="290" y="12" width="180" height="38"/>
<text class="t" x="380" y="30" text-anchor="middle">개발 노트북</text>
<text class="s" x="380" y="44" text-anchor="middle">브라우저 · kubectl · Playwright</text>
<path class="ln" d="M380,52 L380,80"/>
<text class="s" x="392" y="70">https · tailnet 100.x · split DNS</text>
<rect class="host" x="40" y="84" width="680" height="66"/>
<text class="h" x="56" y="106">test-server</text>
<text class="s" x="56" y="122">Arch Linux · 12GB · WiFi only · sudo 는 비밀번호 필요</text>
<rect class="box" x="430" y="94" width="270" height="46"/>
<text class="t" x="565" y="112" text-anchor="middle">nginx :443 — TLS 종료</text>
<text class="s" x="565" y="128" text-anchor="middle">auth / app1 / app2 (SAN 3개, 와일드카드 아님)</text>
<path class="ln" d="M240,152 L200,186"/><path class="ln" d="M520,152 L560,186"/>
<text class="s" x="330" y="172" text-anchor="middle">http · libvirt NAT (virbr0)</text>
<rect class="node" x="40" y="190" width="320" height="250"/>
<text class="h" x="56" y="212">kc-lab-1 · 5120MB</text>
<text class="s" x="56" y="228">k3s server · 10.42.0.0/24</text>
<rect class="box" x="56" y="238" width="130" height="26"/><text class="t" x="121" y="255" text-anchor="middle">traefik ×1</text>
<rect class="box" x="196" y="238" width="148" height="26"/><text class="t" x="270" y="255" text-anchor="middle">coredns</text>
<rect class="box" x="56" y="272" width="130" height="26"/><text class="t" x="121" y="289" text-anchor="middle">keycloak-1</text>
<rect class="box" x="196" y="272" width="148" height="26"/><text class="t" x="270" y="289" text-anchor="middle">bff (1/2)</text>
<rect class="box" x="56" y="306" width="288" height="26"/><text class="t" x="200" y="323" text-anchor="middle">oauth2-proxy (1/2)</text>
<rect class="box" x="56" y="340" width="288" height="46"/>
<text class="t" x="200" y="358" text-anchor="middle">prometheus (PVC) · grafana</text>
<text class="s" x="200" y="374" text-anchor="middle">관측 스택은 여기 고정 — 죽이지 않는다</text>
<rect class="node" x="400" y="190" width="320" height="250"/>
<text class="h" x="416" y="212">kc-lab-2 · 4096MB</text>
<text class="s" x="416" y="228">k3s agent · 10.42.1.0/24</text>
<rect class="box" x="416" y="238" width="288" height="26"/><text class="t" x="560" y="255" text-anchor="middle">keycloak-0</text>
<rect class="box" x="416" y="272" width="140" height="26"/><text class="t" x="486" y="289" text-anchor="middle">bff (2/2)</text>
<rect class="box" x="566" y="272" width="138" height="26"/><text class="t" x="635" y="289" text-anchor="middle">oauth2-proxy</text>
<rect class="box" x="416" y="306" width="140" height="46"/>
<text class="t" x="486" y="324" text-anchor="middle">postgres</text><text class="s" x="486" y="340" text-anchor="middle">PVC (노드 고정)</text>
<rect class="box" x="566" y="306" width="138" height="46"/>
<text class="t" x="635" y="324" text-anchor="middle">redis</text><text class="s" x="635" y="340" text-anchor="middle">PVC + AOF</text>
<text class="s" x="560" y="374" text-anchor="middle">장애 주입은 여기</text>
<path d="M360,290 L400,290" stroke="#0969da" stroke-width="1.6" fill="none" marker-end="url(#a)"/>
<path d="M400,300 L360,300" stroke="#0969da" stroke-width="1.6" fill="none" marker-end="url(#a)"/>
<text class="s" x="380" y="284" text-anchor="middle">7800</text>
<text class="s" x="380" y="318" text-anchor="middle">JGroups</text>
<text class="s" x="40" y="460">A-0 에서 확인: 세션은 이 7800 이 아니라 postgres 를 통해 공유된다</text>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

@@ -0,0 +1,9 @@
=== A-3 이 가정만 하고 재지 않은 값 ===
name | setting | unit | source
------------------------+---------+------+---------
commit_delay | 0 | | default
synchronous_commit | on | | default
wal_writer_delay | 200 | ms | default
wal_writer_flush_after | 128 | 8kB | default
(4 rows)
@@ -0,0 +1,19 @@
# 주의 — 이 파일은 원 실험 시점에 0바이트로 저장됐다.
# 리다이렉션이 stdout 만 받았는데 출력이 stderr 로 갔거나 tee 앞 파이프가
# 비어 있었던 것으로 보인다. README 는 그 사이 파일 내용을 서술하고 있었는데,
# 그것은 화면에서 본 것을 적은 것이지 이 파일에서 온 것이 아니었다.
#
# 아래는 사후에 다시 수집한 것이며, 원 시점의 DROP 규칙(0 패킷)은 이미
# 제거되어 재현되지 않는다. 구조적 사실(kube-router 가 자기 체인을 FORWARD
# 최상단에 유지한다)만 확인할 수 있다.
# 원 실험의 결정적 증거는 04-correct-direction.txt 의 패킷 카운터 19/21 이다.
=== A-5 재수집 — filter 테이블 규칙이 CNI 체인에 밀리는 것 ===
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
1 1690 3386K KUBE-ROUTER-FORWARD 0 -- * * 0.0.0.0/0 0.0.0.0/0 /* kube-router netpol - TEMCG2JMHZYE7H7T */
2 7 612 KUBE-PROXY-FIREWALL 0 -- * * 0.0.0.0/0 0.0.0.0/0 ctstate NEW /* kubernetes load balancer firewall */
3 40 13196 KUBE-FORWARD 0 -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes forwarding rules */
(원 실험 시점의 규칙은 이미 제거됐다. 아래는 kube-router 가 자기 체인을
FORWARD 최상단에 유지한다는 구조적 사실만 보여준다 — 그것이 실패 원인이었다.)
@@ -16,3 +16,10 @@
1. **세션은 옮겨졌고 토큰은 안 옮겨졌다.** 빈 81개가 늘었는데 authorized client 관련은 하나도 안 바뀌었다.
2. **refresh token 은 Redis 에 평문으로 있는 게 아니라 아예 없다.** 암호화를 고민하기 전에 이걸 알아야 한다.
3. **"로그인은 되어 있는데 아무것도 못 하는" 상태가 만들어진다** — 완전 로그아웃보다 나쁘다.
## 스크린샷 주의
`b1-login-works-two-replicas.png``b1-token-boundary-after-redis.png`
**동일 파일**이며 `b2-before-relogin.png` 와도 같다 (md5 `6de826a7…`).
세 시점 모두 `accessTokenStoredOnServer: false` 인 같은 화면이었다.
**시점 구별은 터미널 출력과 Redis/DB 조회가 한다.**
@@ -19,3 +19,14 @@
2. **refresh token 은 평문이다.** DB 읽기 권한이면 작동하는 토큰을 얻는다.
3. **같은 사용자의 두 번째 로그인이 첫 번째를 덮어쓴다.** 기본키에 session id 가 없어 구조적으로 그렇다.
4. **로그아웃은 셋 중 하나만 지운다.** 평문 토큰과 Keycloak SSO 세션이 남는다.
## 스크린샷 주의
`b2-tokens-shared-across-instances.png` 는 **B-0 의
`b0-bff-token-boundary.png` 와 동일 파일**이다 (md5 `9ed00537…`).
두 시점 모두 `accessTokenStoredOnServer: true` 인 같은 화면이라 바이트가 같다.
**그래서 이 png 는 "JDBC 전환으로 토큰이 공유된다" 를 단독으로 증명하지
못한다.** 그 증명은 `01-jdbc-store-deploy.txt`(테이블 생성)과
`03-plaintext-tokens.txt`(행에 토큰이 들어 있음)가 한다.
`b2-before-relogin.png` 는 B-1 의 캡처와 동일 파일이다.
+8
View File
@@ -17,3 +17,11 @@
1. **SSO 는 user session 1개에 client session N개** 구조다 — A-3(전체 소실)과 B-3(client 만 제거)의 차이가 여기서 의미를 갖는다.
2. **IdP 세션을 죽여도 두 앱은 계속 동작한다.** 세 층(IdP·앱·토큰)의 수명이 각자이기 때문이다.
3. **IdP 는 "로그인 경로"의 단일 장애점이지 "이미 로그인한 사용자"의 단일 장애점이 아니다.** 장애는 앱 세션 수명만큼 지연되어 몰려온다.
## 스크린샷 주의
`c1-sso-app2-no-login-screen.png``c1-apps-alive-after-idp-logout.png`
**바이트 단위로 동일한 파일**이다 (md5 `2c703176…`). 두 시점의 화면이 실제로
같은 내용이었기 때문이며, 조작이 아니다. **다만 그래서 두 시점을 구별하는
증거가 되지 못한다** — 구별은 `03-``04-` 의 터미널 출력(client_sessions
1→2, 그리고 IdP 세션 삭제 후 Redis 키 잔존)이 한다.
+2 -1
View File
@@ -7,10 +7,11 @@
|---|---|
| `01-pre-upgrade.txt` | 백업 396KB · 이미지 26.7.0 · **마이그레이션 210건** · 세션 4 |
| `02-rollback-attempt.txt` | 26.0 으로 내리자 `Running(0/1) → Error → CrashLoopBackOff`. **`liquibase.exception.ValidationFailedException`** |
| `d2-upgrade-window.png` | Grafana — 26.7.3 업그레이드 구간의 `cluster_size` 2→1→2 두 번과 파드별 `up` 시계열 교체 (후속 작업에서 촬영) |
| `03-roll-forward.txt` | **서비스는 `HTTP 200` 유지**(ready 주소 1개) · 오류 원인 `1 changesets check sum` · 26.7.0 복귀 후 마이그레이션 210·세션 4 그대로 |
## 핵심 세 줄
1. **롤백은 안 된다.** 체크섬이 안 맞아 Liquibase 가 기동 자체를 거부한다 — "모르는 변경"이 아니라 "아는 변경인데 정의가 다르다".
1. **스키마가 바뀌었으면 롤백은 안 된다.** (26.7.0↔26.7.3 처럼 안 바뀌면 된다 — [`followup`](../followup/) 참조.) 체크섬이 안 맞아 Liquibase 가 기동 자체를 거부한다 — "모르는 변경"이 아니라 "아는 변경인데 정의가 다르다".
2. **StatefulSet 이 사고를 절반에서 멈춰줬다.** 한 파드가 남아 외부 200 을 유지했다. replica 1 이었다면 전면 장애다.
3. **실패한 기동은 스키마를 안 건드렸다.** 그래서 이미지만 되돌려도 복구됐다 — 이미 적용된 뒤였다면 DB 복구(D-1)가 필요하다.
Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

@@ -0,0 +1,38 @@
=== 현재 인증서 (외부 관측, sudo 불필요) ===
subject=CN = auth.hyeonworks.com
issuer=C = US, O = Let's Encrypt, CN = YE2
notBefore=Sep 3 00:47:23 2026 GMT
notAfter=Dec 2 00:47:22 2026 GMT
X509v3 Subject Alternative Name:
DNS:app1.hyeonworks.com, DNS:app2.hyeonworks.com, DNS:auth.hyeonworks.com
→ 세 호스트가 같은 인증서를 쓴다 (SAN 3개, 와일드카드 아님)
=== 체인 완결성 (fullchain vs cert 실수 확인) ===
0 s:CN = auth.hyeonworks.com
1 s:C = US, O = Let's Encrypt, CN = YE2
2 s:C = US, O = ISRG, CN = Root YE
3 s:C = US, O = Internet Security Research Group, CN = ISRG Root X2
Verify return code: 0 (ok)
→ 중간 인증서가 함께 제공된다. fullchain.pem 이 올바로 설정되어 있다.
=== 갱신 자동화 ===
NEXT LEFT LAST PASSED UNIT
Fri 2026-09-04 17:03:46 KST 1h 54min Fri 2026-09-04 03:19:39 KST 11h ago certbot-renew.timer
타이머 enabled: enabled
타이머 active: active
=== 남은 기간 ===
만료: Dec 2 00:47:22 2026 GMT
남은 일수: 88일
Let's Encrypt 90일 발급 · 30일 남으면 갱신 → 실제 갱신까지 약 58일
=== 강제 갱신은 하지 못했다 ===
$ sudo -n -l
sudo: a password is required
$ sudo -n systemctl reload nginx
sudo: a password is required
→ test-server 의 sudo 는 비밀번호를 요구한다 (게스트 kc-lab-1/2 는 무암호).
certbot renew --force-renewal 도 nginx reload 도 실행할 수 없다.
@@ -0,0 +1,19 @@
D-4 대조군 — 주입 없는 상태의 가용성 잡음 바닥
수집 시각(dev): 2026-09-04T08:03:07Z UTC
대상: https://auth.hyeonworks.com/realms/master · 0.2초 간격 900회 = 180초
형식: http_code time_total time_appconnect(TLS 핸드셰이크까지)
왜: 갱신 중 000 이 한 번 나와도, 평시 오류율을 모르면 그게 갱신 탓인지 알 수 없다.
----------------------------------------------------------------
표본 900 개
[상태코드 분포]
900 200
[응답시간 ms]
최소 67 중앙 98 p95 195 최대 1121 평균 106.9
[TLS 핸드셰이크 ms — 0 이면 연결 재사용, >0 이면 새 핸드셰이크]
핸드셰이크 발생 900회 / 900 평균 83 ms 최대 1100 ms
[비정상 응답 원문 — 있으면 아래에 전부]
비200 총 0
@@ -0,0 +1,22 @@
D-4 대조군 2 — '진행 중이던 요청' 측정 장치의 무주입 동작
수집 시각(dev): 2026-09-04T08:08:45Z UTC
왜 이 장치가 따로 필요한가
05-control 의 0.2초 폴링은 매 요청이 새 TCP 연결이다(핸드셰이크 900/900).
그래서 '새 연결을 받아주는가'는 재지만, D-4 가 묻는 '이미 진행 중이던
요청이 어떻게 되는가'는 재지 못한다. 재하려면 reload 순간에 실제로
전송 중인 요청이 있어야 한다.
장치
845KB 짜리 관리 콘솔 번들을 --limit-rate 20k 로 내려받는다.
응답을 일부러 느리게 읽어 요청을 ~42초 동안 살아 있게 만든다.
대상: https://auth.hyeonworks.com/resources/55yjq/admin/keycloak.v2/assets/main-BbID33M6.js
----------------------------------------------------------------
[대조군: 주입 없이 1회]
코드=200 받은바이트=845361 총시간=41.392198s 연결수=1 실효속도=20423B/s
기대 크기 845361 / 실제 845361 bytes
판정 기준 (주입 시 이 값들과 비교한다)
· 코드 200 + 크기 845361 = 진행 중이던 요청이 끝까지 살아남았다(graceful)
· 코드 000 또는 크기 부족 = reload 가 진행 중이던 연결을 끊었다
· 연결수 2 이상 = 중간에 끊겨 curl 이 다시 붙었다
@@ -0,0 +1,112 @@
D-4 — 갱신은 되는데 nginx 가 집지 않는다: 배포 훅 부재
=========================================================
수집: 2026-09-04 17:2x KST · test-server (sudo 없이 읽을 수 있는 범위)
발단
----
사용자가 `sudo certbot renew --force-renewal` 을 실행했다고 알려왔다.
그런데 밖에서 본 인증서가 바뀌지 않았다.
serial=0520BB6416D569E26697B1691440F523B853
notBefore=Sep 3 00:47:23 2026 GMT ← 어제 것 그대로
notAfter=Dec 2 00:47:22 2026 GMT
일련번호 감시 161표본(약 13분) 동안 단 한 번도 바뀌지 않았다.
[증거 1] nginx 는 reload 된 적이 없다
--------------------------------------
$ ps -eo pid,ppid,etimes,lstart,args | grep nginx
585 1 80529 Thu Sep 3 19:00:39 2026 nginx: master process /usr/bin/nginx
586 585 80529 Thu Sep 3 19:00:39 2026 nginx: worker process
읽는 법 — nginx 의 reload 는 마스터를 유지한 채 **워커만 새로 띄운다.**
· 마스터 585, 워커 586 = 마스터 기동 직후의 첫 fork
· 둘의 lstart 가 같고 etimes 도 같다(80529초 = 22.4시간)
→ reload 가 한 번도 없었다. 22.4시간 전 기동 그대로다.
인증서 파일이 새로 써졌더라도, nginx 는 기동 시점에 읽어 메모리에 들고 있다.
reload 가 없으면 **옛 인증서를 계속 서빙한다.**
[증거 2] ★ 갱신 유닛에 reload 가 없다
--------------------------------------
$ systemctl cat certbot-renew.service
# /usr/lib/systemd/system/certbot-renew.service
[Unit]
Description=Renew certificates acquired via Certbot
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot -q renew
PrivateTmp=true
`ExecStart` 가 전부다. **ExecStartPost 도, --deploy-hook 도 없다.**
배포판(Arch)이 넣어준 기본 유닛이 그렇다.
$ systemctl cat certbot-renew.timer
OnCalendar=*-*-* 00/12:00:00
RandomizedDelaySec=12h
Persistent=true
[증거 3] 타이머는 정상 동작한다 — 그래서 더 위험하다
------------------------------------------------------
$ systemctl status certbot-renew.service
Active: inactive (dead) since Fri 2026-09-04 17:04:11 KST
Process: 28452 ExecStart=/usr/bin/certbot -q renew (code=exited, status=0/SUCCESS)
$ journalctl -u certbot-renew.service --since today
Sep 04 03:19:39 Starting Renew certificates acquired via Certbot...
Sep 04 03:19:41 Finished Renew certificates acquired via Certbot.
Sep 04 17:04:09 Starting Renew certificates acquired via Certbot...
Sep 04 17:04:11 Finished Renew certificates acquired via Certbot.
오늘 두 번 돌았고 두 번 다 status=0/SUCCESS 다. 만료까지 88일 남아 실제
갱신은 하지 않았으므로 **아직은** 아무 문제가 없다.
[증거 4] nginx 가 무엇을 물고 있나
-----------------------------------
$ grep -rn ssl_certificate /etc/nginx/
/etc/nginx/sites-available/keycloak-lab:18: ssl_certificate /etc/letsencrypt/live/auth.hyeonworks.com/fullchain.pem;
/etc/nginx/sites-available/keycloak-lab:19: ssl_certificate_key /etc/letsencrypt/live/auth.hyeonworks.com/privkey.pem;
`live/` 는 심볼릭 링크다. certbot 이 갱신하면 링크가 새 `archive/` 파일을
가리키도록 바뀐다 — **파일 경로는 그대로인데 내용이 바뀐다.** 그래서 nginx
설정은 고칠 필요가 없고, 바로 그 때문에 "설정이 그대로니 괜찮다"고 착각하기 쉽다.
필요한 것은 설정 변경이 아니라 **reload** 다.
무엇을 확인하지 못했나
----------------------
`/etc/letsencrypt/renewal-hooks/deploy/` 에 스크립트가 있으면 유닛 파일과
무관하게 실행된다. 이 디렉터리는 root 전용이라 읽지 못했다.
$ ls -laR /etc/letsencrypt/renewal-hooks/
ls: cannot access '/etc/letsencrypt/renewal-hooks/': Permission denied
따라서 두 갈래가 남는다.
(a) 강제 갱신이 실패했다 → 파일도 안 바뀌었고 reload 도 없었다
(b) 강제 갱신이 성공했다 → 파일은 바뀌었는데 훅이 없어 reload 가 안 됐다
(b) 라면 이 실험대에서 가장 값진 발견이다. **"갱신 성공"과 "새 인증서 서빙"이
다른 사건**이라는 것을, 88일 뒤가 아니라 지금 잡은 것이기 때문이다.
가리는 방법 — root 권한 한 번:
sudo certbot certificates # notAfter 가 오늘+90일이면 (b)
sudo ls -la /etc/letsencrypt/archive/auth.hyeonworks.com/
sudo tail -60 /var/log/letsencrypt/letsencrypt.log
곁다리로 확인한 것 — CT 로그로는 가릴 수 없다
-----------------------------------------------
발급 사실은 Certificate Transparency 에 남으므로 sudo 없이 확인할 수 있을 것
같았다. 실제로 서빙 중인 인증서에는 SCT 가 2개 박혀 있다.
CT Precertificate SCTs:
Log ID: C2:31:7E:57:...:52:CD Timestamp: Sep 3 01:45:53.183 2026 GMT
Log ID: 46:AF:86:3D:...:50:5F Timestamp: Sep 3 01:45:53.352 2026 GMT
그런데 crt.sh 는 이 이름을 모른다.
$ curl -s 'https://crt.sh/?q=auth.hyeonworks.com&output=json'
[] ← 0건
$ curl -s 'https://crt.sh/?q=hyeonworks.com&output=json'
13건, 최신 not_before=2026-08-11 ← auth 는 없다
**인증서에 SCT 가 박혀 있다는 것과 crt.sh 가 그것을 색인했다는 것은 다르다.**
관측 도구가 진실의 부분집합만 본다는, A-2 의 `up` 지표와 같은 종류의 함정이다.
@@ -0,0 +1,66 @@
D-4 — in-flight 감시에서 나온 76건 실패: 서버 탓이 아니다
============================================================
수집: 2026-09-04 · dev 머신에서 실행한 감시 로그
무엇이 보였나
-------------
42초짜리 in-flight 요청을 연달아 돌리는 감시에서, 91건 중 76건이 실패했다.
08:13:40 코드=200 바이트=845361 시간=41.041830 연결수=1
08:14:22 코드=200 바이트=845361 시간=42.338482 연결수=1
08:15:04 코드=000 바이트=0 시간=0.001148 연결수=0 ← 여기부터
08:15:04 코드=000 바이트=0 시간=0.001140 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000055 연결수=0
... (76건, 전부 08:15:04)
08:15:04 코드=200 바이트=845361 시간=42.236496 연결수=1 ← 곧바로 복귀
08:15:47 코드=200 바이트=845361 시간=42.327129 연결수=1
이걸 "갱신 중 진행 중이던 요청이 끊겼다"로 읽으면 안 되는 이유
----------------------------------------------------------------
1. **같은 순간 폴링은 멀쩡했다.** 0.2초 간격 폴링의 08:15:0008:15:20 구간
표본 49건이 전부 200이다. 같은 호스트, 같은 TLS, 같은 머신이다.
08:15:00.24 200 0.092491
08:15:01.15 200 0.099177
08:15:02.38 200 0.078729
... 49건 / 비200 0건
2. **연결수=0 이다.** curl 이 TCP 연결을 시도조차 못 했다는 뜻이다.
서버가 연결을 끊었다면 연결수는 1이고 바이트는 0보다 크다.
3. **시간이 0.00005초다.** 50마이크로초. DNS 조회조차 이보다 오래 걸린다.
요청이 나가기 전에 로컬에서 실패했다.
4. **76건이 1초 안에 몰렸다.** 실패가 즉시 반환되니 while 루프가 폭주한 것이다.
시간축에 퍼진 장애가 아니라 순간의 스파이크다.
5. **재현되지 않는다.**
같은 URL 100회 연속 → 실패 0/100
DNS 해석 200회 → 실패 0/200
URL 유효성 → 코드=200 크기=845361, 콘솔이 참조하는 경로와 동일
6. **nginx 는 그 시각에 아무 일도 하지 않았다.** 마스터·워커가 22.4시간째
같은 프로세스다(07 참조). reload 도 재시작도 없었다.
결론
----
**로컬(dev 머신) 쪽 일시적 원인이며, 서버 가용성 사건이 아니다.**
정확한 원인은 특정하지 못했다 — curl 을 `-s` 로 돌려 오류 메시지를 버렸고,
종료 코드도 기록하지 않았기 때문이다.
고친 것
-------
감시를 종료 코드까지 남기는 형태로 교체했다. 다음에 같은 일이 생기면
curl 의 종료 코드(6=DNS, 7=connect, 35=TLS ...)로 원인이 바로 나온다.
실패 시 1초 쉬게 해 루프 폭주도 막았다.
R=$(curl -s --limit-rate 20k -o /dev/null -w "..." "$URL" 2>/dev/null); E=$?
echo "$T $R curl종료=$E"
[ $E -ne 0 ] && sleep 1
남기는 교훈
-----------
측정 장치가 실패했을 때 **왜 실패했는지 남기지 않으면, 그 실패를 대상 탓으로
돌릴지 장치 탓으로 돌릴지 판단할 근거가 없다.** 여기서는 대조 폴링이 같은
시각에 멀쩡했다는 사실 하나로 겨우 갈랐다. 대조군이 없었다면 이 76건은
"갱신 중 대규모 요청 실패"라는 그럴듯한 오보가 됐을 것이다.
@@ -0,0 +1,564 @@
08:10:51 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:10:56 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:11:01 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:11:06 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:11:12 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:11:17 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:11:22 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:11:27 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:11:32 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:11:37 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:11:42 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:11:47 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:11:52 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:11:58 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:12:03 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:12:08 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:12:13 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:12:18 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:12:23 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:12:28 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:12:33 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:12:38 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:12:44 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:12:49 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:12:54 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:12:59 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:13:04 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:13:09 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:13:14 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:13:19 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:13:25 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:13:30 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:13:35 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:13:40 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:13:45 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:13:50 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:13:55 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:14:00 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:14:05 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:14:10 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:14:15 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:14:21 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:14:26 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:14:31 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:14:36 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:14:41 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:14:46 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:14:51 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:14:56 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:15:01 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:15:07 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:15:12 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:15:17 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:15:22 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:15:27 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:15:32 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:15:37 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:15:42 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:15:48 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:15:53 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:15:58 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:16:03 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:16:08 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:16:13 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:16:19 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:16:24 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:16:29 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:16:34 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:16:39 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:16:44 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:16:50 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:16:55 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:17:00 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:17:05 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:17:10 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:17:15 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:17:20 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:17:25 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:17:31 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:17:36 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:17:41 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:17:46 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:17:51 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:17:56 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:18:01 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:18:06 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:18:12 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:18:17 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:18:22 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:18:27 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:18:32 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:18:37 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:18:42 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:18:48 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:18:53 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:18:58 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:19:03 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:19:08 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:19:13 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:19:18 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:19:23 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:19:29 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:19:34 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:19:39 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:19:44 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:19:49 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:19:54 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:19:59 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:20:05 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:20:10 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:20:15 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:20:20 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:20:25 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:20:30 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:20:35 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:20:40 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:20:45 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:20:51 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:20:56 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:21:01 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:21:06 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:21:11 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:21:16 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:21:21 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:21:26 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:21:31 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:21:37 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:21:42 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:21:47 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:21:52 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:21:57 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:22:02 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:22:07 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:22:12 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:22:17 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:22:22 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:22:28 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:22:33 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:22:38 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:22:43 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:22:48 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:22:53 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:22:58 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:23:03 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:23:08 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:23:13 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:23:19 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:23:24 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:23:29 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:23:34 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:23:39 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:23:44 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:23:49 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:23:54 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:24:00 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:24:05 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:24:10 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:24:15 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:24:20 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:24:25 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:24:30 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:24:35 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:24:40 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:24:46 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:24:51 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:24:56 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:25:01 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:25:06 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:25:11 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:25:16 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:25:21 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:25:26 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:25:31 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:25:37 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:25:42 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:25:47 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:25:52 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:25:57 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:26:02 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:26:07 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:26:12 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:26:17 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:26:23 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:26:28 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:26:33 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:26:38 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:26:43 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:26:48 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:26:53 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:26:58 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:27:03 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:27:08 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:27:14 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:27:19 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:27:24 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:27:29 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:27:34 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:27:39 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:27:44 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:27:49 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:27:54 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:28:00 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:28:05 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:28:10 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:28:15 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:28:20 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:28:25 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:28:30 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:28:35 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:28:40 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:28:46 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:28:51 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:28:56 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:29:01 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:29:06 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:29:11 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:29:16 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:29:21 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:29:26 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:29:31 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:29:36 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:29:42 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:29:47 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:29:52 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:29:57 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:30:02 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:30:07 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:30:12 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:30:17 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:30:22 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:30:28 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:30:33 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:30:38 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:30:43 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:30:48 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:30:53 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:30:58 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:31:03 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:31:08 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:31:13 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:31:19 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:31:24 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:31:29 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:31:34 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:31:39 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:31:44 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:31:49 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:31:54 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:31:59 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:32:05 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:32:10 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:32:15 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:32:20 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:32:25 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:32:30 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:32:35 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:32:40 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:32:46 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:32:51 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:32:56 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:33:01 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:33:06 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:33:11 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:33:16 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:33:21 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:33:26 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:33:32 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:33:37 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:33:42 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:33:47 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:33:52 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:33:57 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:34:02 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:34:07 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:34:13 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:34:18 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:34:23 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:34:28 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:34:33 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:34:38 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:34:43 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:34:48 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:34:54 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:34:59 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:35:04 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:35:09 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:35:14 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:35:19 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:35:24 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:35:29 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:35:35 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:35:40 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:35:45 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:35:50 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:35:56 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:36:01 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:36:06 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:36:11 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:36:16 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:36:22 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:36:27 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:36:32 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:36:37 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:36:42 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:36:47 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:36:52 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:36:57 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:37:03 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:37:08 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:37:13 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:37:18 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:37:23 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:37:28 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:37:33 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:37:38 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:37:43 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:37:48 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:37:54 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:37:59 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:38:04 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:38:09 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:38:14 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:38:19 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:38:24 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:38:29 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:38:35 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:38:40 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:38:45 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:38:50 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:38:55 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:39:00 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:39:05 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:39:10 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:39:15 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:39:21 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:39:26 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:39:31 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:39:36 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:39:41 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:39:46 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:39:51 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:39:56 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:40:01 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:40:07 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:40:12 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:40:17 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:40:22 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:40:27 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:40:32 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:40:38 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:40:43 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:40:48 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:40:53 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:40:58 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:41:03 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:41:08 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:41:14 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:41:19 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:41:24 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:41:29 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:41:35 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:41:40 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:41:45 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:41:50 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:41:55 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:42:00 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:42:05 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:42:10 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:42:16 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:42:21 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:42:26 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:42:31 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:42:36 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:42:41 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:42:46 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:42:51 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:42:56 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:43:01 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:43:07 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:43:12 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:43:17 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:43:22 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:43:27 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:43:32 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:43:37 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:43:42 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:43:48 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:43:53 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:43:58 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:44:03 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:44:08 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:44:14 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:44:19 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:44:24 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:44:29 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:44:34 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:44:39 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:44:45 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:44:50 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:44:55 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:45:00 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:45:06 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:45:11 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:45:16 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:45:21 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:45:26 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:45:31 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:45:36 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:45:41 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:45:47 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:45:52 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:45:57 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:46:02 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:46:07 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:46:12 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:46:17 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:46:23 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:46:28 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:46:33 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:46:38 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:46:43 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:46:49 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:46:54 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:46:59 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:47:04 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:47:09 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:47:15 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:47:20 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:47:25 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:47:30 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:47:35 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:47:40 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:47:45 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:47:50 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:47:56 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:48:01 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:48:06 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:48:11 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:48:16 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:48:21 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:48:26 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:48:31 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:48:37 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:48:42 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:48:47 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:48:52 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:48:57 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:49:03 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:49:08 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:49:13 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:49:18 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:49:23 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:49:28 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:49:33 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:49:39 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:49:44 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:49:49 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:49:54 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:49:59 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:50:04 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:50:10 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:50:15 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:50:20 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:50:25 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:50:30 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:50:35 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:50:40 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:50:46 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:50:51 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:50:56 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:51:01 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:51:06 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:51:11 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:51:16 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:51:21 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:51:27 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:51:32 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:51:37 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:51:42 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:51:47 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:51:52 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:51:57 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:52:02 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:52:08 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:52:13 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:52:18 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:52:23 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:52:28 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:52:33 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:52:38 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:52:43 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:52:48 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:52:54 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:52:59 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:53:04 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:53:09 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:53:14 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:53:19 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:53:24 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:53:29 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:53:35 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:53:40 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:53:45 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:53:50 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:53:55 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:54:00 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:54:05 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:54:10 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:54:16 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:54:21 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:54:26 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:54:31 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:54:36 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:54:41 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:54:46 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:54:51 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:54:57 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:55:02 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:55:07 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:55:12 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:55:17 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:55:22 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:55:27 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:55:32 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:55:37 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:55:43 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:55:48 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:55:53 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:55:58 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:56:03 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:56:08 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:56:13 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:56:19 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:56:24 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:56:29 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:56:34 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:56:39 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:56:44 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:56:49 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:56:54 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:56:59 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:57:05 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:57:10 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:57:15 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:57:20 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:57:25 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:57:30 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:57:35 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:57:40 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:57:46 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:57:51 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:57:56 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:58:01 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:58:06 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:58:11 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:58:16 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:58:22 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:58:27 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:58:32 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:58:37 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:58:42 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:58:47 serial=0520BB6416D569E26697B1691440F523B853 notAfter=Dec 2 00:47:22 2026 GMT
08:58:52 serial=06C7CB6DF1DA8A6D7995D93C264BB9ECEA1D notAfter=Dec 3 07:21:52 2026 GMT
08:58:52 ★ 일련번호 변경 감지 — 3분 더 재고 종료
@@ -0,0 +1,177 @@
08:10:51.60 200 0.104996
08:10:51.91 200 0.070124
08:10:52.19 200 0.131785
... (중략)
08:58:30.03 200 0.077607
08:58:30.32 200 0.109888
08:58:30.64 200 0.195213
08:58:31.06 200 0.103103
08:58:31.38 200 0.079529
08:58:31.67 200 0.187888
08:58:32.07 200 0.194786
08:58:32.48 200 0.192114
08:58:32.89 200 0.108582
08:58:33.22 200 0.079331
08:58:33.51 200 0.189853
08:58:33.92 200 0.189635
08:58:34.33 200 0.195752
08:58:34.74 200 0.195164
08:58:35.15 200 0.078307
08:58:35.45 200 0.102306
08:58:35.76 200 0.193694
08:58:36.18 200 0.193912
08:58:36.58 200 0.079270
08:58:36.88 200 0.100873
08:58:37.20 200 0.077209
08:58:37.49 200 0.102600
08:58:37.81 200 0.195354
08:58:38.22 200 0.194041
08:58:38.63 200 0.079332
08:58:38.93 200 0.077710
08:58:39.22 200 0.073483
08:58:39.51 200 0.126482
08:58:39.86 200 0.196844
08:58:40.27 200 0.189230
08:58:40.68 200 0.089921
08:58:40.98 200 0.080912
08:58:41.28 200 0.093971
08:58:41.60 200 0.191146
08:58:42.00 200 0.079236
08:58:42.30 200 0.105411
08:58:42.62 200 0.195868
08:58:43.03 200 0.147224
08:58:43.40 200 0.136670
08:58:43.75 200 0.191708
08:58:44.16 200 0.084091
08:58:44.46 200 0.079146
08:58:44.75 200 0.114239
08:58:45.08 200 0.078791
08:58:45.38 200 0.074542
08:58:45.67 200 0.078430
08:58:45.97 200 0.073271
08:58:46.26 200 0.081292
08:58:46.55 200 0.086169
08:58:46.86 200 0.078664
08:58:47.16 200 0.077433
08:58:47.45 200 0.076830
08:58:47.74 200 0.078261
08:58:48.04 200 0.082249
08:58:48.34 200 0.077844
08:58:48.64 200 0.084788
08:58:48.94 200 0.080198
08:58:49.24 200 0.078426
08:58:49.53 200 0.078641
08:58:49.82 200 0.080504
08:58:50.12 200 0.079685
08:58:50.42 200 0.079559
08:58:50.71 200 0.078883
08:58:51.01 200 0.077754
08:58:51.30 200 0.078970
08:58:51.60 200 0.084448
08:58:51.90 200 0.131753
08:58:52.25 200 0.197813
08:58:52.66 200 0.091437
08:58:52.97 200 0.199400
08:58:53.39 200 0.085250
08:58:53.69 200 0.084521
08:58:53.99 200 0.072331
08:58:54.28 200 0.081943
08:58:54.58 200 0.113594
08:58:54.92 200 0.185589
08:58:55.32 200 0.200308
08:58:55.74 200 0.098051
08:58:56.05 200 0.086943
08:58:56.36 200 0.192842
08:58:56.77 200 0.178977
08:58:57.17 200 0.083091
08:58:57.47 200 0.194195
08:58:57.88 200 0.147293
08:58:58.24 200 0.149510
08:58:58.61 200 0.088007
08:58:58.91 200 0.190298
08:58:59.32 200 0.199024
08:58:59.73 200 0.183439
08:59:00.13 200 0.206752
08:59:00.55 200 0.179475
08:59:00.95 200 0.089502
08:59:01.25 200 0.096302
08:59:01.57 200 0.203361
08:59:01.99 200 0.175288
08:59:02.38 200 0.200164
08:59:02.80 200 0.157565
08:59:03.17 200 0.081179
08:59:03.47 200 0.133562
08:59:03.82 200 0.207667
08:59:04.24 200 0.156695
08:59:04.61 200 0.126136
08:59:04.95 200 0.185645
08:59:05.36 200 0.197049
08:59:05.77 200 0.090621
08:59:06.08 200 0.182576
08:59:06.48 200 0.107587
08:59:06.80 200 0.176183
08:59:07.19 200 0.076604
08:59:07.48 200 0.112886
08:59:07.81 200 0.197961
08:59:08.23 200 0.197695
08:59:08.64 200 0.192117
08:59:09.05 200 0.086968
08:59:09.36 200 0.073667
08:59:09.64 200 0.107286
08:59:09.97 200 0.187141
08:59:10.37 200 0.196110
08:59:10.78 200 0.080634
08:59:11.08 200 0.101278
08:59:11.40 200 0.190454
08:59:11.81 200 0.195550
08:59:12.22 200 0.084865
08:59:12.52 200 0.097978
08:59:12.84 200 0.188273
08:59:13.24 200 0.078268
08:59:13.54 200 0.098529
08:59:13.85 200 0.196250
08:59:14.26 200 0.198583
08:59:14.67 200 0.193794
08:59:15.08 200 0.142313
08:59:15.44 200 0.142774
08:59:15.80 200 0.203481
08:59:16.22 200 0.190197
08:59:16.62 200 0.193118
08:59:17.03 200 0.191270
08:59:17.44 200 0.190572
08:59:17.85 200 0.159825
08:59:18.23 200 0.115453
08:59:18.56 200 0.212529
08:59:18.99 200 0.178715
08:59:19.38 200 0.373160
08:59:19.98 200 0.114451
08:59:20.31 200 0.198672
08:59:20.72 200 0.186258
08:59:21.12 200 0.103469
08:59:21.44 200 0.197932
08:59:21.86 200 0.077019
08:59:22.15 200 0.180685
08:59:22.55 200 0.203363
08:59:22.97 200 0.197513
08:59:23.38 200 0.196509
08:59:23.80 200 0.094951
08:59:24.11 200 0.175434
08:59:24.50 200 0.372545
08:59:25.10 200 0.077048
08:59:25.39 200 0.077420
08:59:25.69 200 0.138478
08:59:26.04 200 0.190167
08:59:26.45 200 0.192613
08:59:26.86 200 0.161352
08:59:27.24 200 0.081419
08:59:27.54 200 0.131994
08:59:27.88 200 0.202003
08:59:28.30 200 0.186137
08:59:28.71 200 0.091572
08:59:29.02 200 0.077290
08:59:29.31 200 0.090913
08:59:29.62 200 0.198752
... (중략)
09:01:10.29 200 0.105354
09:01:10.62 200 0.173389
09:01:11.00 200 0.191564
@@ -0,0 +1,146 @@
08:10:51 코드=200 바이트=845361 시간=42.335582 연결수=1
08:11:33 코드=200 바이트=845361 시간=42.328216 연결수=1
08:12:16 코드=200 바이트=845361 시간=42.332256 연결수=1
08:12:58 코드=200 바이트=845361 시간=42.327378 연결수=1
08:13:40 코드=200 바이트=845361 시간=41.041830 연결수=1
08:14:22 코드=200 바이트=845361 시간=42.338482 연결수=1
08:15:04 코드=000 바이트=0 시간=0.001148 연결수=0
08:15:04 코드=000 바이트=0 시간=0.001140 연결수=0
08:15:04 코드=000 바이트=0 시간=0.001124 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000055 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000080 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000077 연결수=0
08:15:04 코드=000 바이트=0 시간=0.001167 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000048 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000079 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000096 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000039 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000083 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000075 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000041 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000061 연결수=0
08:15:04 코드=000 바이트=0 시간=0.001141 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000078 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000053 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000079 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000083 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000079 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000099 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000083 연결수=0
08:15:04 코드=000 바이트=0 시간=0.001164 연결수=0
08:15:04 코드=000 바이트=0 시간=0.001106 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000074 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000087 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000079 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000072 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000085 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000085 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000048 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000046 연결수=0
08:15:04 코드=000 바이트=0 시간=0.001147 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000044 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000053 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000076 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000043 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000091 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000074 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000049 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000051 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000087 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000091 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000079 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000095 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000055 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000076 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000077 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000088 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000085 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000092 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000080 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000085 연결수=0
08:15:04 코드=000 바이트=0 시간=0.001157 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000048 연결수=0
08:15:04 코드=000 바이트=0 시간=0.001097 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000060 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000080 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000087 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000048 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000081 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000084 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000087 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000079 연결수=0
08:15:04 코드=000 바이트=0 시간=0.001153 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000053 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000088 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000052 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000085 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000084 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000080 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000079 연결수=0
08:15:04 코드=000 바이트=0 시간=0.001106 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000051 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000056 연결수=0
08:15:04 코드=200 바이트=845361 시간=42.236496 연결수=1
08:15:47 코드=200 바이트=845361 시간=42.327129 연결수=1
08:16:29 코드=200 바이트=845361 시간=41.442808 연결수=1
08:17:10 코드=200 바이트=845361 시간=42.332025 연결수=1
08:17:53 코드=200 바이트=845361 시간=40.434517 연결수=1
08:18:33 코드=200 바이트=845361 시간=41.438410 연결수=1
08:19:15 코드=200 바이트=845361 시간=42.334617 연결수=1
08:19:57 코드=200 바이트=845361 시간=42.333846 연결수=1
08:20:39 코드=200 바이트=845361 시간=42.232748 연결수=1
08:21:22 코드=200 바이트=845361 시간=42.324095 연결수=1
08:22:04 코드=200 바이트=845361 시간=42.331593 연결수=1
08:22:46 코드=200 바이트=845361 시간=41.626941 연결수=1
08:23:28 코드=200 바이트=845361 시간=42.330280 연결수=1
08:24:59 코드=200 바이트=845361 시간=42.331694 연결수=1 curl종료=0
08:25:42 코드=200 바이트=845361 시간=42.333930 연결수=1 curl종료=0
08:26:24 코드=200 바이트=845361 시간=41.839874 연결수=1 curl종료=0
08:27:06 코드=200 바이트=845361 시간=41.834401 연결수=1 curl종료=0
08:27:48 코드=200 바이트=845361 시간=41.840212 연결수=1 curl종료=0
08:28:30 코드=200 바이트=845361 시간=41.032736 연결수=1 curl종료=0
08:29:11 코드=200 바이트=845361 시간=41.433394 연결수=1 curl종료=0
08:29:52 코드=200 바이트=845361 시간=42.325976 연결수=1 curl종료=0
08:30:34 코드=200 바이트=845361 시간=42.037070 연결수=1 curl종료=0
08:31:16 코드=200 바이트=845361 시간=42.343058 연결수=1 curl종료=0
08:31:59 코드=200 바이트=845361 시간=41.837942 연결수=1 curl종료=0
08:32:41 코드=200 바이트=845361 시간=42.318577 연결수=1 curl종료=0
08:33:23 코드=200 바이트=845361 시간=42.336079 연결수=1 curl종료=0
08:34:05 코드=200 바이트=845361 시간=42.328392 연결수=1 curl종료=0
08:34:48 코드=200 바이트=845361 시간=41.839243 연결수=1 curl종료=0
08:35:30 코드=200 바이트=845361 시간=42.339658 연결수=1 curl종료=0
08:36:12 코드=200 바이트=845361 시간=42.330835 연결수=1 curl종료=0
08:36:54 코드=200 바이트=845361 시간=41.844755 연결수=1 curl종료=0
08:37:36 코드=200 바이트=845361 시간=41.442783 연결수=1 curl종료=0
08:38:18 코드=200 바이트=845361 시간=42.334462 연결수=1 curl종료=0
08:39:00 코드=200 바이트=845361 시간=42.331275 연결수=1 curl종료=0
08:39:42 코드=200 바이트=845361 시간=41.243768 연결수=1 curl종료=0
08:40:23 코드=200 바이트=845361 시간=41.837874 연결수=1 curl종료=0
08:41:05 코드=200 바이트=845361 시간=42.336582 연결수=1 curl종료=0
08:41:48 코드=200 바이트=845361 시간=42.327934 연결수=1 curl종료=0
08:42:30 코드=200 바이트=845361 시간=42.333559 연결수=1 curl종료=0
08:43:12 코드=200 바이트=845361 시간=41.841294 연결수=1 curl종료=0
08:43:54 코드=200 바이트=845361 시간=42.329699 연결수=1 curl종료=0
08:44:37 코드=200 바이트=845361 시간=41.845127 연결수=1 curl종료=0
08:45:18 코드=200 바이트=845361 시간=42.040360 연결수=1 curl종료=0
08:46:00 코드=200 바이트=845361 시간=41.845192 연결수=1 curl종료=0
08:46:42 코드=200 바이트=845361 시간=42.327749 연결수=1 curl종료=0
08:47:25 코드=200 바이트=845361 시간=41.837216 연결수=1 curl종료=0
08:48:06 코드=200 바이트=845361 시간=42.329675 연결수=1 curl종료=0
08:48:49 코드=200 바이트=845361 시간=42.036368 연결수=1 curl종료=0
08:49:31 코드=200 바이트=845361 시간=42.341275 연결수=1 curl종료=0
08:50:13 코드=200 바이트=845361 시간=42.332664 연결수=1 curl종료=0
08:50:56 코드=200 바이트=845361 시간=42.334345 연결수=1 curl종료=0
08:51:38 코드=200 바이트=845361 시간=41.837146 연결수=1 curl종료=0
08:52:20 코드=200 바이트=845361 시간=42.328092 연결수=1 curl종료=0
08:53:02 코드=200 바이트=845361 시간=42.344341 연결수=1 curl종료=0
08:53:44 코드=200 바이트=845361 시간=42.332296 연결수=1 curl종료=0
08:54:27 코드=200 바이트=845361 시간=41.839230 연결수=1 curl종료=0
08:55:09 코드=200 바이트=845361 시간=42.336469 연결수=1 curl종료=0
08:55:51 코드=200 바이트=845361 시간=42.336864 연결수=1 curl종료=0
08:56:33 코드=200 바이트=845361 시간=42.043518 연결수=1 curl종료=0
08:57:15 코드=200 바이트=845361 시간=42.340137 연결수=1 curl종료=0
08:57:58 코드=200 바이트=845361 시간=42.335696 연결수=1 curl종료=0
08:58:40 코드=200 바이트=845361 시간=40.840349 연결수=1 curl종료=0
08:59:21 코드=200 바이트=845361 시간=42.340130 연결수=1 curl종료=0
09:00:03 코드=200 바이트=845361 시간=41.841100 연결수=1 curl종료=0
@@ -0,0 +1,41 @@
Saving debug log to /var/log/letsencrypt/letsencrypt.log
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Found the following certs:
Certificate Name: auth.hyeonworks.com
Serial Number: 6c7cb6df1da8a6d7995d93c264bb9ecea1d
Key Type: ECDSA
Identifiers: auth.hyeonworks.com app1.hyeonworks.com app2.hyeonworks.com
Expiry Date: 2026-12-03 07:21:52+00:00 (VALID: 89 days)
Certificate Path: /etc/letsencrypt/live/auth.hyeonworks.com/fullchain.pem
Private Key Path: /etc/letsencrypt/live/auth.hyeonworks.com/privkey.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
=== renewal-hooks ===
/etc/letsencrypt/archive/auth.hyeonworks.com/:
total 48
drwxr-xr-x 2 root root 4096 2026-09-04 17:22:13.508658811 +0900 .
drwx------ 3 root root 4096 2026-09-03 10:47:40.915270972 +0900 ..
-rw-r--r-- 1 root root 1359 2026-09-03 10:47:40.915923507 +0900 cert1.pem
-rw-r--r-- 1 root root 1359 2026-09-04 17:22:13.508494637 +0900 cert2.pem
-rw-r--r-- 1 root root 3523 2026-09-03 10:47:40.916215769 +0900 chain1.pem
-rw-r--r-- 1 root root 3523 2026-09-04 17:22:13.508658811 +0900 chain2.pem
-rw-r--r-- 1 root root 4882 2026-09-03 10:47:40.916339551 +0900 fullchain1.pem
-rw-r--r-- 1 root root 4882 2026-09-04 17:22:13.508821612 +0900 fullchain2.pem
-rw------- 1 root root 241 2026-09-03 10:47:40.916079294 +0900 privkey1.pem
-rw------- 1 root root 241 2026-09-04 17:22:13.507717972 +0900 privkey2.pem
/etc/letsencrypt/renewal-hooks/deploy/:
total 8
drwxr-xr-x 2 root root 4096 2026-09-03 10:46:54.658474560 +0900 .
drwxr-xr-x 5 root root 4096 2026-09-03 10:46:54.658520760 +0900 ..
/etc/letsencrypt/renewal-hooks/post/:
total 8
drwxr-xr-x 2 root root 4096 2026-09-03 10:46:54.658505772 +0900 .
drwxr-xr-x 5 root root 4096 2026-09-03 10:46:54.658520760 +0900 ..
/etc/letsencrypt/renewal-hooks/pre/:
total 8
drwxr-xr-x 2 root root 4096 2026-09-03 10:46:54.658474560 +0900 .
drwxr-xr-x 5 root root 4096 2026-09-03 10:46:54.658520760 +0900 ..
@@ -0,0 +1,99 @@
D-4 판정 — 갱신은 성공했고, 36분 39초 동안 반영되지 않았다
=============================================================
수집: 2026-09-04 · 감시 08:10:51 ~ 09:02 UTC (KST-9)
════ 답 1 — "갱신 실패"가 아니라 "갱신됐는데 안 집었다" ════
07-renewal-hook-missing.txt 에서 두 갈래로 남겼던 것이 갈렸다.
(a) 강제 갱신이 실패했다
(b) 성공했는데 훅이 없어 reload 가 안 됐다
→ ★ (b) 였다.
디스크 (12-certbot-state.txt)
/etc/letsencrypt/archive/auth.hyeonworks.com/
cert1.pem 2026-09-03 10:47:40 KST ← 처음 발급
cert2.pem 2026-09-04 17:22:13 KST ← ★ 강제 갱신, 새로 써졌다
fullchain2.pem 2026-09-04 17:22:13 KST
privkey2.pem 2026-09-04 17:22:13 KST
Certificate Name: auth.hyeonworks.com
Serial Number: 6c7cb6df1da8a6d7995d93c264bb9ecea1d
Expiry Date: 2026-12-03 07:21:52+00:00 (VALID: 89 days)
밖에서 본 것 (09-serial-timeline.txt, 5초 간격 564표본)
08:10:51 ~ 08:58:47 serial=0520BB...B853 notAfter=Dec 2 ← 옛 것
08:58:52 serial=06C7CB...EA1D notAfter=Dec 3 ← 바뀐 순간
08:22:13 ~ 08:58:52 구간에서 옛 인증서로 관측된 횟수: 428회
시간 계산
새 인증서 디스크 기록 : 08:22:13 UTC (= 17:22:13 KST, archive mtime)
실제 서빙 시작 : 08:58:52 UTC (= 17:58:52 KST, nginx -s reload)
────────────────────────────────────────────
공백 : 2199초 = 36분 39초
그리고 이 36분은 **우연히 짧았을 뿐이다.** reload 를 시킨 것은 사람이지
자동화가 아니다. 아무도 안 했다면 다음 nginx 재시작까지 — 즉 무기한 —
옛 인증서를 계속 서빙했을 것이다.
왜 아무도 reload 하지 않았나 — 원인 세 가지가 겹쳤다
1. certbot-renew.service 에 ExecStartPost 가 없다 (07 참조)
2. /etc/letsencrypt/renewal-hooks/{deploy,post,pre}/ 가 전부 비어 있다
deploy/ total 8 (. 과 .. 뿐)
post/ total 8
pre/ total 8
3. certbot 에 nginx 플러그인이 없다 — 그래서 certbot 이 스스로 못 고친다
Discovered plugins: dns-cloudflare, manual, null, standalone, webroot
(certbot 5.7.0)
★ 세 경로 전부가 비어 있다. 하나라도 있었으면 자동으로 반영됐다.
왜 이게 88일 동안 안 보이나
타이머는 정상이고 오늘도 두 번 status=0/SUCCESS 로 끝났다.
만료 30일 전(= 약 58일 뒤)까지는 certbot 이 갱신 자체를 하지 않으므로
"반영되지 않는다"는 결함이 발현할 기회가 없다. 발현하는 날 증상은
**인증서 만료** 이고, 그때 로그는 여전히 SUCCESS 라고 적혀 있을 것이다.
════ 답 2 — reload 는 무중단이다 (측정됨) ════
계획서의 질문: "nginx reload 중 진행 중이던 요청은 어떻게 되는가"
[새 연결] 0.2초 폴링, 08:10:51 ~ 09:02
전체 표본 8856건 / 비200 **0건**
응답시간 n 중앙 p95 최대
─────────────────────────────────────────────────────────
장기 평시 08:20~08:50 5398 98.0ms 205.7ms 1942.9ms
reload 직전 2분56초 489 116.0ms 200.8ms 387.7ms
reload 직후 2분08초 342 132.5ms 204.3ms 475.0ms
p95 가 205.7 → 204.3 으로 사실상 동일하고, 최대값은 오히려 낮다.
10초 구간 중앙값은 reload 전후 모두 80~190ms 사이를 오간다(WiFi 잡음).
→ **reload 로 인한 지연 증가는 관측되지 않았다.**
[진행 중이던 요청] 845KB 를 20k/s 로 받아 42초간 살려 둔 요청
08:58:40 시작 · 소요 40.84초 → 08:59:21 종료
reload 는 08:58:52, 즉 **요청 시작 12초 뒤 전송 한가운데**였다
코드=200 바이트=845361(전량) 연결수=1 curl종료=0
· 바이트가 전량이다 → 잘리지 않았다
· 연결수가 1이다 → 중간에 끊겨 재연결한 것이 아니다
· 옛 인증서로 시작한 연결이 새 워커 전환을 관통해 끝까지 갔다
in-flight 전체 50건 중 종료코드≠0 은 0건.
(앞서 76건 실패는 로컬 아티팩트였다 — 08-inflight-artifact.txt)
════ 세 줄 요약 ════
1. **"갱신 성공"과 "새 인증서 서빙"은 다른 사건이다.** 36분 39초 동안
디스크에는 새 인증서가, 네트워크에는 옛 인증서가 있었다. 428번 관측했다.
2. **reload 는 정말 무중단이었다.** 8856건 전부 200, p95 변화 없음,
전송 한가운데였던 42초짜리 요청이 845361바이트를 온전히 받았다.
D-4 문서가 "확인하지 않았으므로 그렇게 쓰면 안 된다"고 유보했던 명제가
이제 측정으로 뒷받침된다.
3. **위험한 쪽은 reload 가 아니라 reload 를 부르는 사람이 없다는 것이다.**
훅 디렉터리 3개가 전부 비었고, 유닛에 ExecStartPost 가 없고,
nginx 플러그인도 없다. 그리고 그 상태로 타이머는 SUCCESS 를 찍는다.
@@ -0,0 +1,28 @@
# D-4 — 인증서 갱신 증거
2026-09-04 17:25 18:02 KST
해설: [`docs/experiment-d4-certificate-renewal.md`](../../experiment-d4-certificate-renewal.md)
| 파일 | 무엇을 보여주는가 |
|---|---|
| `01-certificate-state.txt` | SAN 3개(와일드카드 아님) · **체인 4단계, `Verify return code: 0`** · `certbot-renew.timer` enabled·active, 11시간 전 실행 · 88일 남음 · **`sudo: a password is required` 로 강제 갱신 불가** |
| `05-control-no-injection.txt` | **대조군 1 — 잡음 바닥.** 0.2초 × 900회 / 180초 동안 **900전부 200, 오류 0**. 중앙 98ms · p95 195ms. **TLS 핸드셰이크 900/900** = 매 요청이 새 연결이다 |
| `06-inflight-control.txt` | **대조군 2 — '진행 중이던 요청' 측정 장치.** 845KB 번들을 `--limit-rate 20k` 로 받아 요청을 **42초간 살려 둔다.** 무주입 시 코드 200 · 845361바이트 · 연결수 1 |
| `05-control-no-injection.txt` | **대조군 1 — 잡음 바닥.** 0.2초 × 900회 / 180초 동안 **900 전부 200, 오류 0**. 중앙 98ms · p95 195ms. **TLS 핸드셰이크 900/900** = 매 요청이 새 연결 |
| `06-inflight-control.txt` | **대조군 2 — '진행 중이던 요청' 장치.** 845KB 를 `--limit-rate 20k` 로 받아 요청을 **42초간 살려 둔다.** 무주입 시 200 · 845361바이트 · 연결수 1 |
| `07-renewal-hook-missing.txt` | **nginx 는 reload 된 적이 없다** — 마스터 585·워커 586 이 같은 시각 기동, 22.4시간째. 유닛은 `ExecStart=certbot -q renew` 가 전부. crt.sh 는 SCT 가 박힌 인증서를 0건으로 답한다 |
| `08-inflight-artifact.txt` | **76건 실패는 서버 탓이 아니다** — 같은 순간 폴링 49건 전부 200, `연결수=0`, 50µs, 재현 0/100. 대조군이 오보를 막았다 |
| `09-serial-timeline.txt` | **일련번호 564표본.** `08:10:51 ~ 08:58:47` 옛 것 → **`08:58:52` 새 것** |
| `10-reload-poll-window.txt` | reload 전후 60초 새 연결 원문 — **비200 0건**, 최대 373ms |
| `11-inflight-full.txt` | in-flight 전체 50건. **08:58:40 시작 요청이 08:58:52 reload 를 관통**해 845361바이트 전량 수신 |
| `12-certbot-state.txt` | `cert2.pem` **09-04 17:22:13 기록됨** · `renewal-hooks/{deploy,post,pre}/` **셋 다 비었음** · 플러그인 목록에 nginx 없음 |
| `13-verdict.txt` | **판정 전문** — 36분 39초 공백(428회 관측) + reload 무중단(8856건 0실패) |
## 핵심 다섯 줄
1. **「갱신 성공」과 「새 인증서 서빙」은 다른 사건이다.** 새 인증서가 디스크에 있는 채로 **36분 39초** 동안 옛 인증서를 서빙했고, 그 구간에서 **428번** 관측했다.
2. **그 36분은 우연히 짧았다.** reload 를 시킨 것은 사람이다. 아무도 안 했다면 다음 nginx 재시작까지 무기한이었다.
3. **원인이 셋 겹쳤다.** 유닛에 `ExecStartPost` 없음 · 훅 디렉터리 3개 전부 비었음 · certbot 에 nginx 플러그인 없음. 하나라도 있었으면 자동 반영됐다.
4. **reload 자체는 무중단이었다.** 새 연결 8856건 전부 200, p95 205.7 → 204.3ms, 그리고 **전송 12초째에 reload 를 맞은 42초 요청이 845361바이트를 온전히** 받았다(연결수 1).
5. **이 결함은 88일 동안 안 보인다.** 타이머는 오늘도 두 번 `SUCCESS` 로 끝났다. 만료 30일 전까지는 갱신 자체를 하지 않으므로 발현할 기회가 없고, 발현하는 날의 증상은 **인증서 만료**다 — 그날에도 로그는 `SUCCESS` 라고 적혀 있다.
@@ -0,0 +1,32 @@
=== D-1 절차대로 먼저 백업 ===
백업: 395375 bytes
마이그레이션 전: 210
세션 전: 3
=== ★ 정방향 업그레이드 + 1초 간격 가용성 측정 ===
시작: 15:22:59
partitioned roll out complete: 2 new pods have been updated...
완료: 15:24:26
=== 업그레이드 중 외부 응답 시계열 ===
200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200
200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200
200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200
200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200
200 200 200 200 200 200 200
200 응답: 87 회
비200 : 0
0 회
=== 업그레이드 후 ===
quay.io/keycloak/keycloak:26.7.3
Keycloak 26.7.3
마이그레이션 후: 210 (전: 210)
세션 후: 3 (전: 3)
=== 스키마 마이그레이션이 실제로 있었는가 ===
(없으면 26.7.0→26.7.3 에 스키마 변경이 없다는 뜻)
=== 파드 상태와 클러스터 ===
keycloak-0 1/1 Running restarts=0
keycloak-1 1/1 Running restarts=0
cluster: [keycloak-1-11418(v=16.0.14)|47] (2) [keycloak-1-11418(v=16.0.14), keycloak-0-58996(v=16.0.14)]
@@ -0,0 +1,20 @@
=== ★ 가설: 스키마 변경이 없으면 롤백이 된다 (26.7.3 → 26.7.0) ===
시작: 15:25:08
partitioned roll out complete: 2 new pods have been updated...
완료: 15:25:53
200 응답: 43 회 / 비200: 1
keycloak-0 1/1 Running restarts=0
keycloak-1 1/1 Running restarts=0
Keycloak 26.7.0
마이그레이션: 210
세션: 3
=== 롤백 중 응답 시계열 (비200 위치) ===
200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200
200 200 200 200 000 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200
200 200 200 200
비200 값: 000
=== 대조: 정방향 업그레이드 때는 ===
200: 87 / 비200: 0
@@ -0,0 +1,66 @@
# B-4 ③ — IdP 의 클레임 변경은 언제 upstream 에 반영되는가
#
# 이 파일은 재수집본이다. 첫 수집본은 "이제 요청을 반복해 본다" 에서 끝났고,
# 그 뒤의 12회 결과는 브라우저 콘솔에서만 보고 파일로 남기지 않았다.
# 서브에이전트 감사가 "증거 없이 서 있는 결론" 으로 지적했고, 다시 측정했다.
#
# 수집 방법:
# - 요청부: Playwright 로 연 브라우저에서 fetch('/api/echo') 를 반복.
# oauth2-proxy 세션 쿠키가 HttpOnly 라 curl 로는 재현할 수 없어 브라우저를 썼다.
# 아래 출력은 브라우저 콘솔이 돌려준 문자열 그대로다.
# - 변경부·저장소 확인부: test-server 의 kubectl 출력 그대로.
=== [1] 기준선 — 변경 전 (브라우저 fetch) ===
2026-09-04T07:51:23.862Z req#1 HTTP 200 x-forwarded-email=labuser@example.com x-forwarded-preferred-username=labuser
2026-09-04T07:51:24.304Z req#2 HTTP 200 x-forwarded-email=labuser@example.com x-forwarded-preferred-username=labuser
2026-09-04T07:51:24.722Z req#3 HTTP 200 x-forwarded-email=labuser@example.com x-forwarded-preferred-username=labuser
=== [2] IdP 에서 email 을 바꾼다 (kubectl 출력) ===
변경 시각(UTC): 2026-09-04T07:53:32.000Z
IdP 의 값:
[ {
"email" : "changed-labuser@example.com"
} ]
oauth2-proxy 세션: 1 개 (그대로 살아 있다)
=== [시계 보정] 두 시계가 다르다 — 해석에 필요하다 ===
개발 머신(브라우저 fetch 의 타임스탬프): 2026-09-04T07:52:20Z
test-server (kubectl 출력의 타임스탬프): 2026-09-04T07:54:07Z
→ test-server 가 약 107초 앞선다.
브라우저 07:51:56 = 서버 07:53:43 이므로, 아래 12회는 변경(07:53:32) 11초 뒤다.
=== [3] 변경 후 12회 반복 (브라우저 fetch) ===
2026-09-04T07:51:56.300Z req#1 HTTP 200 x-forwarded-email=labuser@example.com
2026-09-04T07:51:56.864Z req#2 HTTP 200 x-forwarded-email=labuser@example.com
2026-09-04T07:51:57.489Z req#3 HTTP 200 x-forwarded-email=labuser@example.com
2026-09-04T07:51:58.018Z req#4 HTTP 200 x-forwarded-email=labuser@example.com
2026-09-04T07:51:58.602Z req#5 HTTP 200 x-forwarded-email=labuser@example.com
2026-09-04T07:51:59.217Z req#6 HTTP 200 x-forwarded-email=labuser@example.com
2026-09-04T07:51:59.743Z req#7 HTTP 200 x-forwarded-email=labuser@example.com
2026-09-04T07:52:00.342Z req#8 HTTP 200 x-forwarded-email=labuser@example.com
2026-09-04T07:52:00.964Z req#9 HTTP 200 x-forwarded-email=labuser@example.com
2026-09-04T07:52:01.574Z req#10 HTTP 200 x-forwarded-email=labuser@example.com
2026-09-04T07:52:02.187Z req#11 HTTP 200 x-forwarded-email=labuser@example.com
2026-09-04T07:52:02.719Z req#12 HTTP 200 x-forwarded-email=labuser@example.com
→ 12회 · 약 6.4초 동안 전부 옛 값. 요청 횟수로는 반영되지 않는다.
=== [4] IdP 값은 이미 바뀌어 있었다 (대조) ===
IdP: "email" : "changed-labuser@example.com"
=== [5] 세션을 지워 재인증을 강제한다 ===
남은 oauth2-proxy 세션: 0 개
(Keycloak SSO 는 살아 있으므로 조용히 재인증된다)
=== [6] 재인증 후 (브라우저 fetch) ===
2026-09-04T07:53:01.121Z req#1 HTTP 200 x-forwarded-email=changed-labuser@example.com
2026-09-04T07:53:01.456Z req#2 HTTP 200 x-forwarded-email=changed-labuser@example.com
2026-09-04T07:53:01.785Z req#3 HTTP 200 x-forwarded-email=changed-labuser@example.com
=== 결론 ===
변경 후 12회 요청(6.4초) → labuser@example.com (옛 값)
세션 삭제 후 재인증 → changed-labuser@example.com (새 값)
→ 반영 시점은 요청 횟수가 아니라 세션이 새로 만들어지는가로 정해진다.
--cookie-refresh 가 설정되어 있지 않으므로, 실제 운영에서는
쿠키 만료(1시간) 또는 재인증까지 옛 값이 간다.
@@ -0,0 +1,17 @@
=== Prometheus 가 실제로 긁는 대상 (2026-09-04 18:10 KST) ===
keycloak 2개
kubelet 2개
node-exporter 2개
prometheus 1개
=== B층 구성 요소의 지표가 있는가 ===
redis_up 시계열 0개
redis_connected_clients 시계열 0개
redis_memory_used_bytes 시계열 0개
pg_up 시계열 0개
pg_stat_database_numbackends 시계열 0개
→ B-1·B-2·B-3·B-5 는 Grafana 증거를 만들 수 없다.
스크린샷을 안 찍은 것이 아니라 긁는 대상에 없다.
보완하려면 redis_exporter · postgres_exporter · BFF 의 /actuator/prometheus 를
scrape 대상에 추가해야 한다.
@@ -0,0 +1,76 @@
재현 절차 명령 실행 검증 — 문서에 적힌 명령을 그대로 돌려본 기록
====================================================================
수집: 2026-09-04, kc-lab-1 에서 sudo kubectl (호스트 test-server 경유)
대상: 감사에서 "산문이라 재현 불가"로 지적된 명령들을 셸 실행형으로 바꾼 뒤,
바꾼 형태가 실제로 도는지 확인한 것.
왜 이 파일이 있나
------------------
바꾼 명령을 돌려보지 않고 "재현 가능하게 고쳤다"고 쓰면, 감사가 잡아낸 바로
그 실수(측정하지 않고 단언)를 반복하는 것이다. 그래서 실행 기록을 남긴다.
그리고 실제로 이 검증에서 한 건이 깨졌다 — 아래 [실패] 항목.
환경
----
K0 = 10.42.1.142 (keycloak-0, kc-lab-2)
K1 = 10.42.0.62 (keycloak-1, kc-lab-1)
PW = keycloak-lab-secrets/KC_BOOTSTRAP_ADMIN_PASSWORD, 19자
TE = /realms/master/protocol/openid-connect/token
[통과] 검증 1 — A-3 의 sid 추출 파이프라인
-------------------------------------------
문서 experiment-a3-database-crash.md 2단계에 넣은 파이프라인
(access_token → 2번째 세그먼트 → '==' 패딩 → base64 -d → sid) 을 그대로 실행.
추출된 sid = YWfXBHn08DNujXvJ6QV1lMAd
/tmp/sids 줄수 = 1
sid 는 UUID 가 아니라 24자 문자열이다. RPO 대조에서 IN 절에 넣을 때
따옴표가 필요한 이유가 이것이다.
[통과] 검증 2 — A-6 단일 요청 지연 (각 15회)
---------------------------------------------
keycloak-0 평균 46 ms (대조군: PostgreSQL 과 같은 노드)
keycloak-1 평균 86 ms (시험군: 노드 간 홉이 하나 더 있다)
주입 없는 평시 값이다. A-6 본문의 22.2초는 지연 주입 상태의 값이므로
이 숫자와 직접 비교하면 안 된다. 여기서 확인한 것은 "명령이 돈다" 뿐이다.
[실패 → 수정] 검증 3 — 동시 20건 부하
--------------------------------------
처음에 쓴 형태:
kubectl -n keycloak-lab run load --rm -i --restart=Never \
--image=curlimages/curl:8.11.1 --quiet --command -- sh -c '... & ... wait'
결과: 출력이 도착하지 않고 세션이 그대로 끊겼다. 두 번 시도해서 두 번 다.
일회성 파드(--rm -i)의 stdout 스트림이 20개 동시 자식 프로세스의 출력을
잃는 문제이며, 이 실험 시리즈에서 이미 한 번 겪은 함정이다.
내가 재현 절차를 고치면서 그 깨진 패턴을 다시 써넣었다.
고친 형태 — 상주 탐침 + 파드 안 파일 수집 + 한 번 cat:
kubectl run a6-probe ... -- sleep 1800
kubectl exec a6-probe -- sh -c '... >> /tmp/load ) & ... wait'
kubectl exec a6-probe -- cat /tmp/load > /tmp/load.txt
결과:
exit=0
수집된 줄수 = 20 ← 20 이어야 한다. 스트림 방식은 여기서 샜다.
상태코드별:
20 200
최대 지연 = 0.968958 s
[통과] 검증 4 — 풀 지표
------------------------
agroal_max_used_count{datasource="default"} 4.0
agroal_active_count{datasource="default"} 0.0
agroal_awaiting_count{datasource="default"} 0.0
agroal_blocking_time_average_milliseconds{datasource="default"} 0.0
동시 20건에도 max_used 가 4 에 그치고 awaiting 이 0 이다.
평시에는 풀이 병목이 아니라는 뜻이며, A-6 이 지연을 주입해야만
경합이 나타난다고 적은 것과 일치한다.
남는 교훈
---------
"명령을 실행 가능하게 고쳤다"와 "고친 명령이 동작한다"는 다른 주장이다.
검증 3 이 그 차이를 보여준다 — 문법적으로는 멀쩡했고, 실행하면 조용히 실패했다.
+19
View File
@@ -0,0 +1,19 @@
# 후속 — 미측정으로 남겼던 항목을 채운 기록
2026-09-04 17:3518:20 KST
해설: [`docs/experiment-followup-untested-items.md`](../../experiment-followup-untested-items.md)
| 파일 | 무엇을 보여주는가 |
|---|---|
| `01-d2-forward-upgrade.txt` | **D-2 정방향** 26.7.0 → 26.7.3. 백업 396KB · **87회 요청 전부 200(무중단)** · 마이그레이션 210 → 210(스키마 변경 없음) · 세션 3 유지 · Infinispan 16.0.12 → 16.0.14 |
| `02-d2-rollback-same-schema.txt` | **스키마가 안 바뀌면 롤백이 된다** — 26.7.3 → 26.7.0 성공. 다만 전환 순간 `000` 1회(3초 타임아웃) |
| `03-b4-role-propagation.txt` | **B-4 ③** IdP 에서 값을 바꿔도 **12회 요청·6초 동안 옛 값**. 세션 삭제 후 재인증에서야 새 값 |
| `04-observability-gap.txt` | **B층에 관측이 없다** — Prometheus 는 keycloak·kubelet·node-exporter·prometheus 만 긁는다. Redis·BFF·PostgreSQL 지표가 0개 |
| `05-command-reproducibility.txt` | **재현 절차 명령을 실제로 돌려본 기록.** 산문이던 측정 장치를 셸 실행형으로 바꾼 뒤 실행 검증 — 4건 중 1건(동시 20건 부하)이 **조용히 실패**했고 상주 탐침 방식으로 고쳐 20/20 수집 |
## 핵심 세 줄
1. **"롤백은 안 된다" 는 조건부였다.** 스키마가 바뀌었으면 안 되고, 안 바뀌었으면 된다 — D-2 의 결론을 정밀화한다.
2. **role 변경은 요청 횟수와 무관하게 반영되지 않는다.** `--cookie-refresh` 가 없으면 쿠키 만료나 재인증까지 옛 값이 간다.
3. **바꾼 명령은 돌려봐야 안다.** 문법이 멀쩡해도 일회성 파드는 동시 출력에서 조용히 샜다 — 05 참조.
4. **B층 실험에 Grafana 증거가 없는 이유가 확인됐다** — 관측 대상에 애초에 없다. 스크린샷이 없는 것이 아니라 지표가 없다.
+19 -2
View File
@@ -471,7 +471,7 @@ kubectl -n keycloak-lab exec keycloak-0 -- \
```sql
select offline_flag, count(*) from offline_user_session group by offline_flag;
select user_session_id, offline_flag, created_on, last_session_refresh
from offline_user_session where user_session_id = '<sid>';
from offline_user_session where user_session_id = '$SID';
```
**이름이 내용을 배신하는 스키마다.** 운영에서 "온라인 세션이 DB 어디 있냐"를
@@ -597,6 +597,23 @@ for n in ('BEFORE_K0','BEFORE_K1','AFTER_K0','AFTER_K1'):
---
---
## 증거 파일
**증거 수집 시각: 2026-09-04 09:52 10:12 KST** (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)
| 파일 | 종류 |
|---|---|
| [`01-cross-node-session.txt`](evidence/session-replication/01-cross-node-session.txt) | 터미널 원문 |
| [`02-cache-delta.txt`](evidence/session-replication/02-cache-delta.txt) | 터미널 원문 |
| [`03-cache-ownership.txt`](evidence/session-replication/03-cache-ownership.txt) | 터미널 원문 |
| [`04-read-path-sql.txt`](evidence/session-replication/04-read-path-sql.txt) | 터미널 원문 |
| [`keycloak-admin-sessions.png`](evidence/session-replication/keycloak-admin-sessions.png) | 스크린샷 |
| [`session-cache-entries-per-pod.png`](evidence/session-replication/session-cache-entries-per-pod.png) | 스크린샷 |
파일별 상세는 [`evidence/session-replication/README.md`](evidence/session-replication/README.md).
## 11. 재현
```bash
@@ -631,6 +648,6 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "select offline_flag, count(*) from offline_user_session group by offline_flag"
# 노드별 캐시 엔트리 (파드 안에서)
curl -s http://<pod-ip>:9000/metrics \
curl -s http://$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}'):9000/metrics \
| grep 'approximate_entries_unique{cache="sessions"'
```
+59 -6
View File
@@ -177,15 +177,34 @@ sudo conntrack -D -p tcp -s 10.42.1.43 -d 10.42.0.35 --sport 7800 --dport 40023
**양쪽 노드에서, 양쪽 방향으로** 지워야 한다. 서버 쪽 노드에는 튜플이 뒤집혀
기록되어 있다.
그리고 **즉시 끊기지 않는다.**
### ★ 정정 — conntrack 삭제가 분단을 만들었다고 볼 근거가 없다
이 문서는 처음에 이렇게 썼다.
```
11:41 conntrack 삭제
11:44 cluster_size 2 → 1 ← 약 3분 뒤
```
TCP 는 상대가 사라졌음을 **재전송 타임아웃**으로 알아낸다. 소켓은 한동안
`ESTABLISHED` 로 남아 있다.
**증거를 다시 보면 그 인과가 성립하지 않는다.**
| 시각 | 증거 |
|---|---|
| 11:41 | conntrack 삭제. 직후 `07-cluster-size.txt`**11:45 까지 전부 `2`** |
| **11:44:23** | **`keycloak-0` 파드의 `startTime`** — 스스로 재시작했다 |
| 11:44:27 | `cluster_size` 2 → 1 |
| 11:46:07 | 내가 `delete pod` 를 실행 (이미 떨어진 뒤) |
**하락은 conntrack 삭제 3분 뒤가 아니라 파드 재시작 4초 뒤에 일어났다.**
같은 문서 6절이 "정책이 걸린 채 재시작되자" 라고 쓴 것이 맞고,
**4절의 "conntrack 삭제 → 3분 뒤 분단" 은 시각이 겹친 것을 인과로 읽은 것이다.**
conntrack 삭제 자체가 무의미했다는 뜻은 아니다 — 다만 **이 실험은
그것만으로 분단이 되는지 판정하지 못했다.** 판정한 것은 A-5 이고,
거기서 `raw` 테이블이 필요하다는 것이 드러났다.
> TCP 가 재전송 타임아웃으로 상대를 알아채는 것은 사실이지만,
> **이 실험에서 그 경로가 발동했다는 증거는 없다.**
---
@@ -427,7 +446,11 @@ kubectl -n keycloak-lab delete networkpolicy a1-block-jgroups-transport
```bash
sudo conntrack -L | grep 7800
sudo conntrack -D -p tcp -s <src> -d <dst> --sport <sp> --dport <dp>
# 위 출력의 src/dst/sport/dport 를 그대로 넣는다. 한 줄로 하려면:
sudo conntrack -L 2>/dev/null | grep 7800 | awk '{
for(i=1;i<=NF;i++){ split($i,a,"="); t[a[1]]=a[2] }
print "sudo conntrack -D -p tcp -s "t["src"]" -d "t["dst"]" --sport "t["sport"]" --dport "t["dport"]
}' | sh
```
### FD_SOCK2 와 포트 규약
@@ -462,6 +485,30 @@ vendor_jgroups_merge3_get_num_merge_events
---
---
## 증거 파일
**증거 수집 시각: 2026-09-04 11:34 11:50 KST** (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)
| 파일 | 종류 |
|---|---|
| [`01-baseline-cluster.txt`](evidence/a1-jgroups-transport-block/01-baseline-cluster.txt) | 터미널 원문 |
| [`02-control-before-block.txt`](evidence/a1-jgroups-transport-block/02-control-before-block.txt) | 터미널 원문 |
| [`03-block-applied.txt`](evidence/a1-jgroups-transport-block/03-block-applied.txt) | 터미널 원문 |
| [`04-after-block-state.txt`](evidence/a1-jgroups-transport-block/04-after-block-state.txt) | 터미널 원문 |
| [`05-conntrack-problem.txt`](evidence/a1-jgroups-transport-block/05-conntrack-problem.txt) | 터미널 원문 |
| [`06-partition-observed.txt`](evidence/a1-jgroups-transport-block/06-partition-observed.txt) | 터미널 원문 |
| [`07-cluster-size.txt`](evidence/a1-jgroups-transport-block/07-cluster-size.txt) | 터미널 원문 |
| [`08-restart-forced-partition.txt`](evidence/a1-jgroups-transport-block/08-restart-forced-partition.txt) | 터미널 원문 |
| [`09-cross-node-under-partition.txt`](evidence/a1-jgroups-transport-block/09-cross-node-under-partition.txt) | 터미널 원문 |
| [`10-logout-not-propagated.txt`](evidence/a1-jgroups-transport-block/10-logout-not-propagated.txt) | 터미널 원문 |
| [`11-service-impact.txt`](evidence/a1-jgroups-transport-block/11-service-impact.txt) | 터미널 원문 |
| [`12-recovery.txt`](evidence/a1-jgroups-transport-block/12-recovery.txt) | 터미널 원문 |
| [`a1-cluster-size-partition-recovery.png`](evidence/a1-jgroups-transport-block/a1-cluster-size-partition-recovery.png) | 스크린샷 |
파일별 상세는 [`evidence/a1-jgroups-transport-block/README.md`](evidence/a1-jgroups-transport-block/README.md).
## 11. 재현 절차 (명령어)
```bash
@@ -481,8 +528,14 @@ kubectl -n keycloak-lab get pods -o wide | grep keycloak # restarts=0 확인
ssh kc-lab-1 'sudo conntrack -L | grep 7800'
# 4. conntrack 삭제 (양쪽 노드, 양쪽 방향). 반영까지 약 3분
ssh kc-lab-1 'sudo conntrack -D -p tcp -s <k1ip> -d <k0ip> --sport <sp> --dport 7800'
ssh kc-lab-2 'sudo conntrack -D -p tcp -s <k0ip> -d <k1ip> --sport 7800 --dport <sp>'
# 노드마다 자기 conntrack 표에서 7800 튜플을 뽑아 그대로 지운다.
# 방향(누가 client 인지)은 재시작마다 바뀐다 — 그래서 손으로 적으면 틀린다(A-5 에서 실제로 틀렸다).
for N in kc-lab-1 kc-lab-2; do
ssh $N "sudo conntrack -L 2>/dev/null | grep 7800 | awk '{
for(i=1;i<=NF;i++){ split(\$i,a,\"=\"); t[a[1]]=a[2] }
print \"sudo conntrack -D -p tcp -s \"t[\"src\"]\" -d \"t[\"dst\"]\" --sport \"t[\"sport\"]\" --dport \"t[\"dport\"]
}' | sh"
done
# 5. 분단 확인
curl -s "http://localhost:19090/api/v1/query?query=vendor_cluster_size"
+37 -2
View File
@@ -9,6 +9,15 @@
---
## 구조
![A-2 구조 — DB 정지 시 살아남는 노드가 없다](diagrams/a2-database-loss.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터
| 예측 | 결과 |
@@ -94,12 +103,19 @@ kubectl -n keycloak-lab wait --for=delete pod -l app=postgres --timeout=90s
① 캐시를 가진 노드(keycloak-0)에서 refresh HTTP 500
② 캐시가 없는 노드(keycloak-1)에서 refresh HTTP 500
③ 새 로그인 HTTP 500
④ 관리 API (세션 조회 필요) HTTP 500
④ 관리 API (세션 조회 필요) HTTP 500 ← 5절의 재측정값
--- 오류 본문 ---
{"error":"unknown_error","error_description":"For more on this error consult the server log."}
```
> **④ 의 첫 측정은 오염됐다** —
> [`03-four-paths.txt`](evidence/a2-database-loss/03-four-paths.txt) 에는
> `HTTP 000000{"error":"HTTP 401 Unauthorized"}401` 이 남아 있다.
> `curl -w %{http_code}` 출력에 본문이 섞인 것이고, 재시도가 `000` 을 세 번
> 찍은 뒤 `401` 이 왔다. **위 표의 `500` 은 5절에서 다시 잰 값**이며,
> 첫 측정을 그대로 쓰지 않았다.
### ① 이 500 인 것이 중요하다
**캐시에 세션을 들고 있어도 refresh 는 실패한다.**
@@ -263,6 +279,23 @@ DB 가 돌아와도 CrashLoopBackOff 의 백오프 때문에 회복이 늦어진
---
---
## 증거 파일
**증거 수집 시각: 2026-09-04 11:53 11:56 KST** (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)
| 파일 | 종류 |
|---|---|
| [`01-baseline.txt`](evidence/a2-database-loss/01-baseline.txt) | 터미널 원문 |
| [`02-setup-sessions.txt`](evidence/a2-database-loss/02-setup-sessions.txt) | 터미널 원문 |
| [`03-four-paths.txt`](evidence/a2-database-loss/03-four-paths.txt) | 터미널 원문 |
| [`04-health-and-service.txt`](evidence/a2-database-loss/04-health-and-service.txt) | 터미널 원문 |
| [`05-recovery.txt`](evidence/a2-database-loss/05-recovery.txt) | 터미널 원문 |
| [`a2-up-stayed-1-during-outage.png`](evidence/a2-database-loss/a2-up-stayed-1-during-outage.png) | 스크린샷 |
파일별 상세는 [`evidence/a2-database-loss/README.md`](evidence/a2-database-loss/README.md).
## 9. 재현 절차 (명령어)
```bash
@@ -275,7 +308,9 @@ kubectl -n keycloak-lab wait --for=condition=Ready pod/a2-probe --timeout=120s
kubectl -n keycloak-lab exec a2-probe -- sh -c \
'curl -s -X POST http://<k0>:8080/realms/master/protocol/openid-connect/token \
-d grant_type=password -d client_id=admin-cli \
-d username=admin -d password=<pw> > /tmp/tok.json'
-d username=admin -d password='"$PW"' > /tmp/tok.json'
# PW=$(kubectl -n keycloak-lab get secret keycloak-lab-secrets \
# -o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d)
# 2. DB 정지
kubectl -n keycloak-lab scale deployment/postgres --replicas=0
+59 -7
View File
@@ -181,8 +181,23 @@ database system is ready to accept connections
★ p5XybeQIYmAs818gO4Vl_5ea
```
**약 2.6% 유실.** 초당 19건 정도 로그인하던 중이었으므로
**대략 마지막 0.2초 분량**이다 — `wal_writer_delay` 기본값(200ms)과 맞는다.
**약 2.6% 유실.**
처음 이 문서는 *"초당 19건 … `wal_writer_delay` 기본값(200ms)과 맞는다"*
썼는데, **그 시점에 `wal_writer_delay` 를 조회한 적이 없었다.** 나중에 쟀다.
```
name | setting | unit | source
------------------------+---------+------+---------
wal_writer_delay | 200 | ms | default
wal_writer_flush_after | 128 | 8kB | default
synchronous_commit | on | | default
```
[`08-wal-settings.txt`](evidence/a3-database-crash/08-wal-settings.txt)
**값은 맞았지만 그때는 추정이었다.** 그리고 로그인 속도도 정확히는
증거의 `8초에 112건` ≈ **초당 14건**이며 19건이 아니다. 4건은 그 속도에서
**약 0.29초 분량**이고, 200ms 창과 같은 자릿수이되 정확히 일치하지는 않는다.
### 사용자에게 어떻게 보이는가
@@ -284,6 +299,25 @@ ALTER DATABASE keycloak SET synchronous_commit = on; -- SET LOCAL 이 이깁
---
---
## 증거 파일
**증거 수집 시각: 2026-09-04 11:58 16:32 KST** (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)
| 파일 | 종류 |
|---|---|
| [`01-crash-injection.txt`](evidence/a3-database-crash/01-crash-injection.txt) | 터미널 원문 |
| [`02-design-check.txt`](evidence/a3-database-crash/02-design-check.txt) | 터미널 원문 |
| [`03-loss-measurement.txt`](evidence/a3-database-crash/03-loss-measurement.txt) | 터미널 원문 |
| [`04-comparison.txt`](evidence/a3-database-crash/04-comparison.txt) | 터미널 원문 |
| [`05-true-crash.txt`](evidence/a3-database-crash/05-true-crash.txt) | 터미널 원문 |
| [`06-backend-kill-crash.txt`](evidence/a3-database-crash/06-backend-kill-crash.txt) | 터미널 원문 |
| [`07-loss-result.txt`](evidence/a3-database-crash/07-loss-result.txt) | 터미널 원문 |
| [`08-wal-settings.txt`](evidence/a3-database-crash/08-wal-settings.txt) | 터미널 원문 |
파일별 상세는 [`evidence/a3-database-crash/README.md`](evidence/a3-database-crash/README.md).
## 8. 재현 절차 (명령어)
```bash
@@ -296,8 +330,21 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "delete from offline_user_session"
# 2. 로그인 루프 (호스트에서 백그라운드 exec — 파드 안 & 는 exec 종료와 함께 죽는다)
kubectl -n keycloak-lab exec a2-probe -- sh -c '<로그인 반복, sid 를 /tmp/sids 에>' &
# 2. 로그인 루프 호스트에서 백그라운드로 exec 한다.
# 파드 안에서 ( ... ) & 로 띄우면 exec 세션이 끝날 때 같이 죽는다(실측).
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
kubectl -n keycloak-lab exec a2-probe -- sh -c "
i=0
while [ \$i -lt 200 ]; do
R=\$(curl -s --max-time 5 -X POST http://$K0:8080/realms/master/protocol/openid-connect/token \
-d grant_type=password -d client_id=admin-cli -d username=admin -d password=lab-admin-change-me)
S=\$(echo \"\$R\" | sed -n 's/.*\"access_token\":\"\\([^\"]*\\)\".*/\\1/p' \
| cut -d. -f2 | sed 's/\$/==/' | base64 -d 2>/dev/null \
| sed -n 's/.*\"sid\":\"\\([^\"]*\\)\".*/\\1/p')
[ -n \"\$S\" ] && echo \"\$S\" >> /tmp/sids
i=\$((i+1))
done" >/dev/null 2>&1 &
LOOP=$!
# 3. 진짜 크래시 — 백엔드 프로세스에 SIGKILL
kubectl -n keycloak-lab exec deploy/postgres -- \
@@ -306,9 +353,14 @@ kubectl -n keycloak-lab exec deploy/postgres -- \
# 4. 주입이 걸렸는지 확인 — 이게 없으면 결과를 해석하지 않는다
kubectl -n keycloak-lab logs deploy/postgres | grep -E "not properly shut down|redo"
# 5. 대조
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
"select count(*) from offline_user_session where user_session_id in (<sid 목록>)"
# 5. 대조 — 클라이언트가 200 을 받은 sid 를 그대로 IN 절로 만든다
kill $LOOP 2>/dev/null
kubectl -n keycloak-lab exec a2-probe -- cat /tmp/sids > /tmp/sids.txt
TOTAL=$(wc -l < /tmp/sids.txt)
IN=$(sed "s/^/'/; s/$/'/" /tmp/sids.txt | paste -sd,)
FOUND=$(kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
"select count(*) from offline_user_session where offline_flag='0' and user_session_id in ($IN)")
echo "클라이언트 성공 $TOTAL / DB 존재 $FOUND / 유실 $((TOTAL-FOUND))"
```
---
+31 -2
View File
@@ -24,7 +24,7 @@
| 외부 응답 | **503** | **000** (연결 자체가 안 됨) |
| `kubectl` | 정상 | **불통** |
| 살아 있는 워크로드 | keycloak-1 (하지만 DB 없음) | **keycloak-0 은 계속 돌고 있다** |
| 복구 시간 | **60초** | **60초** |
| **`virsh start` 이후** 복구 | **60초** | **60초** |
**둘 다 전면 장애**지만 이유가 다르다. 4a 는 **DB 가 같이 죽어서**, 4b 는
**들어갈 길이 없어서**다.
@@ -204,6 +204,15 @@ virsh start kc-lab-2
**60초.** 사람 개입 없이 전부 제자리로 돌아왔다.
> **이 60초는 MTTR 이 아니다.** `virsh start` 를 친 뒤의 시간이며,
> 실제 장애 구간은 **12:07:43(차단) → 12:17:31(서비스 복귀) ≈ 10분**이다.
> 그 대부분은 내가 관찰하며 보낸 시간이고, **사람이 알아채고 결정하는 시간이
> 복구 시간의 대부분**이라는 점이 오히려 현실적이다.
>
> 그리고 본문의 `40초`(node-monitor-grace-period)와 `5분`(tolerationSeconds)은
> **쿠버네티스 기본값을 인용한 것**이며, 관측된 전이 시점(+45초, +270초)이
> 그 값과 모순되지 않는다는 것까지가 이 실험이 말할 수 있는 범위다.
---
## 4b. 컨트롤 플레인 노드 상실 (`kc-lab-1`)
@@ -358,6 +367,26 @@ virsh start kc-lab-1
---
---
## 증거 파일
**증거 수집 시각: 2026-09-04 12:05 12:23 KST** (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)
| 파일 | 종류 |
|---|---|
| [`01-baseline.txt`](evidence/a4-node-loss/01-baseline.txt) | 터미널 원문 |
| [`02-worker-node-killed.txt`](evidence/a4-node-loss/02-worker-node-killed.txt) | 터미널 원문 |
| [`03-state-during-loss.txt`](evidence/a4-node-loss/03-state-during-loss.txt) | 터미널 원문 |
| [`04-eviction-timing.txt`](evidence/a4-node-loss/04-eviction-timing.txt) | 터미널 원문 |
| [`05-recovery.txt`](evidence/a4-node-loss/05-recovery.txt) | 터미널 원문 |
| [`06-control-plane-inventory.txt`](evidence/a4-node-loss/06-control-plane-inventory.txt) | 터미널 원문 |
| [`07-control-plane-loss.txt`](evidence/a4-node-loss/07-control-plane-loss.txt) | 터미널 원문 |
| [`08-control-plane-recovery.txt`](evidence/a4-node-loss/08-control-plane-recovery.txt) | 터미널 원문 |
| [`a4-up-dropped-per-node.png`](evidence/a4-node-loss/a4-up-dropped-per-node.png) | 스크린샷 |
파일별 상세는 [`evidence/a4-node-loss/README.md`](evidence/a4-node-loss/README.md).
## 6. 재현 절차 (명령어)
```bash
@@ -375,7 +404,7 @@ kubectl get node kc-lab-2 # 40초 뒤 NotReady
kubectl -n keycloak-lab get pods -o wide # Running 인 채로 얼어 있다
kubectl get node kc-lab-2 -o jsonpath='{.spec.taints}'
# 5분 뒤 Terminating + 새 파드 Pending
kubectl -n keycloak-lab describe pod <new-pod> | grep -A4 Events
kubectl -n keycloak-lab describe pod $(kubectl -n keycloak-lab get pods -l app=postgres --field-selector=status.phase=Pending -o jsonpath='{.items[0].metadata.name}') | grep -A4 Events
# 4b 컨트롤 플레인 상실 — kubectl 이 죽으므로 노드에서 직접 본다
virsh destroy kc-lab-1
+33 -3
View File
@@ -137,6 +137,16 @@ kubectl -n keycloak-lab logs keycloak-0 --since=20m | grep ISPN000094 | awk '$2
suspected = 0
```
> **맥락 하나가 빠져 있었다** —
> [`06-view-history-and-cleanup.txt`](evidence/a5-asymmetric-partition/06-view-history-and-cleanup.txt)
> 를 보면 뷰 13 은 **주입(03:33:58)보다 9초 앞선 03:33:49 의 `MergeView`** 로
> 만들어졌고, 그 직전에는 `|12] (1)` — 즉 **막 분단됐다가 합쳐진 직후**였다.
> `merge_events = 1.0` 도 그 병합의 것이다.
>
> **"주입 전부터 그대로" 는 맞지만, 그 "전" 이 9초였다.**
> 앞선 실패한 주입 시도들이 만든 흔들림이고, 주입 이후 뷰가 변하지 않았다는
> 결론 자체는 유지된다.
### 왜 안 갈라졌는가 — **연결 방향이 뒤집혔다**
```
@@ -281,6 +291,26 @@ JGroups 코디네이터는 **가장 오래된 멤버**다. 분단이 나면
---
---
## 증거 파일
**증거 수집 시각: 2026-09-04 12:29 16:34 KST** (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)
| 파일 | 종류 |
|---|---|
| [`01-injection.txt`](evidence/a5-asymmetric-partition/01-injection.txt) | 터미널 원문 |
| [`02-injection-verify.txt`](evidence/a5-asymmetric-partition/02-injection-verify.txt) | 터미널 원문 |
| [`03-raw-table-injection.txt`](evidence/a5-asymmetric-partition/03-raw-table-injection.txt) | 터미널 원문 |
| [`04-correct-direction.txt`](evidence/a5-asymmetric-partition/04-correct-direction.txt) | 터미널 원문 |
| [`05-reconnect-observed.txt`](evidence/a5-asymmetric-partition/05-reconnect-observed.txt) | 터미널 원문 |
| [`06-view-history-and-cleanup.txt`](evidence/a5-asymmetric-partition/06-view-history-and-cleanup.txt) | 터미널 원문 |
| [`07-bidirectional-block.txt`](evidence/a5-asymmetric-partition/07-bidirectional-block.txt) | 터미널 원문 |
| [`08-coordinator-and-recovery.txt`](evidence/a5-asymmetric-partition/08-coordinator-and-recovery.txt) | 터미널 원문 |
| [`a5-cluster-size-bidirectional-block.png`](evidence/a5-asymmetric-partition/a5-cluster-size-bidirectional-block.png) | 스크린샷 |
파일별 상세는 [`evidence/a5-asymmetric-partition/README.md`](evidence/a5-asymmetric-partition/README.md).
## 6. 재현 절차 (명령어)
```bash
@@ -288,14 +318,14 @@ JGroups 코디네이터는 **가장 오래된 멤버**다. 분단이 나면
ssh kc-lab-1 'sudo conntrack -L | grep 7800'
# 2. 수신측 노드의 raw PREROUTING 에 넣는다 (filter 는 CNI 와 경쟁한다)
ssh kc-lab-1 'sudo iptables -t raw -I PREROUTING 1 -p tcp -d <수신 파드IP> --dport 7800 -j DROP'
ssh kc-lab-1 'sudo iptables -t raw -I PREROUTING 1 -p tcp -d <수신 파드IP> --dport 57800 -j DROP'
ssh kc-lab-1 'sudo iptables -t raw -I PREROUTING 1 -p tcp -d $(kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}') --dport 7800 -j DROP'
ssh kc-lab-1 'sudo iptables -t raw -I PREROUTING 1 -p tcp -d $(kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}') --dport 57800 -j DROP'
# 3. 걸렸는지 카운터로 확인 — 0 이면 해석 금지
ssh kc-lab-1 'sudo iptables -t raw -L PREROUTING -n -v'
# 4. 양방향으로 하려면 반대 노드에도 (한 방향만으로는 자가 치유된다)
ssh kc-lab-2 'sudo iptables -t raw -I PREROUTING 1 -p tcp -d <반대 파드IP> --dport 7800 -j DROP'
ssh kc-lab-2 'sudo iptables -t raw -I PREROUTING 1 -p tcp -d $(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}') --dport 7800 -j DROP'
# 5. 분단 확인
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
+69 -8
View File
@@ -130,10 +130,14 @@ qdisc netem 30: parent 1:3 limit 1000 delay 200ms
```
=== 두 노드 지연 비교 (기준선: k0=70ms k1=66ms) ===
keycloak-0 평균 41 ms 최대 57 ms ← 영향 없음
keycloak-0 평균 41 ms 최대 57 ms ← 기준선 70ms 대비 -41%
keycloak-1 평균 1872 ms 최대 1887 ms ← 28배
```
> **대조군도 변했다** — keycloak-0 은 기준선 70ms 에서 41ms 로 **41% 빨라졌다.**
> 주입과 무관한 변동(JIT 워밍업, 캐시)이며, **"영향 없음" 이라고 쓴 것은
> 부정확했다.** 다만 keycloak-1 의 28배 증가와는 자릿수가 달라 결론은 유지된다.
### 왜 200ms 가 1,872ms 가 되는가
A-0 에서 잡은 로그인 트랜잭션의 SQL 이 답이다.
@@ -156,6 +160,10 @@ COMMIT
200 ms × 9 왕복 ≈ 1,800 ms 실측 1,872 ms
```
> **9 는 SQL 목록을 센 것이고 패킷을 추적한 값이 아니다.** 자릿수가 맞는다는
> 것까지가 이 계산이 말할 수 있는 범위이며, **왕복 수를 확정하려면
> `tc -s` 나 패킷 캡처가 필요하다.**
> **네트워크 지연은 왕복 횟수만큼 증폭된다.**
> "DB 가 200ms 느려졌다"는 "애플리케이션이 200ms 느려졌다"가 아니다.
> **쿼리 수를 줄이는 것이 지연 환경에서 결정적인 이유**가 이것이다.
@@ -296,6 +304,23 @@ histogram_quantile(0.99, rate(http_server_requests_seconds_bucket[5m]))
---
---
## 증거 파일
**증거 수집 시각: 2026-09-04 13:12 13:16 KST** (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)
| 파일 | 종류 |
|---|---|
| [`01-baseline.txt`](evidence/a6-latency-injection/01-baseline.txt) | 터미널 원문 |
| [`02-delay-injected.txt`](evidence/a6-latency-injection/02-delay-injected.txt) | 터미널 원문 |
| [`03-flannel-injection.txt`](evidence/a6-latency-injection/03-flannel-injection.txt) | 터미널 원문 |
| [`04-pool-under-load.txt`](evidence/a6-latency-injection/04-pool-under-load.txt) | 터미널 원문 |
| [`05-recovery.txt`](evidence/a6-latency-injection/05-recovery.txt) | 터미널 원문 |
| [`a6-connection-pool-blocking.png`](evidence/a6-latency-injection/a6-connection-pool-blocking.png) | 스크린샷 |
파일별 상세는 [`evidence/a6-latency-injection/README.md`](evidence/a6-latency-injection/README.md).
## 8. 재현 절차 (명령어)
```bash
@@ -312,15 +337,51 @@ ssh kc-lab-2 '
# 3. 걸렸는지 카운터로 확인 — Sent 가 0 이면 해석 금지
ssh kc-lab-2 'sudo tc -s qdisc show dev flannel.1 | grep -A2 netem'
# 4. 단일 요청 지연 (대조군은 같은 노드의 keycloak-0)
kubectl -n keycloak-lab run t --rm -i --restart=Never --image=curlimages/curl:8.11.1 \
--command -- curl -s -o /dev/null -w '%{time_total}\n' -X POST http://<pod>:8080/realms/master/protocol/openid-connect/token ...
# 변수 준비 — 아래 명령들이 이 값을 쓴다
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
K1=$(kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
PW=$(kubectl -n keycloak-lab get secret keycloak-lab-secrets \
-o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d)
TE=/realms/master/protocol/openid-connect/token
# 5. 동시 부하로 풀 고갈 재현
# ( curl ... ) & 를 20개 띄우고 wait
# 4. 상주 탐침을 먼저 띄운다.
# ★ kubectl run --rm -i 로 20건을 동시에 돌리면 출력이 유실된다(실측: 20줄 중
# 일부만 도착하거나 아예 끊긴다). 결과는 파드 안 파일에 모으고 한 번에 cat 한다.
kubectl -n keycloak-lab run a6-probe --image=curlimages/curl:8.11.1 \
--restart=Never --command -- sleep 1800
kubectl -n keycloak-lab wait --for=condition=Ready pod/a6-probe --timeout=120s
# 6. 풀 지표
curl -s http://<pod>:9000/metrics | grep -E '^agroal_(blocking_time|max_used|awaiting)'
# 5. 단일 요청 지연 — 대조군(keycloak-0, DB 와 같은 노드) 대 시험군(keycloak-1)
kubectl -n keycloak-lab exec a6-probe -- sh -c "
for t in keycloak-0:$K0 keycloak-1:$K1; do
n=\${t%%:*}; ip=\${t#*:}; T=0; i=0
while [ \$i -lt 15 ]; do
D=\$(curl -s -o /dev/null -w %{time_total} -X POST http://\$ip:8080$TE \
-d grant_type=password -d client_id=admin-cli -d username=admin -d password=$PW)
T=\$(echo \"\$T \$D\" | awk '{print \$1+\$2}'); i=\$((i+1))
done
echo \"\$n 평균 \$(echo \$T | awk '{printf \"%.0f\", \$1*1000/15}') ms\"
done"
# 6. 동시 부하 20건 — & 로 띄우고 wait. 순차로 돌리면 풀 경합이 재현되지 않는다.
kubectl -n keycloak-lab exec a6-probe -- sh -c "
rm -f /tmp/load; i=0
while [ \$i -lt 20 ]; do
( curl -s -o /dev/null -w '%{http_code} %{time_total}\n' --max-time 60 \
-X POST http://$K1:8080$TE -d grant_type=password -d client_id=admin-cli \
-d username=admin -d password=$PW >> /tmp/load ) &
i=\$((i+1))
done
wait"
kubectl -n keycloak-lab exec a6-probe -- cat /tmp/load > /tmp/load.txt
wc -l /tmp/load.txt # 20 이 아니면 수집이 샌 것이다
awk '{print $1}' /tmp/load.txt | sort | uniq -c
awk '{print $2}' /tmp/load.txt | sort -g | tail -1 # 최대 지연
# 7. 풀 지표 — 부하 직후에 읽어야 blocking_time 이 남아 있다
kubectl -n keycloak-lab exec a6-probe -- sh -c \
"curl -s http://$K1:9000/metrics \
| grep -E '^agroal_(blocking_time_average|max_used_count|awaiting_count|active_count)'"
# 7. 해제
ssh kc-lab-2 'sudo tc qdisc del dev flannel.1 root'
+25
View File
@@ -9,6 +9,14 @@
---
## 구조
![A-7 구조 — 두 모드의 데이터 흐름과 뒤집힌 결과](diagrams/a7-volatile-inversion.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
---
## 0. 결론부터 — 비교표
| 실험 | persistent (KC 26 기본) | **volatile (KC 24 이전 방식)** |
@@ -225,6 +233,23 @@ kubectl apply -f deploy/lab/k8s/keycloak-cluster.yaml
---
---
## 증거 파일
**증거 수집 시각: 2026-09-04 13:22 13:32 KST** (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)
| 파일 | 종류 |
|---|---|
| [`01-switch-to-volatile.txt`](evidence/a7-volatile-comparison/01-switch-to-volatile.txt) | 터미널 원문 |
| [`02-a0-rerun.txt`](evidence/a7-volatile-comparison/02-a0-rerun.txt) | 터미널 원문 |
| [`03-a8-rerun-restart.txt`](evidence/a7-volatile-comparison/03-a8-rerun-restart.txt) | 터미널 원문 |
| [`04-a1-rerun-partition.txt`](evidence/a7-volatile-comparison/04-a1-rerun-partition.txt) | 터미널 원문 |
| [`05-a2-rerun-db-loss.txt`](evidence/a7-volatile-comparison/05-a2-rerun-db-loss.txt) | 터미널 원문 |
| [`06-restore-persistent.txt`](evidence/a7-volatile-comparison/06-restore-persistent.txt) | 터미널 원문 |
파일별 상세는 [`evidence/a7-volatile-comparison/README.md`](evidence/a7-volatile-comparison/README.md).
## 8. 재현 절차 (명령어)
```bash
+67 -6
View File
@@ -56,7 +56,14 @@ statefulset.apps/keycloak restarted
200 200 200 200 partitioned roll out complete: 2 new pods have been updated...
```
**9번 찍어서 9번 다 `200`.** 한 번도 끊기지 않았다.
**9번 찍어서 9번 다 `200`.**
> **표본은 9개다.** 5초 간격으로 찍었으므로 **5초보다 짧은 끊김은 이 측정으로
> 잡히지 않는다.** 실제로 후속 작업에서 1초 간격·3초 타임아웃으로 재보니
> 롤백 전환 순간에 `000` 이 한 번 잡혔다
> ([`followup`](experiment-followup-untested-items.md) 2절).
> **"무중단" 은 관측 해상도에 달려 있으며, 여기서는 "5초 해상도에서 끊김이
> 관측되지 않았다" 까지가 정확한 서술이다.**
### 왜 무중단이 되는가
@@ -137,6 +144,46 @@ readiness 프로브가 이 전환을 정확히 맞춰준다 — A-2 에서 본
---
---
## 개념
### StatefulSet 롤링 재시작의 무중단 조건
```
한 번에 하나씩 내린다 + readiness 로 전환 시점을 맞춘다
└─ 항상 최소 하나는 Ready 다
```
**두 가지가 다 있어야 성립한다.** replica 1 이면 반드시 끊기고,
readiness 프로브가 없으면 아직 기동 중인 파드로 트래픽이 간다.
### 룩어사이드 캐시가 재시작을 견디는 이유
| | 재시작 후 |
|---|---|
| 캐시 (프로세스 메모리) | **사라진다** |
| DB (진실의 원천) | 남는다 |
| 정확성 | **유지된다** — 첫 접근만 느려진다 |
A-0 에서 세운 모델이 여기서 그대로 확인된다.
---
---
## 증거 파일
**증거 수집 시각: 2026-09-04 13:19 13:20 KST** (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)
| 파일 | 종류 |
|---|---|
| [`01-restart-availability.txt`](evidence/a8-rolling-restart/01-restart-availability.txt) | 터미널 원문 |
| [`02-session-survival.txt`](evidence/a8-rolling-restart/02-session-survival.txt) | 터미널 원문 |
| [`a8-cache-reset-cluster-reformed.png`](evidence/a8-rolling-restart/a8-cache-reset-cluster-reformed.png) | 스크린샷 |
파일별 상세는 [`evidence/a8-rolling-restart/README.md`](evidence/a8-rolling-restart/README.md).
## 6. 재현 절차 (명령어)
```bash
@@ -145,10 +192,23 @@ kubectl -n keycloak-lab run a8-probe --image=curlimages/curl:8.11.1 \
--restart=Never --command -- sleep 3600
kubectl -n keycloak-lab wait --for=condition=Ready pod/a8-probe --timeout=120s
# 로그인하고 토큰 보관
# 변수 준비
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
PW=$(kubectl -n keycloak-lab get secret keycloak-lab-secrets \
-o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d)
# 로그인하고 refresh token 을 /tmp/rt 에, sid 를 /tmp/sid 에 보관.
# ★ 아래 5번이 읽는 파일과 같은 이름이어야 한다 — 처음 문서는 /tmp/tok 에 쓰고
# /tmp/rt 를 읽어서 빈 문자열을 보냈고, 그래도 400 이 아니라 통과한 것처럼 보였다.
kubectl -n keycloak-lab exec a8-probe -- sh -c \
'curl -s -X POST http://<pod>:8080/realms/master/protocol/openid-connect/token \
-d grant_type=password -d client_id=admin-cli -d username=admin -d password=<pw> > /tmp/tok'
"curl -s -X POST http://$K0:8080/realms/master/protocol/openid-connect/token \
-d grant_type=password -d client_id=admin-cli -d username=admin -d password=$PW > /tmp/tok
sed -n 's/.*\"refresh_token\":\"\\([^\"]*\\)\".*/\\1/p' /tmp/tok > /tmp/rt
sed -n 's/.*\"access_token\":\"\\([^\"]*\\)\".*/\\1/p' /tmp/tok | cut -d. -f2 \
| sed 's/\$/==/' | base64 -d 2>/dev/null \
| sed -n 's/.*\"sid\":\"\\([^\"]*\\)\".*/\\1/p' > /tmp/sid
echo \"rt \$(wc -c < /tmp/rt) bytes / sid \$(cat /tmp/sid)\""
# 재시작 + 가용성 감시
kubectl -n keycloak-lab rollout restart statefulset/keycloak
@@ -156,12 +216,13 @@ kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=420s
# 세션 생존 확인
kubectl -n keycloak-lab exec a8-probe -- sh -c \
'curl -s -o /dev/null -w "%{http_code}\n" -X POST http://<pod>:8080/realms/master/protocol/openid-connect/token \
'curl -s -o /dev/null -w "%{http_code}\n" -X POST http://'$K0':8080/realms/master/protocol/openid-connect/token \
-d grant_type=refresh_token -d client_id=admin-cli -d refresh_token=$(cat /tmp/rt)'
# DB 대조
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "select user_session_id, created_on, last_session_refresh from offline_user_session where user_session_id='<sid>'"
-c "select user_session_id, created_on, last_session_refresh from offline_user_session
where offline_flag='0' and user_session_id='$(kubectl -n keycloak-lab exec a8-probe -- cat /tmp/sid)'"
```
---
+57 -3
View File
@@ -105,7 +105,7 @@ Bad Gateway
파드 안에서 직접 받아 해결했다. **alpine 기반 JRE 이미지에 `wget` 이 있다.**
```bash
kubectl -n keycloak-lab exec <bff-pod> -- wget -qO- http://localhost:8083/actuator/beans
kubectl -n keycloak-lab exec $(kubectl -n keycloak-lab get pod -l app=bff --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') -- wget -qO- http://localhost:8083/actuator/beans
```
---
@@ -129,7 +129,7 @@ kubectl -n keycloak-lab exec <bff-pod> -- wget -qO- http://localhost:8083/actuat
```bash
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh config credentials \
--server http://localhost:8080 --realm master --user admin --password <pw>
--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)"
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh create realms \
-s realm=keycloak-patterns -s enabled=true -s accessTokenLifespan=60
@@ -245,6 +245,60 @@ kubectl -n keycloak-lab scale deployment/bff --replicas=1
---
---
## 개념
### `AuthenticatedPrincipalOAuth2AuthorizedClientRepository`
이름이 곧 설명이다 — **인증된 주체(principal) 기준**으로 authorized client 를 찾는다.
```
인증되어 있으면 → OAuth2AuthorizedClientService 에 위임
키: (clientRegistrationId, principalName)
└─ session ID 가 없다 ★
인증되지 않았으면 → HttpSession 에 임시 보관
```
**같은 사용자의 두 브라우저가 같은 항목을 본다.** Q1 미지수 3 과 Q3 제약의 기제다.
### 인가 코드 흐름은 왕복이 두 번이다
```
① 브라우저 → 앱 → IdP 로 리다이렉트 (state·PKCE verifier 를 저장)
② IdP → 브라우저 → 앱의 콜백 (저장한 것을 꺼내 검증)
```
**②가 ①과 같은 인스턴스로 가야 한다.** 저장 위치가 인스턴스 메모리면
replica 를 늘리는 순간 로그인 자체가 실패한다.
### 자동구성은 조용히 고른다
빈을 직접 만들지 않으면 Spring Boot 가 조건에 따라 고른다.
**무엇을 골랐는지는 실행 중인 인스턴스를 봐야 안다.**
```bash
kubectl -n keycloak-lab exec $(kubectl -n keycloak-lab get pod -l app=bff --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') -- wget -qO- http://localhost:8083/actuator/beans
```
---
---
## 증거 파일
**증거 수집 시각: 2026-09-04 13:39 13:46 KST** (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)
| 파일 | 종류 |
|---|---|
| [`01-deploy.txt`](evidence/b0-bff-redis-deploy/01-deploy.txt) | 터미널 원문 |
| [`02-autoconfiguration.txt`](evidence/b0-bff-redis-deploy/02-autoconfiguration.txt) | 터미널 원문 |
| [`03-beans-analysis.txt`](evidence/b0-bff-redis-deploy/03-beans-analysis.txt) | 터미널 원문 |
| [`b0-bff-login-success-single-replica.png`](evidence/b0-bff-redis-deploy/b0-bff-login-success-single-replica.png) | 스크린샷 |
| [`b0-bff-token-boundary.png`](evidence/b0-bff-redis-deploy/b0-bff-token-boundary.png) | 스크린샷 |
파일별 상세는 [`evidence/b0-bff-redis-deploy/README.md`](evidence/b0-bff-redis-deploy/README.md).
## 6. 재현 절차 (명령어)
```bash
@@ -266,7 +320,7 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh create rea
kubectl apply -f deploy/lab/k8s/bff-redis.yaml
# 6. 자동구성 결과 — 파드 안에서 (프록시는 큰 응답에서 502)
kubectl -n keycloak-lab exec <bff-pod> -- wget -qO- http://localhost:8083/actuator/beans > beans.json
kubectl -n keycloak-lab exec $(kubectl -n keycloak-lab get pod -l app=bff --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') -- wget -qO- http://localhost:8083/actuator/beans > beans.json
python3 -c "import json;d=json.load(open('beans.json'));[print(n,'->',i['type']) for n,i in
list(d['contexts'].values())[0]['beans'].items() if 'AuthorizedClient' in i['type']]"
```
+29 -4
View File
@@ -10,6 +10,15 @@
---
## 구조
![B-1 구조 — 세션만 Redis 로, 토큰은 프로세스 메모리에](diagrams/b1-store-split.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터
| | before | after | |
@@ -113,7 +122,7 @@ value: http://echo.header-lab.svc:8081
## 2. 자동구성이 실제로 바뀌었는가 — B-0 의 방법을 다시 쓴다
```bash
kubectl -n keycloak-lab exec <bff-pod> -- wget -qO- http://localhost:8083/actuator/beans
kubectl -n keycloak-lab exec $(kubectl -n keycloak-lab get pod -l app=bff --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') -- wget -qO- http://localhost:8083/actuator/beans
```
```
@@ -260,6 +269,22 @@ Q3 는 *"저장소를 직접 열어 refresh token 이 평문으로 남는지 확
---
---
## 증거 파일
**증거 수집 시각: 2026-09-04 13:59 14:03 KST** (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)
| 파일 | 종류 |
|---|---|
| [`01-servicelinks-trap.txt`](evidence/b1-redis-session-store/01-servicelinks-trap.txt) | 터미널 원문 |
| [`02-autoconfig-after.txt`](evidence/b1-redis-session-store/02-autoconfig-after.txt) | 터미널 원문 |
| [`03-redis-contents.txt`](evidence/b1-redis-session-store/03-redis-contents.txt) | 터미널 원문 |
| [`b1-login-works-two-replicas.png`](evidence/b1-redis-session-store/b1-login-works-two-replicas.png) | 스크린샷 |
| [`b1-token-boundary-after-redis.png`](evidence/b1-redis-session-store/b1-token-boundary-after-redis.png) | 스크린샷 |
파일별 상세는 [`evidence/b1-redis-session-store/README.md`](evidence/b1-redis-session-store/README.md).
## 7. 재현 절차 (명령어)
```bash
@@ -272,14 +297,14 @@ Q3 는 *"저장소를 직접 열어 refresh token 이 평문으로 남는지 확
kubectl apply -f deploy/lab/k8s/bff-redis.yaml
# 4. 자동구성이 실제로 바뀌었는지 확인 (B-0 의 방법)
kubectl -n keycloak-lab exec <bff-pod> -- wget -qO- http://localhost:8083/actuator/beans > after.json
kubectl -n keycloak-lab exec $(kubectl -n keycloak-lab get pod -l app=bff --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') -- wget -qO- http://localhost:8083/actuator/beans > after.json
# sessionRepository 가 RedisSessionRepository 인가
# authorizedClientService 는 여전히 InMemory 인가 ← 이쪽이 핵심
# 5. Redis 를 직접 연다
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan
kubectl -n keycloak-lab exec deploy/redis -- redis-cli hkeys "bff:session:sessions:<id>"
kubectl -n keycloak-lab exec deploy/redis -- redis-cli ttl "bff:session:sessions:<id>"
kubectl -n keycloak-lab exec deploy/redis -- redis-cli hkeys "bff:session:sessions:$(kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern 'bff:session:sessions:*' | grep -v expires | head -1 | sed 's/.*://')"
kubectl -n keycloak-lab exec deploy/redis -- redis-cli ttl "bff:session:sessions:$(kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern 'bff:session:sessions:*' | grep -v expires | head -1 | sed 's/.*://')"
# 6. 사용자 관점 확인
# 브라우저로 https://app1.hyeonworks.com/bff/token-boundary
+68 -1
View File
@@ -182,6 +182,14 @@ access_token 헤더 : {"alg":"RS256","typ":"JWT","kid":"OY-caYDNGoP4HMAz-..."}
같은 사용자로 다시 로그인시키고 행을 비교했다.
> **실제로는 브라우저를 두 개 쓰지 않았다.** 증거
> [`04-overwrite-test.txt`](evidence/b2-multi-instance-session/04-overwrite-test.txt)
> 에 `[모의 두 번째 브라우저] 세션만 지우고` 라고 적혀 있다.
> **세션을 지우고 같은 사용자로 다시 로그인시킨 것**이며, 조회 키가
> `(clientRegistrationId, principalName)` 이므로 브라우저가 둘이든 하나든
> **같은 행을 쓴다는 점에서 등가**다. 다만 "두 브라우저에서" 라고 쓴 것은
> 측정하지 않은 것을 측정한 것처럼 적은 것이다.
```
=== 재로그인 전 ===
principal_name | access_token_issued_at | at_md5
@@ -271,6 +279,65 @@ access_token 헤더 : {"alg":"RS256","typ":"JWT","kid":"OY-caYDNGoP4HMAz-..."}
---
---
## 개념
### 조회 키는 저장소와 독립이다
```sql
PRIMARY KEY (client_registration_id, principal_name)
```
**저장소를 메모리에서 DB 로 옮겨도 이 키는 그대로다.**
"공유 저장소로 바꾼다" 와 "세션별로 구분한다" 는 다른 문제이며,
전자만 하면 인스턴스 간 공유는 되고 브라우저 간 격리는 안 된다.
### 스키마 DDL 의 방언 차이
> **정정** — 이 절의 제목은 처음에 "Liquibase 스키마의 방언 차이" 였다.
> **Liquibase 가 아니다.** 여기서 스키마를 태우는 것은 Spring Boot 의
> `spring.sql.init` 이고, DDL 은 `spring-security-oauth2-client` jar 가
> 번들한 파일이다. (Liquibase 는 Keycloak 이 자기 스키마에 쓰며, D-2 의 주제다.)
Spring Security 는 DDL 을 두 벌 제공한다.
| 파일 | 타입 |
|---|---|
| `oauth2-client-schema.sql` | `blob` — PostgreSQL 에 **없는 타입** |
| `oauth2-client-schema-postgres.sql` | `bytea` |
`spring.sql.init.continue-on-error: true` 는 이 실패를 삼킨다.
**"없어도 되는 초기화" 에만 써야 하는 이유다.**
### 로그아웃이 지워야 하는 것은 셋이다
```
① HttpSession (Spring Security 가 지운다)
② OAuth2AuthorizedClient ★ 아무도 안 지운다
③ IdP SSO 세션 ★ RP-initiated logout 을 보내야 한다
```
---
---
## 증거 파일
**증거 수집 시각: 2026-09-04 14:09 14:13 KST** (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)
| 파일 | 종류 |
|---|---|
| [`01-jdbc-store-deploy.txt`](evidence/b2-multi-instance-session/01-jdbc-store-deploy.txt) | 터미널 원문 |
| [`02-schema.txt`](evidence/b2-multi-instance-session/02-schema.txt) | 터미널 원문 |
| [`03-plaintext-tokens.txt`](evidence/b2-multi-instance-session/03-plaintext-tokens.txt) | 터미널 원문 |
| [`04-overwrite-test.txt`](evidence/b2-multi-instance-session/04-overwrite-test.txt) | 터미널 원문 |
| [`05-logout-cleanup.txt`](evidence/b2-multi-instance-session/05-logout-cleanup.txt) | 터미널 원문 |
| [`b2-before-relogin.png`](evidence/b2-multi-instance-session/b2-before-relogin.png) | 스크린샷 |
| [`b2-tokens-shared-across-instances.png`](evidence/b2-multi-instance-session/b2-tokens-shared-across-instances.png) | 스크린샷 |
파일별 상세는 [`evidence/b2-multi-instance-session/README.md`](evidence/b2-multi-instance-session/README.md).
## 8. 재현 절차 (명령어)
```bash
@@ -278,7 +345,7 @@ access_token 헤더 : {"alg":"RS256","typ":"JWT","kid":"OY-caYDNGoP4HMAz-..."}
# + spring-boot-starter-jdbc, postgresql 의존성
# 2. 스키마 — PostgreSQL 판본을 써야 한다
kubectl -n keycloak-lab exec <bff-pod> -- sh -c \
kubectl -n keycloak-lab exec $(kubectl -n keycloak-lab get pod -l app=bff --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') -- sh -c \
'unzip -p /app/app.jar BOOT-INF/lib/spring-security-oauth2-client-*.jar' > /dev/null
# 실제로는 nested jar 를 풀어서 -postgres.sql 을 꺼낸다
kubectl -n keycloak-lab exec -i deploy/postgres -- psql -U keycloak -d keycloak < oauth2-pg.sql
+85 -8
View File
@@ -12,6 +12,15 @@
---
## 구조
![B-3 구조 — 동시 refresh 가 client session 을 제거한다](diagrams/b3-refresh-contention.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터
**"하나는 성공하고 하나는 실패한다"가 아니다. 세션이 파괴된다.**
@@ -66,6 +75,21 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
---
---
## 증거 파일
**증거 수집 시각: 2026-09-04 14:16 14:17 KST** (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)
| 파일 | 종류 |
|---|---|
| [`01-concurrent-refresh.txt`](evidence/b3-refresh-contention/01-concurrent-refresh.txt) | 터미널 원문 |
| [`02-session-impact.txt`](evidence/b3-refresh-contention/02-session-impact.txt) | 터미널 원문 |
| [`03-client-session-removed.txt`](evidence/b3-refresh-contention/03-client-session-removed.txt) | 터미널 원문 |
| [`04-policy-comparison.txt`](evidence/b3-refresh-contention/04-policy-comparison.txt) | 터미널 원문 |
파일별 상세는 [`evidence/b3-refresh-contention/README.md`](evidence/b3-refresh-contention/README.md).
## 2. 재현 — 진짜 동시성을 만든다
B-2 에서 토큰이 PostgreSQL 로 공유되므로 두 replica 가 같은 항목을 본다.
@@ -100,7 +124,7 @@ DB 로 확인했다.
select us.user_session_id,
(select count(*) from offline_client_session cs
where cs.user_session_id = us.user_session_id) as client_sessions
from offline_user_session us where us.user_session_id = '<sid>';
from offline_user_session us where us.user_session_id = '$SID';
```
```
@@ -231,6 +255,44 @@ select VERSION from OFFLINE_USER_SESSION ... for no key update skip locked
---
---
## 개념
### user session 과 client session
```
user session "이 브라우저는 labuser 로 로그인함"
├─ client session : bff-confidential
└─ client session : oauth2-proxy
```
**재사용 탐지는 client session 만 제거한다.** user session 은 껍데기로 남아
`Session doesn't have required client` 가 된다.
### `revokeRefreshToken` 과 `refreshTokenMaxReuse`
| 설정 | 뜻 |
|---|---|
| `revokeRefreshToken` | **회전 스위치.** 켜면 새 토큰 발급 시 옛 토큰을 무효화 |
| `refreshTokenMaxReuse` | 그 위에서 **몇 번까지 봐줄 것인가** |
**이름이 "회전" 이 아니라 "취소" 라서 헷갈린다.**
그리고 `maxReuse` 를 올리는 것은 해법이 아니다 — 동시 요청이 N개면
`N-1` 이 필요하고, 그러면 회전의 보안 목적이 사라진다.
### lock 의 수명은 어디에 묶이는가
| 방식 | 프로세스가 죽으면 |
|---|---|
| **DB 행 잠금** | **연결이 끊기면 자동 해제** |
| Redis lock + TTL | TTL 만료까지 막힌다 |
**잠금 수명이 연결 수명과 묶이는 것이 DB 잠금의 이점**이며,
A-0 에서 Keycloak 자신이 `for no key update skip locked` 를 쓰는 이유다.
---
## 7. 재현 절차 (명령어)
```bash
@@ -238,22 +300,37 @@ select VERSION from OFFLINE_USER_SESSION ... for no key update skip locked
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
update realms/keycloak-patterns -s revokeRefreshToken=true -s refreshTokenMaxReuse=0
# 2. refresh token 하나 확보 (direct grant)
curl -s -X POST $KC -d grant_type=password -d client_id=bff-confidential \
-d client_secret=bff-lab-secret -d username=labuser -d password=labpass -d scope=openid
# 2. refresh token 하나 확보 (direct grant). 이후 단계가 전부 이 변수들을 쓴다.
KC=http://keycloak.keycloak-lab.svc:8080/realms/keycloak-patterns/protocol/openid-connect/token
R=$(curl -s -X POST $KC -d grant_type=password -d client_id=bff-confidential \
-d client_secret=bff-lab-secret -d username=labuser -d password=labpass -d scope=openid)
RT=$(echo "$R" | sed -n 's/.*"refresh_token":"\([^"]*\)".*/\1/p')
SID=$(echo "$R" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p' | cut -d. -f2 \
| sed 's/$/==/' | base64 -d 2>/dev/null | sed -n 's/.*"sid":"\([^"]*\)".*/\1/p')
echo "SID=$SID"
# 3. 동시에 5개 — & 와 wait 이 없으면 재현되지 않는다
i=1; while [ $i -le 5 ]; do ( curl ... -d refresh_token=$RT > /tmp/c$i ) & i=$((i+1)); done; wait
# 3. 동시에 5개 — & 와 wait 이 없으면 재현되지 않는다(순차로는 경합이 생기지 않는다)
i=1
while [ $i -le 5 ]; do
( curl -s -o /tmp/c$i -w '%{http_code}\n' -X POST $KC \
-d grant_type=refresh_token -d client_id=bff-confidential \
-d client_secret=bff-lab-secret -d refresh_token=$RT ) &
i=$((i+1))
done
wait
# 4. ★ 이긴 요청의 토큰을 다시 써본다 — 여기서 진짜 답이 나온다
curl -s -o /dev/null -w '%{http_code}' -X POST $KC -d grant_type=refresh_token -d refresh_token=$NEW
NEW=$(cat /tmp/c1 /tmp/c2 /tmp/c3 /tmp/c4 /tmp/c5 \
| sed -n 's/.*"refresh_token":"\([^"]*\)".*/\1/p' | head -1)
curl -s -o /dev/null -w '%{http_code}\n' -X POST $KC -d grant_type=refresh_token \
-d client_id=bff-confidential -d client_secret=bff-lab-secret -d refresh_token=$NEW
# 5. 기제 확인 — client session 이 지워졌는지
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c \
"select us.user_session_id,
(select count(*) from offline_client_session cs
where cs.user_session_id = us.user_session_id) as client_sessions
from offline_user_session us where us.user_session_id = '<sid>'"
from offline_user_session us where us.user_session_id = '$SID'"
# 6. 정책 비교 — revokeRefreshToken 과 refreshTokenMaxReuse 를 바꿔가며 3~5 반복
```
+65 -2
View File
@@ -10,6 +10,15 @@
---
## 구조
![B-4 구조 — 설정하지 않은 헤더는 통과한다](diagrams/b4-header-forgery.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터
| Q4 의 미지수 | 측정 결과 |
@@ -204,14 +213,68 @@ Keycloak 의 role 이름은 임의 문자열이므로 **막을 수 있는 것이
| 항목 | 상태 |
|---|---|
| ③ role 변경 반영 시점 | **측정.** oauth2-proxy 가 없어 "proxy session" 이 존재하지 않는다 |
| ③ role 변경 반영 시점 | **측정 완료 →** [후속 문서 §3](experiment-followup-untested-items.md). IdP 에서 값을 바꿔도 **12회 요청·6초 동안 옛 값**, 세션 삭제 후 재인증에서야 새 값 |
| ⑤ internal token 을 공통 경계로 이동 | **코드 변경.** `backend/` 의 SecurityConfig 에서 `permitAll` 경로를 좁히고 Filter 로 옮기는 작업 |
| edge 에서 동명 헤더 덮어쓰기 | **nginx 설정 변경 필요**`proxy_set_header X-Auth-Request-Roles ""` 로 먼저 지우고 다시 설정 |
**③ 은 oauth2-proxy 배포가 선행이, 그것 B-7 의 주제와 겹다.**
**③ 은 oauth2-proxy 배포가 선행이었고, 그것 B-7 의 주제와 겹다.**
B-7 에서 oauth2-proxy 를 올린 뒤 후속 작업으로 측정했다 — 결론은
**"요청 횟수와 무관하다. 세션이 새로 만들어져야 한다"** 이다.
세션은 로그인 시점의 스냅샷이고, `--cookie-refresh` 가 없으면 갱신되지 않는다.
---
---
## 개념
### nginx 의 헤더 처리는 조건부다
```nginx
proxy_set_header X-Forwarded-Proto https; # 설정한 것 → 덮어쓴다
# X-Auth-Request-Roles 설정 없음 # 안 한 것 → 통과시킨다
```
HTTP 는 **같은 이름의 헤더가 여러 번 오는 것을 허용**하므로,
edge 가 붙인 것과 클라이언트가 보낸 것이 **함께 도착**한다.
Spring 의 `request.getHeader()` 는 **첫 번째**를 돌려주고,
그 순서는 프록시가 정한다.
### 헤더 크기 한계는 계층마다 다르다
| 크기 | 누가 거부하나 | 클라이언트가 보는 것 |
|---|---|---|
| ~8KB | **Tomcat** (`maxHttpHeaderSize`) | `400` + HTML |
| ~16KB | **nginx** (`large_client_header_buffers`) | 응답 없음 |
**같은 원인이 두 가지로 보인다.** 그리고 점진적이 아니라 절벽이며,
**role 이 많은 사용자만** 깨진다.
### 세 곳이 독립적으로 필요하다
```
① 외부 → upstream 직접 경로 차단 (NetworkPolicy)
② edge 에서 동명 헤더 덮어쓰기 (proxy_set_header)
③ upstream 에서 내부 credential 검증 (공통 경계)
```
**하나라도 빠지면 나머지 둘이 무의미하다.** 2홉 실험의 결론이 그대로 적용되며,
거기서는 쿠키 속성이었지만 **여기서는 신원 자체**다.
---
---
## 증거 파일
**증거 수집 시각: 2026-09-04 14:23 14:23 KST** (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)
| 파일 | 종류 |
|---|---|
| [`01-header-handling.txt`](evidence/b4-edge-authorization/01-header-handling.txt) | 터미널 원문 |
파일별 상세는 [`evidence/b4-edge-authorization/README.md`](evidence/b4-edge-authorization/README.md).
## 7. 재현 절차 (명령어)
```bash
+26 -2
View File
@@ -8,6 +8,15 @@
---
## 구조
![B-5 구조 — readiness 그룹이 갈랐다](diagrams/b5-redis-loss.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터
| | 결과 |
@@ -214,6 +223,21 @@ appendfsync everysec ← 기본값
---
---
## 증거 파일
**증거 수집 시각: 2026-09-04 14:24 14:28 KST** (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)
| 파일 | 종류 |
|---|---|
| [`01-baseline.txt`](evidence/b5-redis-loss/01-baseline.txt) | 터미널 원문 |
| [`02-redis-down.txt`](evidence/b5-redis-loss/02-redis-down.txt) | 터미널 원문 |
| [`03-health-groups.txt`](evidence/b5-redis-loss/03-health-groups.txt) | 터미널 원문 |
| [`04-persistence.txt`](evidence/b5-redis-loss/04-persistence.txt) | 터미널 원문 |
파일별 상세는 [`evidence/b5-redis-loss/README.md`](evidence/b5-redis-loss/README.md).
## 5. 재현 절차 (명령어)
```bash
@@ -222,8 +246,8 @@ kubectl -n keycloak-lab scale deployment/redis --replicas=0
curl -s -o /dev/null -w '%{http_code}\n' https://app1.hyeonworks.com/bff/token-boundary # 000
# ② 왜 파드가 안 빠지는가 — 그룹별로 본다
kubectl -n keycloak-lab exec <bff-pod> -- wget -qO- http://localhost:8083/actuator/health
kubectl -n keycloak-lab exec <bff-pod> -- wget -qO- http://localhost:8083/actuator/health/readiness
kubectl -n keycloak-lab exec $(kubectl -n keycloak-lab get pod -l app=bff --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') -- wget -qO- http://localhost:8083/actuator/health
kubectl -n keycloak-lab exec $(kubectl -n keycloak-lab get pod -l app=bff --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') -- wget -qO- http://localhost:8083/actuator/health/readiness
kubectl -n keycloak-lab get endpoints bff -o jsonpath='{.subsets[*].addresses[*].ip}'
# ③ 복구
+24 -1
View File
@@ -12,6 +12,15 @@
---
## 구조
![B-6 구조 — 겹침 구간과 제거 시점](diagrams/b6-key-rotation.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터
**질문이 두 갈래로 나뉜다.**
@@ -194,6 +203,20 @@ kcadm.sh get components -r keycloak-patterns -q type=org.keycloak.keys.KeyProvid
---
---
## 증거 파일
**증거 수집 시각: 2026-09-04 14:30 14:32 KST** (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)
| 파일 | 종류 |
|---|---|
| [`01-before-rotation.txt`](evidence/b6-key-rotation/01-before-rotation.txt) | 터미널 원문 |
| [`02-rotation.txt`](evidence/b6-key-rotation/02-rotation.txt) | 터미널 원문 |
| [`03-old-key-removed.txt`](evidence/b6-key-rotation/03-old-key-removed.txt) | 터미널 원문 |
파일별 상세는 [`evidence/b6-key-rotation/README.md`](evidence/b6-key-rotation/README.md).
## 6. 재현 절차 (명령어)
```bash
@@ -214,7 +237,7 @@ curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $OLD" http://
kcadm.sh get components -r keycloak-patterns --fields id,name,providerId
# 5. 제거하고 다시 확인 — 여기서 401 이 나와야 정상이다
kcadm.sh delete components/<old-id> -r keycloak-patterns
kcadm.sh delete components/$(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"' | grep '"id"' | cut -d'"' -f4) -r keycloak-patterns
```
---
@@ -218,6 +218,58 @@ _oauth2_proxy-b26111fbd1fdab3ae2182e287001b02a ← ★ 옛 세션. 남아 있
---
---
## 개념
### 상태를 어디에 두는가가 공유 문제의 성격을 정한다
| | 상태 위치 | replica 간 공유 |
|---|---|---|
| BFF | **서버 메모리 / Redis** | **저장소를 공유해야** 한다 |
| oauth2-proxy | **쿠키 (서명·암호화)** | **secret 만 같으면** 된다 |
**공유할 상태가 없으면 공유 문제도 없다.** 대신 secret 이 단일 지점이 된다.
### 세션 티켓
`--session-store-type=redis` 를 쓰면 쿠키에는 **티켓**만 담긴다.
```
_oauth2_proxy=<ticket>|<timestamp>|<mac>
└─ Redis 키를 여기서 계산한다
```
**secret 이 바뀌면 티켓을 못 푼다 → Redis 키를 계산할 수 없다 →
정리도 못 한다.** 고아 세션이 남는 이유다.
### key 식별자가 없으면 회전에 겹침이 없다
B-6 에서 Keycloak 은 `kid` 로 여러 키를 구분해 무중단 회전을 했다.
**oauth2-proxy 의 쿠키에는 그런 식별자가 없고, `--cookie-secret` 도 단수다.**
```
식별자 있음 → 읽기는 여러 key, 쓰기는 하나 → 겹침 가능
식별자 없음 → 전부 한 번에 바뀐다 → 겹침 불가
```
---
---
## 증거 파일
**증거 수집 시각: 2026-09-04 14:35 14:42 KST** (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)
| 파일 | 종류 |
|---|---|
| [`01-deploy.txt`](evidence/b7-cookie-secret/01-deploy.txt) | 터미널 원문 |
| [`02-cookie-portability.txt`](evidence/b7-cookie-secret/02-cookie-portability.txt) | 터미널 원문 |
| [`03-rotation.txt`](evidence/b7-cookie-secret/03-rotation.txt) | 터미널 원문 |
| [`b7-oauth2proxy-login-success.png`](evidence/b7-cookie-secret/b7-oauth2proxy-login-success.png) | 스크린샷 |
파일별 상세는 [`evidence/b7-cookie-secret/README.md`](evidence/b7-cookie-secret/README.md).
## 6. 재현 절차 (명령어)
```bash
+34 -3
View File
@@ -11,6 +11,15 @@
---
## 구조
![C-1 구조 — user session 1 : client session N](diagrams/c1-sso-structure.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터
| 물음 | 답 |
@@ -50,10 +59,15 @@ DB 를 직접 지우고 Keycloak 을 재시작해야 했다 — **캐시 때문
(A-1 에서 확인한 대로, DB 를 직접 지워도 캐시는 남는다).
```
Keycloak 온라인 세션: 0
Keycloak 온라인 세션: 4 ← 초기화가 안 먹었다
app1 HTTP 200 / app2 HTTP 200
```
> **정정** — 이 문서는 처음에 이 값을 `0` 으로 인쇄했다. 증거
> [`01-baseline.txt`](evidence/c1-multi-app-sso/01-baseline.txt) 는 `4` 다.
> `logout-all` 이 듣지 않아 세션이 남아 있었고, 그래서 아래 절차(DB 직접 삭제 +
> Keycloak 재시작)가 필요했다. **`0` 은 그 다음 단계의 값이었다.**
---
## 2. SSO 가 동작한다
@@ -118,7 +132,7 @@ https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-connect/aut
```bash
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
create users/<user-id>/logout -r keycloak-patterns
create users/$(kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh get users -r keycloak-patterns -q username=labuser --fields id --format csv --noquotes | tail -1)/logout -r keycloak-patterns
```
```
@@ -207,6 +221,23 @@ select us.user_session_id, r.name as realm, ...
---
---
## 증거 파일
**증거 수집 시각: 2026-09-04 14:44 14:48 KST** (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)
| 파일 | 종류 |
|---|---|
| [`01-baseline.txt`](evidence/c1-multi-app-sso/01-baseline.txt) | 터미널 원문 |
| [`02-after-app1-login.txt`](evidence/c1-multi-app-sso/02-after-app1-login.txt) | 터미널 원문 |
| [`03-after-app2-visit.txt`](evidence/c1-multi-app-sso/03-after-app2-visit.txt) | 터미널 원문 |
| [`04-sso-session-killed.txt`](evidence/c1-multi-app-sso/04-sso-session-killed.txt) | 터미널 원문 |
| [`c1-apps-alive-after-idp-logout.png`](evidence/c1-multi-app-sso/c1-apps-alive-after-idp-logout.png) | 스크린샷 |
| [`c1-sso-app2-no-login-screen.png`](evidence/c1-multi-app-sso/c1-sso-app2-no-login-screen.png) | 스크린샷 |
파일별 상세는 [`evidence/c1-multi-app-sso/README.md`](evidence/c1-multi-app-sso/README.md).
## 6. 재현 절차 (명령어)
```bash
@@ -228,7 +259,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c
# 4. IdP 세션만 죽인다
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
create users/<user-id>/logout -r keycloak-patterns
create users/$(kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh get users -r keycloak-patterns -q username=labuser --fields id --format csv --noquotes | tail -1)/logout -r keycloak-patterns
# 5. 두 앱을 다시 연다 — 그대로 열리면 앱 세션이 독립적이라는 뜻
```
+29 -6
View File
@@ -27,7 +27,7 @@
### IdP 쪽
```bash
kcadm.sh get clients -r keycloak-patterns -q clientId=bff-confidential --fields attributes
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh get clients -r keycloak-patterns -q clientId=bff-confidential --fields attributes
```
```
@@ -68,7 +68,7 @@ Spring Security 6.2+ 는 백채널 로그아웃을 지원하지만 **명시적
## 2. IdP 쪽만 설정하고 시험했다
```bash
kcadm.sh update clients/<id> -r keycloak-patterns \
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh update clients/$(kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh get clients -r keycloak-patterns -q clientId=bff --fields id --format csv --noquotes | tail -1) -r keycloak-patterns \
-s 'attributes={"backchannel.logout.url":"https://app1.hyeonworks.com/logout/connect/back-channel/keycloak",
"backchannel.logout.session.required":"true"}'
```
@@ -78,6 +78,14 @@ kcadm.sh update clients/<id> -r keycloak-patterns \
backchannel.logout.url = https://app1.hyeonworks.com/logout/connect/back-channel/keycloak
```
> **출처 주의** — 위 확인 출력은
> [`02-configure-idp.txt`](evidence/c2-backchannel-logout/02-configure-idp.txt) 가
> 아니라 그 뒤 별도로 실행한 조회에서 나온 것이다. 그 파일에는
> **`command terminated with exit code 1`** 이 남아 있다 —
> `-s "attributes.backchannel.logout.url=..."` 의 점 표기가 실패한 첫 시도이며,
> JSON 으로 다시 넣어 성공했다. **실패한 시도의 파일에 성공 출력을 붙여
> 인쇄한 것은 잘못이었다.**
### 살아 있는 세션에 로그아웃을 걸었다
```
@@ -86,7 +94,7 @@ kcadm.sh update clients/<id> -r keycloak-patterns \
Redis: 1 키
=== IdP 로그아웃 ===
kcadm.sh create users/<id>/logout -r keycloak-patterns
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh create users/$(kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh get users -r keycloak-patterns -q username=labuser --fields id --format csv --noquotes | tail -1)/logout -r keycloak-patterns
=== 결과 ===
keycloak-patterns 세션: 0 ← IdP 쪽은 끊겼다
@@ -200,16 +208,31 @@ app2(oauth2-proxy)는 못 한다. **한 SSO 안에서 로그아웃 전파가 앱
---
---
## 증거 파일
**증거 수집 시각: 2026-09-04 14:50 14:53 KST** (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)
| 파일 | 종류 |
|---|---|
| [`01-current-state.txt`](evidence/c2-backchannel-logout/01-current-state.txt) | 터미널 원문 |
| [`02-configure-idp.txt`](evidence/c2-backchannel-logout/02-configure-idp.txt) | 터미널 원문 |
| [`03-logout-attempt.txt`](evidence/c2-backchannel-logout/03-logout-attempt.txt) | 터미널 원문 |
| [`04-reachability.txt`](evidence/c2-backchannel-logout/04-reachability.txt) | 터미널 원문 |
파일별 상세는 [`evidence/c2-backchannel-logout/README.md`](evidence/c2-backchannel-logout/README.md).
## 7. 재현 절차 (명령어)
```bash
# 1. 현재 설정 확인 — 어느 쪽에도 없다
kcadm.sh get clients -r keycloak-patterns -q clientId=bff-confidential --fields attributes
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh get clients -r keycloak-patterns -q clientId=bff-confidential --fields attributes
grep -rn "oidcLogout\|backchannel" bff/src/main/java/
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://app1.hyeonworks.com/logout/connect/back-channel/keycloak
# 2. IdP 쪽 설정 — 점 표기는 안 먹는다. JSON 으로
kcadm.sh update clients/<id> -r keycloak-patterns \
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh update clients/$(kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh get clients -r keycloak-patterns -q clientId=bff --fields id --format csv --noquotes | tail -1) -r keycloak-patterns \
-s 'attributes={"backchannel.logout.url":"...","backchannel.logout.session.required":"true"}'
# 3. ★ 살아 있는 세션이 있는지 먼저 확인한다 (realm 을 join 해서)
@@ -218,7 +241,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tA
where r.name='keycloak-patterns' and us.offline_flag='0'"
# 4. 로그아웃하고 앱 세션을 본다
kcadm.sh create users/<user-id>/logout -r keycloak-patterns
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh create users/$(kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh get users -r keycloak-patterns -q username=labuser --fields id --format csv --noquotes | tail -1)/logout -r keycloak-patterns
kubectl -n keycloak-lab exec deploy/redis -- redis-cli dbsize
# 5. 도달성 확인 — 클러스터 안에서 앱 공개 URL 로
+64 -2
View File
@@ -8,6 +8,15 @@
---
## 구조
![D-1 구조 — 파괴와 복구, 그리고 캐시가 가린 것](diagrams/d1-backup-restore.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터
| 측정 | 값 |
@@ -16,7 +25,7 @@
| 복구 시간 | **1초** (`15:00:12 → 15:00:13`), **오류 0건** |
| 서비스 회복 | **재시작 없이 15초 이내** (`restarts=0`) |
| 데이터 일치 | **완전 일치** — realms 2 / clients 15 / users 2 / sessions 3 / authclients 1 |
| **RTO** | **30** (파괴 감지부터 서비스 복귀까지) |
| **RTO** | **41** (14:59:47 파괴 → 15:00:28 서비스 확인) |
| **RPO** | **마지막 덤프 시점** + A-3 의 `synchronous_commit OFF` 손실 |
**그리고 예상 못 한 것 — 스키마를 통째로 지웠는데 서비스가 `200` 을 계속 냈다.**
@@ -160,7 +169,7 @@ volatile 이었다면 세션은 애초에 DB 에 없으므로 **복구해도 전
15:00:13 복구 완료
~15:00:28 서비스 정상 확인
RTO ≈ 30초 (이 규모에서는 대부분이 사람의 판단 시간이다)
RTO = 41초 (이 규모에서는 대부분이 사람의 판단 시간이다)
```
### RPO 는 두 겹이다
@@ -206,6 +215,59 @@ volatile 이었다면 세션은 애초에 DB 에 없으므로 **복구해도 전
---
---
## 개념
### `pg_dump` 의 두 옵션
| 옵션 | 없으면 |
|---|---|
| `--clean` | 복구 시 기존 객체와 충돌 |
| `--if-exists` | 없는 객체 DROP 에서 오류가 쏟아진다 |
### "DB 가 살아 있다" 와 "데이터가 있다" 는 다르다
```
A-2 DB 프로세스 정지 → 커넥션 실패 → readiness DOWN → 파드가 Service 에서 빠짐
D-1 스키마만 삭제 → 커넥션 정상 → readiness UP → ★ 파드가 그대로 트래픽을 받는다
```
**헬스체크는 커넥션만 본다.** 그래서 빈 데이터베이스를 통과시킨다.
그리고 Keycloak 이 realm 캐시로 일부를 계속 서빙해 **부분적으로만 깨진다.**
### RPO 는 두 겹이다
```
① 마지막 덤프 이후의 변경 ← 백업 주기가 정한다
② synchronous_commit OFF 손실 ← A-3 에서 측정한 수백 ms
실제 RPO = ① + ②
```
**백업 주기만 보고 RPO 를 말하면 ②를 빠뜨린다.**
### 백업의 장애 도메인
이번 덤프는 `test-server:/tmp` 에 있었다. **호스트가 죽으면 같이 사라진다.**
A-4 에서 PVC 가 노드에 묶인 것과 같은 문제이며,
**같은 장애 도메인에 있는 백업은 백업이 아니다.**
---
---
## 증거 파일
**증거 수집 시각: 2026-09-04 14:57 14:58 KST** (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)
| 파일 | 종류 |
|---|---|
| [`01-backup.txt`](evidence/d1-backup-restore/01-backup.txt) | 터미널 원문 |
| [`02-destruction.txt`](evidence/d1-backup-restore/02-destruction.txt) | 터미널 원문 |
| [`03-restore.txt`](evidence/d1-backup-restore/03-restore.txt) | 터미널 원문 |
파일별 상세는 [`evidence/d1-backup-restore/README.md`](evidence/d1-backup-restore/README.md).
## 6. 재현 절차 (명령어)
```bash
+29
View File
@@ -7,6 +7,20 @@
선행: [`D-1`](experiment-d1-backup-restore.md) — **백업이 전제다** ·
[`A-8`](experiment-a8-rolling-restart.md) — 롤링 재시작이 안전하다는 것이 전제
> ## ★ 정정 — 이 문서의 결론은 조건부다
>
> 이 문서는 *"롤백이 안 된다"* 고 단정했다. 나중에
> [`후속 문서`](experiment-followup-untested-items.md) 에서 26.7.0 ↔ 26.7.3 을
> 시험하니 **롤백이 성공했다.**
>
> | 버전 차 | `databasechangelog` | 롤백 |
> |---|---|---|
> | 26.7.0 → 26.0 | 체크섬 불일치 | **불가** |
> | 26.7.0 ↔ 26.7.3 | **210 → 210, 변화 없음** | **가능** |
>
> **판단 기준은 버전 번호가 아니라 `databasechangelog` 의 행 수가 바뀌었는가다.**
> 아래 본문은 스키마가 바뀐 경우에 해당한다.
---
## 0. 결론부터
@@ -176,6 +190,21 @@ kubectl -n keycloak-lab set image statefulset/keycloak keycloak=quay.io/keycloak
---
---
## 증거 파일
**증거 수집 시각: 2026-09-04 15:00 16:15 KST** (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)
| 파일 | 종류 |
|---|---|
| [`01-pre-upgrade.txt`](evidence/d2-version-upgrade/01-pre-upgrade.txt) | 터미널 원문 |
| [`02-rollback-attempt.txt`](evidence/d2-version-upgrade/02-rollback-attempt.txt) | 터미널 원문 |
| [`03-roll-forward.txt`](evidence/d2-version-upgrade/03-roll-forward.txt) | 터미널 원문 |
| [`d2-upgrade-window.png`](evidence/d2-version-upgrade/d2-upgrade-window.png) | 스크린샷 |
파일별 상세는 [`evidence/d2-version-upgrade/README.md`](evidence/d2-version-upgrade/README.md).
## 7. 재현 절차 (명령어)
```bash
+24 -2
View File
@@ -6,6 +6,15 @@
---
## 구조
![D-3 구조 — 네 경로 중 하나만 막는다](diagrams/d3-secret-exposure.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터
| 경로 | 감춰지는가 |
@@ -105,7 +114,7 @@ ssh kc-lab-1 'sudo grep -c "lab-postgres-change-me" /var/lib/rancher/k3s/server/
## 3. 파드 안에서는 환경변수다
```bash
kubectl -n keycloak-lab exec <bff-pod> -- sh -c 'env | grep -iE "secret|password"'
kubectl -n keycloak-lab exec $(kubectl -n keycloak-lab get pod -l app=bff --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') -- sh -c 'env | grep -iE "secret|password"'
```
```
@@ -184,6 +193,19 @@ A-0 의 관측 스택에서 `nodes/proxy` 서브리소스를 따로 줘야 했
---
---
## 증거 파일
**증거 수집 시각: 2026-09-04 15:05 15:06 KST** (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)
| 파일 | 종류 |
|---|---|
| [`01-base64-not-encryption.txt`](evidence/d3-secret-management/01-base64-not-encryption.txt) | 터미널 원문 |
| [`02-at-rest.txt`](evidence/d3-secret-management/02-at-rest.txt) | 터미널 원문 |
파일별 상세는 [`evidence/d3-secret-management/README.md`](evidence/d3-secret-management/README.md).
## 6. 재현 절차 (명령어)
```bash
@@ -201,7 +223,7 @@ ssh kc-lab-1 'sudo k3s secrets-encrypt status'
ssh kc-lab-1 'sudo grep -c "lab-postgres-change-me" /var/lib/rancher/k3s/server/db/state.db'
# 5. 파드 안에서는 환경변수
kubectl -n keycloak-lab exec <pod> -- sh -c 'env | grep -i secret'
kubectl -n keycloak-lab exec keycloak-0 -- sh -c 'env | grep -i secret'
# 6. 누가 읽을 수 있는가
kubectl auth can-i get secrets -n keycloak-lab \
+382
View File
@@ -0,0 +1,382 @@
# D-4 — 인증서 갱신
브랜치 `feature/keycloak-d4-certificate-renewal` ·
증거 [`docs/evidence/d4-certificate-renewal/`](evidence/d4-certificate-renewal/) ·
2026-09-04 17:2517:35 KST
---
## 구조
![D-4 구조 — 체인과 SAN 제약](diagrams/d4-cert-chain.svg)
**핵심 발견** — 갱신은 성공했는데 36분 39초 동안 반영되지 않았다.
![D-4 — 갱신과 서빙 사이의 공백](diagrams/d4-renewal-gap.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터
| 확인 | 결과 |
|---|---|
| 인증서 구성 | **SAN 3개** (`auth`/`app1`/`app2`), 와일드카드 아님 |
| 체인 완결성 | **정상.** `Verify return code: 0 (ok)`, 4단계 |
| 갱신 자동화 | 타이머는 **동작한다.** 오늘 두 번 `status=0/SUCCESS` |
| **★ 갱신 반영** | **안 된다.** 새 인증서가 디스크에 있는 채로 **36분 39초** 동안 옛 인증서를 서빙했다 |
| **★ reload 무중단** | **무중단이다.** 8856건 전부 200 · 전송 한가운데였던 42초 요청이 845361바이트 전량 수신 |
### 한 문장으로
> **"갱신 성공"과 "새 인증서 서빙"은 다른 사건이다.**
> 그리고 이 실험대에는 앞의 사건을 뒤의 사건으로 잇는 것이 **아무것도 없다.**
이게 왜 무서운가 — 이 결함은 **88일 동안 보이지 않는다.** 타이머는 정상이고
매번 `SUCCESS` 로 끝난다. 만료 30일 전까지는 certbot 이 갱신 자체를 하지 않아
"반영이 안 된다"는 사실이 발현할 기회가 없기 때문이다. 발현하는 날의 증상은
**인증서 만료**이고, 그날에도 로그에는 `SUCCESS` 라고 적혀 있을 것이다.
---
## 1. 인증서 구성 — B-7 에서 실제로 걸린 제약
```
X509v3 Subject Alternative Name:
DNS:app1.hyeonworks.com, DNS:app2.hyeonworks.com, DNS:auth.hyeonworks.com
```
**세 이름뿐이고 와일드카드가 아니다.**
> **이 제약이 B-7 에서 실제 비용을 만들었다.**
> oauth2-proxy 를 올릴 호스트명이 없어 **Grafana 가 쓰던 `app2` 를 빌려야 했고**,
> 그 때문에 관측 스택의 웹 UI 가 실험 동안 내려가 있었다.
>
> **"인증서에 이름을 몇 개 넣을 것인가" 는 TLS 설정이 아니라
> 나중에 무엇을 배포할 수 있는가를 정하는 결정이다.**
| | 이 실험대 | 와일드카드였다면 |
|---|---|---|
| 새 호스트 추가 | **인증서 재발급 필요** | 바로 가능 |
| DNS-01 검증 | 필요 | 필요 (와일드카드는 DNS-01 만 가능) |
| 노출 | 이름 3개만 | **하위 전체가 한 키에 묶인다** |
---
## 2. 체인이 완전한가 — 흔한 실수 확인
```
0 s:CN = auth.hyeonworks.com ← 리프
1 s:C = US, O = Let's Encrypt, CN = YE2 ← 중간
2 s:C = US, O = ISRG, CN = Root YE
3 s:C = US, O = Internet Security Research Group, CN = ISRG Root X2
Verify return code: 0 (ok)
```
**중간 인증서가 함께 제공된다.**
### 개념 — `fullchain.pem` vs `cert.pem`
certbot 은 두 파일을 만든다.
| 파일 | 내용 | nginx 에 넣으면 |
|---|---|---|
| `cert.pem` | **리프만** | **일부 클라이언트에서 검증 실패** |
| **`fullchain.pem`** | 리프 + 중간 | 정상 |
**브라우저는 중간 인증서를 캐시하고 있어 `cert.pem` 으로도 대개 동작한다.**
그래서 실수해도 개발 중에는 안 드러나고, **캐시가 없는 클라이언트
(모바일 앱, curl, 다른 서버)에서만 깨진다.**
```bash
openssl s_client -connect <host>:443 -servername <host> | grep -E "^ *[0-9] s:"
```
**단계가 2개 이상이면 fullchain 이고, 1개면 cert.pem 을 쓴 것이다.**
이 실험대는 4단계로 정상이다.
---
## 3. 갱신 자동화는 동작한다
```
NEXT LEFT LAST PASSED
Fri 2026-09-04 17:03:46 KST 1h 54min Fri 2026-09-04 03:19:39 KST 11h ago
타이머 enabled: enabled / active: active
```
**하루 두 번 돌고, 11시간 전에 실제로 실행됐다.**
```
만료: Dec 2 00:47:22 2026 GMT
남은 일수: 88일
```
**아직 갱신하지 않은 것이 정상이다** — Let's Encrypt 는 90일 발급이고
certbot 은 **30일 남았을 때** 갱신한다. 지금 실행돼도 아무것도 안 한다.
> **타이머가 돌았다는 것과 갱신이 됐다는 것은 다르다.**
> "타이머가 active 니까 괜찮다" 는 확인이 아니다. **실제 갱신은 58일 뒤**이며,
> 그때 처음으로 절차가 시험된다.
---
## 4. ★ 강제 갱신 — 두 개의 답
### 4-1. 먼저 대조군을 잡았다
주입 전에 평시를 재둔다. **이걸 건너뛰면 주입 결과를 해석할 수 없다**
A-6 에서 −41% 대조군을 "영향 없음"이라 적었던 것도, A-8 에서 표본 9개로
무중단을 주장한 것도 같은 종류의 실수였다.
| 대조군 | 결과 |
|---|---|
| 새 연결 (0.2초 × 900회 / 180초) | **900 전부 200, 오류 0** · 중앙 98ms · p95 195ms |
| 진행 중 요청 (845KB @ 20k/s) | 200 · 845361바이트 · 연결수 1 · **42.3초 완주** |
두 번째 장치가 왜 따로 필요했는가 — 첫 번째 폴링은 **TLS 핸드셰이크가
900/900** 이다. 매 요청이 새 연결이라는 뜻이고, 그래서 "새 연결을 받아주는가"만
잰다. 계획서가 물은 것은 **"진행 중이던 요청은 어떻게 되는가"** 이므로,
reload 순간에 실제로 전송 중인 요청이 있어야 한다. 845KB 짜리 관리 콘솔
번들을 일부러 느리게 받아 요청 하나를 42초 동안 살려 두었다.
### 4-2. 갱신은 성공했는데, 인증서가 바뀌지 않았다
`sudo certbot renew --force-renewal` 을 실행한 뒤에도 밖에서 본 인증서가
그대로였다. 처음에는 갱신이 실패한 줄 알았다. 아니었다.
```
디스크 (/etc/letsencrypt/archive/auth.hyeonworks.com/)
cert1.pem 2026-09-03 10:47:40 KST 처음 발급
cert2.pem 2026-09-04 17:22:13 KST ← 강제 갱신, 새로 써졌다
네트워크 (5초 간격 564표본)
08:10:51 ~ 08:58:47 serial=0520BB…B853 notAfter=Dec 2 ← 옛 것
08:58:52 serial=06C7CB…EA1D notAfter=Dec 3 ← 바뀐 순간
```
| | 시각 (UTC) |
|---|---|
| 새 인증서 디스크 기록 | 08:22:13 |
| 실제 서빙 시작 (`nginx -s reload`) | 08:58:52 |
| **공백** | **2199초 = 36분 39초** |
그 사이 **428번** 옛 인증서로 관측됐다. 그리고 이 36분은 **우연히 짧았을
뿐이다** — reload 를 시킨 것은 사람이지 자동화가 아니다. 아무도 안 했다면
다음 nginx 재시작까지, 즉 무기한 옛 인증서를 서빙했을 것이다.
#### 개념 — 왜 파일이 바뀌어도 nginx 는 모르는가
**무엇인가.** nginx 는 `ssl_certificate` 가 가리키는 파일을 **기동 시점에
한 번 읽어 메모리에 들고 있다.** 요청마다 디스크를 다시 보지 않는다.
**왜 여기 나오나.** certbot 은 `live/` 심볼릭 링크가 새 `archive/` 파일을
가리키도록 바꾼다. **경로는 그대로인데 내용만 바뀐다.** 그래서 nginx 설정을
고칠 필요가 없고, 바로 그 때문에 "설정이 그대로니 괜찮다"고 착각하기 쉽다.
필요한 것은 설정 변경이 아니라 **reload** 다.
**없거나 틀리면.** 인증서가 만료되어 브라우저가 `NET::ERR_CERT_DATE_INVALID`
를 띄운다. 그 시점에 디스크에는 멀쩡한 인증서가 들어 있고 갱신 로그도
`SUCCESS` 다 — 그래서 원인을 찾는 데 오래 걸린다.
**확인.** nginx 는 reload 하면 마스터를 유지한 채 워커만 새로 띄운다.
```bash
ps -eo pid,ppid,etimes,lstart,args | grep nginx
# 585 1 80529 Thu Sep 3 19:00:39 nginx: master process
# 586 585 80529 Thu Sep 3 19:00:39 nginx: worker process
# ↑ 워커가 마스터 기동 직후의 첫 fork(585→586) 그대로 22.4시간째
# = reload 가 한 번도 없었다
```
#### 원인은 하나가 아니라 셋이 겹쳤다
| # | 경로 | 상태 |
|---|---|---|
| 1 | `certbot-renew.service``ExecStartPost` | **없다**`ExecStart=/usr/bin/certbot -q renew` 가 전부 |
| 2 | `/etc/letsencrypt/renewal-hooks/{deploy,post,pre}/` | **셋 다 비어 있다** (`total 8`, `.``..` 뿐) |
| 3 | certbot 의 nginx 플러그인 | **없다**`dns-cloudflare, manual, null, standalone, webroot` |
**세 경로 전부가 비어 있다.** 하나라도 있었으면 자동으로 반영됐다.
certbot 5.7.0 에서 확인한 값이다.
#### 고치는 법
```bash
# 가장 단순한 방법 — deploy 훅 하나
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh <<'EOS'
#!/bin/sh
nginx -t && nginx -s reload
EOS
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
```
`deploy/`**실제로 갱신된 인증서가 있을 때만** 실행된다. `post/` 는 갱신
여부와 무관하게 매번 돌므로, 하루 두 번 쓸데없이 reload 하게 된다.
**확인 방법** — 훅을 넣었다고 끝이 아니다. 다음 강제 갱신 때 `nginx` 워커
PID 가 바뀌는지 봐야 한다. 이 실험이 그 확인 절차다.
### 4-3. reload 는 무중단이다 — 측정됨
계획서의 질문에 대한 답이다.
**새 연결** — 0.2초 폴링, 08:10:51 ~ 09:02, **전체 8856건 / 비200 0건**
| 구간 | n | 중앙 | p95 | 최대 |
|---|---|---|---|---|
| 장기 평시 08:20~08:50 | 5398 | 98.0ms | 205.7ms | 1942.9ms |
| reload 직전 2분56초 | 489 | 116.0ms | 200.8ms | 387.7ms |
| reload 직후 2분08초 | 342 | 132.5ms | **204.3ms** | 475.0ms |
p95 가 205.7 → 204.3 으로 **사실상 동일**하고 최대값은 오히려 낮다.
10초 구간 중앙값은 reload 전후 모두 80~190ms 사이를 오간다 — WiFi 잡음이지
reload 의 흔적이 아니다.
**진행 중이던 요청** — 여기가 계획서가 정확히 물은 지점이다.
```
08:58:40 요청 시작 (845KB @ 20k/s)
08:58:52 ← nginx -s reload. 요청 시작 12초 뒤, 전송 한가운데
08:59:21 종료: 코드=200 바이트=845361(전량) 연결수=1 curl종료=0
```
| 관측 | 읽는 법 |
|---|---|
| 바이트가 전량이다 | 잘리지 않았다 |
| **연결수가 1이다** | 중간에 끊겨 재연결한 게 아니다 |
| 코드 200 | 옛 워커가 이 요청을 끝까지 책임졌다 |
옛 인증서로 시작한 연결이 **새 워커 전환을 관통해** 끝까지 갔다.
in-flight 전체 50건 중 종료코드 ≠ 0 은 **0건**이다.
> 문서가 "nginx 의 reload 는 설계상 무중단이지만 **확인하지 않았으므로 그렇게
> 쓰면 안 된다**"고 유보했던 명제가, 이제 측정으로 뒷받침된다.
> **그리고 유보가 옳았다** — 같은 자리에서 유보 없이 썼다면 맞았겠지만,
> 바로 옆의 "갱신하면 반영된다"는 명제는 틀렸다. 어느 쪽이 틀릴지는 미리 알 수 없다.
### 4-4. 남은 미측정
| 항목 | 상태 |
|---|---|
| `certbot-renew.timer`**실제 갱신**을 하는가 | **미측정.** 약 58일 뒤에야 조건이 성립한다 |
| deploy 훅을 넣으면 자동 반영되는가 | **미측정.** 훅은 아직 넣지 않았다 |
---
## 증거 파일
**증거 수집: 2026-09-04 · 감시 08:10:51 09:02 UTC** (파일 mtime 기준)
| 파일 | 종류 | 무엇을 보여주는가 |
|---|---|---|
| [`01-certificate-state.txt`](evidence/d4-certificate-renewal/01-certificate-state.txt) | 터미널 | SAN 3개 · 체인 4단계 `Verify return code: 0` |
| [`05-control-no-injection.txt`](evidence/d4-certificate-renewal/05-control-no-injection.txt) | 터미널 | **대조군 1** — 900/900 200, 오류 0 · 핸드셰이크 900/900 |
| [`06-inflight-control.txt`](evidence/d4-certificate-renewal/06-inflight-control.txt) | 터미널 | **대조군 2** — 42초 in-flight 장치, 845361바이트 완주 |
| [`07-renewal-hook-missing.txt`](evidence/d4-certificate-renewal/07-renewal-hook-missing.txt) | 터미널 | nginx 워커 PID 로 본 "reload 없음" · 유닛에 `ExecStartPost` 없음 · crt.sh 로는 못 가림 |
| [`08-inflight-artifact.txt`](evidence/d4-certificate-renewal/08-inflight-artifact.txt) | 터미널 | 76건 실패가 **로컬 아티팩트**임을 대조 폴링으로 가려낸 기록 |
| [`09-serial-timeline.txt`](evidence/d4-certificate-renewal/09-serial-timeline.txt) | 터미널 | 일련번호 564표본 — 08:58:52 에 바뀌는 순간 |
| [`10-reload-poll-window.txt`](evidence/d4-certificate-renewal/10-reload-poll-window.txt) | 터미널 | reload 전후 60초 새 연결 원문 (비200 0건) |
| [`11-inflight-full.txt`](evidence/d4-certificate-renewal/11-inflight-full.txt) | 터미널 | in-flight 전체 — reload 를 관통한 08:58:40 요청 포함 |
| [`12-certbot-state.txt`](evidence/d4-certificate-renewal/12-certbot-state.txt) | 터미널 | `certbot certificates` · `archive/` mtime · **훅 3개 디렉터리 전부 비었음** |
| [`13-verdict.txt`](evidence/d4-certificate-renewal/13-verdict.txt) | 터미널 | **판정** — 36분 39초 공백 + reload 무중단, 수치 전량 |
파일별 상세는 [`evidence/d4-certificate-renewal/README.md`](evidence/d4-certificate-renewal/README.md).
## 5. 재현 절차 (명령어)
```bash
# ─────────────────────────────────────────────────────────────
# 1. 인증서 내용 — 밖에서 볼 수 있다(sudo 불필요)
# ─────────────────────────────────────────────────────────────
for H in auth app1 app2; do
echo "-- $H.hyeonworks.com"
echo | openssl s_client -connect $H.hyeonworks.com:443 -servername $H.hyeonworks.com 2>/dev/null \
| openssl x509 -noout -serial -dates -subject -ext subjectAltName
done
# 세 이름의 일련번호가 같으면 → 인증서 1장에 SAN 3개다.
# B-7 이 네 번째 이름을 못 쓴 이유가 여기 있다.
# ─────────────────────────────────────────────────────────────
# 2. 체인 완결성 — 단계가 1개면 cert.pem 을 쓴 것이다
# ─────────────────────────────────────────────────────────────
echo | openssl s_client -connect auth.hyeonworks.com:443 -servername auth.hyeonworks.com 2>/dev/null \
| grep -E '^ *[0-9]+ s:|^ *i:|Verify return code'
# ─────────────────────────────────────────────────────────────
# 3. ★ 대조군을 먼저 잡는다 — 이걸 건너뛰면 주입 결과를 해석할 수 없다
# ─────────────────────────────────────────────────────────────
# 3-a. 새 연결 가용성: 0.2초 × 900회 = 180초
i=0
while [ $i -lt 900 ]; do
curl -s -o /dev/null -w '%{http_code} %{time_total} %{time_appconnect}\n' \
--max-time 5 https://auth.hyeonworks.com/realms/master
i=$((i+1)); sleep 0.2
done > /tmp/d4-control.txt
awk '{print $1}' /tmp/d4-control.txt | sort | uniq -c # 비200 이 몇 개인가
# 3-b. 진행 중 요청: 845KB 를 20k/s 로 받아 42초 동안 살려 둔다
JS=$(curl -s https://auth.hyeonworks.com/admin/master/console/ \
| grep -oE '/resources/[a-z0-9]+/admin/[^"]+\.js' | head -1)
curl -s --limit-rate 20k -o /tmp/inflight.bin \
-w '코드=%{http_code} 바이트=%{size_download} 시간=%{time_total} 연결수=%{num_connects}\n' \
"https://auth.hyeonworks.com$JS"
# ─────────────────────────────────────────────────────────────
# 4. 감시를 켠다 — 일련번호가 바뀌면 3분 더 재고 스스로 멈춘다
# ─────────────────────────────────────────────────────────────
BASE=$(echo | openssl s_client -connect auth.hyeonworks.com:443 \
-servername auth.hyeonworks.com 2>/dev/null | openssl x509 -noout -serial | cut -d= -f2)
rm -f /tmp/d4-stop
# 감시 0 — 일련번호. 바뀌는 순간이 nginx 가 새 인증서를 집은 순간이다
setsid bash -c '
BASE='"$BASE"'
while [ ! -f /tmp/d4-stop ]; do
S=$(echo | openssl s_client -connect auth.hyeonworks.com:443 \
-servername auth.hyeonworks.com 2>/dev/null | openssl x509 -noout -serial -enddate | tr "\n" " ")
echo "$(date -u +%H:%M:%S) $S"
case "$S" in *"$BASE"*) ;; *) sleep 180; touch /tmp/d4-stop;; esac
sleep 5
done' > /tmp/d4-serial.txt 2>&1 < /dev/null &
# 감시 1 — 새 연결
setsid bash -c '
while [ ! -f /tmp/d4-stop ]; do
echo "$(date -u +%H:%M:%S.%2N) $(curl -s -o /dev/null -w "%{http_code} %{time_total}" \
--max-time 5 https://auth.hyeonworks.com/realms/master)"
sleep 0.2
done' > /tmp/d4-poll.txt 2>&1 < /dev/null &
# 감시 2 — 진행 중 요청을 끊김 없이 연달아 (reload 순간에 반드시 하나가 떠 있게)
setsid bash -c '
while [ ! -f /tmp/d4-stop ]; do
echo "$(date -u +%H:%M:%S) $(curl -s --limit-rate 20k -o /dev/null \
-w "코드=%{http_code} 바이트=%{size_download} 시간=%{time_total} 연결수=%{num_connects}" \
https://auth.hyeonworks.com'"$JS"')"
done' > /tmp/d4-inflight.txt 2>&1 < /dev/null &
# ★ setsid 가 필요하다. 그냥 & 로 띄우면 부모 셸이 끝날 때 같이 죽는다
# (A-3 에서 파드 안 & 가 exec 종료와 함께 죽은 것과 같은 함정이다).
# ─────────────────────────────────────────────────────────────
# 5. 주입 — 여기만 sudo 가 필요하다
# ─────────────────────────────────────────────────────────────
ssh test-server
sudo certbot renew --force-renewal
# ─────────────────────────────────────────────────────────────
# 6. 판정
# ─────────────────────────────────────────────────────────────
grep -vE ' 200 ' /tmp/d4-poll.txt # 새 연결이 끊긴 순간 (대조군은 0건)
grep -v '코드=200' /tmp/d4-inflight.txt # 진행 중 요청이 끊긴 순간
grep -v "$BASE" /tmp/d4-serial.txt | head # 새 인증서가 서빙되기 시작한 시각
```
## 6. 남긴 것
| | |
|---|---|
| **강제 갱신 + 무중단 측정** | sudo 권한이 필요하다 |
| **SAN 확장** | 새 호스트를 쓰려면 재발급 — B-7 에서 실제로 걸렸다 |
| **D-3 과 연결** | 인증서 **개인키**도 같은 비밀 관리 문제다 |
+369
View File
@@ -0,0 +1,369 @@
# 후속 — 미측정으로 남겼던 항목을 채운다
브랜치 `feature/keycloak-followup-untested-items` ·
증거 [`docs/evidence/followup/`](evidence/followup/) ·
2026-09-04 17:3518:20 KST
23개 실험을 마치며 **세 항목을 "못 했다" 로 남겼다. 이제 셋 다 채웠다.**
셋째(D-4 강제 갱신)는 호스트 sudo 가 필요해 사람이 함께 실행했고,
결과적으로 **이 세 항목 중 가장 큰 발견**이 거기서 나왔다.
---
## 0. 결론부터
| 항목 | 결과 |
|---|---|
| **D-2 정방향 업그레이드** | **무중단.** 87회 요청 전부 200 |
| **그리고 D-2 의 결론이 정밀해졌다** | **"롤백 불가" 는 조건부다** — 스키마가 바뀌었을 때만 |
| **B-4 ③ role 변경 반영 시점** | **요청 횟수와 무관하다.** 세션이 새로 만들어져야 한다 |
| **B층에 Grafana 증거가 없는 이유** | **관측 대상에 없다.** 안 찍은 것이 아니다 |
| **D-4 강제 갱신** | **★ 갱신은 성공했는데 36분 39초 동안 반영되지 않았다** |
| **D-4 reload 무중단** | **무중단이다.** 8856건 전부 200 · 전송 한가운데였던 요청이 전량 수신 |
---
## 1. D-2 정방향 업그레이드 — 26.7.0 → 26.7.3
### 개념 — 왜 이 방향을 못 했었나
D-2 를 처음 할 때 26.7.0 보다 새 이미지를 몰라 **역방향(26.0)만 시험**했다.
태그 목록을 조회하니 26.7.1 / 26.7.2 / **26.7.3** 이 있었다.
```bash
curl -s "https://quay.io/api/v1/repository/keycloak/keycloak/tag/?limit=40&onlyActiveTags=true"
```
### 절차 — D-1 의 교훈대로 백업이 먼저다
```bash
kubectl -n keycloak-lab exec deploy/postgres -- pg_dump -U keycloak -d keycloak \
--clean --if-exists > /tmp/pre-2673.sql # 396333 bytes
# 1초 간격으로 외부 진입점을 찍으면서 태그를 바꾼다
( for i in $(seq 1 150); do
printf "%s " "$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 https://auth.hyeonworks.com/realms/master)"
sleep 1
done > /tmp/avail.txt ) &
kubectl -n keycloak-lab set image statefulset/keycloak keycloak=quay.io/keycloak/keycloak:26.7.3
kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=600s
```
### 결과 — 무중단
```
200 200 200 ... (87회)
200 응답: 87 회
비200 : 0 회
소요: 15:22:59 → 15:24:26 (87초)
```
![업그레이드 구간의 cluster_size 와 up](evidence/d2-version-upgrade/d2-upgrade-window.png)
**파드가 하나씩 교체되며 `cluster_size` 가 2 → 1 → 2 를 두 번 반복한다.**
각 파드의 `up` 시계열이 끝나고 새 시계열이 시작되는 것이 함께 보인다.
```
마이그레이션: 210 → 210 ← 스키마 변경 없음
세션: 3 → 3 ← 유지
Infinispan: 16.0.12 → 16.0.14
restarts=0
```
---
## 2. ★ 그래서 D-2 의 결론을 정밀화한다
**"스키마 변경이 없었다면 롤백이 될 것" 이라는 가설이 생겼고, 시험했다.**
```bash
kubectl -n keycloak-lab set image statefulset/keycloak keycloak=quay.io/keycloak/keycloak:26.7.0
```
```
200 응답: 43 회 / 비200: 1
Keycloak 26.7.0
마이그레이션: 210 · 세션: 3
restarts=0
```
**롤백이 성공했다.**
| 버전 차 | `databasechangelog` | 롤백 |
|---|---|---|
| 26.7.0 → **26.0** | 체크섬 불일치 | **불가** (`ValidationFailedException`) |
| 26.7.0 ↔ **26.7.3** | **210 → 210, 변화 없음** | **가능** |
> **처음 D-2 에서 "롤백은 안 된다" 고 쓴 것은 과했다.**
> 정확히는 **"스키마가 바뀌었으면 안 된다"** 이고,
> **패치 릴리스처럼 스키마가 그대로면 태그를 되돌리는 것으로 충분하다.**
>
> 판단 기준은 버전 번호가 아니라 **`databasechangelog` 의 행 수가 바뀌었는가**다.
```bash
# 업그레이드 전후로 이것만 비교하면 롤백 가능 여부를 안다
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
"select count(*) from databasechangelog"
```
### 전환 순간의 `000` 1회
```
200 ×24 000 200 ×19
```
**서버 오류가 아니라 `--max-time 3` 타임아웃**이다. 파드 전환 순간 요청 하나가
3초를 넘겼다. 정방향에서는 0회였다.
> **"무중단" 은 관측 해상도에 달려 있다.** 1초 간격·3초 타임아웃으로는
> 44회 중 1회가 걸렸다. **더 촘촘히 보면 더 보일 것이다.**
---
## 3. B-4 ③ — role 변경은 언제 반영되는가
### 왜 못 했었나
B-4 를 할 때 **oauth2-proxy 가 아직 배포되지 않아** "proxy session" 이
존재하지 않았다. B-7 에서 배포했으므로 이제 측정할 수 있다.
### 방법
`X-Auth-Request-Roles` 대신 **이미 전달되고 있는 `x-forwarded-email`** 을 썼다.
role 을 헤더로 내보내려면 추가 설정이 필요한데, **"IdP 의 클레임 변경이
언제 반영되는가" 라는 질문은 어느 클레임이든 같다.**
1 · 3 단계는 **브라우저 콘솔**에서 실행한다. oauth2-proxy 쿠키가 HttpOnly 라
curl 로는 로그인 상태를 재현할 수 없기 때문이다(그래서 셸이 아니라 JS 다).
```js
// 1. 기준선 — 로그인된 app2 탭의 콘솔에서
for (let i = 0; i < 3; i++) {
const r = await (await fetch('/api/echo')).text();
console.log(new Date().toISOString(), r.match(/x-forwarded-email: (.*)/)[1]);
}
// 3. 반복 요청 — 0.5초 간격으로 12번
for (let i = 0; i < 12; i++) {
const r = await (await fetch('/api/echo')).text();
console.log(new Date().toISOString(), r.match(/x-forwarded-email: (.*)/)[1]);
await new Promise(s => setTimeout(s, 500));
}
```
2 단계는 셸에서:
```bash
# 2. IdP 에서 바꾼다
UID=$(kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
get users -r keycloak-patterns -q username=labuser \
--fields id --format csv --noquotes | tail -1)
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
update users/$UID -r keycloak-patterns -s email=CHANGED-labuser@example.com
# 4. Redis 세션을 지워 재인증을 강제한다 (반영 조건 확인)
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern 'oauth2-proxy*' \
| xargs -r kubectl -n keycloak-lab exec deploy/redis -- redis-cli del
```
### 결과 — 반영되지 않는다
```
2026-09-04T07:51:56.300Z req#1 HTTP 200 x-forwarded-email=labuser@example.com
2026-09-04T07:51:56.864Z req#2 HTTP 200 x-forwarded-email=labuser@example.com
...
2026-09-04T07:52:02.719Z req#12 HTTP 200 x-forwarded-email=labuser@example.com
→ 12회 · 6.4초 동안 전부 옛 값
```
> **이 결과는 처음에 증거 파일로 남기지 않았다.** 브라우저 콘솔에서 보고
> 문서에만 옮겨 적었고, 서브에이전트 감사가 "증거 없이 서 있는 결론" 으로
> 지적했다. **다시 측정해
> [`03-b4-role-propagation.txt`](evidence/followup/03-b4-role-propagation.txt)
> 에 타임스탬프와 함께 남겼다.**
>
> 재측정 중 **두 시계가 107초 어긋나 있다**는 것도 드러났다 —
> 브라우저(개발 머신)와 test-server 의 시각을 그대로 비교하면 안 되고,
> 증거 파일에 보정값을 적어두었다.
세션을 지우고 재인증시키자
```
2026-09-04T07:53:01.121Z req#1 HTTP 200 x-forwarded-email=changed-labuser@example.com
2026-09-04T07:53:01.456Z req#2 HTTP 200 x-forwarded-email=changed-labuser@example.com
2026-09-04T07:53:01.785Z req#3 HTTP 200 x-forwarded-email=changed-labuser@example.com
```
### 개념 — 세션은 로그인 시점의 스냅샷이다
```
로그인 → IdP 가 준 클레임을 세션에 담는다
이후 요청 → 세션에서 읽어 헤더로 내보낸다
└─ IdP 를 다시 부르지 않는다
IdP 에서 변경 → 세션은 모른다
```
**oauth2-proxy 에 `--cookie-refresh` 가 설정되어 있지 않다.**
설정하면 그 주기마다 토큰을 갱신하며 클레임을 다시 받는다.
| 설정 | 반영 시점 |
|---|---|
| 지금 (`--cookie-refresh` 없음) | **쿠키 만료(1시간) 또는 재인증까지 안 됨** |
| `--cookie-refresh=5m` | 최대 5분 |
> **Q4 는 "몇 번째 요청부터 반영되는지" 를 물었는데, 답은 "요청으로는 안 된다" 이다.**
> 요청 횟수가 아니라 **세션의 나이**가 정한다.
>
> **이것이 Q4 의 설계 판단 2번(role·tenant 변경이 즉시 반영돼야 하는가)에
> 직접 답한다** — 즉시가 필요하면 헤더 방식은 맞지 않는다.
---
## 4. B층에 Grafana 증거가 없는 이유
문서 감사에서 **B-1·B-3·B-4·B-5 에 스크린샷이 없는 것**이 드러나
소급해서 찍으려다 원인을 확인했다.
```
=== Prometheus 가 실제로 긁는 대상 ===
keycloak 2개
kubelet 2개
node-exporter 2개
prometheus 1개
=== B층 구성 요소의 지표가 있는가 ===
redis_up 시계열 0개
redis_connected_clients 시계열 0개
pg_up 시계열 0개
pg_stat_database_numbackends 시계열 0개
```
**Redis 도 PostgreSQL 도 BFF 도 긁는 대상에 없다.**
> **스크린샷을 안 찍은 것이 아니라 지표가 없다.**
> A층이 Grafana 증거를 남길 수 있었던 것은 Keycloak 이 `/metrics` 를
> 내놓고 그것을 scrape 대상에 넣어뒀기 때문이다.
>
> **관측은 "나중에 붙이는 것" 이 아니라 실험 설계에 포함되어야 한다.**
> A-2 에서 `kube-state-metrics` 가 없다는 것을, A-6 에서 응답 시간
> 히스토그램이 없다는 것을 찾았는데, **B층 전체가 빠져 있던 것은
> 문서 감사를 하고서야 드러났다.**
### 보완하려면
| 대상 | 방법 |
|---|---|
| Redis | `redis_exporter` 사이드카 또는 Deployment |
| PostgreSQL | `postgres_exporter` |
| BFF | 이미 actuator 가 있다 — `/actuator/prometheus` 노출 + scrape 추가 |
| 파드 readiness | `kube-state-metrics` (A-2 에서 이미 찾은 항목) |
---
## 5. D-4 강제 갱신 — 셋째 항목, 그리고 가장 큰 발견
### 왜 못 했었나
```
$ sudo -n -l
sudo: a password is required
```
호스트 sudo 가 비밀번호를 요구해 `certbot renew --force-renewal` 을 실행할 수
없었다. 게스트(kc-lab-1/2)는 무암호라 A층에서 `conntrack`·`tc` 를 자유롭게
썼는데 **호스트는 다르다.** 이 사실 자체가 B-7 에서 처음 드러났던 것이다 —
nginx 설정을 읽으려던 시도가 계속 빈 결과였고, 그게 sudo 의 조용한 실패였다.
### 방법 — 주입보다 대조군을 먼저
사람이 실행할 명령은 하나뿐이므로, **그 한 번을 헛되게 쓰지 않는 것**이
관건이었다. 주입 전에 평시를 재두지 않으면 주입 중에 나온 값을 해석할 수 없다.
| 대조군 | 결과 |
|---|---|
| 새 연결 (0.2초 × 900회 / 180초) | **900 전부 200, 오류 0** · 중앙 98ms · p95 195ms |
| 진행 중 요청 (845KB @ 20k/s) | 200 · 845361바이트 · 연결수 1 · **42.3초 완주** |
두 번째 장치가 따로 필요했던 이유가 중요하다. 첫 번째 폴링은 **TLS
핸드셰이크가 900/900** 이다 — 매 요청이 새 연결이라는 뜻이고, 그래서
"새 연결을 받아주는가"만 잰다. 계획서가 물은 것은 **"진행 중이던 요청은
어떻게 되는가"** 이므로, reload 순간에 실제로 전송 중인 요청이 있어야 한다.
845KB 짜리 번들을 일부러 느리게 받아 요청 하나를 42초간 살려 두었다.
### 결과 ① — 갱신은 성공했는데 인증서가 바뀌지 않았다
```
디스크 cert2.pem 2026-09-04 17:22:13 KST 기록됨
네트워크 08:10:51 ~ 08:58:47 serial=0520BB…B853 (옛 것)
08:58:52 serial=06C7CB…EA1D (새 것)
```
| | 시각 (UTC) |
|---|---|
| 새 인증서 디스크 기록 | 08:22:13 |
| 실제 서빙 시작 (`nginx -s reload`) | 08:58:52 |
| **공백** | **2199초 = 36분 39초** (그 사이 428회 관측) |
그리고 그 36분은 **우연히 짧았을 뿐이다** — reload 를 시킨 것은 사람이지
자동화가 아니다. 원인이 셋 겹쳤고 **전부 비어 있었다.**
| | 상태 |
|---|---|
| `certbot-renew.service``ExecStartPost` | 없음 |
| `renewal-hooks/{deploy,post,pre}/` | **셋 다 비었음** |
| certbot 의 nginx 플러그인 | 없음 |
**이 결함은 88일 동안 보이지 않는다.** 타이머는 오늘도 두 번 `SUCCESS`
끝났다. 만료 30일 전까지는 갱신 자체를 하지 않으므로 발현할 기회가 없고,
발현하는 날의 증상은 **인증서 만료**다 — 그날에도 로그는 `SUCCESS` 다.
### 결과 ② — reload 는 무중단이다
**0.2초 간격으로 잰 이유** — 2절에서 1초 간격으로는 전환을 거의 못 잡았다.
nginx reload 는 그보다 짧을 것이므로 해상도를 올렸다. 결과적으로 해상도는
충분했고, **끊김 자체가 없었다.**
새 연결 **8856건 전부 200**. p95 는 평시 205.7ms 대 직후 204.3ms.
```
08:58:40 845KB 요청 시작 (20k/s)
08:58:52 ← nginx -s reload. 전송 12초째, 한가운데
08:59:21 코드=200 바이트=845361(전량) 연결수=1
```
연결수가 1이라는 것이 핵심이다 — 끊겨서 다시 붙은 것이 아니라, **옛 워커가
그 요청을 끝까지 책임졌다.**
### 곁다리 — 측정 장치가 거짓말할 뻔했다
in-flight 감시에서 76건이 실패했다. 그대로 적었으면 "갱신 중 대규모 요청
실패"라는 오보가 됐을 것이다. 서버 탓이 아니었다.
| 근거 | 값 |
|---|---|
| 같은 순간 폴링 | 49건 **전부 200** |
| 연결수 | **0** — TCP 연결 시도조차 못 했다 |
| 소요 시간 | **50µs** — DNS 조회보다도 짧다 |
| 재현 | **0/100** |
**대조군이 오보를 막았다.** 상세는
[`08-inflight-artifact.txt`](evidence/d4-certificate-renewal/08-inflight-artifact.txt).
전체 판정은 [D-4 문서](experiment-d4-certificate-renewal.md) 와
[`13-verdict.txt`](evidence/d4-certificate-renewal/13-verdict.txt).
---
## 증거 파일
**증거 수집 시각: 2026-09-04 15:23 16:17 KST** (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)
| 파일 | 종류 |
|---|---|
| [`01-d2-forward-upgrade.txt`](evidence/followup/01-d2-forward-upgrade.txt) | 터미널 원문 |
| [`02-d2-rollback-same-schema.txt`](evidence/followup/02-d2-rollback-same-schema.txt) | 터미널 원문 |
| [`03-b4-role-propagation.txt`](evidence/followup/03-b4-role-propagation.txt) | 터미널 원문 |
| [`04-observability-gap.txt`](evidence/followup/04-observability-gap.txt) | 터미널 원문 |
파일별 상세는 [`evidence/followup/README.md`](evidence/followup/README.md).
+111
View File
@@ -0,0 +1,111 @@
# 실험 색인 — 23개 전부
각 실험은 **자기 브랜치**에 있고, 해설 문서와 증거 폴더가 한 벌이다.
| # | 실험 | 브랜치 | 한 줄 결과 |
|---|---|---|---|
| **A-0** | 세션 복제 확인 | `...multinode-cluster-jdbc-ping` | 세션 공유는 Infinispan 이 아니라 **PostgreSQL** 이 한다 |
| **A-1** | TCP 7800 차단 | `...a1-jgroups-transport-block` | 세션은 견디지만 **로그아웃 무효화가 7800 을 탄다** |
| **A-2** | DB 정상 정지 | `...a2-database-loss` | 전면 장애. **그런데 `up` 은 1이었다** |
| **A-3** | DB 강제 종료 | `...a3-database-crash` | **153건 중 4건의 로그인이 사라졌다** |
| **A-4** | 노드 전원 차단 | `...a4-node-loss` | **죽은 파드가 산 파드보다 건강해 보인다** |
| **A-5** | 비대칭 파티션 | `...a5-asymmetric-partition` | 단방향은 자가 치유. 완전 분단도 **한쪽은 산다** |
| **A-6** | 지연 주입 | `...a6-latency-injection` | **200ms → 22초** (왕복 × 풀 큐잉) |
| **A-7** | volatile 비교 | `...a7-volatile-comparison` | **세 결과가 정반대로 뒤집힌다** |
| **A-8** | 롤링 재시작 | `...a8-rolling-restart` | 무중단 + 세션 생존 |
| **B-0** | 자동구성 확인 | `...b0-bff-redis-deploy` | 조회 키에 **session id 가 없다** |
| **B-1** | Redis 세션 | `...b1-redis-session-store` | 세션만 옮겨지고 **토큰은 남는다** |
| **B-2** | 다중 인스턴스 | `...b2-multi-instance-session` | 공유는 되지만 **평문·덮어쓰기·로그아웃 미정리** |
| **B-3** | refresh 경쟁 | `...b3-refresh-token-contention` | 경쟁이 아니라 **세션이 파괴된다** |
| **B-4** | Edge 인가 | `...b4-edge-authorization-scope` | nginx 는 **설정하지 않은 헤더를 덮어쓰지 않는다** |
| **B-5** | Redis 상실 | `...b5-redis-loss-persistence` | **파드가 Ready 인 채로 계속 실패한다** |
| **B-6** | key 회전 | `...b6-key-rotation` | 회전은 안전, **옛 키를 버리는 순간이 위험** |
| **B-7** | cookie secret | `...b7-cookie-secret-rotation` | **겹침 구간이 없고 세션이 고아로 남는다** |
| **C-1** | 다중 앱 SSO | `...c1-multi-app-sso` | **IdP 세션을 죽여도 아무도 로그아웃되지 않는다** |
| **C-2** | 백채널 로그아웃 | `...c2-backchannel-logout` | **받는 쪽을 아무도 구현하지 않았다** |
| **D-1** | 백업·복구 | `...d1-backup-restore` | **빈 데이터베이스가 `200` 을 냈다** |
| **D-2** | 버전 업그레이드 | `...d2-version-upgrade` | **이미지를 되돌려도 스키마는 안 돌아온다** |
| **D-3** | 비밀 관리 | `...d3-secret-management` | **RBAC 만 실제로 감춘다** |
| **D-4** | 인증서 갱신 | `...d4-certificate-renewal` | **갱신은 됐는데 36분 39초 반영 안 됨** (훅 3경로 전부 비었음). reload 자체는 **무중단**(8856건 0실패) |
| **후속** | 미측정 3항목 채우기 | `...followup-untested-items` | **셋 다 완료.** 정방향 업그레이드 무중단 · **롤백 불가는 조건부였다** · role 변경은 요청으로 반영 안 됨 · **D-4 갱신 36분 39초 미반영** |
## 시각 자료
| | |
|---|---|
| [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg) | 실험대 전체 구조 |
| [`diagrams/_style.md`](diagrams/_style.md) | 다이어그램 규약 (붉은 점선 = 죽인 것) |
| `diagrams/*.svg` | 실험별 구조도 12개 |
| `evidence/*/*.png` | Grafana · 브라우저 스크린샷 |
**스크린샷이 없는 실험은 11개다**`A-3 A-7 B-3 B-4 B-5 B-6 C-2 D-1 D-3 D-4 후속`.
그중 B층은 **Prometheus 가 Redis·BFF·PostgreSQL 을 긁지 않아** 만들 수가 없다 —
[`followup/04-observability-gap.txt`](evidence/followup/04-observability-gap.txt) 에 측정해 두었다.
## 문서 지도
| 문서 | 용도 |
|---|---|
| [`session-lab-prerequisites.md`](session-lab-prerequisites.md) | **먼저 읽을 것** — 왜 이런 걸 재는지 |
| [`experiment-plan.md`](experiment-plan.md) | 23개의 구조도·주입 방법·예측 |
| [`open-questions-coverage.md`](open-questions-coverage.md) | 공개 열린 질문 4개 대조 |
| [`session-lab-concepts.md`](session-lab-concepts.md) | 등장 개념 전체 (13층) |
## 문서가 자기 증거와 어긋났던 곳
**서브에이전트 감사(2026-09-04)에서 찾아 정정한 것들이다.**
| 문서 | 무엇이 틀렸나 |
|---|---|
| C-1 | 세션 수를 `0` 으로 인쇄. 증거는 `4` |
| C-2 | `exit code 1` 로 실패한 명령의 성공 출력을 인쇄 |
| A-1 | conntrack 삭제에 분단을 귀속. 실제로는 **파드 재시작 4초 뒤** |
| A-2 | ④ 의 첫 측정이 오염됐는데 정제된 값만 인쇄 |
| A-3 | `wal_writer_delay` 를 재지 않고 "기본값과 맞는다" |
| A-6 | 대조군이 −41% 변했는데 "영향 없음" |
| A-8 | 표본 9개로 "무중단" |
| B-2 | "두 브라우저" — 실제로는 세션만 지웠다. `Liquibase` 오귀속 |
| D-1 | RTO 30초 — 실제 41초 |
| D-2 | "롤백 불가" 단정 — 조건부였다 |
**png 6장이 실은 3장**(동일 내용이라 바이트가 같다)이고,
`a5/02-injection-verify.txt` 는 0바이트였다. 각 증거 README 에 명시했다.
## 반복해서 배운 것 — 주입이 안 걸린 사례
**아홉 번 있었다. 전부 "아무 일도 없었다" 로 보였다.**
| 실험 | 안 걸린 주입 | 원인 |
|---|---|---|
| A-1 | NetworkPolicy 로 7800 차단 | conntrack `ESTABLISHED` 가 먼저 통과시킨다 |
| A-3 | `delete --grace-period=0 --force` | 컨테이너 런타임이 SIGTERM → 정상 종료 |
| A-3 | `kill -9 1` | 컨테이너 안에서 **PID 1 은 SIGKILL 을 무시**한다 |
| A-5 | `iptables -I FORWARD 1` | kube-router 가 자기 체인을 위로 재삽입 |
| A-5 | raw 테이블, 반대 노드 | **연결 방향이 뒤집혀 있었다** |
| A-6 | `tc ... dev eth0` | 인터페이스가 `enp1s0` 이다 |
| A-6 | `enp1s0` 에 파드 IP 필터 | VXLAN 캡슐화로 **안 보인다** |
| B-2 | `spring.sql.init` 스키마 | PostgreSQL 에 없는 `blob` 타입 + `continue-on-error` |
| B-7·D-4 | `sudo nginx -T` 등 | **호스트 sudo 가 비밀번호를 요구**한다. D-4 는 사용자가 직접 실행해 해소했다 |
**그래서 실험마다 "주입 성공 신호" 를 먼저 정하게 됐다**
`cluster_size` 하락, `not properly shut down` 로그, iptables 패킷 카운터,
`tc -s qdisc``Sent`.
## 예측이 빗나간 곳
| 실험 | 예측 | 실제 |
|---|---|---|
| A-1 | 로그아웃 전파는 안 깨진다 | **깨졌다** — 무효화는 7800 을 탄다 |
| A-2 | `up` 이 잡아줄 것 | **1로 평평했다** |
| A-6 | 낙관적 락 충돌이 는다 | **0건** — 로그인은 새 행을 만든다 |
| B-4 | nginx 가 동명 헤더를 덮어쓴다 | **둘 다 도착했다** |
| B-6 | JWKS 캐시가 유예를 준다 | **즉시 401** |
## 말할 수 있는 것과 없는 것
**말할 수 있다** — Keycloak 멀티노드에서 세션과 토큰이 어디에 저장되고
각 저장소가 죽으면 무엇이 어떻게 실패하는지 **재현하고 복구했다.**
버전에 따라 결론이 뒤집힌다는 것을 **같은 주입으로 양쪽 다 측정했다.**
**말하면 안 된다** — "운영해봤다", "대규모 트래픽을 다뤄봤다".
규모·시간·다른 사람·실제 사용자·비용은 이 실험대에 없다.