强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

08 - USER 与权限

08 - USER 与权限:非 root 运行与权限管理

8.1 为什么需要非 root 运行

默认情况下,Docker 容器以 root 用户运行。这带来严重的安全风险:

风险说明
容器逃逸如果存在内核漏洞,root 用户更容易实现容器逃逸
文件系统破坏root 可以修改容器内任何文件
权限提升攻击者获得容器 root 权限后可进一步利用
合规要求安全基线(如 CIS Docker Benchmark)要求非 root 运行

最佳实践:始终在 Dockerfile 中使用 USER 指令切换到非 root 用户。

8.2 USER 指令详解

基本语法

# 使用用户名
USER appuser

# 使用 UID:GID
USER 1001:1001

# 使用用户名:组名
USER appuser:appgroup

完整示例

FROM node:20-alpine

# 创建用户和组
RUN addgroup -g 1001 appgroup && \
    adduser -u 1001 -G appgroup -s /bin/sh -D appuser

WORKDIR /app

# 复制文件并设置权限
COPY --chown=appuser:appgroup package*.json ./
RUN npm ci --production

COPY --chown=appuser:appgroup . .

# 切换到非 root 用户
USER appuser

CMD ["node", "server.js"]

8.3 各基础镜像创建用户的方式

Alpine

FROM alpine:3.19

# -G: 指定组  -s: shell  -D: 不创建密码  -H: 不创建主目录
RUN addgroup -g 1001 appgroup && \
    adduser -u 1001 -G appgroup -s /bin/sh -D -H appuser

USER appuser

Debian/Ubuntu

FROM ubuntu:22.04

# --no-create-home: 不创建主目录  --shell: 指定 shell
RUN groupadd -g 1001 appgroup && \
    useradd -u 1001 -g appgroup --no-create-home --shell /bin/bash appuser

USER appuser

使用数字 UID

# 推荐在生产环境使用数字 UID(避免用户名查找开销)
USER 1001:1001

注意:使用数字 UID 时,文件的 chown 也应使用数字,确保一致性。

8.4 文件所有权管理

COPY –chown

# ✅ 复制时设置所有权(不产生额外层)
COPY --chown=appuser:appgroup . /app/

# ✅ 使用数字 UID/GID
COPY --chown=1001:1001 . /app/

# ❌ 先复制再 chown(产生额外层)
COPY . /app/
RUN chown -R appuser:appgroup /app/

多阶段构建中的权限处理

FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /server ./cmd/server

FROM gcr.io/distroless/static-debian12:nonroot
# nonroot 镜像自带 UID 65532 的用户
COPY --from=builder /server /server
# 直接使用 nonroot 用户
USER nonroot:nonroot
ENTRYPOINT ["/server"]

需要写入的目录

FROM node:20-alpine

RUN addgroup -g 1001 appgroup && \
    adduser -u 1001 -G appgroup -s /bin/sh -D appuser

WORKDIR /app

# 应用代码(只读)
COPY --chown=appuser:appgroup . .

# 创建需要写入的目录并设置权限
RUN mkdir -p /app/data /app/logs && \
    chown -R appuser:appgroup /app/data /app/logs

USER appuser

# 数据和日志可以通过 volume 挂载
VOLUME ["/app/data", "/app/logs"]

CMD ["node", "server.js"]

8.5 权限提升场景

临时使用 root

FROM node:20-alpine

RUN addgroup -g 1001 appgroup && \
    adduser -u 1001 -G appgroup -s /bin/sh -D appuser

WORKDIR /app
COPY . .

# 安装需要 root 权限的系统包
USER root
RUN apk add --no-cache tini

# 切回非 root 用户
USER appuser

ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.js"]

使用 gosu 降权

FROM postgres:16

# entrypoint 脚本以 root 启动,完成初始化后降权到 postgres 用户
COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["postgres"]
#!/bin/bash
# docker-entrypoint.sh 片段
set -e

# 以 root 执行初始化
if [ "$1" = 'postgres' ]; then
    # 初始化数据库...
    chown -R postgres /var/lib/postgresql/data
    
    # 使用 gosu 降权到 postgres 用户执行主进程
    exec gosu postgres "$@"
