Pod chạy bằng user nginx (không phải root), mount một thư mục NFS, và mọi thư mục con đều thuộc root. Lệnh chown trong postStart không đổi được gì, cache báo permission denied. Một giả thuyết dễ nghĩ tới là "máy NFS không có user nginx nên lấy mặc định là root", nhưng giả thuyết đó sai. Bài này giải thích cơ chế thật: NFS chỉ so sánh con số UID/GID, root_squash biến root của client thành một user khác, và anonuid quyết định user khác đó là ai. Hiểu ba điều này, bạn đặt owner cho thư mục mount chính xác ngay từ lần đầu.

Bài trước, Permission denied trên volume: vì sao chown trong Dockerfile không đủ, đã nói về các cách đặt owner chung. Bài này đào sâu riêng trường hợp NFS.

Triệu chứng: thư mục mount luôn thuộc root

Image đặt user mặc định là nginx. Deployment có hook postStart chạy chown -R nginx:nginx storage. Nhưng trên máy NFS, nhìn vào thư mục của site:

ls -ln /data/site-a/
drwxr-xr-x 8 1001 1001 4096 .
drwxr-xr-x 2    0    0 4096 cache
drwxr-xr-x 2    0    0 4096 public
drwxr-xr-x 2    0    0 4096 sessions

Ứng dụng ghi cache vào cache và nhận lỗi Permission denied. Hai chuyện cần tách riêng: lỗi ghi vào thư mục (quyền) khác với lỗi No such file or directory (thư mục chưa được tạo). Đừng gộp hai loại này khi chẩn đoán.

Giả thuyết "NFS không có user nginx" là sai

Với NFS dùng xác thực mặc định AUTH_SYS (còn gọi AUTH_UNIX), client gửi kèm mỗi yêu cầu một cặp số UID và GID. Credential AUTH_SYS còn kèm cả danh sách nhóm phụ. Máy chủ quyết định quyền truy cập dựa trên số UID/GID trong credential của client, không dựa trên tên user. Với NFSv4, tên có thể được ánh xạ qua idmap để hiển thị, nhưng việc kiểm quyền vẫn dùng số. Vì vậy máy NFS có tạo user tên nginx hay không không quan trọng với quyền truy cập; chỉ khi bạn muốn ls in ra tên thay vì số thì mới cần user tương ứng.

Vì vậy, khi chẩn đoán, luôn dùng dạng số ở cả hai phía:

# trong pod: UID của worker php-fpm (id chỉ cho biết user của phiên exec)
ps -o user,pid,comm
# trên máy NFS
ls -ln /data/site-a/
exportfs -v

Nếu worker chạy UID 1001 mà thư mục trên NFS thuộc UID 0, bạn đã thấy ngay vấn đề. Tham khảo trang manual exports(5) cho các tuỳ chọn dưới đây.

Cách NFS xử lý root: root_squash, no_root_squash, anonuid

Tuỳ chọn root_squash tồn tại vì root của client có quyền tuyệt đối trên máy client, nhưng máy chủ NFS không muốn tin tưởng vô điều kiện. Có ba tuỳ chọn trong /etc/exports:

Tuỳ chọnKhi client gửi UID 0Hệ quả
root_squash (mặc định)Biến thành user ẩn danh (anonuid, mặc định 65534, tức nobody)Root của client không có quyền root trên share, đây là lý do root_squash an toàn hơn
no_root_squashGiữ nguyên UID 0Root của client là root thật trên share. File root tạo ra thuộc root
anonuid=N, anongid=NĐổi user ẩn danh thành UID/GID NMọi thứ bị squash sẽ mang số N thay vì 65534

Điều này giải thích hiện tượng ban đầu, và vì sao root_squash lại quan trọng: thư mục con do một tiến trình chạy bằng root trong pod tạo ra (ví dụ initContainer chép dữ liệu; tiến trình cụ thể chưa được xác nhận). Nếu share dùng no_root_squash thay vì root_squash, root đó vẫn là UID 0 trên máy NFS, nên thư mục thuộc root. Pod chạy nginx sau đó không ghi được, và vì cũng không phải root, chown trong pod không thể giành lại quyền.

Chuỗi thử nghiệm và kết quả

Các tổ hợp dưới đây giúp bạn thấy cơ chế hoạt động. Image đặt nginx là UID/GID 1001 (đổi bằng usermod -u 1001 nginx và groupmod -g 1001 nginx, cần gói shadow trên Alpine), và máy NFS có thư mục gốc thuộc UID 1001:

  1. UID 1001 hai phía, có no_root_squash và anonuid=1001: thư mục con vẫn thuộc root. Vì root không bị squash nên anonuid không được dùng.
  2. Bỏ no_root_squash (tức dùng root_squash mặc định), giữ anonuid=1001,anongid=1001: triển khai thành công. Root bị squash thành 1001, thư mục con được tạo với owner 1001, đúng owner của pod.
  3. Bỏ cả no_root_squash lẫn anonuid: không tạo được thư mục mount. Với root_squash, root bị squash thành 65534, và user này không có quyền ghi vào thư mục gốc thuộc 1001 với quyền 755.
  4. Đổi UID một phía (ví dụ UID của user trong image khác UID owner thư mục trên NFS và khác anonuid): pod không ghi được. Kết quả "đổi một phía thì hỏng" ban đầu chưa tách được khỏi cấu hình exports còn no_root_squash. Cũng lưu ý usermod -u không chown lại dữ liệu cũ, nên cần chown lại dữ liệu hiện có.

