分析
起因是大学Python课,不想每次课都带笔记本过去太重了,尝试能不能一个平板就搞定。找来找去发现coder/code-server: VS Code in the browser很符合要求,同时还能兼顾跨平台。当时自己部署了一个玩玩,后来朋友他们也想要,就在思考怎么弄能够兼顾多用户使用和安全性。最开始考虑过容器化,后来想先用 Linux 用户和 venv 管理各自的文件与 Python 包,就选择了二进制部署。这里没有做容器和直接部署的性能对比。
需求很简单,大致概括如下:
- 每个用户可以用自己的域名登录进入CodeServer
- 每个用户可以自己修改登录密码
- 每个用户有独立的 Python 包环境,可自行安装 pip 包
- 用户间不应该能互相访问到对方的文件
- 防止用户误操作删掉系统关键组件
- 防止用户误操作写出死循环之类的耗尽服务器资源
- 防止用户利用服务器挖矿/做跳板攻击别人
经过分析和与ai的讨论,最终定下来的方案大致是这样:
- 利用 Linux 多用户机制,每个用户在自己的家目录下运行一个 CodeServer,通过文件所有权和权限限制互相访问。家目录应设置为仅本人可访问(如
0700),仅仅创建不同用户并不会自动隐藏所有文件;域名接入还需要另外配置反向代理。 - 监听端口根据uid计算。每个用户下手动创建一个venv,并通过.bashrc让用户启动shell的时候自动启用,解决需求3;
- 利用 systemd 限制资源占用和部分进程行为,减少误操作的影响。这不能阻止所有挖矿或滥用网络的行为。
- 利用systemd的Path机制监听
$HOME/.config/code-server/config.yaml的修改并自动重启daemon,使得用户可以通过修改配置文件来改登录密码,较为优雅地实现需求2。
对于7,我觉得根据大一同学的计算机水平倒是不用太担心(应该吧?)
没有直接断网是因为考虑到有些需求需要联网下软件包,或者可能涉及到爬虫操作什么的。目前仍允许普通 IPv4/IPv6 出站连接,因此这些限制不能代替出站访问控制和对滥用行为的管理。
本文讨论不涉及SELinux,因为个人认为当前需求使用SELinux有点过度设计了,并且我也对SELinux/SEPolicy不太熟(这才是主要原因吧233),同时也想尽量让用户在合理使用过程中不受限制,因此并未使用SELinux来限制。
部署
配置systemd
下载下来codeserver二进制文件传到服务器上,比如我丢在/usr/bin/code-server。然后编写systemd配置,创建/etc/systemd/system/code-server@.service:
# /etc/systemd/system/code-server@.service[Unit]Description=Code-Server for %iAfter=network.target
[Service]Type=simpleUser=%iGroup=%iWorkingDirectory=/home/%iMemoryMax=1GCPUQuota=150%TasksMax=200IOWeight=50ProtectSystem=strictReadWritePaths=/home/%iPrivateTmp=yesPrivateDevices=yesNoNewPrivileges=yesRestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX# ExecStart=/bin/bash -c "PASSWORD=$(echo -n "%i" | md5sum | cut -d' ' -f1) /usr/bin/code-server --bind-addr 0.0.0.0:$((7000 + $(id -u %i))) --auth password"ExecStart=/bin/bash -c ' \ PORT=$((7000 + $(id -u %i) %% 10000)); \ /usr/bin/code-server --bind-addr 0.0.0.0:$PORT; \ 'Restart=alwaysRestartSec=5
[Install]WantedBy=multi-user.target这里创建的文件名是code-server@.service,@之后的内容会被当作%i替换。设计上是利用%i传递用户名称,计算端口并自启动。端口计算逻辑是7000+uid%10000,一般创建的uid是1000开始,因此正好按照创建顺序从8000开始监听端口。
ProtectSystem=strict 会让家目录也变成只读,因此用 ReadWritePaths=/home/%i 显式允许当前用户的工作目录写入。这个选项不会授予额外的文件权限,仍需正确设置家目录的所有权和权限。venv 只用于分开管理 Python 包,不是阻止用户访问宿主机或其他文件的安全沙盒。
值得注意的是这部分配置:
MemoryMax=1GCPUQuota=150%TasksMax=200IOWeight=50ProtectSystem=strictPrivateTmp=yesPrivateDevices=yesNoNewPrivileges=yesRestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX(我懒得解释了,让ai解释一下吧) 它主要分为两个方面:资源限制(Resource Control) 和 安全沙盒隔离(Security and Sandboxing)。
一、 资源限制 (Resource Control)
这部分利用 Linux 的 cgroups 机制,防止该服务消耗过多系统资源,影响其他程序运行。
MemoryMax=1G- 作用: 将该服务所在 cgroup 的内存使用上限设为 1 GiB。
- 结果: 当服务所在 cgroup 的内存使用达到限制且无法通过回收缓解时,可能触发该组内的 OOM 处理。
1G在这里表示 1 GiB。
CPUQuota=150%- 作用: 限制该服务的 CPU 使用率最高为 150%。
- 结果: 100% 代表一个完整的 CPU 核心。150% 意味着该服务最多可以占满一个核心,并使用第二个核心的一半(即最多使用 1.5 个 CPU 核心的算力)。
TasksMax=200- 作用: 限制该服务可以创建的最大任务(进程或线程)数量为 200 个。
- 结果: 防止服务因为遭遇漏洞或死循环(如 Fork Bomb)而无限创建子进程,从而耗尽系统 PID 资源使系统崩溃。
IOWeight=50- 作用: 设置磁盘 I/O(输入/输出)的权重。默认值通常是 100。
- 结果: 在底层设备和 I/O 控制器支持权重调度时,设置为 50 会降低它在竞争中的相对份额。这不是固定的读写速率上限,也不能保证其他服务不受影响。
二、 安全与隔离 (Security & Isolation)
这部分限制服务对文件系统、设备和套接字的访问,用于减少误操作的影响及部分攻击路径。
ProtectSystem=strict- 作用: 严格保护系统文件。
- 结果: 将文件系统层级设为只读,特殊 API 文件系统及显式配置的可写路径除外。这里通过
ReadWritePaths保留用户家目录的写入能力;这种挂载限制不能替代内核漏洞修补。
PrivateTmp=yes- 作用: 为该服务提供独立的临时文件目录。
- 结果: 服务会看到一个自己专属的
/tmp和/var/tmp目录。它无法看到或修改其他用户/服务放在全局/tmp中的文件,反之亦然。这能有效防止基于临时文件的符号链接攻击或数据泄露。
PrivateDevices=yes- 作用: 隔离物理设备。
- 结果: 为服务提供一个只含必要伪设备的私有
/dev,例如/dev/null、/dev/zero和/dev/urandom,限制直接访问宿主机设备节点。这不等于隔离了所有可能的硬件访问路径。
NoNewPrivileges=yes- 作用: 限制通过执行新程序获得额外权限。
- 结果: 服务及其子进程不能通过
execve()利用 setuid/setgid 位或文件 capabilities 获得额外权限。它不能防住所有提权方式,例如直接利用内核漏洞修改进程凭据。
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX- 作用: 限制该服务可以使用的网络协议族。
- 结果: 允许创建以下地址族的套接字:
AF_INET:IPv4 网络通信AF_INET6:IPv6 网络通信AF_UNIX:本地 UNIX 域套接字(用于本地进程间通信)
- 在支持相应 seccomp 过滤的环境中,创建其他地址族套接字的调用会被限制。这可以缩小攻击面,但不限制 IPv4/IPv6 的连接目标,也不会撤销已经继承或传入的套接字。
创建新用户时直接:
adduser <username>systemctl enable --now code-server@<username>即可同时创建对应的CodeServer服务
配置修改密码自动重启
创建文件/etc/systemd/system/code-server-restart@.path:
# /etc/systemd/system/code-server-restart@.path[Unit]Description=Monitor code-server config change for %iAfter=network.target
[Path]PathChanged=/home/%i/.config/code-server/config.yaml
[Install]WantedBy=multi-user.target创建文件/etc/systemd/system/code-server-restart@.service:
[Unit]Description=Triggered safe restart for %i
[Service]Type=oneshotUser=rootExecStart=/usr/local/bin/safe-restart-codeserver.sh %i
[Install]WantedBy=multi-user.target这里的运行逻辑是这样的:
第一步:启动监控 (由 .path 文件负责)
- 系统或管理员启动了
code-server-restart@<username>.path。 - systemd会解析这个
.path文件,将%i替换为用户名。 PathChanged=/home/<username>/.config/code-server/config.yaml:systemd 会在内核层面(利用 inotify 机制)开始静默监听这个特定路径的config.yaml文件。
第二步:检测变更并触发
- 在 code-server 网页端修改了设置,或者通过命令行直接编辑了
config.yaml并保存。 PathChanged检测到文件被修改并且被关闭(这意味着写入已完成,避免读到写了一半的脏数据)。- 隐式绑定: 因为
.path文件中没有使用Unit=显式指定要触发哪个服务,systemd 的默认行为是触发与它同名去掉后缀的.service文件。 - 于是,systemd 自动拉起
code-server-restart@<username>.service。
第三步:执行动作 (由 .service 文件负责)
- systemd 开始执行
code-server-restart@<username>.service。同样,这里的%i也会被替换为用户名。 Type=oneshot:告诉系统这不是一个常驻后台的服务,而是一次性的任务。执行完就结束。User=root:这个重启操作需要较高的权限,因此强制以root身份执行。ExecStart=/usr/local/bin/safe-restart-codeserver.sh <user>:这是整个逻辑的最终落脚点。系统以 root 身份运行了这个自定义的 shell 脚本,并将用户名作为参数传递给它。
没有直接在restart.service里重启主daemon,因为考虑到CodeServer编辑文件是每修改一点就会自动保存,因此当用户修改密码过程中,可能会存在修改了一半就被触发重启,导致后面的部分没有成功写入的情况,因此编写了一个脚本,判断等文件没有修改之后才触发重启:
/usr/local/bin/safe-restart-codeserver.sh
#!/bin/bashUSER_NAME=$1CONFIG_FILE="/home/$USER_NAME/.config/code-server/config.yaml"STAMP_FILE="/tmp/code-server-restart-${USER_NAME}.stamp"
# 1. 前置拦截:如果记录文件存在,且配置文件比记录文件旧(或时间相同)# 说明这一次触发是 systemd 排队遗留下来的无意义事件,直接退出if [ -f "$STAMP_FILE" ] && [ "$CONFIG_FILE" -ot "$STAMP_FILE" ]; then echo "Config hasn't changed since last restart, exiting." exit 0fi
# 2. 原有的防抖等待逻辑(保持不变)while true; do last_md5=$(md5sum "$CONFIG_FILE") sleep 5 current_md5=$(md5sum "$CONFIG_FILE")
if [ "$last_md5" == "$current_md5" ]; then # 文件稳定了,执行重启 systemctl restart code-server@$USER_NAME
# 3. 关键动作:重启成功后,更新时间戳文件的修改时间 touch "$STAMP_FILE" break else echo "Config file for $USER_NAME is still changing, waiting..." fidone那个时间戳机制是为了防止多次触发导致无效重启,若检测到配置文件编辑时间早于上次重启时间则拒绝重启。
完成编辑后启用触发器:systemd enable --now code-server-restart@<username>.path(注意是.path不是.service),接着在web界面中尝试缓慢编辑config文件中的密码,查看日志就可以发现类似于如下记录:
○ code-server-restart@lyr.service - Triggered safe restart for lyr Loaded: loaded (/etc/systemd/system/code-server-restart@.service; disabled; preset: enabled) Active: inactive (dead) since Sat 2026-06-06 06:43:55 UTC; 29s ago Invocation: 0bd37fcb6ea44a58870c06b4fde5300cTriggeredBy: ● code-server-restart@lyr.path Process: 6997 ExecStart=/usr/local/bin/safe-restart-codeserver.sh lyr (code=exited, status=0/SUCCESS) Main PID: 6997 (code=exited, status=0/SUCCESS) Mem peak: 2.5M CPU: 43ms
Jun 06 06:43:40 CodeServer systemd[1]: Starting code-server-restart@lyr.service - Triggered safe restart for lyr...Jun 06 06:43:45 CodeServer safe-restart-codeserver.sh[6997]: Config file for lyr is still changing, waiting...Jun 06 06:43:50 CodeServer safe-restart-codeserver.sh[6997]: Config file for lyr is still changing, waiting...Jun 06 06:43:55 CodeServer systemd[1]: code-server-restart@lyr.service: Deactivated successfully.Jun 06 06:43:55 CodeServer systemd[1]: Finished code-server-restart@lyr.service - Triggered safe restart for lyr.当停止编辑5秒之后CodeServer才会重启,后续可以使用新密码登录。
配置Python venv
首先系统里需要安装Python3,这就不多赘述了,包管理器直接装即可。同时安装一个python3-venv:
apt install python3-venv接着,进入用户的家目录,我选择创建了一个名为.venv的文件夹用于存放虚拟环境。在.venv中执行python3 -m venv myvenv创建一个名叫myvenv的虚拟环境,并在用户的.bashrc后加上一行:
fifi
source ~/.venv/myvenv/bin/activate之后,启动shell的时候就会自动启用venv。
为CodeServer安装Python插件并指派使用venv
这一步就不过多赘述了,在网页上给CodeServer安装Python和调试器插件,并指定Python解释器为venv下的python即可。
后记
其实这个配置很早就做了但是一直没写,当时还没爆出来 CopyFail 和 DirtyFrag 这样的漏洞。披露后,我在这套配置下运行了当时拿到的 PoC,测试中没有成功提权。这是对当时环境和样本的观察,不能推广为能防住所有相关变种。
从测试样本看,利用链需要创建 AF_ALG,或 AF_NETLINK、AF_RXRPC 等套接字,而这里的 RestrictAddressFamilies 没有允许这些地址族,因此可能在套接字创建阶段阻断这些利用路径。要确认具体阻断点,还需要结合对应 PoC、内核版本和系统日志分析。
NoNewPrivileges=yes 则限制了另一类路径:进程及其子进程不能在 execve() 时通过 setuid/setgid 位或文件 capabilities 获得额外权限。但它不阻止所有不经过 execve() 的权限变化,也不能保证受损的系统文件不会影响其他高权限进程。因此,系统及时更新仍然必要,不能用这套配置代替漏洞修补。
本文没有展开 SELinux,当前方案主要面向朋友共用、非关键业务的环境。systemd 的这些选项结合了 namespace、cgroup 和 seccomp 等机制,但不是完整的租户安全边界。SELinux/AppArmor 可以进一步限制进程访问,也不能保证挡住所有内核漏洞;面对互不信任的用户,还需要根据威胁模型评估更强的隔离方式。 当然,这个方案也存在其他一些局限,比如当用户非常多,或者uid分配有特殊需求,超过10000时原先计算端口的方式可能会失效;对于科学计算等其他正常需求,目前的限制可能过于严格等。本文仅作为一种思路探讨和参考。
碎碎念:在容器化和云原生的时代,传统systemd还有很多值得我们去探求的特性可以做到很多事情呢……
参考文章: