วิธีป้องกัน API Key รั่วไหลเมื่อใช้งาน AI Agents
เรียนรู้วิธีป้องกันการรั่วไหลของ API Key จาก AI Agents เมื่อเกิดเหตุการณ์ Prompt Injection โดยการใช้ระบบ Credential Gateway และจำกัดสิทธิ์ด้วย Token อายุสั้นแทนการใช้กุญแจจริง
ความหมายของการเก็บความลับให้ห่างจาก AI agents
AI agent คือกระบวนการทำงานบน Linux ตามปกติที่รันคำสั่งต่างๆ ตัวแปรสภาพแวดล้อม (environment variable) ทุกตัวที่กระบวนการนั้นถือครองอยู่ โค้ดที่รันโดย agent นั้นสามารถอ่านได้ ดังนั้น API key ที่อยู่ในสภาพแวดล้อมของ agent จึงเป็นกุญแจที่ agent สามารถส่งไปยังโฮสต์ใดก็ตามที่มันเข้าถึงได้ การเก็บความลับให้ห่างจาก agent หมายถึงการมอบ "ตัวจัดการ" (handle) ให้แทนที่จะเป็นกุญแจจริง เช่น token ที่มีอายุสั้นและจำกัดขอบเขตการใช้งาน หรือตัวแทน (placeholder) ที่ระบบอื่นจะสลับเปลี่ยนเป็นค่าจริงให้ที่ขอบเขตของเครือข่าย
นี่ไม่ใช่เรื่องราวเกี่ยวกับโมเดลที่กลายเป็นศัตรู แต่กลไกนั้นเรียบง่ายกว่านั้นมาก Agent จะอ่านหน้าเว็บ, ไฟล์ README หรือความคิดเห็นใน issue ที่มีคำสั่งต่างๆ อยู่ และมันจะปฏิบัติตามคำสั่งเหล่านั้น เพราะสำหรับโมเดลภาษาแล้ว ไม่มีข้อแตกต่างระหว่างข้อความที่คุณเขียนกับข้อความที่มันดึงมาได้ นี่คือการทำ prompt injection เมื่อเหตุการณ์นี้เกิดขึ้น ความเสียหายจะถูกจำกัดด้วยสิ่งเดียวเท่านั้น คือสิ่งที่กระบวนการนั้นสามารถอ่านได้ หากคุณยังไม่ได้กำหนดขอบเขตการทำงาน การรัน coding agent อย่างปลอดภัยบนเซิร์ฟเวอร์ จะครอบคลุมถึงระดับการแยกส่วน (isolation) ซึ่งเป็นพื้นฐานที่คู่มือนี้ตั้งอยู่บนนั้น
แบบจำลองภัยคุกคามในภาษาที่เข้าใจง่าย
ให้รันคำสั่งนี้ในฐานะผู้ใช้เดียวกับที่ agent ของคุณใช้งานอยู่
tr '\0' '\n' < /proc/self/environ | grep -iE 'key|token|secret|password'ทุกบรรทัดที่แสดงผลออกมานั้นอยู่ห่างจากเซิร์ฟเวอร์ของบุคคลภายนอกเพียงแค่การส่ง HTTP request ครั้งเดียวเท่านั้น ตอนนี้ให้พิจารณาสิ่งที่อยู่บนดิสก์ในบริเวณใกล้เคียงกับตัว 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 ไม่จำเป็นต้องใช้ช่องโหว่ที่ซับซ้อนเพื่อขโมยข้อมูลเหล่านั้นออกไป มีเส้นทางปกติ 4 เส้นทางที่ทำงานนี้ได้ และทั้ง 4 เส้นทางดูเหมือนการทำงานปกติใน log:
- การใช้
curlหรือfetchขาออกไปยัง host ใดก็ได้ โดยมีค่าข้อมูลอยู่ใน query string - การใช้
git commitและgit pushไปยัง repository ที่ agent สามารถเขียนข้อมูลได้ - สคริปต์ติดตั้งแพ็กเกจ ซึ่งจะรันโค้ดใดๆ ก็ตามในฐานะผู้ใช้ของ agent
- การทำ DNS lookup ของ hostname ที่มีค่าข้อมูลนั้นอยู่ ซึ่งวิธีนี้ยังคงใช้งานได้แม้จะมีการบล็อก HTTP egress ไว้ก็ตาม
คุณไม่สามารถแก้ไขปัญหานี้ได้ด้วยการตรวจสอบโค้ดเพียงอย่างเดียว วิธีแก้ไขคือต้องทำให้แน่ใจว่าไม่มีข้อมูลที่มีค่าอยู่ในระยะที่เข้าถึงได้
ความลับที่อยู่ใน working tree คือความลับที่อยู่ใน context window
เอเจนต์จะอ่านไฟล์ต่างๆ ไฟล์ .env ที่อยู่ใน repository ที่เอเจนต์กำลังทำงานอยู่จะถูกอ่าน และเมื่อถูกอ่านแล้ว ข้อมูลนั้นจะเข้าไปอยู่ใน context window ซึ่งหมายความว่าข้อมูลจะอยู่ในบันทึกการสนทนา (transcript) ใน log ใดๆ ที่คุณเก็บไว้ และในทุกสิ่งที่เอเจนต์เขียนขึ้นหลังจากนั้น
ก่อนหน้านี้ เมื่อมีคีย์วางอยู่ใน tree ที่เอเจนต์ทำงาน:
cd ~/projects/billing
cat .env
# STRIPE_SECRET_KEY=sk_live_...
# DATABASE_URL=postgres://app:hunter2@db.internal:5432/billingหลังจากนี้ เมื่อย้ายไฟล์ออกไปให้พ้นระยะ:
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ผู้ใช้ของเอเจนต์จะไม่สามารถเปิดไฟล์นั้นได้อีกต่อไป เพราะ working tree ไม่มีไฟล์ดังกล่าวอยู่อีกแล้ว กฎการปฏิเสธ (deny rules) ใน config ของเอเจนต์เองเป็นเพียงชั้นป้องกันที่สอง ไม่ใช่ชั้นแรก Claude Code จะอ่านกฎการอนุญาตจาก .claude/settings.json ในโปรเจกต์:
{
"permissions": {
"deny": ["Read(./.env)", "Read(./secrets/**)", "Read(./**/*.pem)"]
}
}สิ่งนี้ช่วยป้องกันความผิดพลาดโดยสุจริตจากการที่เอเจนต์เปิดไฟล์ขณะสำรวจข้อมูล แต่มันไม่สามารถป้องกันคำสั่งที่ถูกแทรกเข้ามาจากการรัน base64 .env ได้ เพราะนั่นเป็นคำสั่ง shell ไม่ใช่การอ่านไฟล์ การที่คุณจะถูกถามก่อนที่คำสั่งนั้นจะทำงานหรือไม่ ขึ้นอยู่กับโหมดการอนุญาตของเซสชันนั้นๆ และ auto mode จะกลายเป็นค่าเริ่มต้นของ Claude Code ในเดือนสิงหาคม 2026 ดังนั้นเซิร์ฟเวอร์ที่คุณไม่ได้เฝ้าดูอยู่จะรันคำสั่งเหล่านั้นโดยไม่มีการแจ้งเตือนมากขึ้น ข้อจำกัดเดียวกันนี้ใช้กับทุกสิ่งที่กำหนดพฤติกรรมของเอเจนต์แทนที่จะเป็นสิทธิ์ของมัน: ทักษะที่บังคับให้เอเจนต์ทำการเปลี่ยนแปลงที่เล็กที่สุดเท่าที่ใช้งานได้ ช่วยป้องกันไม่ให้การทำงานหลงเข้าไปในไฟล์ที่ไม่มีความจำเป็นต้องเปิด แต่ก็ยังเป็นเพียงคำแนะนำที่โมเดลอาจถูกโน้มน้าวให้ละเลยได้ ให้ถือว่า config เป็นราวกันตกและสิทธิ์ในระบบไฟล์เป็นกำแพง การแบ่งแยกในลักษณะเดียวกันนี้ใช้ได้กับภายในคอนเทนเนอร์: env files และความลับใน Docker Compose ครอบคลุมปัญหาในเวอร์ชันนี้ที่ลึกลงไปอีกหนึ่งชั้น
กำหนดให้เอเจนต์แต่ละตัวมีผู้ใช้ที่ไม่มีสิทธิ์พิเศษของตนเอง
หากเอเจนต์ทำงานในฐานะผู้ใช้ของคุณ มันจะได้รับสิทธิ์เข้าถึง SSH keys, ข้อมูลรับรองระบบคลาวด์ และประวัติการใช้งาน shell ของคุณ การสร้างผู้ใช้แยกต่างหากมีค่าใช้จ่ายเพียงแค่การรันคำสั่งเดียว แต่ช่วยลดความเสี่ยงทั้งหมดที่กล่าวมา
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 หากคำสั่งแสดงผลลัพธ์เป็นคีย์ แสดงว่าไดเรกทอรี home ของคุณถูกตั้งค่าให้กลุ่มหรือบุคคลทั่วไปอ่านได้ ซึ่ง chmod 700 ~ จะช่วยแก้ไขปัญหานี้ อย่าเพิ่มผู้ใช้ของเอเจนต์เข้าไปใน sudo และอย่ากำหนดกฎ NOPASSWD ที่กว้างเกินกว่าคำสั่งเดียวที่จำเป็นต้องใช้จริง ผู้ใช้ที่มีสิทธิ์น้อยที่สุดบน VPS จะอธิบายรายละเอียดเกี่ยวกับกลุ่มและ sudoers ไว้ โปรดจำหลักการแยกส่วนนี้ไว้เมื่อคุณรันมากกว่าหนึ่งเซสชันบนเครื่องเดียวกัน เพราะ เซสชัน Claude Code หนึ่งสามารถส่งข้อความตรงไปยังอีกเซสชันหนึ่งได้ และสิ่งที่เซสชันแรกถือครองอยู่สามารถข้ามผ่านช่องทางนั้นไปได้ในข้อความเดียว
มีอีกหนึ่งขอบเขตที่ควรเพิ่มบนระบบคลาวด์ VPS บริการ metadata ของอินสแตนซ์จะตอบกลับที่ที่อยู่ link local แบบคงที่ และมักจะมอบข้อมูลรับรองบทบาทให้กับทุกสิ่งที่ร้องขอเข้ามา
sudo iptables -A OUTPUT -m owner --uid-owner agent-review -d 169.254.169.254 -j REJECTตรวจสอบจากฝั่งของเอเจนต์ sudo -u agent-review curl -s --max-time 3 http://169.254.169.254/ ควรแสดงผลลัพธ์ว่างเปล่าและจบการทำงานด้วยสถานะที่ไม่ใช่ศูนย์ เนื่องจากแพ็กเกจถูกปฏิเสธก่อนที่จะออกจากเครื่อง
การฉีดข้อมูลประจำตัวที่ขอบเขตของระบบ
รูปแบบที่ช่วยแก้ปัญหานี้ได้จริงคือการฉีดข้อมูลประจำตัว (credential injection) โดยที่ตัว agent จะไม่เก็บกุญแจจริงไว้เลย แต่จะส่งคำขอผ่าน local gateway ซึ่ง gateway จะทำหน้าที่สลับค่า placeholder ให้เป็น secret จริงในขณะที่ส่งคำขอออกไป โดย secret จะถูกเก็บไว้ในพื้นที่จัดเก็บของ gateway ซึ่งทำงานใน process อื่นและเป็นเจ้าของโดยผู้ใช้อื่น
OneCLI เป็นหนึ่งใน implementation แบบ open source ของแนวทางนี้ โดยใช้สัญญาอนุญาตแบบ Apache-2.0 และทำงานเป็น container ควบคู่ไปกับ agent ณ เดือนกรกฎาคม 2026 โครงการได้จัดทำเอกสารการตั้งค่านี้ไว้ดังนี้:
git clone https://github.com/onecli/onecli.git
cd onecli
docker compose -f docker/docker-compose.yml up -d --waitdashboard จะฟังการเชื่อมต่อที่พอร์ต 10254 และ gateway ที่พอร์ต 10255 คุณต้องจัดเก็บ credential จริงไว้เพียงครั้งเดียว จากนั้นจึงกำหนดค่า placeholder ให้กับ agent แต่ละตัวแทนการใช้กุญแจจริง พร้อมกับ scoped access token ของ agent นั้นๆ ซึ่งจะถูกส่งไปใน header Proxy-Authorization ตัว gateway จะจับคู่คำขอขาออกตาม host และ path จากนั้นจะถอดรหัส credential ที่ตรงกันแล้วนำไปแทนที่ ทำให้ในสภาพแวดล้อมของ agent ไม่มีข้อมูลใดที่มีค่าพอให้ขโมยได้
คุณค่าของวิธีนี้ไม่ใช่เรื่องของการเข้ารหัส แต่คือการที่คำถามที่ว่า "agent นี้ใช้ข้อมูลอะไรและเมื่อใด" จะกลายเป็นเพียงการสืบค้น log คุณสามารถอ่าน audit trail เพียงชุดเดียวแทนที่จะต้องมาคาดเดาว่าสภาพแวดล้อมใดใน 6 แห่งที่เก็บสำเนากุญแจนั้นไว้
ส่งความลับให้กระบวนการโดยตรง แทนการใช้ environment variable
หากคุณรัน agent ภายใต้ systemd คุณไม่จำเป็นต้องใช้ environment variable เลย LoadCredential= จะจัดเก็บความลับไว้ในไดเรกทอรีส่วนตัวที่ service นั้นอ่านได้เพียงผู้เดียว โดยเปิดเผยผ่าน %d ในไฟล์ unit และเป็น $CREDENTIALS_DIRECTORY ภายในกระบวนการ ค่าดังกล่าวจะไม่ปรากฏใน /proc/<pid>/environ ดังนั้น ps eww จึงไม่สามารถแสดงค่านี้ได้ และไดเรกทอรีจะถูกลบออกเมื่อ 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 ซึ่งเป็นการพิสูจน์ว่าไฟล์ที่เข้ารหัสสามารถถอดรหัสบนโฮสต์นี้ได้ จากนั้นให้ระบุอ้างอิงไฟล์ดังกล่าวจาก 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 จะคงอยู่ตลอดอายุของกระบวนการ และสืบทอดไปยังทุกกระบวนการลูกที่ถูกสร้างขึ้น
เลือกใช้โทเค็นที่มีอายุการใช้งานสั้นแทนคีย์ที่มีอายุการใช้งานยาวนาน
คีย์ที่ไม่มีวันหมดอายุจะยังคงใช้งานได้เสมอหากหลุดไปอยู่ใน log หรือบันทึกการสนทนาแม้เวลาจะผ่านไปหลายเดือนก็ตาม ในกรณีที่บริการรองรับ session token ให้เลือกใช้ session token นั้นและกำหนดอายุการใช้งานให้สั้นที่สุดเท่าที่งานจะเอื้ออำนวย
aws sts assume-role \
--role-arn arn:aws:iam::123456789012:role/agent-readonly \
--role-session-name agent-review \
--duration-seconds 90015 นาทีคือระยะเวลาขั้นต่ำที่ AWS STS (security token service) ยอมรับ ซึ่งโดยปกติแล้วเพียงพอสำหรับงานของ agent หนึ่งงาน สำหรับ GitHub ให้สร้างบัญชีผู้ใช้แยกต่างหากสำหรับ agent โดยใช้ gh login พร้อมกับ fine grained token ที่จำกัดขอบเขตไว้เฉพาะ repository ที่ต้องทำงานเท่านั้น เพื่อให้ gh auth token ภายใน session นั้นส่งคืนผลลัพธ์ที่ไม่สามารถเข้าถึงส่วนอื่นได้ ให้จำกัดขอบเขตตามทรัพยากรก่อน แล้วจึงจำกัดตามเวลา
ตรวจสอบ และตรวจสอบซ้ำ
มี 3 การตรวจสอบที่ควรทำหลังจากเปลี่ยนแปลงการตั้งค่าของ agent ให้รันคำสั่งเหล่านี้ในฐานะผู้ใช้ของ 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 การตรวจสอบที่สามจะบอกคุณว่าเส้นทางเครือข่ายของ agent แสดงตัวตนอย่างไร ซึ่งเป็นคำถามที่รูปแบบ gateway ถูกสร้างขึ้นมาเพื่อตอบ: 401 หมายความว่า agent ไม่ได้ถือ credential ของ GitHub ไว้ และ 200 หมายความว่า agent ถือ credential อยู่ ดังนั้นคุณควรทราบว่ามันคือ token ใด หากคุณรัน agent แบบไม่มีผู้ดูแล การควบคุมค่าใช้จ่ายของ AI agent บน VPS จะครอบคลุมถึงการจำกัดงบประมาณที่ต้องใช้ควบคู่ไปกับการจำกัดสิทธิ์การเข้าถึงเหล่านี้
FAQ
ฉันจะเชื่อใจโมเดลไม่ให้ทำกุญแจรั่วไหลได้หรือไม่?
ไม่ได้ เพราะในโมเดลภัยคุกคามนี้ โมเดลไม่ใช่ผู้โจมตี ตัว Agent จะอ่านข้อความจากหน้าเว็บ, repository และระบบติดตามปัญหา (issue tracker) ซึ่งข้อความเหล่านั้นอาจมีคำสั่งแฝงอยู่ โมเดลไม่มีวิธีที่น่าเชื่อถือในการแยกแยะระหว่างคำสั่งของคุณกับข้อความที่มันดึงมา การควบคุมใดๆ ที่ขึ้นอยู่กับการตัดสินใจของโมเดลจะล้มเหลวทันทีที่มีคำสั่งฉีด (injected instruction) ที่ดูน่าเชื่อถือ ดังนั้นการควบคุมจึงต้องอยู่ที่ระดับระบบปฏิบัติการหรือเครือข่ายแทน
environment variable แย่สำหรับความลับของ Agent จริงหรือ?
มันแย่ในแง่เฉพาะคือ: มันถูกสืบทอด (inherited) ทุก child process ที่ Agent สร้างขึ้นจะได้รับสำเนาของตัวแปรเหล่านี้ไปด้วย รวมถึง build script, test runner และ package install hook ต่างๆ นอกจากนี้ตัวแปรเหล่านี้ยังสามารถอ่านได้ผ่าน /proc/<pid>/environ โดยผู้ใช้คนเดียวกัน ดังนั้นทุกสิ่งที่ Agent รันจึงสามารถอ่านค่าเหล่านี้ได้โดยที่ Agent ไม่จำเป็นต้องส่งต่อให้ การอ่านไฟล์ ณ ขณะที่ใช้งานจริงด้วย LoadCredential= หรือผ่าน gateway จะช่วยจำกัดการเปิดเผยข้อมูลให้เกิดขึ้นเฉพาะในช่วงเวลานั้นเท่านั้น
การเก็บความลับไว้ใน vault ช่วยแก้ปัญหานี้ได้ด้วยตัวเองหรือไม่?
ได้เพียงบางส่วนเท่านั้น Vault ช่วยแก้ปัญหาเรื่องการจัดเก็บ หากคุณ self-host vault นั้น คุณจำเป็นต้องทำ hardening เพิ่มเติม เพราะ เซิร์ฟเวอร์ Vaultwarden มักถูกเจาะผ่าน admin token หรือไฟล์สำรองข้อมูล ไม่ใช่ผ่านรายการที่เข้ารหัสไว้ภายใน มันไม่ได้แก้ปัญหาขั้นตอนสุดท้ายที่บางอย่างดึงความลับออกจาก vault แล้วส่งให้ Agent ในรูปแบบ environment variable ซึ่งจะทำให้คุณกลับไปจุดเดิม สิ่งที่สำคัญคือใครเป็นผู้แทนที่ค่า (substitution) หาก Agent เป็นผู้ดึงความลับ Agent ก็จะถือความลับนั้นไว้ แต่หาก gateway หรือ init system เป็นผู้แทนที่ค่าภายนอก process ของ Agent ตัว Agent ก็จะไม่เคยถือความลับนั้นเลย
ฉันจะรู้ได้อย่างไรว่า Agent ทำข้อมูลรั่วไหลไปแล้ว?
โดยปกติคุณไม่สามารถทราบได้หลังจากเหตุการณ์เกิดขึ้น ซึ่งเป็นเหตุผลที่ต้องใช้ gateway หากไม่มี gateway หลักฐานของคุณจะกระจัดกระจายอยู่ใน shell history, บันทึกการสนทนาของ Agent และ log การเชื่อมต่อขาออกที่คุณอาจไม่ได้เก็บไว้ แต่ถ้ามี credential gateway การใช้งาน credential ทุกครั้งจะเป็นหนึ่งบรรทัดที่มีระบุตัวตนของ Agent และประทับเวลา หากคุณสงสัยว่ามีการรั่วไหล ให้หมุนเวียน (rotate) กุญแจก่อนแล้วค่อยตรวจสอบทีหลัง การหมุนเวียนกุญแจมีต้นทุนต่ำ แต่ความแน่นอนนั้นไม่มี
สิ่งที่ควรทำน้อยที่สุดในวันนี้คืออะไร?
ย้ายไฟล์ .env ทั้งหมดออกจากไดเรกทอรีที่ Agent ของคุณทำงานอยู่ และสร้างผู้ใช้ที่ไม่มีสิทธิ์ (unprivileged user) แยกหนึ่งบัญชีต่อหนึ่ง Agent การเปลี่ยนแปลงสองอย่างนี้ใช้เวลาประมาณสิบนาทีและปิดช่องทางที่พบบ่อยที่สุด ซึ่งก็คือการที่ Agent อ่านไฟล์ credential ที่ไม่ควรจะวางอยู่ข้างๆ โค้ด การใช้ gateway และ token ระยะสั้นเป็นขั้นตอนถัดไป ไม่ใช่ขั้นตอนแรก จุดเริ่มต้นเดียวกันนี้ใช้ได้กับ Agent runtime ทุกรูปแบบ รวมถึง การรัน autonomous agent อย่างปลอดภัยบน VPS