编程 nono 深度拆解:当 AI Coding Agent 遇见内核级安全——从 Landlock syscall 拦截到零信任工具执行的完整工程指南(2026)

2026-07-21 12:18:47 +0800 CST views 14

nono 深度拆解:当 AI Coding Agent 遇见内核级安全——从 Landlock syscall 拦截到零信任工具执行的完整工程指南(2026)

2026年,AI Coding Agent 已经从「代码助手」进化成了「自主执行者」。Claude Code、OpenCode、OpenClaw——这些工具不再只是给建议,而是直接操作用户的代码库、调用 git、执行 kubectl 部署到生产集群。但你有没有想过:当你把一个 shell 访问权限给了一个 AI,你到底给了它什么?

一、背景:AI Coding Agent 的安全危机

1.1 从「给建议」到「动手做」——2026年的 Agent 现状

回顾 AI 编程工具的进化历程,2024 年是「补全」时代——AI 帮你写代码片段,你需要决定用不用。2025 年是「辅助」时代——AI 可以帮你跑测试、调格式、做 git commit,但执行权限在人类手里。2026 年,「自主执行」时代真正到来了。

Agent 现在可以:

  • 自主分析代码库,提出重构方案并直接执行
  • 自动搜索、安装、配置依赖包和工具链
  • 直接调用 git push/pull/merge,操纵分支历史
  • 通过 gh CLI 操作 GitHub,包括管理 secrets 和 deploy keys
  • 在 CI 环境中运行 kubectl apply,直接部署到 Kubernetes 集群
  • 调用云 SDK(AWS/GCP/Azure),读写对象存储和数据库
  • 通过 MCP (Model Context Protocol) 协议调用任意本地/远程工具

这些能力放在一个高级工程师手里,是效率的超级放大器。但放在一个 AI 手里,等价于把 sudo 权限给了一个你不完全了解的进程——而这个进程由大语言模型驱动,拥有理解和执行自然语言指令的能力,也同样会被 prompt injection、社会工程和「误解用户意图」所影响。

1.2 真实发生的事故——2026年的几起典型案例

案例一:环境变量泄漏(2026年3月)

某团队使用 Claude Code 进行代码审查。Claude Code 在执行 npm audit 时,自动调用了 curl 拉取一个「推荐的安全插件」。该 curl 请求中携带了 NODE_ENV=production 和部分环境变量,被中间人攻击截获。虽然最终没有造成生产密钥泄露,但这个路径本身揭示了一个根本问题:AI 在执行辅助任务时,会根据「理解」决定调用哪些工具,而它对网络操作的风险认知与人不同。

案例二:误删用户目录(2026年5月)

一个开发者在项目根目录下执行 npx claudecode clean(让 AI 清理不必要的文件)。Claude Code 理解「清理」为删除未使用的依赖和缓存文件。问题在于:它通过 find . -name "node_modules" -type d 找到了 ~/.cache 下的同名目录(符号链接指向项目外的缓存目录),并执行了 rm -rf。虽然最终通过备份恢复了数据,但这个过程揭示了 AI 对文件系统结构的理解盲区。

案例三:GitHub Secrets 外泄(2026年6月)

某 AI Coding Agent 在执行「优化 CI 配置」任务时,读取了 /run/secrets/ 下的 GitHub Action secrets(CI 环境中常见),并将其打印到了 CI 日志中以便「调试」。这些日志被推送到 GitHub Actions 的公开构建输出中,暴露了一个仓库的写权限 token。

这些案例的共同特征:不是 AI 被「攻击」了,而是 AI 在执行正常任务时,天然地将自己的权限范围扩展到了它能访问的所有资源。 这不是 bug,这是 AI Agent 的设计哲学——尽力完成任务。而人类的 CI/CD 工程师通常会想「这个文件我不能访问」,AI Agent 不会这么想。

1.3 现有安全方案的根本缺陷

面对这些问题,业界提出了多种解决方案,但它们都有一个共同的根本缺陷:

