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
DongHyeonkaandClaude Opus 5 027c24ee27 docs: D-3 — only RBAC actually hides anything
Every secret in the lab prints in four commands, while kubectl describe shows just a byte count and creates the impression that something is hidden. k3s reports encryption at rest disabled and the plaintext password is present in state.db, so one node disk carries the whole cluster's secrets, and inside the pod they are ordinary environment variables visible to exec, /proc and crash dumps.

The default service account cannot read secrets, which makes RBAC the one control doing real work here and the thing worth tightening.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 15:07:35 +09:00
DongHyeonkaandClaude Opus 5 df140ab218 docs: D-2 — rolling back the image does not roll back the schema
Downgrading from 26.7.0 to 26.0 fails with liquibase ValidationFailedException on a changeset checksum, which is stricter than an unknown migration: the old version knows the changeset but its definition differs. The pod goes CrashLoopBackOff and never starts.

The StatefulSet stopped the rollout at the first pod, so the other kept serving and the front door stayed at 200, which replica 1 would not have done. The failed start never touched the schema, so restoring the image was enough; had the migration already applied, the D-1 database restore would have been the only way back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 15:05:36 +09:00
DongHyeonkaandClaude Opus 5 df5af95cb3 docs: D-1 — an empty database still answered 200
Dropping the schema left Keycloak serving realm metadata and JWKS from its Infinispan cache, so the front door stayed at 200 while only the paths that read the database failed. That is a different shape from A-2, where the connection itself broke and readiness pulled the pods out of the Service; here the connection is fine and the tables are simply gone, which the health check does not notice.

Restoring the pg_dump took one second with zero errors and no pod restart, and the row counts matched the backup exactly, sessions included. The real RPO is the backup interval plus the synchronous_commit loss measured in A-3, and this dump sits in the host's /tmp, which is the same failure domain as the thing it protects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 14:59:56 +09:00
DongHyeonkaandClaude Opus 5 6c310c93b7 docs: C-2 — nothing propagates because nobody implemented the receiving end
Neither client had a backchannel logout URL and the BFF has no oidcLogout configuration, so the three candidate paths all answer 302, which is the authentication redirect rather than a handler. Setting the URL on the identity provider alone changed nothing: with a live session, logging the user out emptied the Keycloak side and left the Redis session untouched.

