介绍
容器负责打包和运行应用,但打包本身不会选择安全的进程身份。这个预置 DeskLite 工作程序会生成一份小型工单报告,目前以 root 身份运行。
你将学习最基本的 Docker 工作流程,然后重新构建并运行工作程序,使其使用非 root 身份,且只拥有任务所需的文件访问权限。Docker 和固定基础镜像均已准备好;无需镜像仓库账号或 Docker 经验。
认识预置容器项目
在本步骤中,你将识别构成报告任务的文件。
镜像是打包好的文件系统和启动配置。容器是镜像的运行实例,具有自己的进程和可写文件系统层。Dockerfile 描述如何构建镜像。标签(如 ticket-report:practice)是镜像的便捷名称;重新构建后,它可以指向新镜像。
cd /home/labex/project/ticket-worker
ls
预期看到 Dockerfile、worker.sh、input 和 output。预置工作程序读取 CSV 行,将行数写入报告,然后保持运行,供我们检查。
cat input/tickets.csv
表头之后有三条工单记录。预期报告为 tickets=3。
cat Dockerfile
FROM 选择已缓存的基础镜像,WORKDIR 选择镜像的工作目录,COPY 添加工作程序。USER root 选择进程身份。CMD 是默认启动命令,以列表表示。你将只更改 USER。
基础标签 desklite-base:1.37.0 已从固定 BusyBox 镜像缓存。后续所有构建和运行都使用本地文件。Docker 命令使用 sudo 访问本实验的守护进程。这种宿主机层面的访问能力权限很大;它与我们正在降低的容器内应用身份权限是两回事。
构建并运行现有工作程序
在本步骤中,你将建立正常工作但权限过高的基线。
docker build 读取 Dockerfile 和构建上下文,即 COPY 可使用的文件所在目录。这里的 . 表示当前目录。-t 指定本地镜像标签:
sudo docker build -t ticket-report:practice .
预期构建成功。镜像已存在,但工作程序尚未运行。
docker run 创建容器。-d 使它在后台运行,--name 赋予它可复用名称。--network none 禁用网络,因为报告只需要文件。
绑定挂载将宿主机路径暴露到容器路径。每个 --mount 描述类型、来源(src)和目标(dst)。基线没有 readonly 选项,因此两个目录均以可写方式挂载。稍后会限制输入目录。
sudo docker run -d --name ticket-worker --network none --mount type=bind,src=/home/labex/project/ticket-worker/input,dst=/data/input --mount type=bind,src=/home/labex/project/ticket-worker/output,dst=/data/output ticket-report:practice
Docker 打印容器 ID。它不是工作程序的用户 ID。原始工作程序应生成报告,下一步我们会检查它。
检查实际运行身份
在本步骤中,你将区分成功生成报告和以最小权限运行。
docker ps 列出运行中的容器:
sudo docker ps
找到 ticket-worker。docker logs 读取应用的标准输出:
sudo docker logs ticket-worker
预期显示 worker uid=0 和 tickets=3。UID 0 表示 root。业务任务正常工作,但使用了超出需要的权限。
docker exec 在这个运行中的容器内执行检查命令。id -u 打印其有效数字用户 ID:
sudo docker exec ticket-worker id -u
预期再次得到 0。docker inspect 读取配置。--format 选择一个字段;请准确复制引号中的模板。.Config.User 是配置的用户:
sudo docker inspect --format '{{.Config.User}}' ticket-worker
预期为 root。镜像设置和真实进程检查结果一致。文件中写着“non-root”并不能提供这种证据。
使用预置非 root 用户重新构建
在本步骤中,你将设置镜像的默认应用身份。
缓存基础镜像中已包含 nobody,UID 为 65534。无需创建用户或设置密码。打开 Dockerfile:
nano Dockerfile
只更改 USER 指令。完整文件应为:
FROM desklite-base:1.37.0
WORKDIR /app
COPY worker.sh /app/worker.sh
USER nobody
CMD ["sh", "/app/worker.sh"]
使用 Ctrl+O、Enter 保存,使用 Ctrl+X 退出。
sudo docker build -t ticket-report:practice .
标签现在标识重新构建的镜像。现有容器不会自动切换到重新构建的镜像;旧 root 容器仍然是旧实例。下一步,你将替换它,并赋予新身份所需的输出访问权限。
使用受限挂载替换容器
在本步骤中,你将结合非 root 身份和可正常工作的文件权限。
docker rm 移除容器。若容器正在运行,-f 会先停止它。只移除本练习指定名称的容器:
sudo docker rm -f ticket-worker
报告目录最初由 root 拥有。与文件权限实验一样,非 root 进程需要所有权和权限才能写入输出。chown -R 递归地将目录及其已有报告的所有者 UID 和 GID 更改为 65534。模式 750 赋予所有者完整访问权限,同时不授予公共写权限:
sudo chown -R 65534:65534 output
sudo chmod 750 output
现在创建替代容器。输入挂载增加 readonly;输出挂载保持可写。这样既阻止输入被修改,也允许生成报告:
sudo docker run -d --name ticket-worker --network none --mount type=bind,src=/home/labex/project/ticket-worker/input,dst=/data/input,readonly --mount type=bind,src=/home/labex/project/ticket-worker/output,dst=/data/output ticket-report:practice
预期得到新容器 ID。镜像现在提供非 root 用户;无需在运行时覆盖为 root。
验证身份、访问权限和业务输出
在本步骤中,你将在替代容器上检查结果的全部三个方面。
sudo docker exec ticket-worker id -u
预期为 65534。检查工作程序的实际输出:
sudo docker logs ticket-worker
预期显示 worker uid=65534 和 tickets=3。通过容器读取真实输出文件:
sudo docker exec ticket-worker cat /data/output/report.txt
预期为 tickets=3。这证明降低权限没有破坏任务。
检查挂载。引号中的模板选择 .Mounts,并将其格式化为 JSON:
sudo docker inspect --format '{{json .Mounts}}' ticket-worker
找到 /data/input 对应的 "RW":false,以及 /data/output 对应的 "RW":true。RW 表示读/写。相同输入内容仍可读取,但挂载拒绝写入。
尝试无害的文件创建,测试该限制。如果允许,touch 会创建空文件:
sudo docker exec ticket-worker touch /data/input/probe.txt
预期出现 Read-only file system 错误,并以非零状态退出。这次拒绝是有意的。不要为了消除它而让输入可写;报告已经使用只读输入成功生成。
确认最终工作程序仍在运行
在本步骤中,你将让预期工作程序保持可供检查的状态。
sudo docker ps
名为 ticket-worker 的容器应处于运行状态。再次确认配置用户和报告:
sudo docker inspect --format '{{.Config.User}}' ticket-worker
sudo docker exec ticket-worker cat /data/output/report.txt
预期分别为 nobody 和 tickets=3。课程检查也会比较运行镜像与你的交付标签,检查真实工作进程 PID,并测试原始输入和输出。仅重新构建 Dockerfile 而不替换容器并不够。
本练习降低应用身份权限并限制文件访问。它不会让 Docker 守护进程访问变成低权限操作,也不构成完整的生产容器加固策略。
总结
你构建了使用非 root 应用身份的真实镜像,以受限输入和正常输出访问权限运行了该镜像,并验证了实际进程和报告。镜像配置、运行身份、挂载模式和功能行为共同构成最终状态。



