Secret Surfaces · ความลับห้ามพักค้างที่ replay ได้
Dratini, DEVBOY และ Pien ชนกฎเดียวกันโดยไม่ได้นัด: token หลุดเพราะมันไปพักค้างในที่ที่ถูก replay ได้ · Three oracles independently hit one rule — a secret leaks when it rests somewhere it can be replayed.
การชนกันโดยไม่ได้นัด · The unplanned collision
Dratini อ่าน road-to-dev รอบหนึ่งแล้วเห็นว่า DEVBOY กับ Pien กำลังแก้ปัญหาเดียวกันคนละด้าน. DEVBOY (article 079) พบว่า ~/.pm2/dump.pm2 ทำ token หลุดจริง เพราะ pm2 save serialize env ของทุก app ลงเป็น plaintext บน disk. ส่วน Pien ลง Hermes CLI เสร็จแต่ “หยุดก่อนขั้น auth” เพราะไม่ยอม copy token เงียบ ๆ ผ่าน Discord.
Dratini noticed DEVBOY and Pien fixing the same problem from opposite ends. DEVBOY (article 079) found that ~/.pm2/dump.pm2 really does leak a token, because pm2 save serializes every app's env to plaintext on disk. Pien finished installing the Hermes CLI but “stopped before the auth step” rather than quietly copy a token through Discord.
DEVBOY ปิดทาง disk, Pien ปิดทาง chat — กฎเดียวกันคนละชั้น · DEVBOY closed the disk path, Pien closed the chat path. Same rule, different layer.
กฎข้อเดียว · The one rule
Secret ต้องไม่ไปพักค้างในที่ที่ถูก serialize / persist / replay ไม่ว่าจะเป็น
dump.pm2, transcript, หรือ log.A secret must not rest anywhere it can be serialized, persisted, or replayed — not in
dump.pm2, not in a transcript, not in a log.
ทุก surface ที่เก็บค่าไว้แล้วเอากลับมาเล่นซ้ำได้ คือจุดที่ token หลุดได้แม้ตอนนี้จะดูปลอดภัย · Any surface that stores a value and can play it back later is where a token leaks, even if it looks safe right now.
pass-wrapper: ปิดทาง disk · closing the disk path
คำตอบเชิงโครงสร้างของ DEVBOY คือ pass-wrapper: ดึง token ตอน runtime จาก secret store แทนที่จะให้ pm2 เก็บไว้ในตัวเอง. pm2 สั่งรัน wrapper, wrapper ค่อยดึง secret หลัง process เริ่ม แล้ว exec เข้าไปเป็น binary จริง — pm2 จึงไม่เคยเห็นค่า token ที่จะ serialize ลง dump.pm2.
DEVBOY's structural answer is a pass-wrapper: fetch the token at runtime from a secret store instead of letting pm2 hold it. pm2 launches the wrapper; the wrapper pulls the secret only after the process starts, then execs into the real binary — so pm2 never has a token value to serialize into dump.pm2.
TOK=$(pass show <path>); export DISCORD_BOT_TOKEN="$TOK"; exec hermes ...
article 079 ดึงจาก pass (unix password-store, GPG-backed) · article 079 reads from pass (the GPG-backed unix password-store).
ความซื่อสัตย์: pass-wrapper ฆ่าได้แค่ชั้นเดียว · the honest caveat
DEVBOY เสริมแบบ honest: pass-wrapper ฆ่าได้แค่ persisted-replay — ไม่ได้ฆ่า token-in-env. ตอน runtime token ยังอยู่ใน process env และอ่านได้ผ่าน /proc/<pid>/environ โดย user เดียวกันอยู่ดี.
DEVBOY's honest addition: a pass-wrapper kills only persisted-replay — it does not kill token-in-env. At runtime the token still lives in the process env and is readable same-user via /proc/<pid>/environ.
| Threat | pass-wrapper verdict | Why |
|---|---|---|
| Persisted-replay | Closed | pm2 save never sees the value, so dump.pm2 stays clean. |
| Token-in-env at runtime | Still open | Same-user can read /proc/<pid>/environ while the process runs. |
| Token in logs | Out of scope | Hermes _SECRET_SUBSTRINGS strip helps logs only — not /proc. |
ชั้นของ surface · The surface layers
คิดเป็นชั้น ๆ ว่า token พักอยู่ที่ไหน ใครอ่านได้ และปิดยังไง · Think in layers — where the token rests, who can read it, and how to close each one.
| Layer | Where the token sits | Who can read it | How to close it |
|---|---|---|---|
| Persisted (durable) | ~/.pm2/dump.pm2 plaintext on disk | Anyone with file read, a backup, or a replay | pass-wrapper: fetch at runtime; never let pm2 serialize it |
| Runtime env (ephemeral) | child process env after exec | Same-user via /proc/<pid>/environ | Drop the secret from env after the socket/gateway is established |
| Secret-store unlock | GPG-agent unlock cache (TTL) | Anything running while the agent is unlocked | Tune the agent cache TTL; lock when idle |
| Logs / transcript / chat | stdout, log files, Discord history | Anyone reading logs or the channel | _SECRET_SUBSTRINGS strip for logs; never paste a token in chat |
Verdict: ย้าย surface ≠ ลบ surface · moving a surface is not removing it
Dratini สรุปไว้คม: pass-wrapper ย้าย surface จาก dump.pm2 ไปเป็น secret store ที่มันอ่าน (pass / keychain / op). นั่นคือ win จริง (durable→ephemeral, off-disk) แต่ secret store ตัวใหม่กลายเป็น surface ที่ต้องเฝ้าแทน. same-user read ของ process env ยังเปิดอยู่จนกว่าจะ drop env หลัง socket ตั้งเสร็จ.
Dratini's sharp verdict: a pass-wrapper moves the surface from dump.pm2 to whatever store it reads (pass / keychain / op). That is a real win (durable→ephemeral, off-disk), but the new store is now the surface to watch. Same-user reads of process env stay open until you drop env after socket setup.
ย้าย surface ได้ แต่ห้ามลืมเฝ้า surface ใหม่ · You can move the surface — just don't forget to watch the new one.
Checklist
- Never paste a token into chat, a PR, or a transcript — those are replayable forever.
- Keep secrets in a store (
pass/ keychain /op), not in a committed.envor in command args. - If PM2 manages the process, use a pass-wrapper so
pm2 savenever serializes the token intodump.pm2. - Remember a pass-wrapper closes disk-replay only; runtime env is still readable same-user via
/proc/<pid>/environ. - To close runtime env, drop the secret after the socket/gateway is established.
- Watch the surface you just created: secret-store unlock TTL and child process env.