- Dù phần mềm đã phát triển rất nhanh, hệ thống biến môi trường của hệ điều hành vẫn giữ nguyên cấu trúc từ hàng chục năm trước
- Biến môi trường có dạng từ điển chuỗi toàn cục, là một cấu trúc đơn giản không có namespace hay kiểu dữ liệu
- Trên Linux, biến môi trường được truyền từ tiến trình cha sang tiến trình con thông qua lời gọi hệ thống
execve
- Bash, glibc, Python v.v. mỗi bên quản lý biến môi trường theo dạng hash map, mảng, hoặc lớp bao bọc từ điển
- Chuẩn POSIX không bắt buộc tên phải viết hoa, và trên thực tế còn có các quy tắc linh hoạt như khuyến nghị dùng tên viết thường trong một số trường hợp
Biến môi trường là gì
- Dù ngôn ngữ lập trình phát triển rất nhanh, cấu trúc nền tảng để chạy tiến trình do hệ điều hành cung cấp, đặc biệt là phần biến môi trường, hầu như không thay đổi
- Khi cần truyền tham số runtime lúc chạy ứng dụng mà không dùng file riêng hay IPC, thực tế thường buộc phải dùng giao diện dựa trên biến môi trường
- Biến môi trường đóng vai trò như một từ điển chuỗi phẳng, không có namespace và cũng không có kiểu dữ liệu
Cấu trúc tạo và truyền biến môi trường
- Biến môi trường là cách truyền giá trị truyền thống giữa các tiến trình, được gửi kèm khi tiến trình cha thực thi tiến trình con
- Nói cách khác, đây là cấu trúc được kế thừa từ tiến trình cha sang tiến trình con
- Trên Linux, system call
execve nhận file thực thi, tham số, và mảng biến môi trường (envp) làm đối số
- Ví dụ lệnh thực thi:
ls -lah thì
- filename:
/usr/bin/ls
- argv:
['ls', '-lah']
- envp:
['PATH=...','USER=...']
- Tiến trình cha có thể chuyển nguyên môi trường hiện có cho tiến trình con, hoặc tạo một môi trường hoàn toàn mới
- Gần như mọi công cụ (Bash,
subprocess.run của Python, thư viện C execl v.v.) đều chuyển nguyên biến môi trường
- Ngoại lệ là một số công cụ như
login, vốn tạo môi trường mới
Vị trí lưu trữ và cách xử lý nội bộ của biến môi trường
- Khi chương trình khởi động, kernel lưu các biến môi trường trên stack dưới dạng chuỗi kết thúc bằng null
- Dữ liệu này khó để chương trình sửa trực tiếp, nên thường được sao chép vào cấu trúc riêng bên trong chương trình để quản lý
- Cách các ngôn ngữ và shell lưu trữ biến môi trường
- Bash: quản lý bằng hash map (từ điển) theo cấu trúc stack
- Mỗi lần gọi hàm sẽ tạo một map có phạm vi cục bộ
- Chỉ các biến được
export mới được truyền sang tiến trình con
- Các biến khai báo bằng
local cũng có thể truyền sang tiến trình con thông qua export
- Ví dụ: dùng
export PATH để phản ánh thay đổi cục bộ sang tiến trình con nhưng không ảnh hưởng tới phạm vi toàn cục
- glibc (thư viện C): quản lý
environ dạng mảng động thông qua putenv, getenv
- Vì là cấu trúc mảng nên cả tra cứu lẫn sửa đổi đều có độ phức tạp thời gian tuyến tính
- Do đó không phù hợp để dùng làm nơi lưu dữ liệu đòi hỏi hiệu năng cao
- Python: cung cấp
os.environ như một từ điển nội bộ, nhưng thực chất nó liên kết với mảng environ của thư viện C
- Khi thay đổi giá trị trong
os.environ, os.putenv sẽ được gọi để phản ánh sang thư viện C
- Chiều ngược lại không được đồng bộ, nên tồn tại tính một chiều
Định dạng và phạm vi cho phép của biến môi trường
- Linux kernel và glibc rất dễ dãi với định dạng biến môi trường
- Có thể tồn tại nhiều giá trị trùng tên
- Có thể đăng ký cả mục không có
=, và cũng không hạn chế ký tự đặc biệt như emoji
- Giới hạn kích thước khả dụng
- Biến đơn lẻ: 128 KiB (thường trong môi trường x64)
- Tổng cộng: 2 MiB (dùng chung với tham số dòng lệnh)
- Biến môi trường bị giới hạn không vượt quá 1/4 không gian stack
Điểm đặc biệt và các edge case của biến môi trường
- Với các biến môi trường bất thường (trùng lặp, mục không có
= v.v.), Bash sẽ loại bỏ tên trùng và bỏ qua các mục không hợp lệ
- Nếu tên biến có khoảng trắng, Bash không thể tham chiếu tên đó, nhưng vẫn có thể truyền sang tiến trình con
- Ví dụ: Nushell, Python v.v. có thể tạo biến có tên chứa khoảng trắng
- Bash quản lý các mục như vậy bằng cách lưu chúng trong một hash map riêng (
invalid_env)
Định dạng biến môi trường chuẩn và quy tắc đặt tên
- Chuẩn POSIX coi một mục là biến miễn là tên không chứa dấu bằng (
=)
- Khuyến nghị chính thức: tên chỉ nên gồm chữ hoa, số và dấu gạch dưới (ký tự đầu không được là số)
- Biến viết thường dành cho namespace riêng của ứng dụng
- Công cụ tiêu chuẩn chỉ dùng chữ hoa, nhưng việc dùng biến viết thường vẫn được cho phép
- Trên thực tế, các nhà phát triển chủ yếu đặt tên theo kiểu ALL_UPPERCASE
- Quy tắc được khuyến nghị: tên biến theo regex
^[A-Z_][A-Z0-9_]*$, giá trị dùng UTF-8
- Nếu lo ngại ngoại lệ hoặc vấn đề tương thích, nên dùng Portable Character Set (ASCII) của POSIX
Kết luận
- Biến môi trường vẫn là một giao diện cũ kỹ nhưng thiết yếu, đóng vai trò là bề mặt ranh giới giữa hệ điều hành và ứng dụng
- Bất chấp các giới hạn về cấu trúc, Bash, C, Python v.v. vẫn đang bao bọc và sử dụng nó theo các cách khác nhau
- Trong các hệ thống hiện đại, nhu cầu về cách quản lý cấu hình có namespace rõ ràng và hệ thống kiểu dữ liệu đầy đủ ngày càng lớn
2 bình luận
Thoạt nhìn có vẻ tầm quan trọng đã giảm bớt, nhưng với sự xuất hiện của Docker và cloud, nó lại trở thành thứ không thể tránh khỏi.
Ý kiến trên Hacker News
Tôi làm SRE/Sysadmin/DevOps/Whatever, và dù bài blog chỉ nói nhẹ nhàng về việc chuẩn hóa biến môi trường, tôi muốn chỉ ra rằng các phương án thay thế cũng gây bức bối theo cách tương tự, nhất là khi có thông tin bảo mật (secrets) liên quan
Cấu trúc để application truy cập vào một kho bí mật (vault) cụ thể như Hashicorp Vault/OpenBao/Secrets Manager rất nhanh sẽ dẫn tới vendor lock-in nghiêm trọng, và việc thay thế trở nên cực khó vì lan xuống tận cấp thư viện
Tính sẵn sàng của Vault trở nên cực kỳ quan trọng, và khi cần nâng cấp hay bảo trì thì đội vận hành sẽ rất khổ sở
Nếu truyền thông tin bí mật bằng file config thì lại khó ở chỗ phải chứa secret như thế nào, vì file config thường nằm ở đường dẫn công khai
Rốt cuộc thường phải dựa vào một trong hai cách: "hệ thống đặc quyền thay thế bằng template trước khi đưa cho app" hoặc "lưu toàn bộ file config trong kho bí mật rồi chuyển cho app"
Làm template thì rất dễ sai, còn chuyển cả file config vào kho bí mật cũng gây căng thẳng vì luôn có nguy cơ ai đó upload nhầm
Ngày nay phần lớn hệ thống chạy trên container, và nếu không phải công ty cực kỳ nghiêm túc về hạ tầng thì file config thường nằm lung tung ở những chỗ kỳ quặc, khiến quá trình mount càng rối và sai sót xảy ra thường xuyên
Dù dùng JSON/YAML/TOML hay định dạng nào thì chuyện gặp bug kỳ quặc là như cơm bữa, ví dụ như vấn đề Norway của YAML
Tôi đã thấy cách nhận secret qua Kubernetes Secrets API, nhưng cách này cũng gặp vấn đề vendor lock-in rất mạnh
Trừ khi đang thiết kế hẳn một hệ thống như operator, còn không thì tôi không chủ động khuyến khích cách này
Tôi cũng đã thấy các vấn đề nảy sinh khi thiết lập biến môi trường qua subprocess, nhưng tôi nghĩ các đội ngày nay dùng hệ thống dựa trên message bus nhiều hơn, nên vững chắc hơn và mở rộng độc lập tốt hơn
Nhóm của tôi từng làm một thư viện Secrets dùng chung, gọn nhẹ, chỉ gắn các backend theo từng vendor như AWS Secrets Manager ở phía sau theo kiểu plugin
Có hỗ trợ cache cục bộ và tùy chọn bỏ qua cache theo từng tham số, nên logic phụ thuộc vendor thực tế chỉ còn nằm ở backend, còn thư viện và application thì vẫn giữ được tính không phụ thuộc vendor
Khi chuyển sang Vault cũng chỉ cần thêm một backend và đổi cấu hình là chạy ổn, không vấn đề gì lớn
Tôi tò mò vì sao Kubernetes secret API lại bị xem là vấn đề vendor lock-in
Có phải là trường hợp muốn dùng deployment yaml cho mục đích khác ngoài triển khai Kubernetes không
Với đa số app, có thể mount secret vào container rồi inject vào app dưới dạng biến môi trường hoặc file json, như vậy app có thể đọc ghi độc lập với môi trường
Tôi hiểu là mã hóa backend etcd cũng có thể cấu hình bằng KMS
Tôi cũng không thực sự hiểu vì sao nhận secret qua Kubernetes Secrets API lại bị coi là lock-in
Về cơ bản K8s secrets không được lưu ở trạng thái mã hóa, nên theo tôi chỉ có ý nghĩa khi (0) dùng chính K8s, (1) đã cấu hình mã hóa ở control plane, và (2) bắt buộc dùng thêm giải pháp như CSI driver
Ngoài ra Secret Store CSI Driver còn hỗ trợ nhiều backend như Conjur, nên xét theo nghĩa đó thì lại là chống lock-in hơn
Vì những lý do này mà tôi vẫn chủ yếu dùng env vars và dotenv cho config
Cấu trúc cấu hình dựa trên biến môi trường quá đơn giản, lại tương thích tốt với nhiều công cụ khác nhau như secret manager
Vài năm gần đây tôi bắt đầu quan tâm dần tới sOps dựa trên YAML
YAML thực sự rất trực quan để biểu diễn cấu trúc cấu hình ứng dụng, và với sops thì cũng dễ quản lý bằng cách chỉ mã hóa một phần
Tuy nhiên quản lý khóa GPG hơi phiền, dù có thể giải quyết bằng Vault hay OpenBao
Chỉ là làm vậy thì lại phát sinh vấn đề vendor lock-in một lần nữa, dù OpenBao có vẻ đỡ hơn phần nào
Cũng có thể nhận biến môi trường từ kết quả thực thi command, nên có thể xử lý mà không cần bước template và cũng không bị vendor lock-in
Một điều thú vị nữa là setenv() về bản chất bị hỏng trong POSIX, nên tôi nghĩ tuyệt đối không nên dùng trong code thư viện
Ngay cả trong code ứng dụng, nó cũng chỉ nên là phương án cuối cùng, và bắt buộc chỉ dùng trước khi tạo thread
getenv() trả về trực tiếp con trỏ tới dữ liệu gốc của biến môi trường, nên khi setenv() ghi đè biến thì không có bất kỳ cơ chế bảo vệ nào
Cần cực kỳ cẩn thận
Tôi nghĩ cách đúng để thiết lập biến môi trường là dùng execve()
Cách này chỉ phù hợp khi truyền thông tin qua biến môi trường ở thời điểm trước/sau exec()
Tôi không hiểu vì sao code thư viện lại phải dùng setenv
Solaris đã giải quyết vấn đề này, còn Linux thì vẫn cố chấp giữ cách cũ
NetBSD từ lâu đã có một phương án thay thế an toàn là getenv_r(), và gần đây FreeBSD cũng đã đưa vào
macOS có lẽ cũng sẽ sớm theo sau
Trước đây đã từng có nỗ lực đưa nó vào glibc hoặc POSIX nhưng bị từ chối
Khi được phổ biến trên nhiều nền tảng hơn, tôi hy vọng rồi sẽ có ngày nó được chấp nhận chính thức
Tài liệu NetBSD về getenv_r
Commit của FreeBSD
Biến môi trường thường được dùng để truyền thông tin bí mật, nhưng tôi không nghĩ đó là thực hành tốt
Trên Linux, mọi process chạy dưới cùng một user đều có thể nhìn trộm biến môi trường của nhau
Dù mô hình đe dọa được đặt ra thế nào thì đây vẫn là điều đáng lo, nhất là trên máy của developer nơi có vô số process chạy dưới cùng user
Vấn đề này còn nghiêm trọng hơn khi có nhiều process chạy ngoài container, như các LLM agent
Ngoài ra biến môi trường thường bị truyền nguyên xuống cả process con, nên ngay cả khi chỉ một process cần secret thì nó vẫn có xu hướng bị lộ rộng rãi
systemd còn hiển thị biến môi trường cho mọi system client qua DBUS, và trong tài liệu chính thức cũng cảnh báo không nên đặt bí mật trong biến môi trường
Nếu điều này là thật thì có nghĩa là biến môi trường được đặt cho unit chỉ dành cho root cũng có thể bị user thường nhìn thấy, và điều đó hẳn sẽ gây sốc với nhiều quản trị viên hệ thống
Rốt cuộc tôi nghĩ chỉ có cấu trúc để secret manager chuyển secret qua cơ chế chia sẻ file tạm thời (ví dụ: op cli của 1Password, flask, terraform...) mới là lời giải giúp thoát khỏi việc lộ secret qua biến môi trường và file văn bản thuần
Hệ thống credentials của systemd là kiểu như vậy. Nhưng hiện vẫn được hỗ trợ rất ít
Nếu ai biết cách hay để truyền secret mà không dùng biến môi trường hay file văn bản thuần thì mong được chia sẻ
Tham khảo thêm, với op client của 1Password, mỗi phiên đều cần tôi phê duyệt nên khi dùng trong phiên CLI tôi thấy khá an toàn; kể cả nếu một process độc hại gọi binary op thì nó cũng cần một lần phê duyệt riêng
Giờ vấn đề còn lại là làm sao chuyển secret đó cho đúng process cần nó, và cảm giác như lại quay về điểm xuất phát
Liên kết tài liệu chính thức của systemd về biến môi trường
Từ khoảng năm 2012, biến môi trường đã an toàn tương đương bộ nhớ thông thường
Lịch sử commit liên quan
Muốn đọc biến môi trường của process khác thì bắt buộc phải có quyền ptrace, và nếu đã có thể đọc bằng ptrace thì thực ra cũng đọc được mọi secret khác rồi, nên lo ngại đó là vô nghĩa
Chuyện thông tin dòng lệnh (cmdline) thì là vấn đề khác, nhưng biến môi trường thì theo cách này không còn dễ bị lộ nữa
Trong mô hình bảo mật của đa số hệ điều hành, việc chạy dưới cùng một user về cơ bản đồng nghĩa với việc trao toàn bộ quyền của user đó
Có một số ngoại lệ như capsicum của FreeBSD, landlock của Linux, SELinux, AppArmor, hay integrity label của Windows, nhưng hầu hết đều có giới hạn khá rõ
Rốt cuộc tôi có thể tự do giết, dừng hoặc debug process của chính mình, và secret trong process tôi sở hữu thì lúc nào cũng có thể truy cập bằng ptrace/process_vm_readv/ReadProcessMemory v.v.
Cũng có những mô hình bảo mật hoàn toàn khác, như OS dựa hoàn toàn trên capability, nhưng đa số vẫn theo mô hình này, nên cần nhận thức rõ giới hạn và trách nhiệm của nó
Một cách hay để truyền secret mà không dùng biến môi trường hay file văn bản thuần là memfd_secret
Trang man của memfd_secret
Chưa có nhiều hỗ trợ theo từng ngôn ngữ hay framework, nên có lẽ đáng thử qua FFI, nhất là với Rust hoặc cả Go nếu làm được
Tôi từng cân nhắc tự bọc nó cho phía PHP, nhưng không muốn sửa cả php-fpm nên đã dừng lại
Trên thực tế, an toàn nhất có lẽ là process manager mở sẵn secret file descriptor rồi chuyển cho process con, để có thể dùng mà không lộ ra bộ nhớ hay nơi khác
Mô hình bảo mật Unix cổ điển, dù đã được cải tiến đôi chút, vẫn còn rất phổ biến, nhưng có giới hạn rõ rệt trong môi trường điện toán giá rẻ hoặc các môi trường hiện đại
Nếu cần giấu secret khỏi process khác thì cách bài bản là ngay từ đầu phải tách chúng ra chạy dưới user khác
Hoặc có thể chuyển sang truy cập từ xa, nhưng dĩ nhiên cũng đi kèm nhược điểm và độ phức tạp riêng
Gần đây trên các nền tảng container, việc truyền config hay secret bằng biến môi trường có xu hướng được khuyến nghị
Trong container, hệ thống được thiết kế để process khác không thể nhìn trộm biến môi trường
Việc biến môi trường được process con kế thừa cũng là chủ ý trong thiết kế, vì bên cấu hình môi trường là bên đang nắm secret và cũng trực tiếp thiết lập môi trường đó
Tôi không thấy phần lớn các vấn đề được lo ngại ở đây là quá nghiêm trọng, nhưng nếu cần thì sẵn sàng bàn kỹ hơn
Nhiều bình luận tập trung vào quản lý secret và những vấn đề của nó, nhưng cũng đáng dành chút thời gian nghĩ về ưu điểm của biến môi trường
Biến môi trường là một dạng "ràng buộc biến động mở rộng động với phạm vi vô hạn" dùng để liên kết có cấu trúc giữa các process Unix
Thay vì so trực tiếp với file văn bản đơn giản, có lẽ nên nhớ rằng lý do tồn tại của biến môi trường là để truyền ngữ cảnh, tức truyền thông tin an toàn cho process con
Cấu trúc process càng phức tạp, như shell lồng nhau hay các subprocess của chương trình phức tạp, thì vai trò của biến môi trường càng phát huy rõ hơn
Tôi thực sự muốn giới thiệu Varlock vì nó rất hữu ích
Nó cho phép định nghĩa rõ biến môi trường nào là bắt buộc/tùy chọn, kiểu dữ liệu ra sao, lấy từ đâu, và rất dễ quản lý
Trang chính thức của Varlock
Theo kinh nghiệm thực tế, để thấy biến môi trường có thể trở nên phức tạp đến mức nào, tôi từng hoàn toàn lạc lối khi debug xem một ENV cụ thể được set ở đâu tại công ty cũ
Ban đầu tôi tưởng nó chỉ được đặt ở .bashrc hay đâu đó đơn giản, nhưng hóa ra biến môi trường đang được thiết lập qua ít nhất 10 lớp: cấp toàn công ty, khu vực, đơn vị kinh doanh, nhóm, cá nhân, v.v.
Cuối cùng tôi phải bật cờ debug của bash mới lần mò được từng chỗ mà nó được set
Tôi không biết các ngôn ngữ khác có hỗ trợ không, nhưng Node.js gần đây đã thêm một cờ dòng lệnh để theo dõi chính xác việc truy cập và thay đổi biến môi trường
Tài liệu Node.js về --trace-env
Vì có vô số API có thể set/thay đổi giá trị nên tôi nghĩ nó sẽ cực kỳ hữu ích khi debug các ca phức tạp
Đây đúng là kiểu trường hợp khiến người ta nghĩ "chỉ cần một namespace thôi chẳng phải là đủ sao"
Tôi đã từ bỏ việc dùng biến môi trường từ lâu
Giờ tôi đặt file dmd.conf cạnh compiler và để compiler tự đọc file này
Vấn đề nghiêm trọng nhất của biến môi trường là tính ngầm định và thiếu minh bạch của nó
Trong thế giới *nix, phần lớn app đều có xu hướng dựa vào biến môi trường
Ngay cả khi có hỗ trợ thêm những cách cấu hình tường minh và minh bạch hơn như file cấu hình, dịch vụ từ xa, hay tham số dòng lệnh, thì hỗ trợ biến môi trường vẫn là truyền thống trong giới này
Suy cho cùng, biến môi trường cũng chỉ là một global hashmap, được clone và mở rộng cho process con; đó có thể là thiết kế hợp lý vào năm 1979, nhưng giờ nhiều khi lại thành chất độc
Ví dụ Kubernetes mặc định làm ô nhiễm môi trường container bằng các biến môi trường "service link"
Nếu biến môi trường mà app mong đợi va chạm với default env var thì việc debug sẽ cực kỳ khó khăn
Tham khảo tài liệu chính thức của Kubernetes
Ngoài ra tôi còn cảm thấy có rất nhiều thói quen tiếp tục duy trì vô thức các khuôn mẫu cũ như /bin, /usr/bin, /lib, /usr/lib
Tham khảo: Ubuntu Q&A về việc duy trì các thư mục legacy
hjkl trong vi bắt nguồn từ dumb terminal cách đây 40 năm, mà đó còn là loại terminal bán rất ít
(thậm chí còn ít hơn cả Nokia N9)
Mỗi lần thiết lập biến môi trường trên Linux là tôi lại thấy bất an
Cách làm được xem là chính thức lại hơi khác nhau tùy bản phân phối, và làm theo hướng dẫn trên mạng xong thì chỉ cần reboot hoặc đóng terminal là mọi thứ biến mất
Tôi ước gì có một GUI editor đơn giản để dùng biến môi trường toàn cục như trên Windows
Trên Windows cũng có phiền toái là phải mở lại terminal để thay đổi có hiệu lực, nhưng ngoài chuyện đó ra thì nó luôn hoạt động tốt
Biến môi trường đương nhiên không tồn tại qua các phiên làm việc, nên bình thường phải ghi chúng ở nơi được chạy lại mỗi phiên (đăng nhập/terminal...)
Khi đăng nhập thì .bash_profile chạy, còn ở phiên con thì .bashrc chạy
Nếu source .bashrc từ .bash_profile và để phần lớn cấu hình trong .bashrc thì sẽ dễ quản lý hơn
Nếu không dùng Bash mà dùng zsh/fish hay shell khác thì phải cấu hình theo shell đó
Linux không có một GUI chính thức và thống nhất để áp dụng cho mọi terminal
Có thể làm một GUI phải parse đủ thứ phức tạp, nhưng nhìn chung sửa bằng text editor còn dễ hơn
Từ góc nhìn của tôi, người chủ yếu dùng Linux, thì cách làm của Windows lại thấy bất tiện hơn
Quá nhiều app làm ô nhiễm biến môi trường, nên cứ khi có gì không chạy là lại phát hiện $SOFTWARE đang được chạy từ một thư mục kỳ quặc nào đó
Nếu dùng systemd thì cũng có thể ghi KEY=VALUE vào /etc/environment hoặc /etc/environment.d/
Thực ra có thể làm GUI cho cách này
Nhưng biến môi trường không thể inject vào process đang chạy, nên bắt buộc phải restart mới áp dụng được, đó là một giới hạn cố hữu
Tham khảo tài liệu chính thức của systemd
Truyện tranh Standards của xkcd
Nó mô tả rất dí dỏm rằng khi Linux đã có sẵn 14 cách cạnh tranh để thiết lập biến môi trường mà bạn lại nói "hãy thống nhất thành một", thì hôm sau sẽ có 15 tiêu chuẩn
Mẩu trivia về biến môi trường tôi thích nhất là những thứ ai cũng nghĩ hiển nhiên là biến môi trường như PS1 thực ra không phải, mà là shell variable
PS1 cũng không thể xem bằng lệnh
env