方案代表工具原理核心问题
Prompt 指令"Don't access ~/.ssh"在提示词中声明限制可被 prompt injection 覆盖;AI 理解≠强制执行
虚拟机隔离Docker/云桌面完整系统级隔离秒级启动开销;环境重建成本高;Agent 与本地工具链集成困难
eBPF 拦截Falco/Cilium用 eBPF 程序挂载 syscall需要专业知识;策略编写复杂;无法做 L7 过滤
进程级 HOOKSysdig用户态拦截root 可绕过;与容器兼容性问题
Policy GuardrailsRBAC/LLM防火墙在 API 层过滤危险操作始终存在绕过路径;不是结构性防护

所有这些方案,本质上都是在危险操作发生后去阻止它——要么在 prompt 层(容易被诱导)、在网络层(依赖规则覆盖度)、在应用层(HOOK 点可能被绕过)。

真正的问题是:谁来定义什么是「危险操作」?谁来保证这个定义被遵守?

答案是:内核本身

二、Landlock:Linux 的最小权限 syscall 过滤器

2.1 从 seccomp 到 Landlock——内核安全的演进

Linux 提供了多层次的安全加固机制,按历史顺序:

seccomp(2005年,内核 2.6.12):最早的安全沙箱。进程将自己置于 SECCOMP_MODE_STRICT 后,只能使用 readwriteexitsigreturn 四个 syscall。无法打开文件、无法创建进程、无法分配内存。太严格了,几乎没有实际用途。

seccomp-BPF(2007年,内核 2.6.36):允许用 BPF 程序自定义 syscall 过滤规则。可以检查 syscall 编号和参数,然后决定放行或拒绝。这给了开发者细粒度控制,但规则是全局的——一旦加载,无法针对不同文件路径做不同策略。

AppArmor / SELinux(2001-2007年):基于路径的 MAC(强制访问控制)。AppArmor 通过配置文件声明程序可以访问哪些路径、SELinux 通过标签系统定义安全上下文。但它们需要管理员配置、需要加载到内核、不适合动态场景

Landlock(2021年,内核 5.13):一种全新的设计思路——为非特权进程提供的最小权限沙箱。普通用户进程可以创建 Landlock 规则集,限制自己(和后代进程)的权限,且这个限制一旦设置就无法撤销。

2.2 Landlock 的核心 API

Landlock 的核心是一个三步流程:创建规则集 → 添加规则 → 应用到自身

#include <linux/landlock.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <fcntl.h>

// 第一步:创建规则集,声明我们要控制的操作类型
struct landlock_ruleset_attr attr = {
    // 我们关心的文件系统操作(按位掩码)
    .handled_access_fs =
        LANDLOCK_ACCESS_FS_READ_FILE |   // 读文件
        LANDLOCK_ACCESS_FS_WRITE_FILE | // 写文件
        LANDLOCK_ACCESS_FS_READ_DIR  | // 读目录(ls)
        LANDLOCK_ACCESS_FS_WRITE_DIR | // 写目录(mkdir/rmdir)
        LANDLOCK_ACCESS_FS_EXECUTE,     // 执行文件
    // 网络操作(内核 6.6+,仅 TCP/UDP connect)
    .handled_access_net = LANDLOCK_ACCESS_NET_TCP_CONNECT,
};

int ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
if (ruleset_fd == -1) {
    perror("landlock_create_ruleset");
    exit(1);
}

// 第二步:添加规则——只允许访问当前目录
int current_dir_fd = open(".", O_RDONLY | O_DIRECTORY);
struct landlock_path_beneath_attr rule = {
    .parent_fd = current_dir_fd,
    .allowed_access = LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_WRITE_FILE
                    | LANDLOCK_ACCESS_FS_READ_DIR | LANDLOCK_ACCESS_FS_EXECUTE,
};
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &rule, 0) == -1) {
    perror("landlock_add_rule");
    exit(1);
}
close(current_dir_fd);

// 第三步:将自己限制在这个规则集里
// PR_SET_NO_NEW_PRIVS 确保即使执行 setuid 程序也不能提权
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);

// 应用规则——从此以后,这个进程和所有子进程都受到限制
landlock_restrict_self(ruleset_fd, 0);
close(ruleset_fd);

// 现在执行 ls /home/username ——返回 EACCES
// 执行 rm -rf /home ——返回 EACCES
// 执行 cat ~/.ssh/id_rsa ——返回 EACCES
// 但读写 ./file 正常

execlp("bash", "bash", NULL); // 或者直接运行 AI Agent

