Roadmap Docker: Người Dùng, Nhóm và Quyền Hạn trong Linux cho Người Mới Bắt Đầu với Docker

Chào mừng các bạn quay trở lại với series “Roadmap Docker”! Sau khi tìm hiểu Container là gì, phân biệt Container với VM/Bare Metal, khám phá cấu trúc Docker và OCI, làm quen với những kỹ năng Linux cơ bản cùng Package Managers, hôm nay chúng ta sẽ lặn sâu hơn vào một chủ đề cực kỳ quan trọng mà mọi kỹ sư DevOps và nhà phát triển làm việc với Docker cần nắm vững: **Người dùng, Nhóm và Quyền hạn trong Linux**.

Tại sao lại cần hiểu điều này khi dùng Docker? Đơn giản là vì Docker chạy trên nền Linux (hoặc sử dụng kernel Linux trên các hệ điều hành khác). Mọi thứ bên trong container của bạn, từ file cấu hình, mã nguồn ứng dụng cho đến dữ liệu được lưu trữ, đều tuân theo hệ thống quyền hạn của Linux. Việc hiểu rõ cách quản lý người dùng, nhóm và quyền hạn sẽ giúp bạn tránh được vô số vấn đề đau đầu liên quan đến truy cập file, bảo mật và hoạt động ổn định của ứng dụng trong container.

Hiểu Về Người Dùng (Users) và Nhóm (Groups) trong Linux

Hệ điều hành Linux được thiết kế để hỗ trợ môi trường đa người dùng. Điều này có nghĩa là nhiều người có thể đăng nhập và sử dụng cùng một hệ thống cùng lúc, mỗi người có không gian làm việc và quyền hạn riêng biệt. Để quản lý việc này, Linux sử dụng khái niệm Người dùng và Nhóm.

Người dùng (Users)

Mỗi người dùng trên hệ thống Linux được gán một ID duy nhất gọi là UID (User ID). Có hai loại người dùng chính:

  • Người dùng Root (Root User): Đây là “siêu người dùng” với UID là 0. Người dùng root có toàn quyền truy cập và thực hiện mọi hành động trên hệ thống, bao gồm cả việc thay đổi cấu hình hệ thống, cài đặt phần mềm, và truy cập/sửa đổi bất kỳ file nào. Quyền lực đi kèm với trách nhiệm, và việc sử dụng tài khoản root một cách bất cẩn có thể gây ra hậu quả nghiêm trọng.
  • Người dùng Thông thường (Regular Users): Đây là những tài khoản người dùng bình thường mà bạn tạo ra để sử dụng hàng ngày. Mỗi người dùng thông thường có UID lớn hơn 0. Quyền hạn của họ bị giới hạn bởi hệ thống file permissions và các cấu hình bảo mật khác. Để thực hiện các tác vụ yêu cầu quyền root, người dùng thông thường thường phải sử dụng lệnh sudo (Superuser Do).

Bạn có thể xem thông tin về người dùng hiện tại bằng lệnh whoami hoặc xem chi tiết UID, GID và các nhóm bằng lệnh id:

$ whoami
your_username

$ id
uid=1000(your_username) gid=1000(your_username) groups=1000(your_username),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),126(sambashare)

Nhóm (Groups)

Nhóm là tập hợp của nhiều người dùng. Mỗi nhóm được gán một ID duy nhất gọi là GID (Group ID). Mục đích của nhóm là đơn giản hóa việc quản lý quyền hạn. Thay vì gán quyền cho từng người dùng riêng lẻ, bạn có thể gán quyền cho một nhóm, và tất cả người dùng là thành viên của nhóm đó sẽ kế thừa quyền hạn đó.

  • Mỗi người dùng thuộc về ít nhất một nhóm, gọi là nhóm chính (primary group). Thường thì khi bạn tạo một người dùng mới, một nhóm có cùng tên và GID được tạo ra làm nhóm chính cho người dùng đó.
  • Một người dùng cũng có thể là thành viên của nhiều nhóm phụ (secondary groups).

