Người ta hay xếp các cách lưu bí mật thành một cái thang "tệ → tốt" rồi học thuộc, nhưng ít khi hỏi câu quan trọng nhất: đo bằng cái gì? Cách hữu ích hơn là hình dung mỗi bí mật như một tin đồn — giá trị của việc giữ kín nó tỉ lệ nghịch với số người có thể nghe được. Một tin nói vào tai một người thì kín; một tin dán lên bảng thông báo công ty thì ai đi qua cũng đọc. Câu hỏi duy nhất đáng đo, cụ thể và kiểm chứng được, là: ai đọc được bí mật? Bề mặt rò rỉ càng rộng — càng nhiều người, nhiều tiến trình, nhiều nơi chạm tới — thì cách lưu càng tệ. Bài trước đã đo rằng một bí mật lỡ vào git thì rất khó dọn; bài này đo bốn chỗ hay để bí mật và bề mặt đọc của từng chỗ.

Tôi dựng từng cách trong container dùng một lần với bí mật giả, thử đọc trộm bằng đúng những đường kẻ tấn công hay dùng, đo 3 lần rồi tự dọn. Không nhắm vào hệ thống của ai khác. Và có một chỗ tôi đo sai ở lượt đầu — sẽ kể ở cách 2.

Cách 1: hardcode trong mã nguồn

Viết thẳng API_TOKEN = "..." vào file rồi commit. Đo:

nam trong git? CO
ai doc duoc: bat ky ai co repo / clone / lich su

Bề mặt rộng nhất có thể. Không chỉ người trong nhóm mà bất cứ ai từng clone repo, và như bài trước đã đo, bí mật còn ở lại trong lịch sử kể cả sau khi xoá. Đây là đáy của thang — không có cấu hình nào cứu được cách này ngoài việc đừng làm.

Cách 2: biến môi trường

Đây là lời khuyên phổ biến nhất — "đưa bí mật ra biến môi trường". Nó tốt hơn hardcode vì bí mật không nằm trong mã. Nhưng bề mặt của nó rộng hơn nhiều người tưởng. Đo hai đường:

tien trinh con (khong duoc truyen gi) doc duoc: tok_live_FAKEsecret123456
doc /proc/<pid>/environ tu ben ngoai -> thay API_TOKEN? CO

Điểm thứ nhất: mọi tiến trình con thừa kế biến môi trường. Tôi chạy một tiến trình con mà không truyền gì cho nó, thế mà nó vẫn đọc được API_TOKEN — vì con thừa hưởng toàn bộ môi trường của cha. Nghĩa là mỗi lần code của bạn gọi một lệnh ngoài (git, curl, một thư viện gọi subprocess), bí mật đi theo; một thư viện độc hại chỉ cần đọc os.environ là thấy hết.

Điểm thứ hai là chỗ tôi đo sai lần đầu, và tôi giữ lại đây vì nó đúng là loại bẫy dễ mắc. Lượt đầu tôi kiểm /proc/self/environ của chính shell và thấy không có API_TOKEN, suýt kết luận "biến môi trường không lộ qua /proc". Sai. Lý do: /proc/<pid>/environ chụp môi trường lúc tiến trình được exec, và không cập nhật khi sau đó biến mới được export; shell của tôi đã chạy trước khi tôi đặt biến, nên bản chụp của nó không có. Khi đo lại cho đúng — khởi động một tiến trình với biến đã đặt sẵn rồi đọc /proc/<pid>/environ của nó từ bên ngoài — bí mật hiện ra ngay, cả 3 lần. Bài học của cái sai đó: một tiến trình thật (ứng dụng của bạn) chạy với bí mật trong môi trường thì bất kỳ ai cùng quyền hoặc root đọc /proc là thấy.

Cộng thêm: biến môi trường lộ qua docker inspect, qua log khi ứng dụng in cả environ lúc gặp lỗi, qua các trang lỗi debug. Bề mặt của "cách tốt hơn hardcode" hoá ra vẫn khá rộng. Điều này không có nghĩa bỏ hẳn biến môi trường — chúng vẫn tiện và tránh được cách 1 — nhưng nó nói rõ giới hạn: đừng để ứng dụng in cả environ vào log lúc gặp lỗi, đừng chuyển biến chứa bí mật cho tiến trình con nếu con không cần (một số ngôn ngữ cho phép lọc môi trường khi spawn), và nhớ rằng trên một máy nhiều người, quyền đọc /proc là chuyện cần siết. Biến môi trường hợp với bí mật đời sống ngắn được cấp lúc chạy, hơn là bí mật lâu dài nằm mãi trong một file khởi động.

Cách 3: file .env trên đĩa

Để bí mật trong một file .env, ứng dụng đọc lúc khởi động. Ở đây quyền file quyết định tất cả. Tôi tạo file rồi thử đọc bằng một user khác (nobody):

quyen 644: -rw-r--r--  ->  user 'nobody' doc duoc? CO
quyen 600: -rw-------  ->  user 'nobody' doc duoc? khong