Reachability is not the blocker here, since a Keycloak pod fetches the app's public URL with a 200, but that is a property of this tailnet split-DNS lab and is the assumption most likely to fail in production, where it fails silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 14:56:58 +09:00
106 changed files with 5810 additions and 60 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 @@
- generic [active] [ref=f50e1]: "{\"pattern\":\"AP3-backend-for-frontend\",\"principal\":\"labuser\",\"accessTokenStoredOnServer\":true,\"refreshTokenStoredOnServer\":true,\"browserTokenCount\":0,\"csrfProtectionEnabled\":true}"
@@ -0,0 +1,16 @@
- generic [ref=f51e3]:
- banner [ref=f51e4]:
- generic [ref=f51e5]: keycloak-patterns
- main [ref=f51e6]:
- heading "Sign in to your account" [level=1] [ref=f51e8]
- generic [ref=f51e12]:
- generic [ref=f51e13]:
- generic [ref=f51e14]: Username or email
- textbox "Username or email" [active] [ref=f51e17]
- generic [ref=f51e18]:
- generic [ref=f51e19]: Password
- generic [ref=f51e21]:
- textbox "Password" [ref=f51e24]
- button "Show password" [ref=f51e26] [cursor=pointer]:
- generic [aria-hidden] [ref=f51e27]:
- button "Sign In" [ref=f51e30] [cursor=pointer]
@@ -0,0 +1,7 @@
- main [ref=f52e2]:
- heading "AP3 · Backend-for-Frontend" [level=1] [ref=f52e3]
- paragraph [ref=f52e4]: 브라우저에는 OAuth token이 전혀 전달되지 않습니다. HttpOnly session cookie로 BFF만 호출하고, BFF가 서버 보관 access token을 Resource Server 요청에 붙입니다.
- button "Keycloak 로그인" [ref=f52e5] [cursor=pointer]
- button "token 경계 확인" [ref=f52e6] [cursor=pointer]
- button "BFF 경유 API 호출" [ref=f52e7] [cursor=pointer]
- button "CSRF token으로 상태 변경" [ref=f52e8] [cursor=pointer]
@@ -0,0 +1,16 @@
- generic [ref=f53e3]:
- banner [ref=f53e4]:
- generic [ref=f53e5]: keycloak-patterns
- main [ref=f53e6]:
- heading "Sign in to your account" [level=1] [ref=f53e8]
- generic [ref=f53e12]:
- generic [ref=f53e13]:
- generic [ref=f53e14]: Username or email
- textbox "Username or email" [ref=f53e17]
- generic [ref=f53e18]:
- generic [ref=f53e19]: Password
- generic [ref=f53e21]:
- textbox "Password" [ref=f53e24]
- button "Show password" [ref=f53e26] [cursor=pointer]:
- generic [aria-hidden] [ref=f53e27]:
- button "Sign In" [ref=f53e30] [cursor=pointer]
@@ -0,0 +1,16 @@
- generic [ref=f53e3]:
- banner [ref=f53e4]:
- generic [ref=f53e5]: keycloak-patterns
- main [ref=f53e6]:
- heading "Sign in to your account" [level=1] [ref=f53e8]
- generic [ref=f53e12]:
- generic [ref=f53e13]:
- generic [ref=f53e14]: Username or email
- textbox "Username or email" [ref=f53e17]: labuser
- generic [ref=f53e18]:
- generic [ref=f53e19]: Password
- generic [ref=f53e21]:
- textbox "Password" [active] [ref=f53e24]: labpass
- button "Show password" [ref=f53e26] [cursor=pointer]:
- generic [aria-hidden] [ref=f53e27]:
- button "Sign In" [ref=f53e30] [cursor=pointer]
@@ -0,0 +1,16 @@
- generic [ref=f53e3]:
- banner [ref=f53e4]:
- generic [ref=f53e5]: keycloak-patterns
- main [ref=f53e6]:
- heading "Sign in to your account" [level=1] [ref=f53e8]
- generic [ref=f53e12]:
- generic [ref=f53e13]:
- generic [ref=f53e14]: Username or email
- textbox "Username or email" [ref=f53e17]: labuser
- generic [ref=f53e18]:
- generic [ref=f53e19]: Password
- generic [ref=f53e21]:
- textbox "Password" [active] [ref=f53e24]: labpass
- button "Show password" [ref=f53e26] [cursor=pointer]:
- generic [aria-hidden] [ref=f53e27]:
- button "Sign In" [ref=f53e30] [cursor=pointer]
@@ -0,0 +1,16 @@
- generic [ref=f53e3]:
- banner [ref=f53e4]:
- generic [ref=f53e5]: keycloak-patterns
- main [ref=f53e6]:
- heading "Sign in to your account" [level=1] [ref=f53e8]
- generic [ref=f53e12]:
- generic [ref=f53e13]:
- generic [ref=f53e14]: Username or email
- textbox "Username or email" [ref=f53e17]: labuser
- generic [ref=f53e18]:
- generic [ref=f53e19]: Password
- generic [ref=f53e21]:
- textbox "Password" [active] [ref=f53e24]: labpass
- button "Show password" [ref=f53e26] [cursor=pointer]:
- generic [aria-hidden] [ref=f53e27]:
- button "Sign In" [ref=f53e30] [cursor=pointer]
@@ -0,0 +1,16 @@
- generic [ref=f54e3]:
- banner [ref=f54e4]:
- generic [ref=f54e5]: keycloak-patterns
- main [ref=f54e6]:
- heading "Sign in to your account" [level=1] [ref=f54e8]
- generic [ref=f54e12]:
- generic [ref=f54e13]:
- generic [ref=f54e14]: Username or email
- textbox "Username or email" [ref=f54e17]
- generic [ref=f54e18]:
- generic [ref=f54e19]: Password
- generic [ref=f54e21]:
- textbox "Password" [ref=f54e24]
- button "Show password" [ref=f54e26] [cursor=pointer]:
- generic [aria-hidden] [ref=f54e27]:
- button "Sign In" [ref=f54e30] [cursor=pointer]
@@ -0,0 +1,16 @@
- generic [ref=f54e3]:
- banner [ref=f54e4]:
- generic [ref=f54e5]: keycloak-patterns
- main [ref=f54e6]:
- heading "Sign in to your account" [level=1] [ref=f54e8]
- generic [ref=f54e12]:
- generic [ref=f54e13]:
- generic [ref=f54e14]: Username or email
- textbox "Username or email" [ref=f54e17]: labuser
- generic [ref=f54e18]:
- generic [ref=f54e19]: Password
- generic [ref=f54e21]:
- textbox "Password" [active] [ref=f54e24]: labpass
- button "Show password" [ref=f54e26] [cursor=pointer]:
- generic [aria-hidden] [ref=f54e27]:
- button "Sign In" [ref=f54e30] [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]
- 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 키 잔존)이 한다.
@@ -0,0 +1,12 @@
=== 현재 클라이언트의 백채널 로그아웃 설정 ===
--- bff-confidential ---
"frontchannelLogout" : false,
--- oauth2-proxy ---
"frontchannelLogout" : false,
=== BFF 가 백채널 로그아웃 엔드포인트를 갖고 있는가 ===
=== 실제로 그 경로가 있는가 ===
/logout/connect/back-channel/keycloak HTTP 302
/backchannel-logout HTTP 302
/oauth2/sign_out HTTP 302
@@ -0,0 +1,8 @@
=== IdP 쪽에만 백채널 로그아웃 URL 을 설정한다 ===
client id: 9055fa46-6abb-4d6d-a339-8a9183bbf26d
command terminated with exit code 1
=== 로그인 상태를 만든다 ===
(브라우저에 이미 세션이 있다)
Keycloak 세션: 2
Redis: 2 키
@@ -0,0 +1,28 @@
=== 로그아웃 전 상태 ===
Redis: 2 키
keycloak-patterns 세션: 0
=== ★ IdP 로그아웃 — Keycloak 이 백채널 알림을 보낼 것이다 ===
시각: 14:53:29
=== Keycloak 로그 — 백채널 요청을 보냈는가, 결과는 ===
=== BFF 로그 — 백채널 요청이 도착했는가 ===
=== 앱 세션이 정리되었는가 ===
Redis: 2 키
_oauth2_proxy-6b028a70f69c8f0da9966eb36972dff2
bff:session:sessions:6e0d9af4-2c8f-47d2-bf83-8b1e9670c679
=== 로그아웃 전 — 실제 세션이 있는가 ===
keycloak-patterns 세션: 1
Redis: 1 키
=== ★ IdP 로그아웃 → 백채널 알림 ===
시각: 14:54:21
=== Keycloak 로그 ===
=== BFF 로그 — 요청이 왔는가 ===
=== 앱 세션 ===
Redis: 1 키
@@ -0,0 +1,15 @@
=== IdP 세션은 실제로 끊겼는가 ===
keycloak-patterns 세션: 0
=== ★ Keycloak 파드가 app1.hyeonworks.com 에 닿는가 ===
DNS 해석:
Address: 100.83.212.4
Non-authoritative answer:
HTTPS 도달:
HTTP 200 (0 이면 못 닿음)
=== Keycloak 로그 전체에서 backchannel 흔적 ===
keycloak-0: 0 줄
keycloak-1: 0 줄
@@ -0,0 +1,17 @@
# C-2 — 백채널 로그아웃 증거
2026-09-04 16:3016:55 KST
해설: [`docs/experiment-c2-backchannel-logout.md`](../../experiment-c2-backchannel-logout.md)
| 파일 | 무엇을 보여주는가 |
|---|---|
| `01-current-state.txt` | 두 클라이언트 모두 `backchannelLogoutUrl` 없음 · BFF 소스에 `oidcLogout` 없음 · 후보 경로 셋 다 **302**(핸들러 없음) |
| `02-configure-idp.txt` | IdP 쪽에만 `backchannel.logout.url` 설정 (점 표기는 실패, JSON 으로 성공) |
| `03-logout-attempt.txt` | **살아 있는 세션(1)에 로그아웃 → IdP 세션 0, Redis 세션은 1 그대로.** Keycloak·BFF 로그에 흔적 없음 |
| `04-reachability.txt` | **Keycloak 파드가 `app1.hyeonworks.com` 에 `HTTP 200` 으로 닿는다** — 네트워크 문제가 아님 |
## 핵심 세 줄
1. **백채널 로그아웃은 어느 쪽에도 구현되어 있지 않았다.** C-1 이 관측한 "전파 안 됨"의 원인이다.
2. **IdP 쪽만 설정해도 소용없다.** 받을 엔드포인트와 `sid → 세션` 역인덱스가 앱에 있어야 한다.
3. **도달성이 숨은 전제다.** 이 실험대는 닿지만, 앱이 사설망에 있으면 설정해도 조용히 실패한다.
@@ -0,0 +1,15 @@
=== 백업 전 상태 ===
realms|clients|users|sessions|authclients = 2|15|2|3|1
=== pg_dump — 전체 덤프 ===
시작: 14:59:30
완료: 14:59:30
크기: 394945 bytes (6956 줄)
포함된 테이블 수: 101
=== 덤프에 세션이 들어 있는가 ===
offline_user_session 언급: 13
COPY public.offline_user_session (user_session_id, user_id, realm_id, created_on, offline_flag, data, last_session_refre
E1q5xI7tt4U_WhZpW7rEPIF2 48b37d33-8419-49aa-9b5b-7731975be50c 7845f394-723a-4d07-b530-c7416b2e1d31 1788500836 0 {"ipAddr
2ap3DyRiBF8OdMiqCodsJ0mp 48b37d33-8419-49aa-9b5b-7731975be50c 7845f394-723a-4d07-b530-c7416b2e1d31 1788501029 0 {"ipAddr
Zsk4QcgXf_qgyMKzde5AG-Fz 48b37d33-8419-49aa-9b5b-7731975be50c 7845f394-723a-4d07-b530-c7416b2e1d31 1788501263 0 {"ipAddr
@@ -0,0 +1,17 @@
=== ★ 파괴 — 스키마를 통째로 지운다 ===
시각: 14:59:47
DROP SCHEMA
CREATE SCHEMA
남은 테이블: 0
=== 서비스 영향 ===
https://auth.hyeonworks.com/realms/master HTTP 200
https://app1.hyeonworks.com/ HTTP 200
bff-555df79c97-6j86w 1/1 Running 0 49m
bff-555df79c97-vgg6g 1/1 Running 0 49m
keycloak-0 1/1 Running 0 4m15s
keycloak-1 1/1 Running 0 4m38s
=== Keycloak 이 무엇을 말하는가 ===
2026-09-04 05:58:02,598 WARN [org.keycloak.jgroups.protocol.KEYCLOAK_JDBC_PING2] (blocking-thread--p3-t2) Failed to fetch the cluster members from the database.: org.postgresql.ut
at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2904)
@@ -0,0 +1,29 @@
=== 무엇이 실제로 깨지는가 ===
/.well-known/openid-configuration HTTP 500
/protocol/openid-connect/certs HTTP 200
토큰 발급 (DB 쓰기 필요) HTTP 400
=== ★ 복구 — 덤프에서 되돌린다 ===
시작: 15:00:12
완료: 15:00:13
오류 줄: 0
=== 복구 후 데이터 ===
realms|clients|users|sessions|authclients = 2|15|2|3|1
=== 복구 직후 — 재시작 없이 되는가 ===
+15초 well-known=200 토큰발급=200
→ 재시작 없이 회복
=== 복구 전 세션이 살아났는가 ===
user_session_id | realm
--------------------------+-------------------
E1q5xI7tt4U_WhZpW7rEPIF2 | master
2ap3DyRiBF8OdMiqCodsJ0mp | master
Zsk4QcgXf_qgyMKzde5AG-Fz | master
vsDgCVo12-qX0CC63ZmYzbYF | keycloak-patterns
(4 rows)
=== 파드 재시작 횟수 ===
keycloak-0 restarts=0
keycloak-1 restarts=0
+16
View File
@@ -0,0 +1,16 @@
# D-1 — 백업·복구 리허설 증거
2026-09-04 16:5517:05 KST
해설: [`docs/experiment-d1-backup-restore.md`](../../experiment-d1-backup-restore.md)
| 파일 | 무엇을 보여주는가 |
|---|---|
| `01-backup.txt` | `pg_dump --clean --if-exists` — 395KB · 101 테이블 · **세션 데이터 포함** |
| `02-destruction.txt` | `DROP SCHEMA public CASCADE` → 테이블 0개. **그런데 외부는 `HTTP 200`** — Keycloak 이 realm 캐시로 서빙한다 |
| `03-restore.txt` | 깨지는 것과 안 깨지는 것(`certs` 200 / `well-known` 500 / 토큰 400) · **복구 1초 · 오류 0건 · 데이터 완전 일치 · 재시작 0회** |
## 핵심 세 줄
1. **데이터베이스를 통째로 비웠는데 서비스가 200 을 냈다.** 헬스체크는 "DB 가 살아 있다"만 보고 "데이터가 있다"는 안 본다.
2. **복구는 1초, 오류 0건, 재시작 불필요.** 절차가 맞다는 것은 확인됐다.
3. **RPO 는 두 겹이다** — 백업 주기 + A-3 에서 측정한 `synchronous_commit OFF` 손실. 그리고 이번 덤프는 호스트의 `/tmp` 에 있어 **같은 장애 도메인**이다.
@@ -0,0 +1,9 @@
=== D-1 의 교훈: 업그레이드 전에 백업한다 ===
백업: 396333 bytes
=== 현재 버전과 스키마 상태 ===
quay.io/keycloak/keycloak:26.7.0
총 마이그레이션 수: 210
=== 로그인 상태 만들기 (업그레이드 후 살아남는지 볼 것) ===
현재 세션: 4
@@ -0,0 +1,17 @@
=== ★ 롤백 시도: 26.7.0 → 26.0 ===
시각: 15:02:20
statefulset.apps/keycloak image updated
+20초 keycloak-0:Running(1/1) keycloak-1:Running(0/1)
+40초 keycloak-0:Running(1/1) keycloak-1:Running(0/1)
+60초 keycloak-0:Running(1/1) keycloak-1:Running(0/1)
+80초 keycloak-0:Running(1/1) keycloak-1:Error(0/1)
+100초 keycloak-0:Running(1/1) keycloak-1:Running(0/1)
+120초 keycloak-0:Running(1/1) keycloak-1:Error(0/1)
+140초 keycloak-0:Running(1/1) keycloak-1:CrashLoopBackOff(0/1)
+160초 keycloak-0:Running(1/1) keycloak-1:Running(0/1)
=== 새 파드가 무엇을 말하는가 ===
2026-09-04 06:03:25,877 ERROR [org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler] (main) ERROR: Failed to start server in (production) mode
2026-09-04 06:03:25,877 ERROR [org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler] (main) ERROR: liquibase.exception.ValidationFailedException: Validation Failed:
2026-09-04 06:03:25,877 ERROR [org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler] (main) ERROR: Validation Failed:
2026-09-04 06:03:25,877 ERROR [org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler] (main) For more details run the same command passing the '--verbose' option. Also you can use '--help' to see the detai
@@ -0,0 +1,20 @@
=== 서비스는 살아 있는가 (StatefulSet 롤링이 막아줬다) ===
https://auth.hyeonworks.com/realms/master HTTP 200
Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice
ready 주소: [10.42.1.140]sed: -e expression #1, char 27: unknown option to 's'
=== Liquibase 오류 상세 ===
2026-09-04 06:03:25,877 ERROR [org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler] (main) ERROR: liquibase.exception.ValidationFailedException: Validation Failed:
1 changesets check sum
2026-09-04 06:03:25,877 ERROR [org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler] (main) ERROR: Validation Failed:
1 changesets check sum
=== ★ 앞으로 되돌린다 (26.7.0) ===
statefulset.apps/keycloak image updated
partitioned roll out complete: 2 new pods have been updated...
keycloak-0 1/1 Running 0 10m
keycloak-1 1/1 Running 0 28s
=== 데이터는 무사한가 ===
realms|clients|migrations|sessions = 2|15|210|4
외부 진입점 HTTP 200
@@ -0,0 +1,17 @@
# D-2 — 버전 업그레이드 증거
2026-09-04 17:0517:15 KST
해설: [`docs/experiment-d2-version-upgrade.md`](../../experiment-d2-version-upgrade.md)
| 파일 | 무엇을 보여주는가 |
|---|---|
| `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. **스키마가 바뀌었으면 롤백은 안 된다.** (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,17 @@
=== 실험대의 Secret 목록 ===
bff-secrets Opaque keys=1
keycloak-lab-secrets Opaque keys=2
oauth2-proxy-secrets Opaque keys=3
=== ★ base64 는 암호화가 아니다 — 한 줄로 읽힌다 ===
keycloak-lab-secrets/POSTGRES_PASSWORD = lab-postgres-change-me
keycloak-lab-secrets/KC_BOOTSTRAP_ADMIN_PASSWORD = lab-admin-change-me
bff-secrets/KEYCLOAK_CLIENT_SECRET = bff-lab-secret
oauth2-proxy-secrets/COOKIE_SECRET_A = lab-cookie-secret-aaaaaaaaaaaaaa
=== describe 는 값을 감춘다 (그래서 안전하다고 착각한다) ===
Type: Opaque
Data
====
KEYCLOAK_CLIENT_SECRET: 14 bytes
@@ -0,0 +1,23 @@
=== k3s 의 데이터 저장소 ===
Encryption Status: Disabled, no configuration file found
=== 저장 파일 ===
total 23336
drwx------ 2 root root 4096 Sep 2 09:12 .
drwx------ 8 root root 4096 Sep 4 03:23 ..
-rw-r--r-- 1 root root 13078528 Sep 4 06:05 state.db
-rw-r--r-- 1 root root 32768 Sep 4 06:06 state.db-shm
-rw-r--r-- 1 root root 10769712 Sep 4 06:06 state.db-wal
=== ★ 저장 파일에서 비밀번호가 그대로 보이는가 ===
state.db 안의 평문 일치: 2
=== 평문이 저장 파일에 있다는 것을 눈으로 ===
client secret 평문 등장 횟수: 0
=== 누가 Secret 을 읽을 수 있는가 ===
default SA: no
(Role 이 없으면 네임스페이스에 별도 제한이 없다는 뜻)
=== 파드 안에서는 어떻게 보이는가 ===
KEYCLOAK_CLIENT_SECRET=bff-lab-secret
BFF_DB_PASSWORD=lab-postgres-change-me
@@ -0,0 +1,15 @@
# D-3 — 비밀 관리 증거
2026-09-04 17:1517:25 KST
해설: [`docs/experiment-d3-secret-management.md`](../../experiment-d3-secret-management.md)
| 파일 | 무엇을 보여주는가 |
|---|---|
| `01-base64-not-encryption.txt` | 실험대의 **모든 비밀이 명령 네 줄로** 평문 출력. `describe``14 bytes` 만 보여줘 착각을 준다 |
| `02-at-rest.txt` | **`Encryption Status: Disabled`** · `state.db` 안에 비밀번호 평문 **2회 일치** · 파드 안에서는 `KEYCLOAK_CLIENT_SECRET=bff-lab-secret` 환경변수 · `default` SA 는 **읽을 수 없음** |
## 핵심 세 줄
1. **base64 는 감추려는 것이 아니라 YAML 에 바이트를 담기 위한 것이다.** `describe` 가 값을 가려 안전하다는 착각을 준다.
2. **저장소 암호화가 꺼져 있고 노드 디스크에 평문이 있다.** 노드 디스크 하나가 전 클러스터의 비밀이다.
3. **네 경로 중 RBAC 만 제 역할을 한다.** 그것이 실질적 방어선이며, 관리자에게는 아무 방어가 없다.
@@ -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. 두 앱을 다시 연다 — 그대로 열리면 앱 세션이 독립적이라는 뜻
```
+261
View File
@@ -0,0 +1,261 @@
# C-2 — 로그아웃이 전 앱에 퍼지는가 (백채널 로그아웃)
브랜치 `feature/keycloak-c2-backchannel-logout` ·
증거 [`docs/evidence/c2-backchannel-logout/`](evidence/c2-backchannel-logout/) ·
2026-09-04 16:3016:55 KST
선행: [`C-1`](experiment-c1-multi-app-sso.md) — **IdP 로그아웃이 앱에 전파되지 않는다**를 관측했다
---
## 0. 결론부터
| 확인 | 결과 |
|---|---|
| 백채널 로그아웃이 설정되어 있었는가 | **아니다.** 두 클라이언트 모두 `backchannelLogoutUrl` 없음 |
| BFF 에 그 엔드포인트가 있는가 | **아니다.** 소스에 `oidcLogout` 설정이 없다 |
| IdP 쪽만 설정하면 되는가 | **★ 안 된다.** 앱 세션이 그대로 남았다 |
| Keycloak 이 앱 URL 에 닿기는 하는가 | **닿는다** (`HTTP 200`) — 네트워크 문제가 아니다 |
**C-1 이 관측한 "로그아웃이 안 퍼진다" 의 원인은 단순했다 —
아무도 구현하지 않았다.**
---
## 1. 현재 상태 — 어느 쪽에도 없다
### IdP 쪽
```bash
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh get clients -r keycloak-patterns -q clientId=bff-confidential --fields attributes
```
```
frontchannelLogout : false
(backchannel.logout.url 없음)
```
`oauth2-proxy` 클라이언트도 마찬가지였다.
### 앱 쪽
```bash
grep -rn "oidcLogout\|backchannel" bff/src/main/java/
```
**아무것도 안 나온다.**
Spring Security 6.2+ 는 백채널 로그아웃을 지원하지만 **명시적으로 켜야 한다.**
```java
.oidcLogout(oidc -> oidc.backChannel(Customizer.withDefaults()))
```
이 설정이 없으면 `/logout/connect/back-channel/{registrationId}` 경로가
**생기지 않는다.**
```
/logout/connect/back-channel/keycloak HTTP 302 ← 로그인으로 리다이렉트
/backchannel-logout HTTP 302
/oauth2/sign_out HTTP 302
```
**302 는 "그런 핸들러가 없어서 인증 요구로 떨어졌다"는 뜻**이다.
엔드포인트가 있었다면 POST 를 받아 200 이나 400 을 돌려줬을 것이다.
---
## 2. IdP 쪽만 설정하고 시험했다
```bash
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"}'
```
```
backchannel.logout.session.required = true
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 으로 다시 넣어 성공했다. **실패한 시도의 파일에 성공 출력을 붙여
> 인쇄한 것은 잘못이었다.**
### 살아 있는 세션에 로그아웃을 걸었다
```
=== 로그아웃 전 ===
keycloak-patterns 세션: 1
Redis: 1 키
=== IdP 로그아웃 ===
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 쪽은 끊겼다
Redis: 1 키 ← ★ 앱 세션은 그대로다
```
**IdP 세션만 사라지고 앱 세션은 남았다.** C-1 과 같은 결과다.
### 네트워크 문제가 아님을 확인했다
```
=== Keycloak 파드가 app1.hyeonworks.com 에 닿는가 ===
DNS 해석: Address: 100.83.212.4
HTTPS 도달: HTTP 200
```
**클러스터 안에서 공개 이름으로 앱에 닿는다.** 이 실험대는 tailnet + split DNS
구성이라 헤어핀이 되는데, **운영에서는 안 되는 경우가 흔하다.**
> **백채널 로그아웃의 숨은 전제** — IdP 가 **앱의 공개 URL 로 서버에서 서버로**
> 요청을 보낼 수 있어야 한다. 앱이 사설망에 있고 IdP 가 밖에 있으면
> **설정을 해도 도달하지 못한다.** 그때는 로그도 안 남고 조용히 실패한다.
---
## 3. 그래서 왜 안 퍼졌는가
```
IdP 로그아웃
├─ ① Keycloak 이 backchannel.logout.url 로 POST 를 보낸다 (설정함)
├─ ② 앱이 그 POST 를 받는 엔드포인트를 갖고 있다 ★ 없다
└─ ③ 앱이 logout token 을 검증하고 sid 로 세션을 찾아 지운다 ★ 없다
```
**②와 ③이 없다.** ①만 설정해도 받을 사람이 없다.
Keycloak 로그에 `backchannel` 문자열이 **0줄**이었다 — 다만 이것만으로
"보내지 않았다"고 단정할 수는 없다 (DEBUG 레벨일 수 있다).
**확실한 것은 앱 세션이 남았다는 관측**이다.
---
## 4. 개념 — 백채널 로그아웃의 구조
```
사용자가 어느 앱에서든 로그아웃
Keycloak 이 SSO 세션에 붙은 client session 목록을 본다 (C-1 의 그 구조)
├──POST──▶ app1 의 backchannel.logout.url
└──POST──▶ app2 의 backchannel.logout.url
본문: logout_token (JWT)
{ "sid": "...", "sub": "...", "events": {...} }
```
### `sid` 가 여기서 쓰인다
**A-0 에서 확인한 그 `sid`** 다 — JWT·DB·관리 API 에서 같은 문자열이었던.
```
logout_token 의 sid → 앱이 "그 sid 로 만든 내 세션"을 찾아 지운다
```
**그래서 앱은 `sid → 자기 세션 ID` 역인덱스를 갖고 있어야 한다.**
Spring Security 는 이를 위해 `OidcSessionRegistry` 를 쓰며,
**여러 인스턴스가 있으면 그 레지스트리도 공유 저장소여야 한다**
B-1·B-2 에서 겪은 것과 **같은 문제가 한 겹 더 있다.**
### 부분 실패는 어떻게 되는가
```
app1 로그아웃 성공, app2 는 응답 없음
└─ Keycloak 은 재시도하는가? 얼마나?
└─ 사용자는 app2 에서 여전히 로그인 상태다
```
**로그아웃은 원자적이지 않다.** 앱이 늘어날수록 "일부만 로그아웃된 상태"가
생길 확률이 올라간다.
---
## 5. 겪은 문제
| 문제 | |
|---|---|
| `kcadm -s "attributes.backchannel.logout.url=..."` 이 exit 1 | 점 표기가 안 먹는다. **JSON 으로 통째로** 줘야 한다 |
| 첫 시험이 무의미했다 | 로그아웃 전 IdP 세션이 **이미 0** 이었다. 끊을 대상이 없었다 |
| 세션 수를 realm 없이 셌다 | C-1 과 같은 실수 — `master` 의 admin 세션이 섞인다 |
| Keycloak 재시작 후 로그인 폼이 안 넘어갔다 | 인증 세션 쿠키가 무효화된 상태에서 폼을 재사용했다 |
**두 번째가 A층에서 반복한 교훈이다** — **주입 대상이 실제로 존재하는지
먼저 확인한다.** 세션이 없는 상태에서 로그아웃을 걸고 "전파가 안 된다"고
결론지을 뻔했다.
---
## 6. 구현하려면 무엇이 필요한가
| 계층 | 할 일 |
|---|---|
| **IdP** | 클라이언트마다 `backchannel.logout.url` 설정 (완료) |
| **앱** | `.oidcLogout(oidc -> oidc.backChannel(...))` 활성화 |
| **앱** | `OidcSessionRegistry` 를 **공유 저장소**로 (인스턴스가 여럿이므로) |
| **네트워크** | IdP → 앱 공개 URL 도달 (이 실험대는 됨, 운영은 확인 필요) |
| **oauth2-proxy** | **지원하지 않는다.** 별도 방안이 필요하다 |
**마지막이 C-1 과 맞물린다** — app1(BFF)은 구현할 수 있지만
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. 현재 설정 확인 — 어느 쪽에도 없다
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 으로
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 해서)
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
"select count(*) from offline_user_session us join realm r on r.id=us.realm_id
where r.name='keycloak-patterns' and us.offline_flag='0'"
# 4. 로그아웃하고 앱 세션을 본다
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 로
kubectl -n keycloak-lab run t --rm -i --restart=Never --image=curlimages/curl:8.11.1 \
--command -- curl -s -o /dev/null -w '%{http_code}\n' https://app1.hyeonworks.com/
```
---
## 8. 다음에 남기는 것
| | |
|---|---|
| **구현** | `.oidcLogout()` 활성화 + `OidcSessionRegistry` 공유 |
| **oauth2-proxy** | 백채널 로그아웃 미지원 — SSO 안에서 앱마다 동작이 갈린다 |
| **운영** | IdP → 앱 도달성이 전제다. 안 되면 **조용히 실패한다** |
| **B-2 와 연결** | 로그아웃이 지우는 것은 지금도 세 곳 중 하나뿐이다 |

Some files were not shown because too many files have changed in this diff Show More