Bạn có thể xem các nhóm mà người dùng hiện tại là thành viên bằng lệnh groups:

$ groups
your_username adm cdrom sudo dip plugdev lpadmin sambashare

Việc hiểu người dùng và nhóm là nền tảng để chúng ta đi sâu vào phần tiếp theo: Quyền hạn file.

Quyền Hạn File (File Permissions) trong Linux

Hệ thống quyền hạn file trong Linux là cách chính để kiểm soát ai có thể truy cập và làm gì với file và thư mục. Khi bạn xem danh sách file bằng lệnh ls -l, bạn sẽ thấy thông tin chi tiết về quyền hạn ở cột đầu tiên:

$ ls -l
-rw-r--r-- 1 your_username your_username 1234 Oct 26 10:00 my_document.txt
drwxr-xr-x 2 your_username your_username 4096 Oct 25 15:30 my_directory

Chuỗi ký tự ở cột đầu tiên (ví dụ: -rw-r--r-- hoặc drwxr-xr-x) chính là biểu diễn của quyền hạn.

Giải mã Chuỗi Quyền hạn

Chuỗi này gồm 10 ký tự và được chia thành 4 phần:

  1. **Loại file (1 ký tự):**
    • -: File thông thường
    • d: Thư mục (Directory)
    • l: Liên kết tượng trưng (Symbolic Link)
    • Các ký tự khác ít phổ biến hơn (ví dụ: c cho thiết bị ký tự, b cho thiết bị khối).
  2. **Quyền của Chủ sở hữu (Owner Permissions) (3 ký tự):**
    • r: Quyền đọc (Read). Cho phép xem nội dung file hoặc liệt kê nội dung thư mục.
    • w: Quyền ghi (Write). Cho phép sửa đổi, xóa file hoặc tạo/xóa file trong thư mục.
    • x: Quyền thực thi (Execute). Cho phép chạy file (nếu đó là chương trình) hoặc truy cập (đi vào) thư mục.
  3. **Quyền của Nhóm (Group Permissions) (3 ký tự):** Quyền tương tự (rwx) nhưng áp dụng cho tất cả thành viên của nhóm sở hữu file/thư mục.
  4. **Quyền của Người khác (Others Permissions) (3 ký tự):** Quyền tương tự (rwx) nhưng áp dụng cho *tất cả những người dùng khác* không phải là chủ sở hữu và không thuộc nhóm sở hữu.

Nếu một quyền không được cấp, vị trí của nó sẽ là dấu -.

Ví dụ:

  • -rw-r--r--: File thông thường. Chủ sở hữu có quyền đọc và ghi (rw-). Nhóm có quyền đọc (r–). Những người khác có quyền đọc (r–).
  • drwxr-xr-x: Thư mục. Chủ sở hữu có quyền đọc, ghi, thực thi (rwx). Nhóm có quyền đọc, thực thi (r-x). Những người khác có quyền đọc, thực thi (r-x).

Đối với thư mục:

  • r (đọc): Cho phép liệt kê nội dung thư mục (xem tên file/thư mục con).
  • w (ghi): Cho phép tạo, xóa, đổi tên file/thư mục *bên trong* thư mục này.
  • x (thực thi): Cho phép đi vào thư mục (sử dụng lệnh cd) và truy cập các file bên trong nó. Quyền x là bắt buộc để có thể làm việc với nội dung của thư mục, ngay cả khi bạn có quyền r.

Ngoài biểu diễn ký tự, quyền hạn còn có thể được biểu diễn bằng số (Octal notation), trong đó:

  • r = 4
  • w = 2
  • x = 1
  • – = 0

Tổng các giá trị này cho mỗi bộ (owner, group, others) tạo thành một số có 3 chữ số. Ví dụ:

  • rwx = 4 + 2 + 1 = 7
  • rw- = 4 + 2 + 0 = 6
  • r-x = 4 + 0 + 1 = 5
  • r-- = 4 + 0 + 0 = 4
  • --- = 0 + 0 + 0 = 0