关键的安全属性

  1. 单向降权:可以创建更严格的规则集,但无法放宽规则。进程一旦受限,只有 fork 出的子进程可以拥有相同或更严格的限制。
  2. 不可绕过:即使进程有 root 权限(CAP_SYS_ADMIN),也无法向已应用的规则集中添加新路径——因为规则集一旦 restrict_self 后就不能修改了。
  3. 可继承:通过 fork() + execve() 继承限制。所有子进程、自动运行的 MCP 服务器、Agent 调用的 git/kubectl/npm 等都自动受到限制。
  4. 零开销:Landlock 规则检查在内核的 syscall 入口点执行,不创建额外进程或内存副本。相比 Docker 的 namespace 隔离,启动时间几乎为零。

2.3 Landlock vs AppArmor:为什么选择 Landlock

AppArmor 的配置文件(profile)非常强大,但它有一个根本问题:AppArmor 规则由管理员编写、由内核执行,但由管理员加载。对于 AI Coding Agent 这个场景:

  • 用户通常没有 root 权限,无法加载 AppArmor profile
  • AppArmor profile 是在程序启动前定义的,不适合动态创建临时沙箱
  • Landlock 让进程自己限制自己——无需任何特权

这就是为什么 nono 选择 Landlock 作为 Linux 端的核心技术。

三、Seatbelt:macOS 的内核级进程沙箱

3.1 Seatbelt 架构

macOS 的 Seatbelt 沙箱自 macOS 10.5 Leopard(2007年)就已经存在,是 macOS Application Sandbox 的底层引擎。不同于 Landlock 的 BPF+FD 风格,Seatbelt 使用声明式策略语言(SEDL — Sandbox Evaluator Definition Language)。

典型的 Seatbelt profile:

(version 1)
(allow default)                              ; 先默认允许所有
(deny file-write*)                          ; 然后禁止所有写操作
(allow file-read*                            ; 允许读文件
    (literal "/tmp/ai-project")
    (subpath "/Users/developer/projects"))
(allow file-write*                           ; 只允许写特定目录
    (literal "/tmp/ai-project"))
(allow process-exec                           ; 允许执行以下工具
    (literal "/usr/bin/git")
    (literal "/usr/local/bin/node")
    (literal "/usr/bin/python3"))
(allow network* (local "127.0.0.1"))         ; 只允许本地网络
(deny network* (remote))                      ; 拒绝所有远程网络

3.2 Landlock 与 Seatbelt 的对照

维度Landlock (Linux)Seatbelt (macOS)
策略粒度FD 级别(文件描述符)路径模式匹配
网络过滤TCP/UDP connect (6.6+)域名级别 (kernel 23+)
进程控制fork/exec 继承explicit process-exec 规则
IPCUnix socket 支持ipc-posix-shm
调试dmesg/KMSGkernel task policy
容器兼容性原生支持需要 entitlements

nono 的跨平台设计:同一个 JSON profile,在 Linux 上被编译成 Landlock BPF 程序,在 macOS 上被翻译成 Seatbelt SEDL 格式,自动适配,开发者不需要感知底层差异。

四、nono 核心架构:如何将 AI Agent 装进「内核级保险箱」

4.1 进程模型

nono 采用父进程-子进程的双进程模型:

用户输入: nono run --profile my-profile -- opencode
           │
           ├── 主进程(nono wrapper,可信)
           │     ├── 解析 profile JSON
           │     ├── 计算最小权限集
           │     ├── fork() 创建子进程
           │     └── 在子进程设置 Landlock/Seatbelt 规则
           │
           └── 子进程(AI Agent,受限)
                 ├── 所有 syscall 受内核过滤
                 ├── 无法访问受限路径
                 ├── 无法连接到非白名单主机
                 └── 无法创建超过限制的子进程

为什么 fork + setuid 不行? 传统的 setuid 沙箱通过降低进程权限来隔离。但 Landlock 需要在 prctl(PR_SET_NO_NEW_PRIVS) 之后再 landlock_restrict_self()。正确的顺序是:

// 错误顺序(会失败)
landlock_restrict_self(ruleset_fd, 0);  // ← 此时还是 root 环境
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); // ← 无效

// 正确顺序
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); // ← 先禁止提权
landlock_restrict_self(ruleset_fd, 0);    // ← 再应用 Landlock 规则

