ป้องกัน API key รั่วไหลจาก AI agent ด้วย token อายุสั้น
AI agent อ่าน API key ใน environment ได้และอาจส่งออกใน tool call เดียว เรียนรู้การใช้ credential gateway กับ token แบบจำกัดสิทธิ์และมีอายุสั้น
ความหมายของการไม่ให้ secrets อยู่ใน AI agents
AI agent คือ Linux process ทั่วไปที่ใช้รัน commands ทุก environment variable ที่ process นั้นถืออยู่สามารถถูกอ่านได้โดย code ที่ process รัน ดังนั้น API key ที่อยู่ใน environment ของ agent ก็คือ key ที่ agent สามารถส่งไปยัง host ใดก็ได้ที่เข้าถึงได้ การไม่ให้ secrets อยู่ใน agent หมายถึงการให้ handle แทน key เช่น token แบบมีขอบเขตและมีอายุสั้น หรือ placeholder ที่ให้ระบบอื่นแทนค่าด้วยค่าจริงที่ network boundary
เรื่องนี้ไม่ใช่กรณีที่ model กลายเป็นอันตราย กลไกจริงเรียบง่ายกว่านั้น agent อ่าน web page, README หรือ issue comment ที่มีคำสั่ง แล้วทำตาม เพราะสำหรับ language model แล้วไม่มีความแตกต่างระหว่างข้อความที่คุณเขียนกับข้อความที่ agent ดึงมา นี่คือ prompt injection เมื่อเกิดขึ้นแล้ว ขอบเขตความเสียหายจะขึ้นอยู่กับสิ่งเดียวเท่านั้น คือ process สามารถอ่านอะไรได้บ้าง หากยังไม่ได้กำหนด boundary การรัน coding agent อย่างปลอดภัยบนเซิร์ฟเวอร์จะอธิบายระดับของ isolation ที่คู่มือนี้สร้างต่อยอดขึ้นมา
แบบจำลองภัยคุกคามโดยสรุป
เรียกใช้คำสั่งนี้ด้วย user เดียวกับที่ agent ใช้งาน
tr '\0' '\n' < /proc/self/environ | grep -iE 'key|token|secret|password'ทุกบรรทัดที่คำสั่งนี้แสดงหมายถึงมี HTTP request เพียงครั้งเดียวก็สามารถส่งข้อมูลไปยังเซิร์ฟเวอร์ของบุคคลภายนอกได้ ตอนนี้ให้ตรวจดูว่ามีอะไรอยู่บน disk ใกล้กับ agent บ้าง
grep -rIl --exclude-dir=.git -e 'API_KEY' -e 'SECRET' ~/projects
find ~ -maxdepth 3 -name '.env' -o -name 'credentials' -o -name '*.pem'agent ที่มี shell ไม่จำเป็นต้องใช้ exploit ที่ซับซ้อนเพื่อส่งข้อมูลเหล่านั้นออกไป วิธีทั่วไป 4 แบบก็เพียงพอ และทั้ง 4 แบบจะดูเหมือนการทำงานปกติใน log:
- การทำ
curlหรือfetchแบบ outbound ไปยัง host ใดก็ได้ โดยใส่ค่าไว้ใน query string - การทำ
git commitและgit pushไปยัง repository ที่ agent มีสิทธิ์เขียน - package install script ซึ่งสามารถรันโค้ดใดก็ได้ด้วย user ของ agent
- การ lookup DNS ของ hostname ที่มีค่านั้นอยู่ภายใน ซึ่งข้อมูลยังถูกส่งออกได้แม้จะบล็อก HTTP egress แล้ว
คุณไม่สามารถแก้ปัญหานี้ได้ด้วยการ review เพียงอย่างเดียว วิธีแก้คือทำให้แน่ใจว่าไม่มีข้อมูลสำคัญอยู่ในขอบเขตที่ agent เข้าถึงได้
Secret ใน working tree คือ Secret ใน context window
Agent จะอ่านไฟล์ต่าง ๆ ไฟล์ .env ที่อยู่ใน repository ซึ่ง agent กำลังทำงานจะถูกอ่าน และเมื่ออ่านแล้ว ไฟล์นั้นจะอยู่ใน context window ซึ่งหมายความว่าไฟล์จะปรากฏใน transcript, ใน log ที่คุณเก็บไว้ และในสิ่งที่ agent เขียนต่อไป
ก่อนหน้านี้ เมื่อ key ยังอยู่ใน tree ที่ agent ทำงานอยู่:
cd ~/projects/billing
cat .env
# STRIPE_SECRET_KEY=sk_live_...
# DATABASE_URL=postgres://app:hunter2@db.internal:5432/billingหลังจากย้ายไฟล์ออกไปจน agent เข้าถึงไม่ได้:
sudo install -d -m 750 -o root -g agent-review /etc/agent-review
sudo install -m 640 -o root -g agent-review ~/projects/billing/.env /etc/agent-review/billing.env
rm ~/projects/billing/.envผู้ใช้ของ agent จะเปิดไฟล์ไม่ได้อีกต่อไป เพราะ working tree ไม่มีไฟล์นั้นแล้ว กฎ deny ใน config ของ agent เป็นเพียงชั้นที่สอง ไม่ใช่ชั้นแรก Claude Code จะอ่าน permission rules จาก .claude/settings.json ใน project:
{
"permissions": {
"deny": ["Read(./.env)", "Read(./secrets/**)", "Read(./**/*.pem)"]
}
}การตั้งค่านี้ป้องกันไม่ให้ agent เปิดไฟล์โดยไม่ตั้งใจขณะสำรวจ และมีผลครอบคลุม file tools ด้วย โดย Claude Code จะตรวจสอบกฎเหล่านี้กับ file commands ที่รู้จักภายใน Bash เช่น cat และ head รวมถึง target ของ redirection เช่น < .env แต่จะไม่ป้องกันคำสั่งที่มาจาก instruction ที่ถูก inject ซึ่งสั่งให้รัน grep -r KEY . ภายใน project directory หรือ short Python script ที่เปิด .env ด้วยตนเอง เนื่องจากเอกสาร permission ระบุว่า deny rules ไม่มีผลกับคำสั่งที่อ่านไฟล์โดยไม่ระบุชื่อไฟล์ หรือกับ subprocess ที่เปิดไฟล์ด้วยตนเอง สำหรับกรณีเหล่านี้ เอกสารแนะนำ sandbox ของ Claude Code ซึ่งบังคับใช้การบล็อกในระดับ operating system การที่ระบบจะถามก่อนรันคำสั่งดังกล่าวหรือไม่ ขึ้นอยู่กับ permission mode ของ session และ auto mode เป็นโหมดเริ่มต้นที่มีมาให้สำหรับ interactive Claude Code session ในปัจจุบัน ดังนั้น server ที่คุณไม่ได้เฝ้าดูอาจรันคำสั่งเหล่านี้โดยไม่ถามมากขึ้น ข้อจำกัดเดียวกันนี้ใช้กับทุกสิ่งที่ปรับพฤติกรรมของ agent แทนการกำหนด permission เช่น skill ที่กำหนดให้ agent ทำการเปลี่ยนแปลงเท่าที่จำเป็นและใช้งานได้ จะช่วยไม่ให้การทำงานเข้าไปในไฟล์ที่ไม่ควรเปิด แต่ยังคงเป็นเพียงคำแนะนำที่สามารถชักจูงให้ model ละทิ้งได้ ให้มอง config เป็นราวกั้น และมอง filesystem permission เป็นกำแพง การแยกหน้าที่แบบเดียวกันนี้ใช้ภายใน container ด้วย โดย env files และ secrets ใน Docker Compose อธิบายปัญหาในรูปแบบเดียวกันที่อยู่ลึกลงไปอีกหนึ่งชั้น
กำหนด user ที่ไม่มีสิทธิ์พิเศษแยกให้ agent แต่ละตัว
หาก agent ทำงานภายใต้ user เดียวกับคุณ agent จะได้รับ SSH keys, cloud credentials และ shell history ของคุณไปด้วย การสร้าง user แยกใช้เพียงหนึ่งคำสั่ง และตัดการเข้าถึงข้อมูลเหล่านี้ทั้งหมด
sudo adduser --disabled-password --gecos "" agent-review
sudo chmod 700 /home/agent-review
sudo -u agent-review cat ~/.ssh/id_ed25519คำสั่งบรรทัดสุดท้ายต้องล้มเหลวพร้อม cat: /home/you/.ssh/id_ed25519: Permission denied หากแสดง key ออกมาแทน แสดงว่า user ของ agent เข้าถึงไฟล์ดังกล่าวได้ การเปิดไฟล์ตามชื่อจำเป็นต้องมีสิทธิ์ search (x) บนทุก directory ที่อยู่เหนือไฟล์นั้น ไม่ใช่สิทธิ์ read ดังนั้น home directory ที่มี mode เป็น 711 ซึ่ง user อื่นไม่สามารถแสดงรายการได้ ยังคงเปิดทางไปถึง key ได้ หากไฟล์ key เองสามารถอ่านได้ chmod 700 ~ จะปิดเส้นทางดังกล่าว และ chmod 600 ~/.ssh/id_ed25519 จะแก้ไขสิทธิ์ของ key อย่าเพิ่ม user ของ agent เข้าไปใน sudo และอย่าให้กฎ NOPASSWD ที่กว้างกว่าคำสั่งเดียวที่จำเป็นต้องใช้ user ที่มีสิทธิ์น้อยที่สุดบน VPS อธิบายรายละเอียดของ group และ sudoers เพิ่มเติม ควรรักษาการแยกนี้ไว้เมื่อคุณเริ่มใช้งานมากกว่าหนึ่ง session บนเครื่องเดียวกัน เพราะ session ของ Claude Code หนึ่งรายการสามารถส่งข้อความตรงไปยังอีก session หนึ่งได้ และข้อมูลใดก็ตามที่ session แรกถืออยู่สามารถส่งผ่านช่องทางนี้ได้ภายในข้อความเดียว
ยังมีขอบเขตด้านความปลอดภัยอีกจุดที่ควรเพิ่มบน cloud VPS บริการ instance metadata จะตอบสนองที่ link-local address แบบคงที่ และมักมอบ role credentials ให้กับทุกสิ่งที่ร้องขอ ปลายทางนี้ไม่ได้ตรวจสอบว่า user ภายในเครื่องใดเป็นผู้เรียกใช้: บน EC2, AWS ระบุว่าคำขอ IMDSv1 ไม่ต้องใช้ token และคำแนะนำของ AWS สำหรับป้องกันไม่ให้ user หนึ่งเข้าถึงบริการนี้ คือการใช้กฎ local firewall ที่อ้างอิง ID ของ user นั้น
sudo iptables -I OUTPUT 1 -p tcp -m owner --uid-owner agent-review -d 169.254.169.254 -j REJECTให้แทรกกฎไว้ด้านบนสุดด้วย -I แทนการเพิ่มต่อท้ายด้วย -A บน host ที่ใช้ ufw ซึ่งมีนโยบายเริ่มต้นเป็น allow outgoing, กฎสุดท้ายใน OUTPUT chain ของ ufw จะยอมรับการเชื่อมต่อ TCP ใหม่ทั้งหมด ดังนั้นระบบจะไม่ไปถึงกฎที่เพิ่มต่อท้าย กฎ iptables จะหายไปหลัง reboot เช่นกัน หากคุณไม่ได้บันทึกกฎให้คงอยู่ และหากเปิดใช้งาน IPv6 endpoint ของบริการ metadata แล้ว ซึ่งคือ [fd00:ec2::254] ให้เพิ่มกฎเดียวกันด้วย ip6tables
ตรวจสอบจากฝั่งของ agent โดย sudo -u agent-review curl -s --max-time 3 http://169.254.169.254/ ต้องไม่แสดงผลใด ๆ และต้องจบการทำงานด้วยสถานะ non zero เนื่องจาก packet ถูกปฏิเสธก่อนออกจากเครื่อง หากแสดงรายการออกมา แสดงว่ายังไปไม่ถึงกฎดังกล่าว
แทรก credential ที่ขอบเขต
รูปแบบที่แก้ปัญหานี้ได้จริงคือการแทรก credential agent จะไม่ถือ key จริง แต่จะส่งคำขอผ่าน local gateway แล้ว gateway จะแทนที่ placeholder ด้วย secret จริงระหว่างส่งออกไป secret จะเก็บอยู่ใน storage ของ gateway ภายใน process อื่น และอยู่ภายใต้ผู้ใช้คนอื่น
OneCLI เป็น implementation แบบ open source รายการหนึ่ง โดยใช้สัญญาอนุญาต Apache-2.0 ยกเว้นไดเรกทอรี ee/ ซึ่งใช้สัญญาอนุญาต enterprise แยกต่างหากและต้องมี subscription สำหรับการใช้งานจริง ระบบทำงานใน containers ที่อยู่ถัดจาก agent ตามเอกสารของโครงการ ณ เดือน October 2026 การตั้งค่า Docker Compose นี้อ่าน secret ที่สร้างไว้ 3 รายการจากไฟล์ .env ซึ่งอยู่ข้างไฟล์ compose:
git clone https://github.com/onecli/onecli.git && cd onecli/docker
cat > .env <<EOF
SECRET_ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)
GATEWAY_INTERNAL_SECRET=$(head -c 32 /dev/urandom | base64)
BETTER_AUTH_SECRET=$(head -c 32 /dev/urandom | base64)
COMPOSE_PROFILES=runner
EOF
chmod 600 .env
docker compose up -d --waitสำรองไฟล์ .env นี้ไว้ เพราะหากสูญเสีย SECRET_ENCRYPTION_KEY secret ทุกค่าที่เก็บไว้ด้วยกันจะกลายเป็น orphan dashboard รับฟังที่พอร์ต 10254 และ gateway รับฟังที่พอร์ต 10255 โดยทั้งคู่ bind กับ 127.0.0.1 เป็นค่าเริ่มต้น คุณจัดเก็บ credential จริงเพียงครั้งเดียว จากนั้นกำหนดค่า placeholder ให้ agent แต่ละตัวแทน key พร้อมกับ access token ที่กำหนดขอบเขตเฉพาะของ agent นั้น ซึ่ง agent จะส่งไปใน header Proxy-Authorization gateway จะจับคู่คำขอขาออกตาม host และ path ถอดรหัส credential ที่ตรงกัน แล้วแทนที่ค่าให้ agent จึงไม่มีข้อมูลใดใน environment ที่คุ้มค่าต่อการขโมย
ประโยชน์สำคัญไม่ได้อยู่ที่การเข้ารหัส แต่อยู่ที่คำถามว่า “agent นี้ใช้ค่าใด และเมื่อใด” กลายเป็นคำค้นหาใน log ได้ คุณอ่าน audit trail เพียงรายการเดียว แทนการคาดเดาว่าสำเนา key อยู่ใน environment ใดจากทั้งหมด 6 แห่ง
ส่ง secret ให้ process ไม่ใช่ environment
หากรัน agent ภายใต้ systemd คุณไม่จำเป็นต้องใช้ environment variable เลย LoadCredential= จะเก็บ secret ไว้ใน private directory ที่อ่านได้เฉพาะ user ของ service โดยจะแสดงเป็น %d ใน unit file และเป็น $CREDENTIALS_DIRECTORY ภายใน process การจำกัดนี้อ้างอิงตาม user ไม่ใช่ service ดังนั้นควรกำหนด user แยกให้ agent แต่ละ service เพราะ process อื่นที่ทำงานด้วย user เดียวกันยังสามารถเปิด directory ได้ เว้นแต่ unit จะใช้ filesystem namespacing เช่น PrivateMounts=yes ซึ่งจะซ่อน directory นี้จาก service อื่น ค่า secret จะไม่ปรากฏใน /proc/<pid>/environ ดังนั้น ps eww จึงไม่สามารถแสดงค่าได้ และ directory จะหายไปเมื่อ service หยุดทำงาน
เข้ารหัส credential ให้กับเครื่องก่อน คำสั่งเหล่านี้มาจากเอกสารของ systemd และใช้งานได้บน systemd 250 หรือใหม่กว่า ซึ่งครอบคลุม Ubuntu 24.04 และ Debian 13:
echo -n 'sk-example-value' > /tmp/plain.txt
sudo systemd-creds encrypt --name=api_key /tmp/plain.txt /etc/credstore/api_key.cred
shred -u /tmp/plain.txt
sudo systemd-run -P --wait -p LoadCredentialEncrypted=api_key:/etc/credstore/api_key.cred \
systemd-creds cat api_keyคำสั่งสุดท้ายจะแสดง sk-example-value นั่นยืนยันว่าไฟล์ที่เข้ารหัสสามารถถอดรหัสบน host นี้ได้ จากนั้นให้อ้างอิงไฟล์ดังกล่าวจาก unit:
[Service]
User=agent-review
LoadCredentialEncrypted=api_key:/etc/credstore/api_key.cred
Environment=AGENT_KEY_FILE=%d/api_key
ExecStart=/usr/local/bin/agent-workerโค้ดของ agent จะเปิดไฟล์ที่ $AGENT_KEY_FILE เมื่อต้องการใช้ค่า การอ่านไฟล์เกิดขึ้นเพียงชั่วขณะ ส่วน environment variable จะคงอยู่ตลอดอายุของ process และอยู่ในทุก child ที่ process นั้นสร้างขึ้น
ใช้ token อายุสั้นแทน key อายุยาว
key ที่ไม่มีวันหมดอายุยังคงใช้งานได้เมื่อปรากฏใน log หรือ transcript ในอีกหลายเดือนต่อมา หากบริการมี session token ให้ใช้ session token และกำหนดอายุการใช้งานให้สั้นที่สุดเท่าที่งานนั้นรองรับ
aws sts assume-role \
--role-arn arn:aws:iam::123456789012:role/agent-readonly \
--role-session-name agent-review \
--duration-seconds 900AWS STS (security token service) รับอายุการใช้งานขั้นต่ำ 15 นาที และโดยทั่วไปเพียงพอสำหรับงานของ agent 1 งาน สำหรับ GitHub ให้ผู้ใช้ agent มี gh login ของตนเอง พร้อม token แบบ fine-grained ที่จำกัดขอบเขตไว้เฉพาะ repository เดียวที่ agent ใช้งาน เพื่อให้ gh auth token ภายใน session นั้นส่งคืนค่าที่ไม่สามารถเข้าถึงสิ่งอื่นได้ ให้จำกัดขอบเขตตาม resource ก่อน แล้วจึงจำกัดตามเวลา
ตรวจสอบ แล้วตรวจสอบซ้ำ
หลังเปลี่ยนแปลงการตั้งค่าของ agent ควรเรียกใช้การตรวจสอบ 3 รายการ เรียกใช้ในฐานะผู้ใช้ของ agent ไม่ใช่ในฐานะผู้ใช้ของคุณเอง
sudo -u agent-review env | grep -iE 'key|token|secret'
sudo -u agent-review ls -la /home/you/ 2>&1 | head -3
sudo -u agent-review curl -s -o /dev/null -w '%{http_code}\n' --max-time 5 https://api.github.com/userรายการแรกควรไม่แสดงผลลัพธ์ใด ๆ เลย รายการที่สองควรแสดง ls: cannot open directory '/home/you/': Permission denied แต่ผลลัพธ์นี้แสดงเพียงว่าไม่สามารถแสดงรายการในไดเรกทอรีได้ ส่วนการทดสอบ cat ก่อนหน้านี้เป็นสิ่งที่ยืนยันว่าไม่สามารถอ่านไฟล์ภายในไดเรกทอรีได้ รายการที่สามแสดงว่า network path ของ agent ใช้ตัวตนใดในการเชื่อมต่อ ซึ่งเป็นคำถามที่ gateway pattern มีไว้ตอบ: 401 หมายความว่าคำขอไปถึง GitHub โดยไม่มี credential ที่ GitHub ยอมรับ อาจไม่มี credential เลย หรือ credential ไม่ถูกต้องหรือหมดอายุแล้ว ส่วน 200 หมายความว่าคำขอนั้นมี credential ที่ใช้งานได้ ดังนั้นคุณควรทราบว่าเป็น token ใด หากคุณเรียกใช้ agents โดยไม่มีผู้ดูแล การควบคุมค่าใช้จ่ายของ AI agent บน VPS จะอธิบายขีดจำกัดงบประมาณที่ใช้ควบคู่กับข้อจำกัดการเข้าถึงเหล่านี้
FAQ
ฉันเชื่อมั่นได้หรือไม่ว่าโมเดลจะไม่ทำคีย์ของฉันรั่วไหล
ไม่ได้ เพราะโมเดลไม่ใช่ผู้โจมตีใน threat model นี้ agent อ่านข้อความจากเว็บ, repository และ issue tracker และข้อความเหล่านั้นอาจมีคำสั่งแฝงอยู่ โมเดลไม่มีวิธีที่เชื่อถือได้ในการแยกคำสั่งของคุณออกจากข้อความที่ดึงมา การควบคุมใดก็ตามที่ขึ้นอยู่กับการให้โมเดลเลือกทำสิ่งที่ถูกต้องจะล้มเหลวทันทีที่คำสั่งที่ถูกสอดแทรกมีความแนบเนียนเพียงพอ ดังนั้นการควบคุมต้องอยู่ใน operating system หรือ network แทน
environment variable เป็นปัญหาสำหรับ secret ของ agent มากขนาดนั้นจริงหรือ
มีปัญหาในด้านหนึ่งโดยเฉพาะ คือ environment variable ถูกสืบทอด ทุก child process ที่ agent สร้างจะได้รับสำเนาไปด้วย รวมถึง build script, test runner และ package install hook นอกจากนี้ ผู้ใช้รายเดียวกันยังอ่านตัวแปรเหล่านี้ผ่าน /proc/<pid>/environ ได้ ดังนั้นทุกสิ่งที่ agent เรียกใช้ก็สามารถอ่านตัวแปรเหล่านี้ได้โดยไม่ต้องให้ agent ส่งต่อ ไฟล์ที่อ่านในขณะที่ใช้งานจริง โดยใช้ LoadCredential= หรือ gateway จะจำกัดช่วงเวลาที่ secret ถูกเปิดเผย
การเก็บ secret ใน vault แก้ปัญหานี้ได้ด้วยตัวเองหรือไม่
แก้ได้เพียงบางส่วน vault ช่วยแก้ปัญหาด้านการจัดเก็บ หากคุณ self host vault นั้น ก็ต้องทำ hardening ให้กับ vault ด้วย เพราะ Vaultwarden server มักถูกเจาะผ่าน admin token หรือ backup file ไม่ใช่ผ่านรายการที่เข้ารหัสไว้ใน vault vault ไม่ได้แก้ปัญหาในขั้นตอนสุดท้าย ซึ่งมีบางสิ่งดึง secret ออกจาก vault แล้วส่งให้ agent ในรูป environment variable ทำให้กลับไปสู่ปัญหาเดิม สิ่งสำคัญคือใครเป็นผู้ทำ substitution หาก agent เป็นผู้ดึง secret agent ก็จะถือ secret นั้น หาก gateway หรือ init system เป็นผู้ทำ substitution นอก process ของ agent agent ก็จะไม่เคยถือ secret นั้น
ฉันจะทราบได้อย่างไรว่า agent ทำบางสิ่งรั่วไหลไปแล้ว
โดยทั่วไป คุณไม่สามารถทราบภายหลังได้อย่างแน่นอน ซึ่งเป็นเหตุผลที่ควรใช้ gateway หากไม่มี gateway หลักฐานของคุณจะกระจัดกระจายอยู่ใน shell history, transcript ของ agent และ log ของ outbound connection ซึ่งคุณอาจไม่ได้เก็บไว้ หากสงสัยว่ามีการรั่วไหล ให้ rotate key ก่อนแล้วค่อยตรวจสอบภายหลัง การ rotate มีต้นทุนต่ำ แต่ความแน่นอนไม่มี
วันนี้ฉันควรทำอะไรเป็นอย่างน้อย
ย้ายไฟล์ .env ทั้งหมดออกจาก directory ที่ agent ใช้งาน และสร้าง unprivileged user แยกสำหรับ agent แต่ละตัว การเปลี่ยนแปลง 2 อย่างนี้ใช้เวลาประมาณ 10 นาที และปิดช่องทางที่พบบ่อยที่สุด ซึ่งคือ agent อ่านไฟล์ credential ที่ไม่มีเหตุผลต้องวางอยู่ข้าง code gateway และ short lived token เป็นขั้นตอนถัดไป ไม่ใช่ขั้นตอนแรก จุดเริ่มต้นเดียวกันนี้ใช้ได้กับ agent runtime ทุกประเภท รวมถึง การรัน autonomous agent บน VPS อย่างปลอดภัย