Vậy, -rw-r--r-- tương đương với 644. drwxr-xr-x tương đương với 755.

Dưới đây là bảng tóm tắt các quyền cơ bản:

Ký hiệu Giá trị Octal Mô tả Áp dụng cho File Áp dụng cho Thư mục
r 4 Đọc (Read) Xem nội dung file Liệt kê nội dung thư mục
w 2 Ghi (Write) Sửa đổi, xóa file Tạo/xóa file/thư mục con
x 1 Thực thi (Execute) Chạy file (nếu là chương trình) Truy cập/Đi vào thư mục
- 0 Không có quyền

Thay đổi Quyền Hạn và Chủ Sở Hữu: chmod, chown, chgrp

Để quản lý quyền hạn và chủ sở hữu file/thư mục, chúng ta sử dụng các lệnh sau:

chmod (Change Mode)

Lệnh này dùng để thay đổi quyền hạn của file hoặc thư mục. Bạn có thể sử dụng cú pháp ký hiệu hoặc số.

Cú pháp ký hiệu: chmod [aiuog][+-=][rwx] file/directory

  • [aiuog]: Ai bị ảnh hưởng?
    • a: all (mọi người)
    • u: user (chủ sở hữu)
    • g: group (nhóm)
    • o: others (người khác)
  • [+-=]: Hành động?
    • +: Thêm quyền
    • -: Bớt quyền
    • =: Đặt chính xác quyền (ghi đè)
  • [rwx]: Quyền nào? (read, write, execute)

Ví dụ:

# Thêm quyền ghi cho nhóm và người khác trên file
$ chmod go+w my_document.txt

# Bỏ quyền thực thi cho tất cả mọi người trên file
$ chmod a-x my_script.sh

# Đặt chính xác quyền đọc và ghi cho chủ sở hữu, chỉ đọc cho nhóm và người khác trên file
$ chmod ug+rw,o+r,o-wx my_document.txt # Hoặc gọn hơn: chmod ug=rw,o=r my_document.txt

Cú pháp số (Octal): chmod [số 3 chữ số] file/directory

Ví dụ:

# Đặt quyền 644 (rw-r--r--) cho file
$ chmod 644 my_document.txt

# Đặt quyền 755 (rwxr-xr-x) cho thư mục
$ chmod 755 my_directory

# Đặt quyền 700 (rwx------) cho một file cấu hình nhạy cảm (chỉ chủ sở hữu có full quyền)
$ chmod 700 private_config.yaml

Sử dụng tùy chọn -R để thay đổi quyền đệ quy (áp dụng cho cả nội dung bên trong thư mục).

# Thay đổi quyền đệ quy cho thư mục và tất cả nội dung bên trong
$ chmod -R 755 my_directory

chown (Change Owner)

Lệnh này dùng để thay đổi chủ sở hữu (user) và/hoặc nhóm sở hữu (group) của file hoặc thư mục.

Cú pháp: chown [user][:group] file/directory

Ví dụ:

# Thay đổi chủ sở hữu file thành user 'newuser'
$ chown newuser my_document.txt

# Thay đổi nhóm sở hữu file thành group 'newgroup'
$ chown :newgroup my_document.txt # Hoặc chown .newgroup

# Thay đổi cả chủ sở hữu và nhóm sở hữu file
$ chown newuser:newgroup my_document.txt

# Thay đổi chủ sở hữu thư mục đệ quy
$ chown -R newuser my_directory

Lưu ý: Chỉ người dùng root hoặc chủ sở hữu hiện tại (để thay đổi nhóm) mới có thể chạy lệnh chown. Để thay đổi chủ sở hữu sang người dùng khác, bạn cần quyền root (sử dụng sudo).

chgrp (Change Group)

Lệnh này chỉ dùng để thay đổi nhóm sở hữu của file hoặc thư mục.