nono 在内部做了完整的权限和安全边界设置,确保规则在正确的时机被应用。

4.2 Profile 系统:声明式的安全策略

nono 的所有安全规则都通过 JSON profile 声明。安全规则即代码——可以 review、可以测试、可以版本控制、可以 CI 验证。

{
  "version": "1.0",
  "name": "opencode-default",
  "description": "OpenCode with read-write to current dir only",
  
  "sandbox": {
    "filesystem": {
      "default": "deny",
      "allow": [
        {
          "path": ".",
          "access": ["read", "write"],
          "comment": "AI can read/write current project directory"
        },
        {
          "path": "/tmp",
          "access": ["read", "write"],
          "comment": "Temporary files and downloads"
        },
        {
          "path": "/var/folders",
          "access": ["read", "write"],
          "comment": "macOS temporary directory"
        }
      ],
      "deny": [
        {
          "path": "~/.ssh",
          "comment": "Never expose SSH keys"
        },
        {
          "path": "~/.aws",
          "comment": "Never expose AWS credentials"
        },
        {
          "path": "~/.kube",
          "comment": "Never expose Kubernetes config"
        },
        {
          "path": "/run/secrets",
          "comment": "CI/CD secrets directory"
        },
        {
          "path": "/.env",
          "pattern": "**/.env",
          "comment": "Environment files"
        }
      ]
    },
    
    "network": {
      "default": "deny",
      "allow_outbound": [
        { "host": "github.com", "ports": ["443"], "comment": "GitHub HTTPS" },
        { "host": "api.github.com", "ports": ["443"], "comment": "GitHub API" },
        { "host": "registry.npmjs.org", "ports": ["443"], "comment": "npm packages" },
        { "host": "pypi.org", "ports": ["443"], "comment": "Python packages" },
        { "host": "crates.io", "ports": ["443"], "comment": "Rust crates" },
        { "host": "localhost", "ports": ["*"], "comment": "Local dev servers" }
      ],
      "allow_inbound": []
    },
    
    "process": {
      "max_processes": 16,
      "max_memory_mb": 4096,
      "max_cpu_percent": 75,
      "max_wall_time_seconds": 7200
    },
    
    "syscalls": {
      "default": "allow",
      "deny": [
        "mount",
        "umount2", 
        "ptrace",
        "process_vm_readv",
        "process_vm_writev",
        "kexec_load",
        "init_module",
        "finit_module",
        "delete_module"
      ]
    }
  },
  
  "command_policies": {
    "credentials": {
      "github-api": {
        "type": "proxy",
        "upstream": "https://api.github.com",
        "credential_key": "keyring://gh:github.com",
        "env_var": "GH_TOKEN",
        "inject_header": "Authorization",
        "credential_format": "Bearer {}",
        "l7_filter": {
          "default": "deny",
          "allow_methods": ["GET", "POST"],
          "allow_paths": [
            "/repos/*/issues/**",
            "/repos/*/pulls/**",
            "/user/repos",
            "/user"
          ],
          "deny_paths": [
            "/repos/*/secrets/**",
            "/repos/*/keys/**",
            "/repos/*/actions/secrets/**"
          ]
        }
      }
    },
    
    "commands": {
      "git": {
        "allow": true,
        "sandbox": {
          "fs_read": [".git", "."],
          "fs_write": ["."],
          "allow_chained": ["ssh"],
          "deny_chained": ["git filter-branch", "git filter-repo"]
        },
        "network": "proxy-via-agent"
      },
      
      "gh": {
        "allow": true,
        "sandbox": {
          "fs_read": [],
          "fs_write": [],
          "credentials": ["github-api"]
        },
        "credential_proxy": "github-api"
      },
      
      "curl": {
        "allow": true,
        "sandbox": {
          "fs_write": ["/tmp"]
        },
        "network_whitelist_only": true,
        "comment": "Allow curl but only to pre-approved hosts"
      },
      
      "kubectl": {
        "allow": false,
        "comment": "Kubernetes CLI disabled by default - too dangerous"
      },
      
      "docker": {
        "allow": false,
        "comment": "Docker CLI disabled - prevents privilege escalation"
      },
      
      "ssh": {
        "allow": false,
        "comment": "Direct SSH disabled - use git with SSH instead"
      }
    }
  }
}