Với quyền 644 (mặc định hay gặp), mọi user trên máy đọc được — trên một máy chủ dùng chung, đó là bề mặt rộng đáng lo. Đổi sang 600 thì chỉ chủ sở hữu đọc được, và nobody bị chặn ngay. Con số này nói rõ: .env không tự nó an toàn hay không an toàn; nó an toàn đúng bằng quyền bạn đặt. Nhưng .env có một cạm bẫy riêng, nối thẳng với bài trước: nó rất dễ bị commit nhầm — là file thường trong thư mục dự án, và .gitignore không tự có sẵn dòng chặn nó, bạn phải tự thêm \.env; quên một dòng đó là quay lại đúng cách 1 (bí mật vào git kèm lịch sử), và nhiều vụ lộ khoá lớn bắt đầu từ đúng một file .env bị git add . nuốt vào.

Cách 4: file quyền 400 trên tmpfs

Cách chặt nhất trong bốn: bí mật nằm trong một file riêng, quyền 400 (chỉ chủ sở hữu đọc, không ai ghi), gắn qua tmpfs — tức chỉ tồn tại trong RAM, không ghi xuống đĩa lâu dài. Đo:

loai mount: tmpfs (chi trong RAM)
quyen 400: -r--------  ->  user 'nobody' doc duoc? khong

Bề mặt hẹp nhất: chỉ chủ sở hữu đọc; không vào git (nó không phải file mã nguồn); không thừa kế sang tiến trình con như biến môi trường; và vì ở tmpfs, nó biến mất khi tắt máy thay vì nằm lại trên ổ đĩa chờ ai đó khôi phục. Đây chính là mô hình mà các dịch vụ quản lý bí mật (secret manager, vault) và cơ chế "mounted secret" của Kubernetes dùng: cấp bí mật dưới dạng file tạm quyền chặt, thay vì nhét vào môi trường.

Xếp theo bề mặt rò rỉ, từ rộng đến hẹp: (1) hardcode — mọi người xem code + toàn bộ lịch sử git, đáy thang; (2) biến môi trường — mọi tiến trình con, /proc/<pid>/environ, docker inspect, log crash, tốt hơn hardcode nhưng đừng tưởng nó kín; (3) file .env — an toàn đúng bằng quyền của nó (644 = cả máy đọc, 600 = chỉ chủ), và rất dễ bị commit nhầm; (4) file 400 trên tmpfs / secret manager — chỉ chủ sở hữu, không vào git, không thừa kế, mất khi tắt máy, hẹp nhất.

Mẫu số chung

Cái khung "đừng hỏi có an toàn không, hãy đo ai đọc được" không riêng gì file bí mật trên Linux; nó là cách đúng để nghĩ về mọi bí mật trong mọi hệ.

Trong thế giới Java, cùng bề mặt lặp lại: System.getenv() và System.getProperty() đều đọc được từ bất kỳ chỗ nào trong tiến trình, và một heap dump (bài "bảo vệ log" và "danh sách kiểm" đã chạm) có thể chứa cả chuỗi bí mật đang nằm trong bộ nhớ — nên các app nghiêm túc kéo bí mật từ Vault/Spring Cloud Config lúc chạy (đời sống ngắn) thay vì ghim vào application.properties, và tránh in cả Environment khi log lỗi. Cùng nguyên tắc "hẹp bề mặt, đời sống ngắn".

Trong Go, os.Environ() thừa kế y hệt, và exec.Cmd cho bạn đặt Cmd.Env tường minh để lọc — không truyền bí mật cho tiến trình con không cần, đúng lời khuyên ở cách 2; đây là bản Go của "đừng để con thừa kế cái nó không dùng".

Còn ở cơ sở dữ liệu, cùng câu chuyện với thông tin kết nối: nhét mật khẩu DB vào chuỗi kết nối trong biến môi trường có đúng bề mặt của cách 2, nên xu hướng hiện đại là xác thực đời sống ngắn — IAM token của cloud, chứng chỉ client, hay mật khẩu do secret manager cấp và xoay vòng — để cái lộ ra (nếu có) cũng chỉ dùng được trong ít phút, thu hẹp thiệt hại theo thời gian thay vì chỉ theo ai đọc được.

Mẫu số chung: không có nút "an toàn" để bật cho một bí mật — chỉ có bề mặt đọc rộng hay hẹp mà bạn đo được (ai, tiến trình nào, sống bao lâu), và cách lưu tốt là cách thu hẹp cả ba trục đó: ít người/tiến trình chạm tới, không rơi vào bản sao lâu dài (git, log, dump), và đời sống càng ngắn càng tốt. Vì thế câu hỏi đúng khi cất một bí mật không phải "chỗ này có an toàn không" mà là "liệt kê hết những ai và những gì đọc được nó, và nó sống được bao lâu" — trả lời được câu đó thì thứ tự tệ-tới-tốt tự hiện ra, không cần học thuộc.

Muốn tự đo trên một tiến trình của chính bạn đang chạy với bí mật trong môi trường:

# Tim PID ung dung cua ban, roi (voi quyen phu hop) doc environ cua no
tr '\0' '\n' < /proc/<PID>/environ | grep -iE 'token|secret|key|password'

Nếu bí mật hiện ra, đó không phải lỗi — đó là bản chất của biến môi trường: ai đọc được /proc của tiến trình đều thấy. Nếu điều đó làm bạn không thoải mái, đã đến lúc chuyển bí mật từ môi trường sang một file quyền 400, hoặc sang một dịch vụ quản lý bí mật đưa nó vào dưới dạng file tạm.