Cú pháp: chgrp [group] file/directory

Ví dụ:

# Thay đổi nhóm sở hữu file thành group 'webteam'
$ chgrp webteam index.html

# Thay đổi nhóm sở hữu thư mục đệ quy
$ chgrp -R webteam website_files

Lưu ý: Bạn phải là người dùng root hoặc là thành viên của nhóm đích để có thể thay đổi nhóm sở hữu file.

Quyền Hạn trong Thế Giới Docker

Bây giờ, làm thế nào những khái niệm Linux này áp dụng vào Docker? Đây là nơi mọi thứ trở nên thú vị và đôi khi gây nhầm lẫn cho người mới.

Người dùng Mặc định trong Container: Root

Theo mặc định, hầu hết các image Docker chạy quy trình chính (ENTRYPOINT hoặc CMD) với tư cách là người dùng **root** bên trong container. Điều này rất quan trọng cần lưu ý!

Ưu điểm (cho người mới): Đơn giản. Bên trong container, bạn có toàn quyền làm mọi thứ, cài đặt phần mềm, truy cập/ghi file ở bất cứ đâu mà không gặp rào cản về quyền.

Nhược điểm (quan trọng): Rủi ro bảo mật lớn. Nếu ứng dụng trong container bị tấn công và khai thác lỗ hổng, kẻ tấn công sẽ có quyền root *bên trong container*. Điều này tiềm ẩn nguy cơ leo thang đặc quyền ra khỏi container và ảnh hưởng đến hệ thống host, đặc biệt nếu bạn sử dụng các tính năng kém an toàn như bind mounts.

Thực hành tốt nhất: Tránh chạy ứng dụng trong container với tư cách là người dùng root bất cứ khi nào có thể.

Chạy Container với Người dùng Không phải Root (Non-Root User)

Cách phổ biến và được khuyến khích để giảm thiểu rủi ro bảo mật là định cấu hình container chạy với một người dùng cụ thể, không phải root.

Bạn có thể thực hiện điều này bằng cách:

1. Sử dụng lệnh USER trong Dockerfile:

Trong Dockerfile, sau khi cài đặt các phụ thuộc yêu cầu quyền root, bạn chuyển sang một người dùng khác.

# Dockerfile
FROM ubuntu:latest

# Cài đặt ứng dụng (yêu cầu quyền root)
RUN apt-get update && apt-get install -y some-package

# Tạo một người dùng và nhóm mới
RUN groupadd -r appgroup && useradd -r -g appgroup appuser

# Đổi chủ sở hữu thư mục ứng dụng nếu cần
RUN chown -R appuser:appgroup /app

# Chuyển sang người dùng không phải root
USER appuser

# Sao chép mã nguồn
COPY --chown=appuser:appgroup . /app

# Đặt thư mục làm việc
WORKDIR /app

# Lệnh chạy ứng dụng
CMD ["./start_app.sh"]

Lệnh USER appuser sẽ đảm bảo rằng các lệnh sau đó (COPY, WORKDIR, CMD) và quy trình chính của container sẽ chạy dưới tài khoản appuser.

Lưu ý: Người dùng và nhóm này (appuser, appgroup) chỉ tồn tại *bên trong* container. UID và GID của họ (thường là các số lớn, ví dụ 1000) có thể trùng lặp với UID/GID trên hệ thống host, gây ra sự nhầm lẫn về quyền khi sử dụng bind mounts.

2. Sử dụng cờ --user khi chạy container:

Bạn có thể ghi đè người dùng mặc định được chỉ định trong Dockerfile (hoặc mặc định là root) bằng cách sử dụng cờ --user khi chạy lệnh docker run.

# Chạy container với user có UID 1000
$ docker run -d --user 1000 my_image

# Chạy container với user 'appuser' (nếu user này tồn tại trong image)
$ docker run -d --user appuser my_image

# Chạy container với user 'appuser' và nhóm 'appgroup' (nếu user/group này tồn tại trong image)
$ docker run -d --user appuser:appgroup my_image