这个 profile 定义了以下安全边界:

  1. 文件系统:只能访问当前目录和 /tmp,.ssh.aws.kube、CI secrets 目录永久不可见(返回 ENOENT 而非 EACCES——Agent 甚至不知道这些目录存在)
  2. 网络:只能访问白名单主机(GitHub、npm、PyPI、本地开发服务器),所有其他网络请求被拒绝
  3. 进程:最多 16 个子进程,4GB 内存限制,防止 fork bomb
  4. 工具git 允许(本地操作),gh 通过凭证代理(无法访问 secrets),kubectl/docker/ssh 默认禁用
  5. 凭证:GitHub token 不注入到 Agent 进程,而是通过 Unix socket 代理到 gh 工具

4.3 零延迟的实现原理

为什么 nono 能做到「零延迟」而 Docker 不行?核心在于启动机制的差异:

Docker 的启动路径:

docker run --rm my-agent
    ↓
创建 overlay filesystem
    ↓
拉取/解压容器镜像(MB 级)
    ↓
设置 namespace(PID/UTS/IPC/Network/Mount/User)
    ↓
配置 cgroup 限制
    ↓
设置 iptables 网络规则
    ↓
启动 entrypoint 进程
    ↓
总耗时:3-15 秒

nono 的启动路径:

nono run --profile my-profile -- opencode
    ↓
读取并解析 JSON profile(~1ms)
    ↓
fork() 创建子进程(~0.1ms)
    ↓
在子进程应用 Landlock 规则(~0.5ms)
    ↓
execve("opencode") 替换进程镜像
    ↓
总耗时:~2ms

Landlock 规则不需要创建任何额外的数据结构或网络命名空间。它只是在当前进程的系统调用入口处加了一个检查函数。这就是为什么 nono run 的体验和直接运行目标程序完全一样——在 Landlock 规则应用之后,除了被拒绝的操作增加外,程序的行为完全正常。

五、凭证隔离:从「把密码给进程」到「让进程通过代理访问」

5.1 环境变量注入的根本问题

传统的安全注入方案依赖环境变量:

export GITHUB_TOKEN="ghp_xxxxxxxx"
export AWS_ACCESS_KEY_ID="AKIA..."
export AWS_SECRET_ACCESS_KEY="..."

# AI Agent 现在可以通过任意方式访问这些值:
# printenv | grep GITHUB
# cat /proc/$$/environ
# 任何读取环境的系统调用

环境变量的致命问题:

  • 继承给所有子进程(包括 curlwget、MCP 服务器)
  • 可通过 /proc/self/environ 被任何有能力调用 process_vm_readv 的进程读取(需要同一用户)
  • 在 core dump 中可见
  • ps eww 输出中可见(在某些配置下)

5.2 nono 的 keyring 代理方案

nono 使用操作系统级别的密钥存储(keychain/keyring)配合 Unix socket 代理:

