چطور secrets را از عاملهای AI دور نگه داریم
قرار دادن API key در محیط عامل میتواند آن را با یک tool call افشا کند. بهجای کلید واقعی، توکن کوتاهعمر و محدودشده را پشت credential gateway قرار دهید.
منظور از خارج نگهداشتن اسرار از عاملهای AI
یک عامل AI یک فرایند معمولی Linux است که فرمانها را اجرا میکند. هر متغیر محیطی که این فرایند در اختیار دارد، برای کدی که اجرا میکند قابل خواندن است. بنابراین، یک API key در محیط عامل، کلیدی است که عامل میتواند آن را به هر میزبان قابلدسترسی ارسال کند. خارج نگهداشتن اسرار از عامل یعنی بهجای کلید، یک واسط در اختیار آن قرار دهید: یک توکن کوتاهعمر و محدودشده، یا یک placeholder که سامانهای دیگر آن را در مرز شبکه با مقدار واقعی جایگزین کند.
این موضوع درباره خصمانهشدن model نیست. سازوکار آن سادهتر است. عامل یک صفحه وب، یک README یا یک نظر در issue را میخواند که حاوی دستورالعمل است و از آن پیروی میکند، زیرا برای یک language model میان متنی که شما نوشتهاید و متنی که خود دریافت کرده است تفاوتی وجود ندارد. این همان prompt injection است. پس از وقوع آن، دامنه آسیب دقیقاً با یک عامل محدود میشود: آنچه فرایند میتواند بخواند. اگر هنوز مرزی تعیین نکردهاید، اجرای ایمن عامل کدنویسی روی یک server سطوح جداسازیای را توضیح میدهد که این راهنما بر پایه آنها قرار دارد.
مدل تهدید به زبان ساده
این کار را با همان کاربری اجرا کنید که 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 برای خارج کردن این دادهها به یک اکسپلویت پیچیده نیاز ندارد. چهار مسیر معمول این کار را انجام میدهند و هر چهار مورد در log مانند فعالیت عادی به نظر میرسند:
- یک درخواست خروجی
curlیاfetchبه هر host، همراه با مقدار در query string. - اجرای
git commitوgit pushروی repository که agent امکان نوشتن در آن را دارد. - اسکریپت نصب package که کد دلخواه را با کاربر agent اجرا میکند.
- جستوجوی DNS برای hostnameای که مقدار را در خود دارد؛ این مقدار حتی هنگام مسدود بودن خروجی HTTP نیز خارج میشود.
با بازبینی نمیتوانید این مشکل را برطرف کنید. راهحل این است که مطمئن شوید هیچ داده ارزشمندی در دسترس نیست.
یک secret در working tree، در context window نیز secret است
یک 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پس از جابهجایی فایل به محلی خارج از دسترس:
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 را از .claude/settings.json در project میخواند:
{
"permissions": {
"deny": ["Read(./.env)", "Read(./secrets/**)", "Read(./**/*.pem)"]
}
}این کار از اشتباه سهوی agent هنگام بررسی فایلها و بازکردن یک فایل جلوگیری میکند. اما جلوی اجرای یک دستور تزریقشده در base64 .env را نمیگیرد، زیرا آن یک shell command است، نه file read. config را یک guardrail در نظر بگیرید و permission فایلسیستم را دیوار اصلی بدانید. همین جداسازی در containerها نیز وجود دارد: فایلهای env و secretها در Docker Compose نسخه یک لایه پایینتر این مسئله را پوشش میدهد.
به هر agent یک کاربر فاقد دسترسیهای ویژه اختصاص دهید
اگر agent با حساب شما اجرا شود، کلیدهای SSH، اعتبارنامههای cloud و تاریخچه 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 شکست بخورد. اگر بهجای آن یک کلید چاپ شد، دایرکتوری خانگی شما برای گروه یا سایر کاربران قابل خواندن است و chmod 700 ~ این مشکل را برطرف میکند. کاربر agent را به sudo اضافه نکنید و برای آن قانون NOPASSWD گستردهتر از تنها فرمانی که واقعاً به آن نیاز دارد تعریف نکنید. کاربران دارای حداقل دسترسی در یک VPS جزئیات گروهها و sudoers را توضیح میدهد.
در یک cloud VPS، افزودن یک مرز امنیتی دیگر نیز ارزشمند است. سرویس فراداده نمونه به یک نشانی ثابت link-local پاسخ میدهد و اغلب اعتبارنامههای نقش را در اختیار هر چیزی قرار میدهد که درخواست کند.
sudo iptables -A OUTPUT -m owner --uid-owner agent-review -d 169.254.169.254 -j REJECTاین مورد را از سمت agent بررسی کنید. sudo -u agent-review curl -s --max-time 3 http://169.254.169.254/ باید هیچ خروجیای چاپ نکند و با وضعیت non-zero خارج شود، زیرا بسته پیش از خروج از سیستم رد میشود.
تزریق اعتبارنامه در مرز
الگویی که این مشکل را واقعاً حل میکند، تزریق اعتبارنامه است. عامل هرگز کلید واقعی را در اختیار ندارد. درخواست خود را از طریق یک دروازه محلی ارسال میکند و دروازه هنگام خروج، مقدار جایگزین را با راز واقعی تعویض میکند. راز در فضای ذخیرهسازی دروازه قرار دارد؛ این فضا به فرایندی جداگانه و کاربری متفاوت تعلق دارد.
OneCLI یکی از پیادهسازیهای متنباز این روش است، مجوز Apache-2.0 دارد و بهصورت یک کانتینر در کنار عامل اجرا میشود. در July 2026، پروژه این راهاندازی را مستند کرده است:
git clone https://github.com/onecli/onecli.git
cd onecli
docker compose -f docker/docker-compose.yml up -d --waitداشبورد روی port 10254 و دروازه روی port 10255 به درخواستها گوش میدهند. ابتدا اعتبارنامه واقعی را یکبار ذخیره میکنید. سپس به هر عامل، بهجای کلید، یک مقدار جایگزین و یک access token محدود به محدوده همان عامل میدهید. عامل این token را در header با نام Proxy-Authorization ارسال میکند. دروازه درخواست خروجی را بر اساس host و path تطبیق میدهد، اعتبارنامه منطبق را رمزگشایی میکند و آن را جایگزین میکند. محیط عامل هیچ مقدار ارزشمندی برای سرقت ندارد.
ارزش این روش در encryption نیست. ارزش آن این است که پرسش «این عامل از چه چیزی و در چه زمانی استفاده کرده است؟» به یک log query تبدیل میشود. بهجای حدسزدن اینکه کدامیک از 6 محیط یک نسخه از کلید را در خود داشته است، یک audit trail را بررسی میکنید.
راز را به فرایند بسپارید، نه به محیط
اگر agent را تحت systemd اجرا میکنید، اصلاً به متغیرهای محیطی نیاز ندارید. LoadCredential= راز را در یک دایرکتوری خصوصی قرار میدهد که فقط همان سرویس میتواند آن را بخواند؛ این راز در فایل unit بهصورت %d و داخل فرایند بهصورت $CREDENTIALS_DIRECTORY در دسترس است. مقدار راز هرگز در /proc/<pid>/environ ظاهر نمیشود؛ بنابراین ps eww نمیتواند آن را نمایش دهد و دایرکتوری هنگام توقف سرویس حذف میشود.
ابتدا 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 باز میکند. خواندن فایل یک رویداد لحظهای است. متغیر محیطی تا پایان عمر فرایند، و در تمام فرزندفرایندهایی که ایجاد میکند، باقی میماند.
توکنهای کوتاهعمر را به کلیدهای بلندعمر ترجیح دهید
کلیدی که هرگز منقضی نمیشود، هر زمان که ماهها بعد در یک گزارش یا متن ثبتشده ظاهر شود، همچنان معتبر است. هرگاه سرویس توکن نشست ارائه میکند، از توکن نشست استفاده کنید و کوتاهترین عمر مجاز برای کار را تعیین کنید.
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) میپذیرد و معمولاً برای یک وظیفه عامل کافی است. برای GitHub، یک ورود مستقل gh برای کاربر عامل ایجاد کنید و برای آن، توکنی با دامنه دسترسی دقیق تنظیم کنید که فقط به مخزن مورد استفاده عامل محدود باشد؛ بهگونهای که gh auth token در آن نشست مقداری را برگرداند که نتواند به بخش دیگری دسترسی پیدا کند. ابتدا دامنه دسترسی را بر اساس منبع محدود کنید و سپس بر اساس زمان.
ابتدا بررسی کنید، سپس بررسی را ادامه دهید
پس از هر تغییر در پیکربندی یک 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 را نمایش دهد. بررسی سوم مشخص میکند مسیر شبکه agent چه هویتی را ارائه میکند؛ این همان پرسشی است که الگوی gateway برای پاسخدادن به آن ایجاد شده است: 401 یعنی agent هیچ اعتبارنامه GitHub متعلق به خود را ارسال نمیکند و 200 یعنی یک اعتبارنامه را ارسال میکند؛ بنابراین باید بدانید این token کدام است. اگر agentها را بدون نظارت اجرا میکنید، کنترل هزینههای agentهای هوش مصنوعی در VPS محدودیتهای بودجهای متناظر با این محدودیتهای دسترسی را توضیح میدهد.
FAQ
آیا میتوانم فقط به مدل اعتماد کنم که کلیدهایم را افشا نکند؟
خیر، زیرا در این مدل تهدید، مدل مهاجم نیست. عامل، متن را از صفحات وب، مخزنها و ردیابهای issue میخواند و این متن میتواند شامل دستورالعملهایی باشد. مدل راه قابل اعتمادی برای تشخیص دستورالعملهای شما از متنی که دریافت کرده است ندارد. هر کنترلی که به انتخاب درست مدل وابسته باشد، نخستین بار که یک دستورالعمل تزریقشده قانعکننده باشد شکست میخورد. بنابراین کنترل باید در سیستمعامل یا شبکه اعمال شود.
آیا متغیرهای محیطی واقعاً برای اسرار عامل تا این اندازه نامناسباند؟
آنها از یک جهت مشخص نامناسباند: به ارث میرسند. هر فرایند فرزندی که عامل ایجاد میکند، یک نسخه از آنها را دریافت میکند؛ از جمله یک اسکریپت ساخت، یک اجراکننده آزمون و هر hook مربوط به نصب بسته. این متغیرها همچنین از طریق /proc/<pid>/environ برای همان کاربر قابل خواندن هستند. بنابراین هر چیزی که عامل اجرا میکند میتواند بدون انتقال متغیرها توسط عامل، آنها را بخواند. خواندن فایل در لحظه استفاده، با LoadCredential= یا از طریق یک gateway، افشا را به همان لحظه محدود میکند.
آیا قرار دادن اسرار در یک vault بهتنهایی این مشکل را حل میکند؟
فقط تا حدی. یک vault مشکل ذخیرهسازی را حل میکند. اما مرحله آخر را حل نمیکند؛ یعنی زمانی که چیزی secret را از vault بیرون میکشد و آن را بهصورت یک متغیر محیطی در اختیار عامل میگذارد. در این حالت دوباره به نقطه شروع بازمیگردید. عامل تعیینکننده این است که چه کسی جایگزینی را انجام میدهد. اگر عامل secret را دریافت کند، secret را در اختیار دارد. اگر یک gateway یا سیستم init، خارج از فرایند عامل، جایگزینی را انجام دهد، عامل هرگز secret را در اختیار نمیگیرد.
چگونه بفهمم که یک عامل قبلاً چیزی را افشا کرده است؟
معمولاً پس از وقوع حادثه نمیتوانید با قطعیت بفهمید؛ همین موضوع یکی از دلایل استفاده از gateway است. بدون gateway، شواهد شما در history مربوط به shell، transcript عامل و logهای اتصال خروجی پراکنده است؛ logهایی که احتمالاً نگهداری نمیکنید. با یک credential gateway، هر استفاده از یک credential در یک خط، همراه با هویت عامل و timestamp، ثبت میشود. اگر به افشا شدن چیزی مشکوک هستید، ابتدا key را rotate کنید و سپس بررسی کنید. Rotation کمهزینه است، اما قطعیت چنین نیست.
حداقل کاری که امروز باید انجام دهم چیست؟
هر فایل .env را از دایرکتوریهایی که عاملها در آنها کار میکنند خارج کنید و برای هر عامل، یک کاربر unprivileged ایجاد کنید. این دو تغییر حدود 10 دقیقه زمان میبرند و رایجترین مسیر را میبندند: عامل یک فایل credential را میخواند که دلیلی نداشته در کنار کد قرار بگیرد. gateway و tokenهای کوتاهعمر مرحله بعدی هستند، نه مرحله نخست. همین نقطه شروع برای هر runtime عامل، از جمله اجرای ایمن یک عامل خودکار روی VPS، کاربرد دارد.