SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor

Cách bảo mật API key khi chạy AI agent

Đừng cấp API key trực tiếp cho AI agent. Bài viết hướng dẫn cách sử dụng token có phạm vi giới hạn và credential gateway để ngăn chặn rò rỉ dữ liệu qua prompt injection.

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 curl hoặc fetch đi ra ngoài đến bất kỳ host nào, với giá trị nằm trong query string.
  • Một git commit và 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 là một secret trong context window

Agent đọc các file. Một file .env trong repository mà agent đang làm việc sẽ được đọc. Khi đã đọ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/billing

Sau đó, 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/.env

User 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 thứ hai, không phải lớp đầu tiên. Claude Code đọc cá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ó cũng áp dụng vượt ra ngoài các file tool: Claude Code kiểm tra các rule này với những lệnh file mà nó nhận diện trong Bash, chẳng hạn như cat và head, cũng như với các đích của redirection như < .env. Cách này không ngăn được instruction bị inject chạy grep -r KEY . bên trong thư mục project, hoặc một Python script ngắn tự mở .env, vì tài liệu permission nói rằng rule deny không áp dụng cho các lệnh đọc file mà không nêu tên file, cũng không áp dụng cho subprocess tự mở file. Với các trường hợp đó, tài liệu trỏ đến sandbox của Claude Code, nơi áp dụng block ở cấp hệ điều hành. Việc bạn có được hỏi trước khi một lệnh như vậy chạy hay không phụ thuộc vào permission mode của session. auto mode hiện là chế độ khởi động tích hợp sẵn cho các session Claude Code tương tác, nên một server bạn không theo dõi sẽ tự chạy nhiều lệnh như vậy hơn mà không hỏi. 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ó tác dụng giúp một lần chạy không tự ý đi vào những file mà nó không có lý do để mở, nhưng đó vẫn chỉ là hướng dẫn và model có thể bị thuyết phục làm khác. Hãy xem config là lan can bảo vệ, còn permission của filesystem là bức tường. Sự phân tách tương tự cũng áp dụng bên 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 lệnh và 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_ed25519

Dòng cuối phải fail với cat: /home/you/.ssh/id_ed25519: Permission denied. Nếu nó in ra một key, user của agent có thể truy cập file đó. Mở một file theo tên cần quyền search (x) trên mọi thư mục bên trên nó, không cần quyền read. Vì vậy, home directory có mode 711 và không cho user khác liệt kê vẫn cho phép truy cập đến key nếu chính file key có thể đọc được. chmod 700 ~ đóng đường dẫn, còn chmod 600 ~/.ssh/id_ed25519 sửa quyền của key. Không thêm user của agent vào sudo và không cấp cho user này một rule NOPASSWD rộng hơn lệnh 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ì cách tách biệt này khi chạy nhiều hơn một 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 nắm giữ đều có thể đi qua kênh đó trong một message.

Trên cloud VPS, nên bổ sung thêm một ranh giới bảo vệ. Metadata service của instance phản hồi tại một địa chỉ link-local cố định và thường cấp role credentials cho bất kỳ tiến trình nào yêu cầu. Endpoint không kiểm tra user cục bộ nào đang thực hiện request. Trên EC2, AWS ghi rõ rằng request đến IMDSv1 không cần token. AWS cũng khuyến nghị dùng local firewall rule dựa trên ID của user để ngăn một user truy cập service này.

sudo iptables -I OUTPUT 1 -p tcp -m owner --uid-owner agent-review -d 169.254.169.254 -j REJECT

Chèn rule ở đầu bằng -I thay vì thêm vào cuối bằng -A. Trên host chạy ufw với policy mặc định cho phép outgoing, rule cuối của chain OUTPUT trong ufw chấp nhận mọi kết nối TCP mới, nên rule được thêm vào cuối sẽ không bao giờ được xử lý. Các rule iptables cũng biến mất sau reboot nếu bạn không persist chúng. Nếu đã bật IPv6 endpoint của metadata service, [fd00:ec2::254], hãy thêm rule tương tự bằng ip6tables.

Kiể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à exit với mã khác 0, vì packet bị reject trước khi rời khỏi máy. Nếu lệnh in ra danh sách, rule chưa được áp dụng.

Inject credential tại ranh giới

Mẫu thực sự giải quyết vấn đề này là inject 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, ở một process khác và thuộc sở hữu của một user khác.

OneCLI là một implementation mã nguồn mở của mô hình này. Dự án được cấp phép theo Apache-2.0, ngoại trừ các thư mục ee/ sử dụng giấy phép enterprise riêng, yêu cầu subscription khi dùng trong production. OneCLI chạy trong các container cùng với agent. Tính đến October 2026, tài liệu dự án mô tả thiết lập Docker Compose sau. Thiết lập này đọc 3 secret được tạo từ file .env nằm cạnh file 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

Hãy backup file .env, vì mất SECRET_ENCRYPTION_KEY sẽ làm mọi secret được lưu cùng với nó trở thành mồ côi. Dashboard lắng nghe trên port 10254 và gateway lắng nghe trên port 10255. Theo mặc định, cả hai chỉ bind vào 127.0.0.1. Bạn lưu credential thật một lần, sau đó cấp cho mỗi agent một placeholder thay cho key cùng với access token có scope riêng. Agent gửi token đó trong header Proxy-Authorization. Gateway xác định request outbound 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ì có giá trị để kẻ tấn công đánh cắp.

Giá trị của mô hình này không nằm ở encryption. Giá trị nằm ở việc câu hỏi “agent này đã 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ì đoán xem bản sao của key đang nằm trong environment nào trong số 6 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 dùng environment variable. LoadCredential= đặt secret trong một thư mục riêng mà chỉ user của service mới đọc được, hiển thị dưới dạng %d trong unit file và dưới dạng $CREDENTIALS_DIRECTORY bên trong process. Quyền hạn này áp dụng theo user, không theo service. Vì vậy, hãy cấp cho mỗi agent service một user riêng: mọi process khác chạy cùng user đều có thể mở thư mục này, trừ khi unit cũng dùng filesystem namespacing, chẳng hạn như PrivateMounts=yes, để ẩn thư mục khỏi các service khác. Giá trị không bao giờ xuất hiện trong /proc/<pid>/environ, nên ps eww không thể hiển thị giá trị đó. Thư mục cũng biến mất khi service dừng.

Trước tiên, hãy mã hóa credential cho chính máy này. Các command 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_key

Command cuối 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 từ 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

Code của agent mở file tại $AGENT_KEY_FILE khi cần giá trị. Việc đọc file chỉ diễn ra trong một thời điểm. Environment variable tồn tại suốt vòng đời của process và trong mọi child process mà nó 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 900

Mườ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 trong cấu hình của agent, bạn 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/user

Lệnh đầu tiên không được in ra bất kỳ thứ gì. Lệnh thứ hai phải in ra ls: cannot open directory '/home/you/': Permission denied, nhưng kết quả này chỉ cho biết không thể list directory; chính bài test cat ở trên mới chứng minh được không thể đọc file bên trong directory đó. Lệnh thứ ba cho biết network path của agent đang trình diện identity nào. Đây là vấn đề mà gateway pattern dùng để giải quyết: 401 nghĩa là request đã đến GitHub nhưng không kèm credential mà GitHub chấp nhận, tức là không có credential hoặc credential đã không còn hợp lệ, hết hạn; còn 200 nghĩa là request đang mang một credential hợp lệ, vì vậy bạn phải biết token đó là token nào. Nếu bạn chạy agent không cần giám sát, kiểm soát 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.

Bài viết này có hữu ích không?