shell 模块

shell 模块让节点按运营人员列出的调用方的请求,运行运营人员已批准的命令。典型用途是开发板或远程机器:需要从网络另一端重启服务、读取日志或烧写固件,并取回输出,且权限可随时收回。

该模块是唯一把网络请求变成宿主机命令的模块,因此它是加法模块:不属于默认构建,只包含在带有标签 shell 的构建中(install.sh --shell 或 ./build.sh --with shell)。参见 模块。

三项相互独立的决定

节点是否运行远程命令,取决于三项决定,每一项都须有意作出。

  1. 构建。不带标签 shell 的二进制不含这部分代码。带该标签的构建在作出第二项决定之前,不注册任何能力,也不运行任何命令。

  2. 配置。只有 config.json 中有 modules.shell 块,且该块至少定义一条命令或设置了 allow_arbitrary,模块才会工作。含有该模块但没有该块的二进制处于未启用状态。

  3. 调用方名单。有命令而调用方名单为空的块会拒绝每一次调用。名单为空表示没有任何人,而不是所有人。

调用方还必须通过节点的入站策略。默认策略为 closed,因此调用方须先列入 peers.allow(anet peers allow <aid>),调用才会到达模块。两份名单各司其职:peers.allow 决定谁可以把任务交给这台机器,模块的名单决定其中谁可以运行命令。不要把 shell.* 能力列入 inbound.public_capabilities,否则会放陌生人进门,只剩模块的名单这一道屏障。

不带调用方身份的调用会被拒绝,除非 allow_local 为 true。身份为空不能被当作“本机且可信”,否则某个遗漏身份的调用点会不留痕迹地绕过整份名单。

配置

{
  "modules": {
    "shell": {
      "commands": {
        "restart-app": {"run": "systemctl restart myapp", "description": "Restart the application"},
        "tail-log":    {"run": "journalctl -u myapp -n 200 --no-pager"},
        "flash":       {"run": "/opt/tools/flash.sh", "args": true, "timeout_s": 600}
      },
      "allow_file": "/etc/anet/shell-allow",
      "timeout_s": 30,
      "max_output_bytes": 65536
    }
  }
}

键

含义

commands.<name>.run

命令行。它经解释器运行,因此可以使用管道、重定向和 &&。名称不得包含空格、@ 或 /,因为它会成为能力标识的一部分。

commands.<name>.description

能力列表中的说明。

commands.<name>.args

true 允许调用方追加参数。默认为 false。

commands.<name>.dir

工作目录。

commands.<name>.timeout_s

该命令的时间限制,覆盖全局值。

allow

允许调用的 AID,以内联列表给出,在启动时读取。

allow_file

包含允许调用的 AID 的文件,每行一个;# 起始注释。每次调用前都会重新读取。allow 与 allow_file 互斥,同时给出会在加载配置时失败。

allow_local

放行不带调用方身份的调用。默认为 false。

allow_arbitrary

提供 shell.exec,即运行调用方发来的任意命令。默认为 false。

timeout_s

全局时间限制,单位为秒。默认为 30,最大值为 86,400(24 小时)。

max_output_bytes

返回输出的最大大小。默认为 65,536,最大值为 4 MiB。

shell

解释器。默认为 /bin/sh。

启动时,若没有任何调用方被允许,或 allow_arbitrary 已开启,节点会向标准错误写出警告。

能力

能力

行为

shell.list

报告节点定义了哪些命令,以及名单上的调用方数量。远程调用方只得到数量而不是 AID,使被接受的调用方无从得知节点还信任哪些其他机器。本机调用得到完整名单。

shell.run@<name>

运行一条已定义的命令。

shell.exec

运行调用方发来的命令。仅当 allow_arbitrary 为 true 时存在;否则既不在能力卡上,也不能按名调用。

命令的名称会成为能力标识,如 shell.run@restart-app,而节点会通告它实际应答的能力。因此,任何能浏览 hub 目录的人都能看到这台机器提供哪些命令,名单仍然保护着这些命令。不希望其存在被人知道的命令,名称应不携带任何信息。

收回权限

allow_file 所指的文件在每次调用前都会重新读取,不做缓存。删除一行或整个文件,在下一次调用时即生效,无需重启。文件不存在等同于名单为空,即拒绝所有人;它既不是错误,也不是对所有人放行。内联列表 allow 只在重新加载配置时改变,因此生产节点应使用 allow_file。

行为

  • 不提权。命令以守护进程所属用户的身份运行。需要 root 的命令要求守护进程以 root 运行,这是安装时的决定,体现在 unit 文件中。模块既不调用 sudo,也不调用 setuid。

  • 参数不会变成命令。在 args: true 时,调用方的每个参数都会加引号后追加。命令行属于运营人员,参数属于网络,两者不会混在一起。

  • 时间限制会结束整个进程组。只结束 shell,会让它启动的进程留在机器上。

  • 输出有上限。过长的输出会被截断,并在输出中注明截断。

  • 效果状态准确。退出状态 0 为 OK。其他状态为 FAILED,并附带退出状态和标准错误。超过时间限制而被终止的命令为 FAILED,消息中注明其被终止。

  • 每次运行都进入证据链。事件 anet.shell.command 记录调用方、命令、退出状态和耗时。被拒绝的调用记为 anet.shell.refused,并带有调用方的 AID。输出本身不在链上。

run 中的命令行不受限制。写下它的运营人员是被信任的,信任边界在运营人员所写的内容与网络发来的内容之间。