fi

exec "$@"

8.6 Init 系统与进程管理

为什么不直接用 root

# ❌ 以 root 运行 node
CMD ["node", "server.js"]

# ✅ 使用 tini + 非 root
USER appuser
ENTRYPOINT ["tini", "--"]
CMD ["node", "server.js"]

僵尸进程问题

# Python 多进程应用需要 tini 回收僵尸进程
FROM python:3.12-slim
RUN apt-get update && apt-get install -y tini && rm -rf /var/lib/apt/lists/*
RUN groupadd -r appuser && useradd -r -g appuser appuser

WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt

USER appuser
ENTRYPOINT ["tini", "--"]
CMD ["python", "app.py"]

8.7 Volume 与权限

挂载目录的权限问题

# 问题:volume 以 root 权限挂载,非 root 用户无法写入
docker run -v /host/data:/app/data myapp
# Permission denied

# 解决方案一:主机端修改目录权限
chown 1001:1001 /host/data
docker run -v /host/data:/app/data myapp

# 解决方案二:容器内使用 entrypoint 脚本修复权限
# 见下文

Entrypoint 脚本修复 Volume 权限

#!/bin/bash
set -e

# 如果以 root 启动,修复权限后降权
if [ "$(id -u)" = '0' ]; then
    chown -R appuser:appgroup /app/data
    exec gosu appuser "$@"
fi

exec "$@"
FROM node:20-alpine
RUN addgroup -g 1001 appgroup && \
    adduser -u 1001 -G appgroup -s /bin/sh -D appuser

WORKDIR /app
COPY . .
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh

VOLUME ["/app/data"]
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["node", "server.js"]

8.8 安全基线检查

检查容器运行用户

# 查看容器以什么用户运行
docker inspect --format='{{.Config.User}}' mycontainer

# 查看容器内进程的用户
docker top mycontainer

# 在运行中的容器内检查
docker exec mycontainer id
docker exec mycontainer whoami

CIS Docker Benchmark 相关规则

规则说明
4.1确保容器内以非 root 用户运行
4.2使用可信的基础镜像
4.6确保 HEALTHCHECK 指令存在
4.7不要在容器中安装不必要的软件包
5.12限制容器的内存和 CPU

8.9 业务场景

场景一:Web 应用

FROM nginx:alpine

# 创建非 root 用户
RUN addgroup -g 1001 appgroup && \
    adduser -u 1001 -G appgroup -s /bin/sh -D appuser

# 修改 Nginx 以非 root 运行
RUN sed -i 's/listen 80/listen 8080/' /etc/nginx/conf.d/default.conf && \
    sed -i 's/user  nginx/user  appuser/' /etc/nginx/nginx.conf && \
    chown -R appuser:appgroup /var/cache/nginx /var/log/nginx /etc/nginx/conf.d && \
    touch /var/run/nginx.pid && \
    chown appuser:appgroup /var/run/nginx.pid

COPY --chown=appuser:appgroup dist/ /usr/share/nginx/html/

USER appuser

EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s \
    CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1

CMD ["nginx", "-g", "daemon off;"]

场景二:数据库初始化

FROM postgres:16

COPY init.sql /docker-entrypoint-initdb.d/

# Postgres 官方镜像已处理好权限
# postgres 用户在 entrypoint 中创建
# 数据目录由 entrypoint 脚本管理

8.10 常见错误与排查

错误原因解决方案
Permission denied非 root 用户无权访问文件使用 --chownchown
bind: permission denied非 root 无法绑定 < 1024 端口使用 1024 以上端口
Volume 写入失败挂载目录权限不匹配修复主机目录权限或使用 entrypoint
用户不存在未在基础镜像中创建先创建用户再 USER 切换
PID 文件创建失败非 root 无法写入运行目录修改目录权限或使用 tmpfs

8.11 扩展阅读


上一章07 - EXPOSE 与端口 下一章09 - 多阶段构建 — 分阶段编排、构建缓存与最小化镜像。