Quyền Hạn và Volume Mounts (Bind Mounts)

Đây là một trong những nguồn gốc phổ biến nhất của các vấn đề về quyền khi sử dụng Docker, đặc biệt là với bind mounts. Khi bạn mount một thư mục từ host vào container (ví dụ: -v /host/path:/container/path), Docker không sao chép dữ liệu; nó chiếu (project) thư mục từ host vào không gian file system của container.

Điều này có nghĩa là **quyền hạn và chủ sở hữu của các file/thư mục trong /host/path vẫn là của hệ thống host**. Container sẽ nhìn thấy những file này với quyền hạn và chủ sở hữu đó.

Vấn đề phát sinh:

Giả sử bạn có một thư mục /app/data trên host, thuộc sở hữu của người dùng host của bạn (ví dụ: UID 1000). Bạn mount nó vào /data trong container:

$ docker run -v /app/data:/data my_image

Nếu container của bạn chạy với tư cách root (UID 0), và ứng dụng cố gắng ghi vào /data, việc ghi sẽ thành công *bên trong container*. Nhưng file được tạo ra trên host tại /app/data sẽ thuộc sở hữu của root (UID 0). Người dùng host của bạn (UID 1000) có thể không có quyền ghi vào file đó nữa.

Ngược lại, nếu container của bạn chạy với một người dùng không phải root (ví dụ: appuser với UID 1000 bên trong container), và thư mục /host/path trên host thuộc sở hữu của root (UID 0), thì appuser bên trong container (UID 1000) sẽ không có quyền ghi vào thư mục được mount, dẫn đến lỗi “Permission denied”.

Giải pháp:

  1. **Đảm bảo UID/GID của người dùng trong container khớp với người dùng trên host:** Đây là giải pháp thường thấy nhất.
    • Xác định UID và GID của người dùng host sẽ tương tác với dữ liệu được mount. Ví dụ: dùng lệnh id trên host.
    • Trong Dockerfile, tạo một người dùng và nhóm mới với UID và GID **tường minh** khớp với host.
    • # Dockerfile
                  ARG USER_UID=1000 # Mặc định nếu không truyền qua build-arg
                  ARG GROUP_GID=1000 # Mặc định nếu không truyền qua build-arg
      
                  FROM ubuntu:latest
      
                  # Tạo nhóm và người dùng với GID/UID cụ thể
                  RUN groupadd -g ${GROUP_GID} appgroup && useradd -u ${USER_UID} -g appgroup appuser
      
                  # Đổi chủ sở hữu thư mục dữ liệu trong image
                  RUN mkdir /app/data && chown appuser:appgroup /app/data
      
                  # Chuyển sang người dùng này
                  USER appuser
                  # ... các lệnh khác ...
                  
    • Khi build image, bạn có thể truyền UID/GID của host:
      $ docker build --build-arg USER_UID=$(id -u) --build-arg GROUP_GID=$(id -g) -t my_app_image .
                  
    • Khi chạy container, người dùng trong container sẽ có UID/GID khớp với host, giải quyết vấn đề quyền hạn trên volume được mount.
  2. **Sử dụng entrypoint script để sửa quyền:** Bạn có thể viết một script nhỏ chạy khi container khởi động (làm ENTRYPOINT) để kiểm tra và sửa quyền của thư mục được mount bên trong container sao cho người dùng đang chạy container có thể truy cập được.
    # entrypoint.sh
            #!/bin/sh
            # Đảm bảo thư mục /data thuộc sở hữu của người dùng hiện tại (ví dụ: appuser)
            chown appuser:appgroup /data
    
            # Thực thi lệnh chính của container
            exec "$@"
            

    Trong Dockerfile:

    # Dockerfile
            # ... tạo user appuser ...
            USER appuser
            COPY entrypoint.sh /usr/local/bin/
            RUN chmod +x /usr/local/bin/entrypoint.sh
            ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
            CMD ["./start_app.sh"]
            

    Cách này linh hoạt hơn vì nó sửa quyền *sau khi mount*, nhưng cần cẩn thận để không gây ra vấn đề quyền trên host nếu container chạy với quyền đủ cao.

  3. **Sử dụng Named Volumes:** Named volumes (docker volume create mydata và mount bằng -v mydata:/data) do Docker quản lý và thường có quyền mặc định linh hoạt hơn hoặc bạn có thể cấu hình driver volume để xử lý quyền. Tuy nhiên, đối với các trường hợp cần truy cập dữ liệu từ cả host và container (như mount file cấu hình, mã nguồn khi phát triển), bind mounts vẫn phổ biến hơn.

