چطور اسرار را از ایجنتهای هوش مصنوعی دور نگه داریم
یک API key در محیط ایجنت با یک tool call قابل افشاست. بهجای کلید اصلی، توکنهای کوتاهمدت و محدود را پشت credential gateway قرار دهید.
مفهوم دور نگه داشتن اسرار از ایجنتهای هوش مصنوعی
یک ایجنت هوش مصنوعی در واقع یک پردازش عادی در لینوکس است که دستورات را اجرا میکند. هر متغیر محیطی (environment variable) که در اختیار آن پردازش باشد، توسط کدی که اجرا میکند قابل خواندن است؛ بنابراین یک API key در محیط ایجنت، کلیدی است که ایجنت میتواند آن را به هر میزبانی که به آن دسترسی دارد، ارسال کند. دور نگه داشتن اسرار از ایجنت به این معناست که بهجای کلید اصلی، یک دستگیره (handle) به آن بدهید: یک توکن کوتاهمدت با دسترسی محدود، یا یک جاینگهدار (placeholder) که در مرز شبکه توسط سیستم دیگری با مقدار واقعی جایگزین میشود.
این موضوع درباره تبدیل شدن مدل به یک موجود خصمانه نیست. سازوکار آن بسیار سادهتر است. ایجنت یک صفحه وب، یک فایل README یا یک کامنت در issue را میخواند که حاوی دستورالعملهایی است و از آنها پیروی میکند، زیرا برای یک مدل زبانی، تفاوتی بین متنی که شما نوشتهاید و متنی که خودش دریافت کرده وجود ندارد. این همان تزریق دستور (prompt injection) است. هنگامی که این اتفاق میافتد، میزان خسارت دقیقاً توسط یک چیز محدود میشود: آنچه پردازش میتواند بخواند. اگر هنوز مرزبندی را انجام ندادهاید، اجرای ایمن ایجنت کدنویسی روی سرور مراحل ایزولهسازی را پوشش میدهد که این راهنما بر پایه آنها بنا شده است.
مدل تهدید به زبان ساده
این دستور را با کاربری که agent شما با آن اجرا میشود، اجرا کنید.
tr '\0' '\n' < /proc/self/environ | grep -iE 'key|token|secret|password'هر خطی که این دستور چاپ میکند، تنها به اندازه یک درخواست HTTP با سرور یک غریبه فاصله دارد. اکنون به آنچه در دیسک در نزدیکی 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 هوشمندانه ندارد. چهار مسیر معمولی این کار را انجام میدهند و هر چهار مورد در لاگها مانند فعالیتهای عادی به نظر میرسند:
- یک درخواست خروجی
curlیاfetchبه هر میزبان، که مقدار مورد نظر در query string قرار دارد. - یک
git commitوgit pushبه مخزنی که agent اجازه نوشتن در آن را دارد. - یک اسکریپت نصب پکیج، که کد دلخواه را با دسترسی همان کاربرِ agent اجرا میکند.
- یک DNS lookup برای نام میزبان که حاوی آن مقدار است؛ این روش حتی زمانی که خروجی HTTP مسدود شده باشد نیز کار میکند.
شما نمیتوانید با بازبینی دستی از این وضعیت خارج شوید. راه حل این است که اطمینان حاصل کنید هیچ چیز ارزشمندی در دسترس agent قرار ندارد.
یک secret در دایرکتوری کاری، یک secret در پنجره context است
یک agent فایلها را میخواند. یک فایل .env در مخزنی که agent در آن کار میکند خوانده خواهد شد و پس از خوانده شدن، در پنجره context قرار میگیرد؛ این یعنی فایل در transcript، در هر لاگی که نگه میدارید و در هر چیزی که agent در ادامه مینویسد، وجود خواهد داشت.
پیش از این، با وجود کلید در دایرکتوری کاری agent:
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کاربر agent دیگر نمیتواند فایل را باز کند، زیرا دایرکتوری کاری دیگر شامل آن نیست. قوانین deny در پیکربندی خودِ agent، لایه دوم محافظت هستند، نه لایه اول. Claude Code قوانین دسترسی را از .claude/settings.json در پروژه میخواند:
{
"permissions": {
"deny": ["Read(./.env)", "Read(./secrets/**)", "Read(./**/*.pem)"]
}
}این کار از اشتباهات سهوی agent هنگام جستجو در فایلها جلوگیری میکند و فراتر از ابزارهای فایل عمل میکند: Claude Code این قوانین را در برابر دستورات فایلی که در Bash میشناسد، مانند cat و head، و همچنین در برابر مقاصد تغییر مسیر (redirection) مانند < .env بررسی میکند. این موضوع مانع از اجرای grep -r KEY . توسط یک دستور تزریقشده در دایرکتوری پروژه یا یک اسکریپت کوتاه Python که خودش .env را باز میکند، نمیشود؛ زیرا مستندات مجوزها بیان میکنند که قوانین deny برای دستوراتی که فایلها را بدون نام بردن از آنها میخوانند یا برای زیرفرآیندهایی که خودشان فایلها را باز میکنند، اعمال نمیشوند. برای این موارد، مستندات به sandbox در Claude Code اشاره میکنند که این مسدودسازی را در سطح سیستمعامل اعمال میکند. اینکه آیا پیش از اجرای چنین دستوری از شما سوال شود یا خیر، به حالت مجوز (permission mode) نشست بستگی دارد و حالت auto اکنون حالت پیشفرض برای نشستهای تعاملی Claude Code است، بنابراین سروری که آن را نظارت نمیکنید، دستورات بیشتری را بدون پرسش اجرا خواهد کرد. همین محدودیت برای هر چیزی که عادتهای agent را شکل میدهد تا مجوزهای آن، صدق میکند: مهارتی که agent را به کوچکترین تغییرِ کارآمد محدود میکند مانع از ورود ناخواسته به فایلهایی میشود که نیازی به باز کردن آنها نبوده است، اما این همچنان توصیهای است که میتوان مدل را از آن منصرف کرد. با پیکربندی به عنوان یک گاردریل و با مجوزهای فایلسیستم به عنوان یک دیوار برخورد کنید. همین تفکیک در داخل کانتینرها نیز صدق میکند: فایلهای env و secretها در Docker Compose نسخه یک لایه پایینتر از این مشکل را پوشش میدهد.
برای هر agent یک کاربر بدون دسترسیهای ویژه (unprivileged) بسازید
اگر agent با حساب کاربری شما اجرا شود، کلیدهای SSH، اعتبارنامههای ابری و تاریخچه 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 شکست بخورد. اگر به جای آن یک کلید نمایش داده شود، کاربرِ agent به فایل دسترسی دارد. باز کردن یک فایل با نام، نیازمند دسترسی جستجو (x) در تمام دایرکتوریهای والد است، نه دسترسی خواندن؛ بنابراین یک دایرکتوری home با حالت 711 که هیچکس دیگری نمیتواند آن را لیست کند، همچنان اجازه دسترسی به کلید را میدهد اگر خودِ فایل کلید قابل خواندن باشد. chmod 700 ~ مسیر را میبندد و chmod 600 ~/.ssh/id_ed25519 کلید را اصلاح میکند. کاربر agent را به sudo اضافه نکنید و به آن قانونی فراتر از NOPASSWD برای همان یک دستوری که واقعاً نیاز دارد، ندهید. کاربران با حداقل دسترسی روی VPS جزئیات مربوط به گروهها و sudoers را بررسی میکند. این جداسازی را به خاطر داشته باشید، چرا که وقتی بیش از یک نشست روی سرور دارید، یک نشست Claude Code میتواند متن را مستقیماً به نشست دیگر بفرستد و هر چیزی که نشست اول در اختیار دارد، میتواند از طریق یک پیام از آن کانال عبور کند.
یک مرز دیگر نیز ارزش افزودن روی یک VPS ابری را دارد. سرویس metadata نمونه (instance metadata service) به یک آدرس link-local ثابت پاسخ میدهد و اغلب اعتبارنامههای نقش (role credentials) را به هر کسی که درخواست کند، ارائه میدهد. هیچچیز در این endpoint بررسی نمیکند که کدام کاربر محلی در حال فراخوانی است: در EC2، مستندات AWS بیان میکند که فراخوانیهای IMDSv1 نیازی به توکن ندارند و توصیه خودِ AWS برای دور نگه داشتن یک کاربر از این سرویس، استفاده از یک قانون فایروال محلی بر اساس ID آن کاربر است.
sudo iptables -I OUTPUT 1 -p tcp -m owner --uid-owner agent-review -d 169.254.169.254 -j REJECTقانون را با -I در ابتدای لیست قرار دهید، نه اینکه با -A به انتهای آن اضافه کنید. در میزبانی که از ufw با سیاست پیشفرض allow outgoing استفاده میکند، زنجیره OUTPUT نهایی ufw تمام اتصالات TCP جدید را میپذیرد، بنابراین قانونی که در انتها اضافه شود هرگز اجرا نخواهد شد. قوانین iptables نیز هنگام reboot از بین میروند مگر اینکه آنها را دائمی کنید؛ و اگر endpoint نسخه IPv6 سرویس metadata یعنی [fd00:ec2::254] را فعال کردهاید، همان قانون را با ip6tables نیز اضافه کنید.
این مورد را از سمت agent بررسی کنید. sudo -u agent-review curl -s --max-time 3 http://169.254.169.254/ باید هیچ خروجی نداشته باشد و با کد غیر صفر خارج شود، زیرا بسته پیش از خروج از سرور رد میشود. اگر لیستی چاپ شد، یعنی قانون اجرا نشده است.
تزریق اعتبارنامه در مرز دسترسی
الگویی که در واقع این مشکل را حل میکند، تزریق اعتبارنامه (credential injection) است. ایجنت هرگز کلید واقعی را در اختیار ندارد. ایجنت درخواست خود را از طریق یک gateway محلی ارسال میکند و gateway در مسیر خروجی، یک جایگیر (placeholder) را با secret واقعی جایگزین میکند. secret در فضای ذخیرهسازی gateway، در یک پردازش متفاوت و تحت مالکیت کاربری دیگر نگهداری میشود.
ابزار OneCLI یک پیادهسازی متنباز از این الگو است. این ابزار تحت مجوز Apache-2.0 منتشر شده است، به جز دایرکتوریهای ee/ که دارای مجوز تجاری جداگانهای هستند و برای استفاده در محیط عملیاتی (production) نیاز به اشتراک دارند؛ این ابزار در کانتینرهایی در کنار ایجنت اجرا میشود. تا اکتبر 2026، این پروژه پیکربندی Docker Compose زیر را مستند کرده است که سه secret تولیدشده را از یک فایل .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) شوند. داشبورد بهصورت پیشفرض روی پورت 10254 و gateway روی پورت 10255 گوش میدهند و هر دو بهطور پیشفرض به 127.0.0.1 محدود شدهاند. شما اعتبارنامه واقعی را یکبار ذخیره میکنید، سپس به هر ایجنت یک مقدار جایگیر بهجای کلید اصلی به همراه توکن دسترسی محدودشده خودش میدهید که آن را در یک هدر Proxy-Authorization ارسال میکند. gateway درخواست خروجی را بر اساس میزبان (host) و مسیر (path) تطبیق میدهد، اعتبارنامه منطبق را رمزگشایی کرده و جایگزین میکند. محیط ایجنت حاوی هیچ داده ارزشمندی برای سرقت نیست.
ارزش این روش در رمزنگاری آن نیست. ارزش اصلی این است که پاسخ به پرسش «این ایجنت از چه چیزی و در چه زمانی استفاده کرده است» به یک پرسوجوی لاگ تبدیل میشود. شما بهجای حدس زدن اینکه کدامیک از شش محیط، نسخهای از کلید را در اختیار داشته، یک مسیر حسابرسی (audit trail) واحد را مطالعه میکنید.
راز را به پردازش بسپارید، نه به محیط اجرا
اگر agent را تحت systemd اجرا میکنید، اصلاً نیازی به متغیرهای محیطی (environment variables) ندارید. LoadCredential= راز را در یک دایرکتوری خصوصی قرار میدهد که فقط کاربرِ سرویس قادر به خواندن آن است؛ این دایرکتوری در فایل unit به صورت %d و در داخل پردازش به صورت $CREDENTIALS_DIRECTORY در دسترس قرار میگیرد. این محدودیت بر اساس کاربر است، نه سرویس؛ بنابراین برای هر سرویس agent یک کاربر مجزا تعریف کنید. هر پردازش دیگری که با همان کاربر اجرا شود میتواند دایرکتوری را باز کند، مگر اینکه unit از filesystem namespacing مانند PrivateMounts=yes استفاده کند که آن را از سایر سرویسها مخفی نگه میدارد. مقدار این راز هرگز در /proc/<pid>/environ ظاهر نمیشود، بنابراین ps eww نمیتواند آن را نمایش دهد و با توقف سرویس، دایرکتوری نیز حذف میشود.
ابتدا اعتبارنامه را برای همان ماشین رمزنگاری کنید. این دستورات از مستندات 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 باز میکند. خواندن یک فایل، عملیاتی لحظهای است. اما یک متغیر محیطی در تمام طول عمر پردازش و در هر پردازش فرزندی (child process) که ایجاد میکند، باقی میماند.
استفاده از توکنهای کوتاهمدت بهجای کلیدهای بلندمدت
کلیدی که هرگز منقضی نمیشود، ماهها بعد نیز اگر در یک لاگ یا رونوشت ظاهر شود، همچنان معتبر است. هر جا که سرویس، توکن نشست (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 (سرویس توکن امنیتی) میپذیرد و معمولاً برای یک وظیفهٔ agent کافی است. برای GitHub، به کاربر agent یک ورود gh اختصاصی بدهید و از یک توکن با دسترسی محدود (fine-grained) استفاده کنید که فقط به مخزنی که روی آن کار میکند محدود شده باشد؛ بهطوری که gh auth token در آن نشست، نتواند به هیچ منبع دیگری دسترسی پیدا کند. ابتدا محدودسازی را بر اساس منبع (resource) و سپس بر اساس زمان اعمال کنید.
تأیید کنید، سپس به تأیید ادامه دهید
پس از هر تغییر در تنظیمات یک 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 را چاپ کند، هرچند این فقط نشان میدهد که امکان لیست کردن محتویات دایرکتوری وجود ندارد؛ تست cat که پیشتر انجام شد، اثبات میکند که فایلهای درون آن قابل خواندن نیستند. دستور سوم به شما میگوید که مسیر شبکهٔ agent چه هویتی را ارائه میدهد؛ این همان پرسشی است که الگوی gateway برای پاسخ به آن ایجاد شده است: خروجی 401 به این معناست که درخواست بدون اعتبارنامهای که GitHub بپذیرد به آن رسیده است (یا هیچ اعتبارنامهای وجود ندارد، یا اعتبارنامه نامعتبر/منقضی شده است)، و خروجی 200 به این معناست که درخواست حامل یک اعتبارنامه معتبر است، بنابراین باید بدانید کدام توکن در حال استفاده است. اگر agentها را بهصورت خودکار و بدون نظارت اجرا میکنید، کنترل هزینههای AI agent روی VPS محدودیتهای بودجهای را پوشش میدهد که مکمل این محدودیتهای دسترسی هستند.
FAQ
آیا میتوانم به مدل اعتماد کنم که کلیدهای مرا لو نمیدهد؟
خیر، زیرا در این مدل تهدید، مدل نقش مهاجم را ندارد. عامل (agent) متن را از صفحات وب، مخازن و سیستمهای ردیابی خطا میخواند و آن متن میتواند حاوی دستورالعمل باشد. مدل هیچ راه مطمئنی برای تشخیص دستورالعملهای شما از متنی که دریافت کرده است ندارد. هر کنترلی که به انتخاب صحیح مدل وابسته باشد، در اولین باری که یک دستورالعمل تزریقشده متقاعدکننده باشد، شکست میخورد؛ بنابراین کنترل باید در سطح سیستمعامل یا شبکه اعمال شود.
آیا متغیرهای محیطی واقعاً برای اسرار عامل بد هستند؟
آنها از یک جهت خاص بد هستند: به ارث میرسند. هر پردازش فرزندی که عامل ایجاد میکند، یک کپی از آنها را دریافت میکند، از جمله اسکریپتهای ساخت (build script)، اجراکنندههای تست و هر hook نصب بستهای. این متغیرها همچنین از طریق /proc/<pid>/environ توسط همان کاربر قابل خواندن هستند، بنابراین هر چیزی که عامل اجرا میکند میتواند بدون اینکه عامل آنها را منتقل کند، آنها را بخواند. خواندن فایل در لحظه استفاده، با استفاده از LoadCredential= یا یک gateway، میزان افشا را به همان لحظه محدود میکند.
آیا قرار دادن اسرار در یک vault به تنهایی این مشکل را حل میکند؟
فقط تا حدی. vault مشکل ذخیرهسازی را حل میکند. اگر آن vault را خودتان میزبانی میکنید، نیاز به یک مرحله امنسازی جداگانه دارد، زیرا یک سرور Vaultwarden معمولاً از طریق توکن مدیریتی یا فایل پشتیبان خود هک میشود، نه از طریق آیتمهای رمزنگاریشدهای که در خود نگه میدارد. این کار مرحله آخر را حل نمیکند؛ جایی که چیزی راز را از vault بیرون میکشد و به عنوان یک متغیر محیطی به عامل میدهد، که شما را به نقطه اول بازمیگرداند. آنچه اهمیت دارد این است که چه کسی جایگزینی (substitution) را انجام میدهد. اگر عامل راز را دریافت کند، عامل راز را در اختیار دارد. اگر یک gateway یا سیستم init جایگزینی را خارج از پردازش عامل انجام دهد، عامل هرگز آن را در اختیار نخواهد داشت.
چگونه بفهمم که آیا یک عامل قبلاً چیزی را لو داده است؟
معمولاً پس از وقوع حادثه نمیتوانید متوجه شوید، و همین دلیل اصلی برای استفاده از gateway است. بدون آن، شواهد شما در تاریخچه shell، رونوشتهای عامل و لاگهای اتصالات خروجی که احتمالاً نگهداری نمیکنید، پراکنده است. با یک gateway اعتبارنامه، هر بار استفاده از یک اعتبارنامه به صورت یک خط شامل هویت عامل و یک برچسب زمانی ثبت میشود. اگر به نشت اطلاعات مشکوک هستید، ابتدا کلید را تغییر دهید (rotate) و سپس تحقیق کنید. تغییر کلید کمهزینه است، اما رسیدن به قطعیت اینطور نیست.
حداقل کاری که امروز باید انجام دهم چیست؟
تمام فایلهای .env را از دایرکتوریهایی که عاملهای شما در آنها کار میکنند خارج کنید و برای هر عامل یک کاربر بدون امتیاز (unprivileged) ایجاد کنید. این دو تغییر حدود ده دقیقه زمان میبرند و رایجترین مسیر نفوذ، یعنی خواندن فایل اعتبارنامهای که دلیلی برای قرار گرفتن در کنار کد نداشته است، را مسدود میکنند. gateway و توکنهای با عمر کوتاه گامهای بعدی هستند، نه گام اول. همین نقطه شروع برای هر runtime عامل، از جمله اجرای ایمن یک عامل خودمختار روی یک VPS، صدق میکند.