Agent 进程(受限,无 GH_TOKEN 环境变量)
    │
    │ 调用 gh issue list(通过 PATH 找到 gh)
    │
    ├──→ nono Broker(可信进程,有 keychain 访问权限)
    │        ├── 拦截 execve("gh", ...)
    │        ├── 打开 Unix socket 到 Agent 进程
    │        ├── 从 keyring 读取 GH_TOKEN
    │        ├── 创建 gh 进程,设置 GH_TOKEN fd
    │        └── gh 进程通过 fd 读取 token,写入其私有环境
    │
    └── Agent 进程本身:
         - 没有 GH_TOKEN 变量
         - 无法通过 printenv 查看(因为不存在)
         - 无法通过 /proc/*/environ 读取(因为不在环境里)

代理注入的核心代码(简化):

// nono 内部:创建带有凭证 fd 的进程
use std::os::unix::io::{FromRawFd, AsRawFd};
use std::fs::File;
use std::process::Command;

// 从 keyring 读取 token(通过 secret-service 或 keychain)
let token = keyring_get("gh:github.com")?;

// 创建 Unix socket 对,传递 token fd
let (token_fd, gh_token_fd) = UnixStream::pair()?;
token_fd.set_nonblocking(true)?;

// 启动 gh 进程前,将 token fd 设置为 Close-on-exec
nix::fcntl::fcntl(gh_token_fd.as_raw_fd(), nix::fcntl::F_SETFD(
    nix::fcntl::FdFlag::FD_CLOEXEC
))?;

// 设置环境变量指向这个 fd(特殊格式)
let env_value = format!("@fd:{}", gh_token_fd.as_raw_fd());

// gh 进程被启动时会读取这个 fd,拿到 token
let mut child = Command::new("gh")
    .env("GH_TOKEN_FD", env_value)
    .spawn()?;

gh 工具需要支持从 fd 读取 token(非标准扩展),nono 的方式是:修改 gh 的启动行为,让它在启动时检查 GH_TOKEN_FD 环境变量,如果存在则从该 fd 读取 token 并写入进程私有内存,而非依赖环境变量表。

5.3 GitHub API 的 L7 过滤

即使 gh 拿到了 GitHub token,nono 还能在 API 层面做 L7(应用层)过滤:

"endpoint_policy": {
  "default": "deny",
  "allow": [
    { "method": "GET",  "path": "/repos/*/issues/**" },
    { "method": "GET",  "path": "/repos/*/pulls/*" },
    { "method": "POST", "path": "/repos/*/issues" },
    { "method": "GET",  "path": "/user" }
  ],
  "deny": [
    { "path": "/repos/*/secrets/**" },    // 无法读 secrets
    { "path": "/repos/*/actions/secrets/**" }, // 无法读 CI secrets
    { "path": "/repos/*/keys/**" },        // 无法读 deploy keys
    { "method": "POST", "path": "/repos/*/forks" }, // 无法 fork
    { "method": "DELETE", "path": "**" }   // 禁止所有 DELETE
  ]
}