Quyền Hạn Bên trong Container Image

Ngay cả các file được sao chép vào image bằng lệnh COPY hoặc ADD cũng có quyền hạn. Mặc định, Docker sẽ cố gắng giữ lại quyền hạn của file gốc trên host.

Bạn có thể thay đổi hành vi này bằng cờ --chown trong lệnh COPY hoặc ADD để chỉ định chủ sở hữu và nhóm của file/thư mục sau khi sao chép vào image:

# Dockerfile
# ...
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
# ...
COPY --chown=appuser:appgroup ./app /app
# ...

Điều này giúp đảm bảo rằng các file ứng dụng thuộc về người dùng không phải root mà bạn sẽ sử dụng để chạy container, tránh các vấn đề quyền truy cập file ứng dụng ngay từ đầu.

Thực Hành Tốt Nhất Tổng Kết

  1. **Hiểu rõ ai là người dùng chạy container:** Luôn kiểm tra xem container của bạn đang chạy với người dùng nào (mặc định là root nếu không chỉ định).
  2. **Ưu tiên chạy container với người dùng không phải root:** Sử dụng lệnh USER trong Dockerfile hoặc cờ --user khi docker run để tăng cường bảo mật.
  3. **Cẩn trọng với Bind Mounts và Quyền hạn:** Đây là điểm nóng gây ra lỗi “Permission denied”.
    • Đảm bảo UID/GID của người dùng trong container khớp với người dùng trên host khi làm việc với bind mounts.
    • Hoặc sử dụng entrypoint script để điều chỉnh quyền *bên trong* container sau khi mount.
  4. **Đặt quyền phù hợp cho file trong image:** Sử dụng cờ --chown trong COPY/ADD hoặc lệnh RUN chown/chmod trong Dockerfile để đảm bảo file ứng dụng có quyền cần thiết cho người dùng chạy container.
  5. **Tuyệt đối tránh mount các thư mục nhạy cảm từ host vào container với quyền ghi cho root:** Ví dụ: mount / hoặc /etc từ host vào container và chạy container như root là cực kỳ nguy hiểm.

Lời Kết

Việc nắm vững cách thức hoạt động của người dùng, nhóm và quyền hạn trong Linux là một kỹ năng không thể thiếu khi làm việc với Docker. Nó không chỉ giúp bạn giải quyết các lỗi “Permission denied” khó chịu mà còn là yếu tố cốt lõi để xây dựng các ứng dụng container an toàn và đáng tin cậy hơn.

Trong bài viết này, chúng ta đã đi qua những khái niệm cơ bản trong Linux và cách chúng tác động đến môi trường Docker, đặc biệt là khi sử dụng bind mounts. Đây là những kiến thức nền tảng quan trọng trên hành trình làm chủ Docker của bạn. Hãy thực hành thường xuyên để quen thuộc với các lệnh ls -l, chmod, chown và cách áp dụng chúng trong Dockerfile!

Ở bài viết tiếp theo trong series “Roadmap Docker”, chúng ta sẽ cùng nhau khám phá một chủ đề không kém phần quan trọng: Mạng (Networking) trong Docker. Làm thế nào các container giao tiếp với nhau và với thế giới bên ngoài? Đừng bỏ lỡ nhé!

Chỉ mục