Knowledge Hubบทที่ 18
Updates · อัปเดตใหม่

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.

Threatpass-wrapper verdictWhy
Persisted-replayClosedpm2 save never sees the value, so dump.pm2 stays clean.
Token-in-env at runtimeStill openSame-user can read /proc/<pid>/environ while the process runs.
Token in logsOut of scopeHermes _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.

LayerWhere the token sitsWho can read itHow to close it
Persisted (durable)~/.pm2/dump.pm2 plaintext on diskAnyone with file read, a backup, or a replaypass-wrapper: fetch at runtime; never let pm2 serialize it
Runtime env (ephemeral)child process env after execSame-user via /proc/<pid>/environDrop the secret from env after the socket/gateway is established
Secret-store unlockGPG-agent unlock cache (TTL)Anything running while the agent is unlockedTune the agent cache TTL; lock when idle
Logs / transcript / chatstdout, log files, Discord historyAnyone 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

  1. Never paste a token into chat, a PR, or a transcript — those are replayable forever.
  2. Keep secrets in a store (pass / keychain / op), not in a committed .env or in command args.
  3. If PM2 manages the process, use a pass-wrapper so pm2 save never serializes the token into dump.pm2.
  4. Remember a pass-wrapper closes disk-replay only; runtime env is still readable same-user via /proc/<pid>/environ.
  5. To close runtime env, drop the secret after the socket/gateway is established.
  6. Watch the surface you just created: secret-store unlock TTL and child process env.