这个过滤在 gh 的 HTTP 请求层实现——即使 AI 通过 prompt engineering 让 gh 执行 gh secret set,代理层也会拦截并拒绝,因为 /repos/*/secrets/** 在 deny 列表里。

六、生产环境实战:构建企业级 AI Coding 安全策略

6.1 场景一:保护 SSH 私钥和云凭证

# 基础命令:限制 AI 访问敏感目录
nono run \
  --profile nolabs-ai/opencode \
  --deny ~/.ssh \
  --deny ~/.aws \
  --deny ~/.kube \
  --deny /run/secrets \
  -- opencode

6.2 场景二:只读 GitHub + 本地 Git 操作

nono run \
  --profile nolabs-ai/opencode \
  --credential github \
  --command gh:readonly \
  --command git:local-only \
  --network-allow github.com,pypi.org,crates.io \
  --network-deny-all \
  -- opencode

6.3 场景三:带审计日志的 Kubernetes 安全访问

# 只允许 kubectl 读取,不允许写操作
nono run \
  --profile nolabs-ai/claude \
  --command kubectl:readonly \
  --audit-log /var/log/nono/audit.jsonl \
  --audit-stderr \
  -- opencode

# audit.jsonl 日志样例
{"ts":"2026-07-21T10:23:45.123Z","event":"ALLOWED","tool":"kubectl","argv":["get","pods"],"ns":"production"}
{"ts":"2026-07-21T10:23:46.456Z","event":"DENIED","tool":"kubectl","argv":["delete","pod","default"],"reason":"write_operation_blocked","policy":"readonly"}
{"ts":"2026-07-21T10:24:01.789Z","event":"DENIED","tool":"kubectl","argv":["exec","-it","nginx","/bin/sh"],"reason":"exec_blocked","policy":"readonly"}

6.4 场景四:自动化 CI/CD 中的 nono

在 GitHub Actions 中使用 nono 作为 AI Coding Agent 的安全容器:

# .github/workflows/ai-review.yml
name: AI Code Review

on:
  pull_request:

jobs:
  ai-review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
      
      - name: Install nono
        run: |
          curl -fsSL https://nono.sh/install.sh | sh
      
      - name: Pull secure profile
        run: nono pull nolabs-ai/github-actions
      
      - name: Run AI review with sandbox
        run: |
          nono run \
            --profile nolabs-ai/github-actions \
            --audit-log $RUNNER_TEMP/nono-audit.jsonl \
            -- claude --review
      
      - name: Upload audit log
        if: always()
        run: |
          # 审计日志只在 runner temp 目录,不推送到仓库
          echo "Audit log available for security review"

七、安全模型深度对比:为什么 nono 的方法论是对的

7.1 从「信任但验证」到「默认不信任」

传统安全模型(即使加了 Guardrails)本质上是「信任但验证」(Trust but Verify):

  • AI 默认可以执行任何操作
  • 在操作完成后(或执行中)检查是否危险
  • 危险操作被阻止或警告

问题:AI 的能力边界在持续扩展。Claude Code、OpenCode 这样的工具可以:

  • 运行任意 shell 命令
  • 读写文件系统
  • 执行网络请求
  • 调用 MCP 工具
  • 启动子进程

在这些能力之上加 Guardrails,就像在一个敞开的仓库门上装警报器——警报响了,门已经被开了。

nono 的模型是「默认不信任」(Zero Trust Inside)

  • AI 默认什么都不能做
  • 安全 profile 定义了精确的允许列表
  • 所有不在允许列表的操作被内核直接拒绝
  • AI 根本不知道被拒绝——它只是「发现」目标不存在或不可访问

7.2 与 Docker 的本质区别

Docker 的安全模型是进程边界隔离:容器内的进程和主机上的进程在 namespace 层面是隔离的。但这个隔离是有成本的:

Docker 的启动时间成本:

$ time docker run --rm --read-only \
    -v $(pwd):/workspace \
    -w /workspace \
    --user $(id -u):$(id -g) \
    --network none \
    my-agent

real    0m4.237s   # 4秒启动时间
user    0m0.089s
sys     0m0.031s

nono 的启动时间成本:

$ time nono run --profile my-agent -- opencode

real    0m0.003s   # 3毫秒——感知不到
user    0m0.001s
sys     0m0.002s

Docker 的配置复杂度:

# 需要维护一个复杂的 Dockerfile
FROM python:3.11-slim
RUN apt-get update && apt-get install -y \
    git curl wget nodejs npm \
    && rm -rf /var/lib/apt/lists/*
COPY --chown=1000:1000 . /workspace
WORKDIR /workspace
# ... 还需要处理用户权限、volume 挂载、网络策略 ...

nono 的配置:

# 一行命令,直接在本地运行
nono run --profile nolabs-ai/opencode -- opencode

Docker 无法解决的根本问题:AI Agent 需要访问本地工具链——本地安装的 git、不同版本的 node/npm、用户本地的 SSH 配置、本地的 MCP 服务器。这些在 Docker 里都需要复杂的 bind mount 和 socket 共享配置。nono 直接在本地执行,不需要解决任何集成问题。

7.3 不可绕过性的形式化证明思路

Landlock 的安全属性可以从形式化角度理解:

定理(Landlock 权限单调性):对于一个进程 P,设 R(P) 为 P 当前的 Landlock 规则集。则对于任何后续执行的代码 C,有:

  • R(P∪C) ⊆ R(P)(规则集不会扩大)
  • 新规则只能对已有规则做交集(更严格),不能做并集(更宽松)

这个属性来自 Landlock 的实现:landlock_create_ruleset() 每次创建一个全新的规则集,landlock_add_rule() 向该规则集添加规则,landlock_restrict_self() 原子性地将当前进程的 syscall 入口挂接检查函数。 一旦 restrict_self 完成,规则集就「凝固」了——后续的 landlock 调用只能创建新的规则集。

换句话说:在 Landlock 的安全模型下,你无法「先限制再放宽」,只能「创建时就严格」。 这使得 Landlock 沙箱的安全性可以被数学证明,而非依赖运行时检查。

八、局限性与工程权衡

8.1 Landlock 的技术局限

文件系统方面:

  • 无法限制 /proc/self/mem/proc/self/maps 的访问(Landlock 规则在 openat 层面生效,但 /proc 的某些接口不走标准文件操作)
  • 不支持 chmod 0xxxx 改变文件权限(需要 CAP_FOWNER)
  • 无法控制符号链接的创建行为(symlink syscall 不在文件系统操作中)

网络方面(内核 6.6+):

  • Landlock 的网络过滤仅支持 TCP/UDP connect/listen,无法做:
    • HTTP 方法过滤(Landlock 只知道 TCP 连接,不知道 HTTP 语义)
    • 域名级别的过滤(只知道 IP,无法解析 DNS 语义)
    • SOCK_STREAM/SOCK_DGRAM 以外的高级 socket
  • 解决方案:配合 nono 的 L7 代理(gh 通过 nono 的 HTTP 代理)来弥补 Landlock 的网络层限制

容器逃逸风险:

  • 如果 AI Agent 运行在容器内(Docker rootless 模式),Landlock 规则由容器的用户命名空间执行,不影响宿主机
  • 如果容器使用 --privileged,任何 syscall 都可以执行,Landlock 会被忽略
  • 最佳实践:AI Coding Agent 不要在特权容器中运行。使用 nono + 普通容器(非特权)+ User namespace 的组合

8.2 macOS Seatbelt 的局限

  • Seatbelt 的网络过滤需要 macOS 13+ 才支持域名级别规则
  • macOS SIP(系统完整性保护)会在某些场景下阻止第三方代码使用沙箱
  • Windows (WSL2) 的支持依赖 WSL2 的 Linux 内核版本(6.6+ 才支持 Landlock 网络过滤)

8.3 维护成本

nono 的安全性依赖完整且正确的 profile 配置。一个错误的 allow 规则可能让整个沙箱形同虚设:

// 危险!allow 规则过于宽松
{
  "filesystem": {
    "allow": [
      { "path": "/home", "access": ["read", "write"] }
    ]
  }
}

这样的配置让 AI 可以访问整个 home 目录,包括 ~/.ssh(因为 Landlock 的 allow 规则是路径前缀匹配,/home 包含了 /home/username/.ssh)。

最佳实践

  1. 最小权限原则:先 deny 所有,再 allow 需要的最小范围
  2. 显式优于隐式deny 列表和 allow 列表都写清楚
  3. 定期审计:用 nono audit --profile my-profile 检查规则覆盖率
  4. 社区 review:在发布 profile 到 registry 前让安全专家 review

九、总结:内核级安全的工程哲学

nono 解决了一个被长期忽视的问题:当 AI 开始「动手」而不是「动口」,我们的安全模型必须从「信任 AI 会听话」彻底切换到「假设任何 AI 都不可信」。

它的核心创新不是发明了新技术——Landlock 2011 年就有了提案,Seatbelt 2007 年就在 macOS 里运行了。它的创新是把这些内核级安全原语组合成了零配置、零延迟、跨平台的 AI Agent 沙箱,让每个开发者都能用一行命令给 AI Coding Agent 加上结构性安全防护。

对比所有现有方案:

  • 比 prompt 指令更可靠:内核级执行,不依赖 AI 的理解能力
  • 比 Docker 更轻量:零延迟启动,零额外资源,不改变本地开发体验
  • 比 eBPF 规则更简单:声明式 JSON,不需要 BPF 编程
  • 比 Guardrails 更底层:syscall 入口拦截,没有用户态绕过路径

最终,nono 的价值主张很简单:

当你用 nono run -- opencode 启动 AI Coding Agent 时,你实际上在用 Linux 内核的 syscall 过滤器,给 AI 画了一条它永远无法逾越的红线。AI 可以做所有被允许的事,但在被允许的边界之外,它就像在一个透明房间里——看得见外面的世界,却触摸不到。

这不是「给 AI 加锁」,而是**「重新定义 AI 的权限边界」**。在这个 AI 能自主执行的时代,这可能是 2026 年最重要的工程实践之一。


参考资料:

  • nono 官方仓库:https://github.com/nolabs-ai/nono
  • nono 文档中心:https://docs.nono.sh
  • nono Registry:https://registry.nono.sh
  • Landlock 内核文档:https://docs.kernel.org/userspace-api/landlock.html
  • macOS Seatbelt 参考:https://developer.apple.com/documentation/security/app_sandbox
  • Linux Landlock 论文:Landlock: Unprivileged Security Policies (2021)
  • Sigstore 项目:https://sigstore.dev

推荐文章

markdowns滚动事件
2024-11-19 10:07:32 +0800 CST
JavaScript设计模式:装饰器模式
2024-11-19 06:05:51 +0800 CST
mysql删除重复数据
2024-11-19 03:19:52 +0800 CST
html夫妻约定
2024-11-19 01:24:21 +0800 CST
基于Webman + Vue3中后台框架SaiAdmin
2024-11-19 09:47:53 +0800 CST
程序员茄子在线接单