Các kết quả trên là quan sát từ một lần thử, chưa tái lập có kiểm soát, và được giải thích bằng cơ chế root_squash chuẩn của NFS, tức là suy luận kỹ thuật. Hãy đối chiếu dòng exports bằng exportfs -v để chắc rằng no_root_squash thật sự đã bị bỏ trước khi kết luận, và dùng ls -ln để xác nhận owner.

Cấu hình root_squash cuối và pipeline tạo thư mục

Dòng exports phù hợp, với dải mạng ví dụ, ghi rõ root_squash (không có no_root_squash) và có anonuid, anongid:

/data 10.0.0.0/16(rw,sync,no_subtree_check,root_squash,nohide,anonuid=1001,anongid=1001)

Cảnh báo bảo mật: với anonuid=1001 trên cả export, root của mọi client trong dải mạng hoạt động như UID 1001 trên toàn bộ dữ liệu, và mọi site dùng chung một UID thì không tách được quyền giữa các site. Hãy thu hẹp dải mạng được export.

Tạo user và nạp lại cấu hình:

groupadd -g 1001 nginx
useradd -u 1001 -g 1001 -s /sbin/nologin -M nginx
mkdir -p /data
chown -R nginx:nginx /data
exportfs -ra

Lệnh nạp lại là exportfs -ra, không cần restart dịch vụ NFS. Việc tạo user nginx trên máy NFS chỉ để hiện tên cho dễ đọc, UID 1001 mới là phần quyết định.

Khi pipeline tạo thư mục mount cho từng site với root_squash đang bật, thêm bước chown với đúng UID/GID (chown 1001:1001 /data/site-a) để pod có quyền ngay lần đầu mount. Phải nhớ: với root_squash, chown chạy từ phía client (qua mount) không có quyền root nên thường thất bại (EPERM). chown phải chạy ở nơi có quyền root thật trên máy NFS. Cách dựng NFS và pipeline này có trong bài Dựng hạ tầng bằng Linode API: cluster, NFS và MySQL qua pipeline.

Chú ý cấu hình cũ: các hướng dẫn dựng NFS tự quản trước đây thường dùng no_root_squash cho đơn giản. Cấu hình ở trên là bản cập nhật cho trường hợp pod không chạy root.

Bỏ chown và chmod đệ quy khỏi postStart

Khi owner đã đúng từ nguồn, hook postStart không còn cần chown -R hay chmod -R. Bỏ chúng đi không chỉ sạch hơn mà còn tránh thời gian khởi động dài trên thư mục lớn, như phân tích ở bài trước. Cấu hình Helm thống nhất cho mọi site nằm ở Chart dùng chung: tái sử dụng Helm cho Laravel, MySQL và Redis.

Khi bỏ quyền root: những cảnh báo thường gặp

Image all-in-one chạy nhiều tiến trình thường kèm các cảnh báo khi không còn là root. Đây là những dòng thật xuất hiện trong log:

  • nginx: the "user" directive makes sense only if the master process runs with super-user privileges, ignored. Có thể xoá chỉ thị user khỏi cấu hình.
  • php-fpm: 'user' directive is ignored when FPM is not running as root, tương tự với group.
  • crond: can't set groups: Operation not permitted, do cron không còn quyền đổi nhóm.

Các cảnh báo này (không liên quan đến root_squash, mà do tiến trình không còn là root) cho biết chỉ thị bị bỏ qua, chưa chắc là lỗi. Quyền thật sự cần cho từng tiến trình (nginx, php-fpm, crond, supervisord) chưa được lập thành bảng chính xác, nên chúng tôi không khẳng định gì thêm. Cách an toàn là bỏ root từng bước và đọc log mỗi bước.

Điều chưa chốt

  • Lỗi permission denied của cache và lỗi No such file or directory là hai hiện tượng khác nhau. Chưa có kết luận cuối cho từng trường hợp, đừng suy ra chúng cùng một nguyên nhân.
  • Tiến trình nào tạo các thư mục ban đầu bằng root (initContainer, hook hay pipeline) chưa được xác nhận trực tiếp.
  • Hành vi của root_squash phụ thuộc phiên bản và cấu hình máy chủ NFS cụ thể, hãy xác nhận bằng exportfs -v.
  • fsGroup có áp dụng cho NFS hay không phụ thuộc loại volume (xem tài liệu security context của Kubernetes). Đừng dựa vào nó, hãy đặt owner ở nguồn như bài này.

Checklist chẩn đoán lỗi quyền NFS và root_squash

  • So sánh số: UID của worker php-fpm (ps -o user,pid,comm) với ls -ln trên máy NFS.
  • Kiểm exportfs -v để biết share đang dùng root_squash hay no_root_squash, và anonuid là bao nhiêu.
  • Cố định UID/GID của user ứng dụng ở image và ở máy NFS, không đổi một phía.
  • Giữ root_squash cùng anonuid, anongid khớp UID của pod.
  • Đặt chown với UID số ở nơi chạy trên máy NFS, đừng dựa vào client.
  • Đổi UID của user trong image không tự đổi owner file cũ. Cần chown lại dữ liệu hiện có.
  • Nạp lại exports bằng exportfs -ra.

Về nền tảng PV, PVC cho NFS, xem Volume, PersistentVolume, PVC và StorageClass trên Kubernetes.

Bài tiếp theo

Elasticsearch trên Kubernetes: lỗi mount, quyền uid 1000 và phân quyền theo index. Cùng họ lỗi quyền volume nhưng ở một ứng dụng stateful chạy bằng UID cố định.