Giữ secret ngoài AI agent bằng token ngắn hạn
API key trong environment có thể bị agent gửi đến host bất kỳ sau một prompt injection. Dùng token giới hạn quyền, sống ngắn qua credential gateway.
Việc giữ secret ngoài AI agent có nghĩa là gì
AI agent là một tiến trình Linux thông thường dùng để chạy command. Mọi environment variable mà tiến trình đó giữ đều có thể được code mà nó chạy đọc. Vì vậy, API key trong environment của agent là key mà agent có thể gửi đến bất kỳ host nào nó truy cập được. Giữ secret ngoài agent nghĩa là cấp cho agent một handle thay vì key: một token có phạm vi giới hạn và thời gian sống ngắn, hoặc một placeholder để thành phần khác thay bằng giá trị thật tại ranh giới mạng.
Đây không phải là câu chuyện về việc model trở nên thù địch. Cơ chế thực tế đơn giản hơn. Agent đọc một web page, README hoặc comment trong issue có chứa instruction, rồi làm theo, vì với language model, không có khác biệt giữa nội dung bạn viết và nội dung nó tải về. Đó là prompt injection. Khi việc này xảy ra, mức độ thiệt hại bị giới hạn chính xác bởi một yếu tố: tiến trình có thể đọc được gì. Nếu bạn chưa thiết lập boundary, chạy coding agent an toàn trên server trình bày các mức isolation mà hướng dẫn này dựa trên.
Mô hình mối đe dọa theo cách đơn giản
Chạy lệnh này bằng user mà agent đang chạy.
tr '\0' '\n' < /proc/self/environ | grep -iE 'key|token|secret|password'Mỗi dòng mà lệnh in ra tương đương với một HTTP request đến server của người lạ. Bây giờ hãy xem trên disk gần agent có những gì.
grep -rIl --exclude-dir=.git -e 'API_KEY' -e 'SECRET' ~/projects
find ~ -maxdepth 3 -name '.env' -o -name 'credentials' -o -name '*.pem'Agent có shell không cần exploit phức tạp để đưa dữ liệu đó ra ngoài. Bốn đường thông thường là đủ, và cả bốn đều trông như hoạt động bình thường trong log:
- Một
curlhoặcfetchđi ra ngoài đến bất kỳ host nào, với giá trị nằm trong query string. - Một
git commitvàgit pushđến repository mà agent có quyền ghi. - Một package install script, chạy arbitrary code dưới user của agent.
- Một DNS lookup đến hostname có chứa giá trị đó; cách này vẫn làm dữ liệu thoát ra ngoài ngay cả khi HTTP egress bị chặn.
Bạn không thể giải quyết vấn đề này chỉ bằng cách review. Cách sửa là bảo đảm không có dữ liệu có giá trị nào nằm trong phạm vi agent có thể truy cập.
Một secret trong working tree cũng nằm trong context window
Agent đọc file. File .env trong repository mà agent đang làm việc sẽ được đọc. Khi đã được đọc, file đó nằm trong context window. Điều này có nghĩa là file xuất hiện trong transcript, trong mọi log bạn lưu và trong nội dung agent ghi tiếp theo.
Trước đây, khi key nằm trong tree mà agent đang làm việc:
cd ~/projects/billing
cat .env
# STRIPE_SECRET_KEY=sk_live_...
# DATABASE_URL=postgres://app:hunter2@db.internal:5432/billingSau đó, khi file được chuyển ra ngoài phạm vi truy cập:
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/.envUser của agent không còn mở được file vì working tree không còn chứa file đó. Các rule deny trong config riêng của agent chỉ là lớp bảo vệ thứ hai, không phải lớp đầu tiên. Claude Code đọc permission rule từ .claude/settings.json trong project:
{
"permissions": {
"deny": ["Read(./.env)", "Read(./secrets/**)", "Read(./**/*.pem)"]
}
}Cách này ngăn agent vô tình mở file trong lúc khám phá. Nó không ngăn được instruction bị inject chạy base64 .env, vì đó là shell command chứ không phải thao tác đọc file. Việc bạn có được hỏi trước khi command đó chạy hay không phụ thuộc vào permission mode của session. auto mode sẽ trở thành mặc định của Claude Code vào tháng 8 năm 2026, nên một server không có người theo dõi sẽ tự chạy nhiều command như vậy hơn. Giới hạn tương tự cũng áp dụng cho mọi thứ định hình thói quen của agent thay vì permission của nó: một skill buộc agent chỉ thực hiện thay đổi nhỏ nhất có hiệu quả giúp một lần chạy không tự ý đi vào những file mà nó không nên mở, nhưng đó vẫn chỉ là lời hướng dẫn mà model có thể bị thuyết phục bỏ qua. Hãy xem config là lan can bảo vệ và filesystem permission là bức tường. Sự phân tách này cũng áp dụng trong container: env file và secret trong Docker Compose trình bày phiên bản của vấn đề này ở lớp thấp hơn.
Cấp cho mỗi agent một user không có quyền đặc biệt
Nếu agent chạy dưới user của bạn, nó sẽ kế thừa SSH keys, cloud credentials và shell history của bạn. Tạo một user riêng chỉ cần một command nhưng loại bỏ toàn bộ quyền truy cập đó.
sudo adduser --disabled-password --gecos "" agent-review
sudo chmod 700 /home/agent-review
sudo -u agent-review cat ~/.ssh/id_ed25519Dòng cuối phải fail với cat: /home/you/.ssh/id_ed25519: Permission denied. Nếu nó in ra một key, home directory của bạn đang cho phép group hoặc tất cả user đọc, và chmod 700 ~ sẽ khắc phục việc đó. Không thêm agent user vào sudo, và không cấp cho user này một rule NOPASSWD rộng hơn command duy nhất mà nó thực sự cần. User có quyền tối thiểu trên VPS trình bày chi tiết về group và sudoers. Hãy duy trì sự tách biệt này khi bạn chạy nhiều session trên máy, vì một session Claude Code có thể gửi text trực tiếp sang session khác, và mọi thứ session đầu tiên đang giữ đều có thể đi qua kênh đó trong một message duy nhất.
Trên cloud VPS, bạn nên thêm một ranh giới bảo vệ nữa. Instance metadata service phản hồi tại một địa chỉ link-local cố định và thường cấp role credentials cho mọi client gửi yêu cầu.
sudo iptables -A OUTPUT -m owner --uid-owner agent-review -d 169.254.169.254 -j REJECTKiểm tra từ phía agent. sudo -u agent-review curl -s --max-time 3 http://169.254.169.254/ phải không in gì và thoát với mã non-zero, vì packet bị reject trước khi rời khỏi máy.
Chèn credential tại ranh giới
Mẫu thực sự giải quyết vấn đề này là chèn credential. Agent không bao giờ giữ key thật. Nó gửi request qua một local gateway, rồi gateway thay placeholder bằng secret thật trước khi gửi ra ngoài. Secret nằm trong storage của gateway, trong một process khác và thuộc về một user khác.
OneCLI là một implementation mã nguồn mở của cách này, được cấp phép theo Apache-2.0, và chạy dưới dạng container bên cạnh agent. Tính đến tháng 7 năm 2026, project mô tả setup này như sau:
git clone https://github.com/onecli/onecli.git
cd onecli
docker compose -f docker/docker-compose.yml up -d --waitDashboard lắng nghe trên port 10254 và gateway trên port 10255. Bạn lưu credential thật một lần, rồi cấp cho mỗi agent một giá trị placeholder thay cho key, cùng với access token có scope riêng. Agent gửi token này trong header Proxy-Authorization. Gateway đối chiếu request gửi ra ngoài theo host và path, giải mã credential tương ứng rồi thay thế placeholder. Environment của agent không chứa gì đáng để đánh cắp.
Giá trị chính ở đây không nằm ở việc mã hóa. Điểm quan trọng là câu hỏi “agent này đã dùng gì và vào lúc nào” trở thành một truy vấn log. Bạn chỉ cần đọc một audit trail thay vì đoán xem bản sao của key đang nằm trong environment nào trong số sáu environment.
Bàn giao secret cho process, không phải cho environment
Nếu chạy agent dưới systemd, bạn không cần environment variable. LoadCredential= đặt secret trong một thư mục riêng mà chỉ service đó có thể đọc. Secret được cung cấp dưới dạng %d trong unit file và dưới dạng $CREDENTIALS_DIRECTORY bên trong process. Giá trị này không bao giờ xuất hiện trong /proc/<pid>/environ, nên ps eww không thể hiển thị nó. Thư mục cũng biến mất khi service dừng.
Trước tiên, hãy mã hóa credential cho máy này. Các lệnh sau lấy từ tài liệu systemd và hoạt động trên systemd 250 trở lên, bao gồm Ubuntu 24.04 và 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_keyLệnh cuối cùng in ra sk-example-value. Điều đó xác nhận file đã mã hóa có thể được giải mã trên host này. Sau đó tham chiếu file đó trong 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-workerCode của agent mở file tại $AGENT_KEY_FILE khi cần giá trị này. Việc đọc file chỉ diễn ra trong một thời điểm. Environment variable tồn tại trong suốt vòng đời của process và có mặt trong mọi process con mà process đó tạo ra.
Ưu tiên token có thời hạn ngắn thay vì key có thời hạn dài
Một key không bao giờ hết hạn vẫn còn hiệu lực nếu nhiều tháng sau nó xuất hiện trong log hoặc transcript. Nếu service cung cấp session token, hãy dùng session token và đặt thời hạn ngắn nhất phù hợp với công việc.
aws sts assume-role \
--role-arn arn:aws:iam::123456789012:role/agent-readonly \
--role-session-name agent-review \
--duration-seconds 900Mười lăm phút là thời hạn tối thiểu mà AWS STS (security token service) chấp nhận, và thường đủ cho một tác vụ của agent. Với GitHub, hãy cấp cho user của agent một tài khoản đăng nhập gh riêng, kèm token có phạm vi chi tiết chỉ giới hạn trong repository duy nhất mà agent làm việc. Khi đó, gh auth token trong session đó sẽ trả về thông tin không thể truy cập bất kỳ tài nguyên nào khác. Trước tiên, hãy giới hạn theo resource, sau đó giới hạn theo thời gian.
Kiểm tra, rồi tiếp tục kiểm tra
Sau bất kỳ thay đổi nào đối với cấu hình của agent, nên chạy 3 kiểm tra. Hãy chạy chúng bằng user của agent, không phải user của bạn.
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/userLệnh đầu tiên không được in ra bất kỳ nội dung nào. Lệnh thứ hai phải in ra ls: cannot open directory '/home/you/': Permission denied. Lệnh thứ ba cho biết network path của agent đang trình bày identity nào. Đây chính là vấn đề mà gateway pattern dùng để giải quyết: 401 nghĩa là agent không mang GitHub credential riêng, còn 200 nghĩa là agent có mang một credential. Vì vậy, bạn phải biết token đó là token nào. Nếu chạy agent ở chế độ unattended, quản lý chi phí AI agent trên VPS trình bày các giới hạn ngân sách đi kèm với những giới hạn truy cập này.
FAQ
Tôi có thể chỉ cần tin model không làm rò rỉ các key của mình không?
Không, vì model không phải attacker trong threat model này. Agent đọc text từ các web page, repository và issue tracker, và text đó có thể chứa instruction. Model không có cách đáng tin cậy để phân biệt instruction của bạn với text mà nó đã fetch. Mọi cơ chế kiểm soát dựa vào việc model lựa chọn đúng đều thất bại ngay khi một instruction được inject đủ thuyết phục. Vì vậy, cơ chế kiểm soát phải nằm trong operating system hoặc network.
Environment variable có thực sự nguy hiểm đến vậy đối với secret của agent không?
Chúng nguy hiểm theo một cách cụ thể: chúng được kế thừa. Mọi child process mà agent spawn đều nhận được một bản sao, bao gồm build script, test runner và mọi package install hook. Cùng user đó cũng có thể đọc các variable qua /proc/<pid>/environ, vì vậy mọi thứ agent chạy đều có thể đọc chúng mà không cần agent truyền tiếp. Đọc file tại thời điểm sử dụng, bằng LoadCredential= hoặc qua gateway, giới hạn việc lộ secret vào đúng thời điểm đó.
Đưa secret vào vault có tự giải quyết được vấn đề này không?
Chỉ giải quyết được một phần. Vault giải quyết việc lưu trữ. Nếu bạn self-host vault đó, nó cần được hardening riêng, vì một server Vaultwarden thường bị breach qua admin token hoặc file backup, chứ không phải qua các item đã mã hóa mà nó lưu giữ. Vault không giải quyết bước cuối, khi một thành phần lấy secret ra khỏi vault rồi truyền cho agent dưới dạng environment variable. Khi đó bạn lại quay về vấn đề ban đầu. Điều quan trọng là thành phần nào thực hiện việc thay thế. Nếu agent fetch secret, agent có secret. Nếu gateway hoặc init system thực hiện việc thay thế bên ngoài process của agent, agent không bao giờ giữ secret đó.
Làm sao biết agent đã làm rò rỉ thứ gì chưa?
Thông thường bạn không thể biết sau sự việc. Đây là lý do nên dùng gateway. Nếu không có gateway, bằng chứng bị phân tán trong shell history, transcript của agent và outbound connection log mà có thể bạn không hề lưu. Với credential gateway, mỗi lần dùng credential là một dòng log có agent identity và timestamp. Nếu nghi ngờ bị rò rỉ, hãy rotate key trước rồi mới điều tra. Rotation có chi phí thấp, còn sự chắc chắn thì không.
Hôm nay tôi tối thiểu cần làm gì?
Đưa mọi file .env ra khỏi các directory mà agent làm việc, đồng thời tạo một unprivileged user cho mỗi agent. Hai thay đổi này chỉ mất khoảng mười phút và loại bỏ đường lộ phổ biến nhất: agent đọc một credential file vốn không có lý do gì phải nằm cạnh code. Gateway và short-lived token là bước tiếp theo, không phải bước đầu tiên. Điểm bắt đầu tương tự áp dụng cho mọi agent runtime, bao gồm chạy một autonomous agent an toàn trên VPS.