Cách bảo mật API key khi sử dụng AI agent
AI agent có thể vô tình làm lộ API key qua prompt injection. Đừng cấp key gốc, hãy sử dụng token giới hạn phạm vi và thời gian sống qua credential gateway để bảo vệ hệ thống.
Ý nghĩa của việc giữ bí mật bên ngoài các AI agent
Một AI agent là một tiến trình Linux bình thường thực thi các lệnh. Mọi biến môi trường mà tiến trình đó nắm giữ đều có thể được đọc bởi mã mà nó chạy, vì vậy một API key trong môi trường của agent là một key mà agent có thể gửi đến bất kỳ host nào mà nó có thể kết nối tới. Giữ bí mật bên ngoài agent nghĩa là cung cấp cho nó một handle thay vì key: một token có phạm vi giới hạn và thời gian tồn tại ngắn, hoặc một placeholder mà một thành phần khác sẽ hoán đổi bằng giá trị thực tại ranh giới mạng.
Đây không phải là câu chuyện về một mô hình trở nên thù địch. Cơ chế này đơn giản hơn nhiều. Một agent đọc một trang web, một file README, hoặc một bình luận issue có chứa các hướng dẫn, và nó thực hiện theo chúng, bởi vì đối với một mô hình ngôn ngữ, không có sự khác biệt giữa văn bản bạn viết và văn bản mà nó đã lấy về. Đó là prompt injection. Một khi điều đó xảy ra, thiệt hại bị giới hạn bởi đúng một thứ: những gì tiến trình đó có thể đọc. Nếu bạn chưa thiết lập ranh giới, chạy một coding agent an toàn trên server bao gồm các bước cô lập mà hướng dẫn này dựa trên đó.
Mô hình đe dọa giải thích đơn giản
Hãy chạy lệnh này dưới quyền người dùng mà agent của bạn đang sử dụng.
tr '\0' '\n' < /proc/self/environ | grep -iE 'key|token|secret|password'Mỗi dòng lệnh in ra đều cách máy chủ của người lạ một request HTTP. Bây giờ hãy xem những gì có trên ổ đĩa gần agent.
grep -rIl --exclude-dir=.git -e 'API_KEY' -e 'SECRET' ~/projects
find ~ -maxdepth 3 -name '.env' -o -name 'credentials' -o -name '*.pem'Một agent có quyền shell không cần khai thác tinh vi để đưa dữ liệu đó ra ngoài. Bốn đường dẫn thông thường sau đây đều thực hiện được việc đó, và cả bốn đều trông giống như công việc bình thường trong log:
- Một lệnh
curlhoặcfetchgửi ra ngoài đến bất kỳ host nào, với giá trị nằm trong query string. - Một lệnh
git commitvàgit pushtới một repository mà agent có quyền ghi. - Một script cài đặt gói, chạy mã tùy ý dưới quyền người dùng của agent.
- Một truy vấn DNS tới một hostname chứa giá trị đó, cách này vẫn hoạt động ngay cả khi egress HTTP bị chặn.
Bạn không thể giải quyết vấn đề này bằng cách kiểm tra thủ công. Cách khắc phục là đảm bảo không có dữ liệu giá trị nào nằm trong tầm với của agent.
Secret trong cây thư mục làm việc là secret trong context window
Một agent sẽ đọc các file. Một file .env nằm trong repository mà nó đang làm việc sẽ bị đọc, và một khi đã đọc thì nó sẽ nằm trong context window, nghĩa là nó nằm trong bản ghi (transcript), trong bất kỳ log nào bạn lưu giữ, và trong bất cứ thứ gì agent viết tiếp theo.
Trước đây, với key nằm trong cây thư mục mà agent làm việc:
cd ~/projects/billing
cat .env
# STRIPE_SECRET_KEY=sk_live_...
# DATABASE_URL=postgres://app:hunter2@db.internal:5432/billingSau đó, với file đã được di chuyển ra ngoài tầm với:
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/.envNgười dùng của agent không còn có thể mở file đó nữa, vì cây thư mục làm việc không còn chứa nó. Các quy tắc từ chối (deny rules) trong cấu hình của chính agent là lớp bảo vệ thứ hai, không phải lớp đầu tiên. Claude Code đọc các quy tắc phân quyền từ .claude/settings.json trong dự án:
{
"permissions": {
"deny": ["Read(./.env)", "Read(./secrets/**)", "Read(./**/*.pem)"]
}
}Điều đó ngăn chặn lỗi vô ý của agent khi mở file trong lúc đang khám phá. Nó không ngăn được một chỉ dẫn được tiêm vào (injected instruction) chạy lệnh base64 .env, vì đó là một shell command chứ không phải là hành động đọc file. Hãy coi cấu hình là rào chắn và quyền truy cập hệ thống file là bức tường. Sự phân tách tương tự cũng áp dụng bên trong các container: các file env và secret trong Docker Compose đề cập đến phiên bản của vấn đề này ở một lớp sâu hơn.
Cấp cho mỗi agent một user không đặc quyền riêng
Nếu agent chạy dưới quyền của bạn, nó sẽ kế thừa các SSH key, thông tin xác thực cloud và lịch sử shell của bạn. Một user riêng biệt chỉ tốn một câu lệnh và loại bỏ hoàn toàn các rủi ro đó.
sudo adduser --disabled-password --gecos "" agent-review
sudo chmod 700 /home/agent-review
sudo -u agent-review cat ~/.ssh/id_ed25519Dòng cuối cùng phải thất bại với cat: /home/you/.ssh/id_ed25519: Permission denied. Nếu nó in ra một key, thư mục home của bạn đang ở chế độ group hoặc world readable, và chmod 700 ~ sẽ khắc phục điều đó. Đừng thêm agent user vào sudo, và đừng cấp cho nó một quy tắc NOPASSWD rộng hơn câu lệnh duy nhất mà nó thực sự cần. Người dùng đặc quyền tối thiểu trên VPS sẽ đi sâu vào chi tiết về group và sudoers.
Một ranh giới nữa cần thêm vào trên cloud VPS. Dịch vụ metadata của instance phản hồi trên một địa chỉ link local cố định, và nó thường cấp thông tin xác thực role cho bất kỳ thứ gì 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/ sẽ không in ra gì cả và thoát với mã lỗi khác không, vì gói tin bị từ chối trước khi nó rời khỏi máy.
Chèn credential tại biên
Mô hình giải quyết vấn đề này thực sự là chèn credential (credential injection). Agent không bao giờ giữ key thật. Nó gửi request qua một gateway cục bộ, và gateway sẽ thay thế placeholder bằng secret thật khi request đi ra. Secret nằm trong bộ nhớ của gateway, thuộc một tiến trình khác, do một user khác sở hữu.
OneCLI là một triển khai mã nguồn mở cho việc này, sử dụng giấy phép Apache-2.0, và nó chạy dưới dạng container bên cạnh agent. Tính đến tháng 7 năm 2026, dự án tài liệu hóa thiết lập 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, sau đó cấp cho mỗi agent một giá trị placeholder thay cho key cùng với scoped access token riêng của nó, giá trị này được gửi trong header Proxy-Authorization. Gateway khớp request đi ra theo host và path, giải mã credential tương ứng, và thực hiện thay thế. Môi trường của agent không chứa bất cứ thứ gì đáng để đánh cắp.
Giá trị ở đây không nằm ở việc mã hóa. Nó nằm ở chỗ câu hỏi "agent này đã sử dụng gì và khi nào" trở thành một truy vấn log. Bạn đọc một audit trail duy nhất thay vì phải đoán xem môi trường nào trong số sáu môi trường đang giữ một bản sao của key.
Cấp secret cho tiến trình, không phải cho môi trường
Nếu bạn chạy agent dưới systemd, bạn không cần dùng biến môi trường. LoadCredential= đặt secret vào một thư mục riêng tư mà chỉ service đó mới có quyền đọc, được hiển thị dưới dạng %d trong file unit và dưới dạng $CREDENTIALS_DIRECTORY bên trong tiến trình. Giá trị này không bao giờ xuất hiện trong /proc/<pid>/environ, vì vậy ps eww không thể hiển thị nó, và thư mục sẽ biến mất khi service dừng lại.
Trước tiên, hãy mã hóa credential cho máy chủ. Các lệnh này 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 đó chứng minh file đã mã hóa giải mã được trên host này. Sau đó, tham chiếu nó từ file 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 agent của bạn mở file tại $AGENT_KEY_FILE khi cần giá trị đó. Việc đọc file chỉ diễn ra trong một khoảnh khắc. Biến môi trường tồn tại trong suốt vòng đời của tiến trình và trong mọi tiến trình con mà nó tạo ra.
Ưu tiên token thời hạn ngắn hơn là các key thời hạn dài
Một key không bao giờ hết hạn sẽ luôn hợp lệ bất cứ khi nào nó xuất hiện, dù là nhiều tháng sau đó, trong log hoặc bản ghi. Nếu dịch vụ cung cấp session token, hãy sử dụng session token đó và thiết lập thời gian tồn tại ngắn nhất mà công việc cho phép.
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 gian tối thiểu mà AWS STS (security token service) chấp nhận, và thường là đủ cho một tác vụ của agent. Đối với GitHub, hãy cấp cho agent user một tài khoản đăng nhập gh riêng với một fine grained token được giới hạn phạm vi trong đúng repository mà nó làm việc, để gh auth token bên trong phiên làm việc đó trả về kết quả không thể tác động đến bất kỳ thứ gì khác. Hãy giới hạn theo tài nguyên trước, sau đó mới đến thời gian.
Xác minh, rồi tiếp tục xác minh
Có ba bước kiểm tra cần thực hiện sau bất kỳ thay đổi nào đối với thiết lập của agent. Hãy chạy chúng dưới quyền người dùng của agent, không phải quyền 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/userBước đầu tiên không được hiển thị bất kỳ kết quả nào. Bước thứ hai phải hiển thị ls: cannot open directory '/home/you/': Permission denied. Bước thứ ba cho bạn biết danh tính mà đường dẫn mạng của agent đang sử dụng, đây chính là câu hỏi mà mô hình gateway được tạo ra để giải đáp: 401 nghĩa là agent không mang theo bất kỳ credential nào của GitHub, còn 200 nghĩa là nó đang mang theo một credential, vì vậy bạn cần biết đó là token nào. Nếu bạn chạy các agent không có người giám sát, kiểm soát chi phí AI agent trên VPS sẽ đề cập đến các giới hạn ngân sách đi kèm với các giới hạn truy cập này.
FAQ
Tôi có thể tin tưởng model không làm lộ các key của mình không?
Không, vì trong mô hình đe dọa này, model không phải là kẻ tấn công. Agent đọc văn bản từ các trang web, kho lưu trữ và trình theo dõi sự cố, và văn bản đó có thể chứa các chỉ dẫn. Model không có cách nào đáng tin cậy để phân biệt chỉ dẫn của bạn với văn bản mà nó đã lấy về. Bất kỳ biện pháp kiểm soát nào phụ thuộc vào việc model chọn đúng đều sẽ thất bại ngay lần đầu tiên một chỉ dẫn được tiêm vào có sức thuyết phục, vì vậy biện pháp kiểm soát phải nằm ở hệ điều hành hoặc mạng lưới.
Biến môi trường có thực sự tệ đối với các secret của agent không?
Chúng tệ ở một khía cạnh cụ thể: chúng bị kế thừa. Mọi tiến trình con mà agent tạo ra đều nhận được một bản sao, bao gồm script build, trình chạy test và bất kỳ hook cài đặt gói nào. Các biến này cũng có thể đọc được thông qua /proc/<pid>/environ bởi cùng một người dùng, vì vậy bất cứ thứ gì agent chạy đều có thể đọc chúng mà không cần agent truyền đi. Một tệp được đọc tại thời điểm sử dụng, với LoadCredential= hoặc một gateway, sẽ giới hạn mức độ phơi nhiễm trong đúng thời điểm đó.
Việc đưa các secret vào một vault có tự giải quyết được vấn đề này không?
Chỉ một phần. Vault giải quyết vấn đề lưu trữ. Nó không giải quyết bước cuối cùng, nơi một thứ gì đó lấy secret ra khỏi vault và đưa cho agent dưới dạng biến môi trường, điều này đưa bạn trở lại điểm xuất phát. Điều quan trọng là ai thực hiện việc thay thế. Nếu agent lấy secret, agent sẽ nắm giữ secret đó. Nếu một gateway hoặc hệ thống init thực hiện thay thế bên ngoài tiến trình của agent, agent sẽ không bao giờ nắm giữ nó.
Làm sao tôi biết liệu một agent đã làm lộ thứ gì đó chưa?
Thông thường bạn không thể biết sau khi sự việc đã xảy ra, đó là lý do tại sao cần có gateway. Nếu không có nó, bằng chứng của bạn sẽ nằm rải rác trong lịch sử shell, bản ghi của agent và các log kết nối đi mà có lẽ bạn không lưu giữ. Với một credential gateway, mỗi lần sử dụng credential sẽ là một dòng chứa danh tính agent và dấu thời gian. Nếu bạn nghi ngờ bị lộ, hãy xoay vòng key trước rồi mới điều tra sau. Việc xoay vòng rất rẻ, còn sự chắc chắn thì không.
Tôi nên làm gì tối thiểu ngay hôm nay?
Di chuyển mọi tệp .env ra khỏi các thư mục mà agent của bạn làm việc, và tạo một người dùng không có đặc quyền cho mỗi agent. Hai thay đổi đó mất khoảng mười phút và đóng lại con đường phổ biến nhất, đó là việc agent đọc một tệp credential vốn không có lý do gì để nằm cạnh mã nguồn. Gateway và các token có thời hạn ngắn là bước tiếp theo, không phải bước đầu tiên. Điểm khởi đầu tương tự áp dụng cho bất kỳ môi trường chạy agent nào, bao gồm chạy một agent tự động một cách an toàn trên VPS.