aambert/nixos-microsegebpf
GitHub: aambert/nixos-microsegebpf
面向 Linux 工作站的 eBPF 微隔离方案,通过 cgroup 身份实现类似 Cilium 的精细化网络策略控制与 Hubble 可视化。
Stars: 0 | Forks: 0
# nixos-microsegebpf
[English](README.md) · [Français](README.fr.md)
**基于 eBPF 的 Linux 工作站微隔离方案,提供兼容 Hubble 的可观察性层。**
`nixos-microsegebpf` 将 Cilium 的身份感知策略模型引入到单个 Linux 机器中。它在 cgroupv2 根节点挂载 eBPF 程序,根据 YAML 策略查找每个数据包的本地 cgroup,据此执行丢弃或转发操作,并在 gRPC 服务器上发出流事件,该服务器使用与 Hubble 相同的 `cilium.observer.Observer` 协议 —— 因此上游的 [Hubble UI](https://github.com/cilium/hubble-ui) 可以在没有任何 Kubernetes 资源参与的情况下,渲染工作站的实时流量图。
## 架构概览
下图展示了四个信任层(内核 eBPF 数据路径、代理用户空间、可选的同位置服务、配置平面 + 外部端点),数据包如何从 `cgroup_skb` 钩子流经 LPM 字典树到达判决结果,流事件如何到达 Hubble UI 和 SOC,以及每个经过 CVE 评分的安全加固面所在的位置。
```
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#FFFFFF','primaryTextColor':'#0F172A','primaryBorderColor':'#475569','lineColor':'#475569','fontFamily':'monospace','fontSize':'13px'}}}%%
flowchart TB
subgraph KER["Linux kernel - eBPF datapath - cgroup_skb"]
direction LR
K1["cgroup_skb/egress
every outbound packet"] K2["cgroup_skb/ingress
every inbound packet"] K3[("LPM tries
egress_v4/v6, ingress_v4/v6
key: cgroup_id, port, proto, ip")] K4[("tls_sni_lpm + tls_alpn_deny")] K5["TLS ClientHello peeker
SNI + ALPN via bpf_loop
per-CPU 256-byte scratch"] K6{"Verdict
SK_PASS / SK_DROP"} K7[("Ring buffer 1 MiB")] K8[("default_cfg map
enforce, tlsPorts, blockQuic")] K1 -- lookup --> K3 K2 -- lookup --> K3 K5 -- "reversed-LPM" --> K4 K3 --> K6 K4 --> K6 K6 -- "flow event" --> K7 end subgraph AG["microsegebpf-agent.service - CAP_BPF, NET_ADMIN, PERFMON"] direction LR A1["pkg/loader (cilium/ebpf)
load .o, attach cgroupv2"] A2["pkg/policy
delta Map.Update
DNS cache 60s + stale-while-error
16 MiB file cap"] A3["pkg/identity
cgroup walker
inotify pub/sub Subscribe"] A4["pkg/observer (Hubble gRPC)
unix socket or TCP+TLS or mTLS"] A6(["Static Go binary - 4-component runtime closure
iana-etc, mailcap, agent, tzdata
NoNewPrivileges + ProtectSystem strict + SystemCallFilter"]) A3 -- "cgroup events" --> A2 A3 -- "cgroup events" --> A4 end A1 -- "attach + load" --> K1 A1 -- "ring read" --> K7 A2 -- "delta write" --> K3 A2 -- "delta write" --> K4 subgraph OPT["Optional co-located services - each opt-in via NixOS module"] direction LR O1["microsegebpf-log-shipper.service - Vector 0.52, DynamicUser
journald to parse_json to split to 4 sinks"] O2["hubble-ui OCI v0.13.5 (podman)
volume /run/microseg
binds 127.0.0.1:12000 only"] O3["systemd-journald
buffers stdout/stderr per boot
cursor in /var/lib/vector"] O4["microseg-probe CLI
-tls-ca, -tls-cert, -tls-key, -tls-server-name"] O3 --> O1 end A4 -- "gRPC unix or TCP+TLS" --> O2 A4 -- "gRPC" --> O4 AG -. "stdout/stderr" .-> O3 subgraph EXT["Configuration plane and external endpoints"] direction LR E1["GitOps flake + NixOS module
services.microsegebpf - enable, enforce,
policies, hubble.tls, dnsCacheTTL,
logs.opensearch, logs.syslog"] E2["Policy YAML
rules: cidr or host
selector: cgroupPath or systemdUnit
tls.sniDeny, tls.alpnDeny
8 baselines"] E3(["Operator"]) E4["DNS resolver
system /etc/resolv.conf
ideally local DNSSEC validating"] E5[("OpenSearch / SIEM
flows index + agent index
Vector elasticsearch sink")] E6[("Corp syslog SIEM
rsyslog, syslog-ng, Splunk, Wazuh
port 6514 RFC 5425 TLS")] end E1 -- "render flags" --> AG E2 -. "-policy=..." .-> A2 E3 -- "ssh -L 12000" --> O2 E3 -- "CLI inspect" --> O4 A2 -. "host: re-resolve" .-> E4 O1 -. "_bulk HTTPS" .-> E5 O1 -. "RFC 5425 TLS" .-> E6 style KER fill:#DBEAFE,stroke:#1E3A8A,stroke-width:2px style AG fill:#D1FAE5,stroke:#065F46,stroke-width:2px style OPT fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px style EXT fill:#FEF3C7,stroke:#92400E,stroke-width:2px ``` ## 解决了什么问题 ### 本地防火墙过滤与 eBPF 微隔离 —— 关键差异所在 传统的**本地防火墙**(`iptables`、`nftables` 以及工作站内置的主机防火墙)通过**网络身份**进行过滤:源 IP、目标 IP、端口、协议。它的心智模型是一个包含区域和区域间规则的网络图:“允许 `10.0.0.0/24` 访问 `10.0.0.5:443`”。在工作站上,这种机制非常粗糙 —— 用户运行的每个进程共享同一个 IP,因此每个进程都继承相同的策略。受损的浏览器标签页和合法的 `apt update` 在防火墙看来是一模一样的。更糟的是,如果本地防火墙没有明确关闭某些端口,同一内部子网上的两台工作站可以在所有端口上相互访问,这就是单台主机被攻陷后发生横向移动的经典前提条件。 **基于 eBPF 的微隔离**通过**工作负载身份**进行过滤:哪个进程、哪个用户、哪个 systemd 单元、哪个 cgroup。其心智模型是按应用程序制定策略:“Firefox 只能访问 `*.corporate.com:443`,其他一律禁止”。相同 IP 背后的相同目标会根据*谁*在发起请求而获得不同的判决结果。位于相同 `/24` 网段内的两台工作站不再默认互信 —— 每台工作站的代理都在内核级别执行自己的最小权限入站和出站控制,即使底层的网络结构允许它们通信。 `nixos-microsegebpf` 在单个 Linux 设备上为您提供第二种模型,使用自然的工作站身份标识(cgroupv2 id、systemd 单元、uid),而不是 Cilium 所需的 Kubernetes Pod 标签。 #### 横向移动与按应用出站控制示意图 同一机群的两个互补视图。`OK` = 规则集允许;`DROP` = 数据包离开工作站(或入站时在送达之前)被 `cgroup_skb` 钩子丢弃。 **视图 1 —— 扁平 /24 LAN 上的横向移动。** 位于 `10.0.0.0/24` 网段的三台 NixOS 工作站、一个企业堡垒机,以及相同的部署每台工作站的 GitOps flake。传统防火墙将允许每台工作站通过所有端口访问其他工作站;每台工作站上的 eBPF 代理将 LAN 变成了按主机划分的策略区域。 ``` %%{init: {'theme':'base','themeVariables':{'primaryColor':'#FFFFFF','primaryTextColor':'#0F172A','primaryBorderColor':'#475569','lineColor':'#475569','fontFamily':'monospace','fontSize':'13px'}}}%% flowchart LR GIT["Central GitOps flake
services.microsegebpf
+ policy bundle"] subgraph LAN["Corporate LAN 10.0.0.0/24 - flat L2, no firewall between workstations"] direction TB WSA["NixOS WS-A 10.0.0.10
+ microsegebpf-agent"] WSB["NixOS WS-B 10.0.0.20
+ microsegebpf-agent"] WSC["NixOS WS-C 10.0.0.30
+ microsegebpf-agent"] BAS["Bastion 10.0.0.42
any OS, no agent required"] end GIT -- "nixos-rebuild switch
(deploy-rs / colmena / morph)" --> WSA GIT --> WSB GIT --> WSC WSA -- "DROP SMB:445" --> WSB WSB -- "DROP http:80" --> WSC WSA -- "DROP ssh from peer" --> WSC BAS -- "OK ssh:22 (whitelisted)" --> WSA BAS --> WSB BAS --> WSC style GIT fill:#FCE7F3,stroke:#9D174D,stroke-width:2px style LAN fill:#FEF3C7,stroke:#92400E,stroke-width:2px style WSA fill:#D1FAE5,stroke:#065F46,stroke-width:1.5px style WSB fill:#D1FAE5,stroke:#065F46,stroke-width:1.5px style WSC fill:#D1FAE5,stroke:#065F46,stroke-width:1.5px style BAS fill:#E5E7EB,stroke:#475569,stroke-width:1.5px,stroke-dasharray: 4 2 ``` **视图 2 —— 单个工作站上按 cgroup 的出站控制。** 放大 WS-A,展示相同的互联网网关如何根据发起数据包的 cgroup 产生不同的判决结果。基于(源 IP,目标 IP,端口)进行过滤的传统防火墙要么允许来自 `10.0.0.10` 的所有流量,要么阻止所有流量 —— 无法实现按应用的细粒度控制。 ``` %%{init: {'theme':'base','themeVariables':{'primaryColor':'#FFFFFF','primaryTextColor':'#0F172A','primaryBorderColor':'#475569','lineColor':'#475569','fontFamily':'monospace','fontSize':'13px'}}}%% flowchart LR subgraph WSA["NixOS WS-A 10.0.0.10 + microsegebpf-agent"] direction TB A_FF["Firefox
app-firefox.scope"] A_APT["apt
system.slice"] end GW["Internet gateway / NAT"] subgraph NET["Internet"] direction TB CORP["corporate.com:443"] REPO["repo.corp.com:443"] DOH["1.1.1.1 DoH:443"] FBC["fbcdn.net:443
Facebook CDN"] end A_FF -- "OK corp.com:443" --> GW A_FF -- "DROP DoH 1.1.1.1" --> GW A_FF -- "DROP fbcdn" --> GW A_APT -- "OK repo.corp.com:443" --> GW GW --> CORP GW --> REPO GW -. dropped at cgroup_skb .-> DOH GW -. dropped at cgroup_skb .-> FBC style WSA fill:#D1FAE5,stroke:#065F46,stroke-width:1.5px style GW fill:#EDE9FE,stroke:#5B21B6,stroke-width:1.5px style NET fill:#F3F4F6,stroke:#475569,stroke-width:1.5px ``` 这些视图说明了: * **每台工作站都是 NixOS。** 相同的 flake 在 WS-A、WS-B、WS-C 上声明了 `services.microsegebpf` 模块 + 相同的策略包;`nixos-rebuild switch`(或 `deploy-rs` / `colmena` / `morph`)可以在一次推送中将策略变更分发到整个机群。堡垒机可以是任何操作系统,不需要安装此代理 —— 它只是允许的入站流量的*来源*,而不是需要被监管的对等方。 * **横向移动被遏制。** WS-A 尝试对 WS-B 进行 SMB 扫描,或对 WS-C 发起 SSH 会话,会分别被 WS-B 和 WS-C 自身的入站钩子丢弃 —— 即使 LAN 结构中没有任何东西阻止数据包到达。而传统防火墙会将其放行。 * **仅允许来自堡垒机的 SSH。** 每台工作站的 `sshd-restrict` 基线策略仅允许来自 `10.0.0.42/32` 的 22 端口入站连接,并丢弃其他所有流量,因此公司的跳板机仍可用于应急响应 (IR),而受感染的对等主机则无法连接。 * **按应用控制的出站。** WS-A 上的 Firefox 被允许访问 `corporate.com:443`(企业 Web 应用),但被禁止访问 `1.1.1.1`(会绕过企业 DNS)和 `*.fbcdn.net`(合规使用策略)。在 `system.slice` 中运行的 `apt` 被允许在同一端口上访问企业仓库,因为策略的匹配依据是 cgroup,而不是 IP。同一工作站上的两个进程,通过相同的网关,会获得不同的判决结果。 ### 通过 Nix 进行集中化管理及大规模部署 将此项目作为 NixOS 模块 + flake 提供的全部意义在于,运维人员的工作流与管理工作站任何其他配置的工作流完全一致: 1. 机群中每台工作站的微隔离策略包都**位于一个 git 仓库中**,用 Nix 表达。无需在目标主机上编辑逐个主机的 YAML。 2. 策略的变更会经过**与任何其他配置变更相同的审查和 CI 关卡**:`nix flake check` 会启动一个 NixOS 虚拟机,应用新策略,并在变更触及真实工作站之前断言内核内的丢弃判决。 3. 推送部署通过机群已经在使用的任何 NixOS 部署工具(`nixos-rebuild switch`、`deploy-rs`、`colmena`、`morph`)进行。systemd 会注意到 `/nix/store` 中策略文件的路径发生了改变,重新启动 `microsegebpf-agent`,eBPF 映射表会在不到一秒钟内在每台主机上重新加载。 4. 回滚只需执行 `nixos-rebuild --rollback` —— 策略的上一代版本仍然保留在存储中。 这在 **ANSSI 工作站安全加固背景下** 非常重要,对 *poste admin*(管理员工作站)进行微隔离的理由是阻止攻击者在扁平的内部子网上轻易进行横向移动。如果没有这种隔离,您只有两个毫无吸引力的选择: * **网络侧的微隔离**(每台主机独享私有 VLAN,带有基于 MAC 策略的 NAC,内部防火墙网格)—— 运维成本高昂,需要更改交换机/路由器/设备,这通常超出了小型运维团队的能力范围。 * **逐台单独编辑的主机防火墙规则** —— 缺乏一致性,没有审查记录,且一旦某台主机的配置发生偏离,整个机群就会退回到“信任所有内部流量”的状态。 `nixos-microsegebpf` 降低了这些成本:在每台工作站的内核中执行策略(无需购买或运维新设备),并且管理平面是一个 git 仓库,其形态和工具链与团队已经用于其余 NixOS 配置的完全相同。达到 ANSSI 级别的横向移动遏制仅仅是一项配置变更,而不再是一个庞大的基础设施项目。 ### 具体用例 | 目标 | 策略形式 | 防御对象 | |---|---|---| | **遏制受损的浏览器** | `selector: { systemdUnit: "app-firefox-*.scope" }` + 拒绝到 RFC1918 地址的出站流量 | 试图扫描内网主机或以此为跳板的恶意浏览器扩展 | | **强制使用企业 DNS** | `selector: { cgroupPath: /user.slice }` + 拒绝到公共解析器的 TCP/UDP/53, /443, /853 流量 | DNS 隧道数据外泄,DoH/DoT 绕过企业过滤器 | | **将 SMTP 限制为仅限 MTA** | `selector: { cgroupPath: / }` + 仅允许 TCP/25 到中继 CIDR | 使用硬编码 SMTP 服务器进行数据外泄的恶意二进制文件 | | **锁定 sshd 入站连接** | `selector: { systemdUnit: sshd.service }` + 仅允许来自堡垒机 CIDR 的入站流量 | 暴露在互联网上的 `sshd` 遭到凭证填充攻击 | | **拦截已知的 C2 IP** | `selector: { cgroupPath: / }` + 拒绝到威胁情报 IP 列表的出站流量 | 磁盘上已有的恶意二进制文件的信标通信 (Beaconing) | | **在 Hubble 中审计一切** | `enforce = false` + 仅观察模式 | 在编写任何丢弃规则之前,映射工作站的流量表面 | ### 与已有方案的差异 | 已有方案... | 在上述用例中缺失的部分 | microseg-poste 新增的功能 | |---|---|---| | `nftables` / `iptables` | 按进程的规则需要 `cgroup` 匹配扩展,且原生不支持 systemd 单元名 | 开箱即用的按 systemd 单元的规则;提供用于可视化的 Hubble UI | | AppArmor / SELinux | 没有*网络目的地*策略的概念;它们仅限制 syscall 参数和文件访问 | 在数据包边界处的网络层策略执行 | | Tetragon | 执行方式为 `SIGKILL` 或 syscall 覆盖 → 会杀死进程。在桌面环境中具有破坏性(浏览器会话丢失) | 数据包级别的 `SK_DROP` → 连接干净地失败,应用程序继续运行 | | Cilium | 需要 Kubernetes;使用 Pod 标签作为身份标识 | 无需集群,无需 K8s;以 cgroup id + systemd 单元作为身份标识 | | OpenSnitch / Little Snitch | 交互式的逐连接提示;适合个人使用,但不适用于 ANSSI 风格的策略执行 | 声明式 YAML/Nix 策略,对 GitOps 友好,无用户提示 | ### 性能预算 针对工作站而非 10 GbE 路由器进行了规模调整。下表中的每个数据都是基于 `bpf/microseg.c` 中的 eBPF 结构定义以及已发布的 cgroup_skb 基准测试得出的分析最坏情况;有关逐行推导,请参见 [ARCHITECTURE.md §11](ARCHITECTURE.md)。 | 层级 | 开销 | 时机 | |---|---|---| | `cgroup_skb` 每包钩子(查找 + 判决) | **100-250 ns** | 每个数据包,双向 | | TLS ClientHello 探测(通过 `bpf_loop` 解析 SNI + ALPN) | **1-6 μs** | 在 `tlsPorts` 上的每个新 TCP 连接一次 | | BPF 映射表内存(最坏情况,所有容量全满) | **约 12 MB** | 始终;实际策略通常使用 <200 KB | | 代理用户空间 RSS(静态 Go 二进制文件) | **25-40 MB** | 始终 | | 代理稳态 CPU 占用 | **<单核的 0.1 %** | 始终;在增量 Apply 期间峰值约为 5-15 % | | Vector 日志转发器 RSS | 50-150 MB | 仅当 `logs.{opensearch,syslog}.enable = true` 时 | | `hubble-ui` OCI 容器 | 约 80 MB | 仅当 `hubble.ui.enable = true` 且连接了浏览器时 | | **全量开启、所有映射表全满时的总体开销** | **约 280 MB RSS,<单核的 2 %** | 极端的病理部署情况 | 具体来说,在一条饱和的 1 Gbit/s 线路上(约 8 万包/秒),cgroup_skb 的开销在主机上每个流量的双向总计**约占单核的 2%**。实际的办公室工作负载测量值通常在 1% 以下。 ### 何时不适合使用此项目 - **具有高带宽网络吞吐量的服务器。** `cgroup_skb` 每个数据包的开销约为 100-250 ns(参见 [§11.1](ARCHITECTURE.md));适用于工作站,但不适用于 10bE+ 服务器 —— 请在这些场景下使用带有 XDP 的原生 Cilium。 - **您希望按主机名进行过滤**(`*.facebook.com`)。本项目基于已解析的 IP 和(即将支持)TLS SNI 进行工作。要进行纯粹基于主机名的过滤,请配合使用 DNS 策略工具。 - **您需要 L7 检查**(阻止特定的 HTTP 路径,解析 JWT,按 API 端点进行速率限制)。这是 L7 代理(如 Envoy, Traefik, NGINX)的工作。Microseg-poste 刻意保持在 L3/L4 层。 - **您无法运行 ≥ 5.10 的内核。** cgroup_skb 挂载点和 LPM_TRIE 映射类型在更早的版本中就已存在,但 BTF / CO-RE 的可靠性是从 5.10 才真正开始有保障的。本项目在 6.12 上进行了测试。 ## 为什么会有这个项目 Cilium 和 Hubble 是为 Kubernetes 集群设计的。它们的身份模型围绕 Pod 标签构建,其数据路径挂载到 Pod 的 veth 接口,且 Hubble UI 期望流量来自由按节点的 `cilium-agent` 实例提供的 `hubble-relay`。在工作站上没有 Pod、没有 API 服务器,也没有标签 —— 因此 Cilium 并不适用。 [Tetragon](https://github.com/cilium/tetragon),Isovalent 对 Cilium 的裸金属提取版本,是最接近的匹配:它能在主机上加载 eBPF,提供 TracingPolicy CRD,并且无需集群即可运行。但是 Tetragon 有意将其范围限定在**运行时安全可观察性 + syscall 级别的策略执行**(kprobe + `SIGKILL` / 覆盖返回值)。它不提供网络数据路径:Tetragon 仓库中没有与 `bpf_lxc.c` / `bpf_host.c` 对等的组件,没有基于 LPM 的 CIDR 匹配,也没有在数据包级别按流执行的丢弃判决。 `nixos-microsegebpf` 填补了这一空白。它执行了 Cilium 在 Kubernetes 节点上所做的工作 —— 加载掌管数据包路径的 eBPF 程序,评估身份感知策略,发出 Hubble 流量 —— 但使用的是工作站自有的身份原语: - 本地端点的 **cgroupv2 id**(由 `bpf_get_current_cgroup_id` 原生返回) - 其 **systemd 单元名**,从 cgroup 路径派生(`/user.slice/user-1000.slice/app.slice/firefox.service` → `firefox.service`) - 其 **所属用户**,可通过相同的路径遍历获得 这意味着策略可以像 Cilium 策略针对 Pod 标签一样,精确瞄准“由 Firefox 启动的任何程序”或“`user.slice` 下的所有进程”。 ## 它的实际工作原理 代理运行后,每个数据包都会触发以下四件事: 1. **eBPF 钩子触发。** 挂载在 cgroupv2 根节点的 `cgroup_skb/egress`(或 `/ingress`)在数据包即将离开网卡(或刚刚到达时)将其拦截。处理程序读取 IP/L4 头部,向内核查询本地进程所属的 cgroup,并构建一个策略查找键。 2. **LPM 查找。** 代理维护四个 `BPF_MAP_TYPE_LPM_TRIE` 映射 —— `egress_v4`、`ingress_v4`、`egress_v6`、`ingress_v6`。键是一个紧凑的 `(cgroup_id, peer_port, protocol, peer_ip)` 元组,LPM 的 `prefix_len` 被设置为使 cgroup/端口/协议精确匹配,而 IP 匹配到配置的 CIDR 前缀。如果未匹配到,则回退到可配置的默认判决。 3. **应用判决。** eBPF 程序返回 `SK_DROP`(内核丢弃数据包,syscall 看到 `EPERM`)或 `SK_PASS`(正常转发)。无用户空间往返,无代理。 4. **发出流事件。** 与判决无关,程序在一个 1 MiB 的环形缓冲区上保留一个记录,包含五元组、判决、匹配的策略 id 和本地 cgroup。代理排空环形缓冲区,通过定期刷新的缓存用 systemd 单元名装饰每条记录,将其转换为 Cilium `flow.Flow` protobuf,并发布给每个连接的 Hubble 客户端。 ## 策略外观示例 ``` apiVersion: microseg.local/v1 kind: Policy metadata: name: deny-public-dns-from-user-session spec: selector: cgroupPath: /user.slice # every cgroup under this prefix egress: - action: drop cidr: 1.1.1.0/24 # full CIDR, LPM-matched ports: ["53", "443", "853"] # exact ports protocol: tcp - action: drop cidr: 2001:4860::/32 # IPv6 supported natively ports: ["443", "853"] protocol: tcp - action: drop cidr: 127.0.0.0/8 ports: ["8000-8099"] # ranges expanded server-side protocol: tcp ``` 一个策略可简化为“对于每个与选择器匹配的 cgroup,在每个方向上将 N 个条目推入 LPM 映射表。” 选择器可以通过 **systemd 单元 glob**(`firefox.service`,`app-firefox-*.scope`)或 **cgroup 路径前缀**(`/user.slice/user-1000.slice`)进行匹配。 ## 感知 TLS 的 SNI / ALPN 匹配(仅限查看) 基于 IP 的过滤在 CDN 面前会遇到瓶颈:数千个站点共享相同的 Cloudflare / Fastly / Akamai IP,仅靠 IP 规则要么会过度拦截(使合法目标瘫痪),要么会完全失效(如果目标的 IP 在策略写入时和运行时之间发生了变化)。microsegebpf 通过仅查看的 TLS 解析器增强了 L3/L4 数据路径,该解析器从 TLS ClientHello 中读取明文 SNI 主机名和第一个 ALPN 协议标识符,对其进行哈希处理,并应用优先于 IP 级别允许的拒绝判决。 **无解密。** SNI 和 ALPN 在 ClientHello(最初的 TLS 握手消息)中以明文传输。eBPF 解析器仅检查这两个扩展,不涉及其他内容;连接的其余部分对它是不可见的。 ### 模式 ``` apiVersion: microseg.local/v1 kind: Policy metadata: name: ban-doh-providers spec: selector: cgroupPath: / # documentary; see "Limits" below tls: sniDeny: - "1.1.1.1" - "cloudflare-dns.com" - "dns.google" alpnDeny: [] # see warning below ``` 在 Nix 中: ``` services.microsegebpf.policies = with microsegebpf.lib.policies.baselines; [ (deny-sni { hostnames = [ "facebook.com" "tiktok.com" "x.com" ]; }) (deny-alpn { protocols = [ "imap" "smtp" ]; }) # niche, see below ]; ``` ### 为什么这很重要:SNI 与 IP 演示 ``` $ curl https://cloudflare.com # IPv4 path A exit=28 (DROP — SNI matched 'cloudflare.com') $ curl --resolve cloudflare.com:443:1.1.1.1 https://cloudflare.com # IPv4 path B exit=28 (DROP — SNI still 'cloudflare.com', different peer IP) $ curl https://example.com # unrelated exit=0 (ALLOW) ``` 隐藏在不同 IP 背后的相同主机名,或者隐藏在我们无法预测的 CDN 背后的主机名,同样会被拦截。 ### 覆盖范围矩阵 SNI/ALPN 解析器能看到和看不到的内容明细,以免您感到意外: | 协议 / 上下文 | 已覆盖? | 原因 | |---|---|---| | 基于 TLS 的 HTTP/1.1,基于 TLS 的 HTTP/2,基于 TLS 的 gRPC | ✅ | 无论上层承载的是何种 L7 协议,TLS ClientHello 都是相同的。 | | **HTTP/3 / QUIC** | ⚠ 仅支持全面拦截 | QUIC 的 TLS ClientHello 使用从目标 Connection ID 派生的密钥进行加密;在内核中派生它们需要 AES-128-CTR + AES-128-GCM,这超出了 eBPF 的执行能力。设置 `services.microsegebpf.blockQuic = true`(CLI 标志 `-block-quic`)可丢弃所有发往您配置的 `tlsPorts` 的 UDP 出站流量。浏览器会因此回退到 TCP/TLS,此时 SNI 解析器即可发挥作用。 | | **STARTTLS**(SMTP 提交/587,IMAP/143,XMPP) | ❌ | TLS 握手发生在明文交换(`STARTTLS\n`)之后,并在流中间到达。我们的解析器仅检查新 TCP 连接上的第一个数据包。 | | 非标准端口上的 TLS | ✅ 通过配置支持 | 设置 `services.microsegebpf.tlsPorts = [ 443 8443 4443 ];`(或 `-tls-ports=443,8443,4443`)。最多支持 8 个端口。SNI 解析器会在发往这些端口的 TCP 出站流量上被触发。 | | **通配符 SNI**(`*.example.com`) | ✅ | 通过在反转主机名上构建 LPM 字典树来实现(类似于 Cilium 的 FQDN 方法)。模式存储了 `.example.com` 反转后的字节,并以一个点作为终止符;查找时会反转在线传输的 SNI,字典树将选择最长匹配的前缀。仅支持最左侧标签的单级通配符(即 `*.foo.com`,不支持 `evil*.foo.com` 或 `foo.*.com`)。 | | **通过 FQDN 主机名实现的 L3/L4**(`host: api.corp.example.com`) | ✅ | 在任何出站/入站规则中使用 `host:` 而不是 `cidr:`。代理通过系统解析器将 FQDN 解析为 A 和 AAAA 记录,并为每个解析出的地址安装一个 `/32`(v4)或 `/128`(v6)的条目。每次 Apply 时都会重新解析(由 cgroup 事件驱动或由后备定时器触发),因此当 DNS 记录发生变化时,规则也会随之适应。解析失败会记录警告并在该轮操作中跳过该规则。 | ### 限制 - **TLS 1.3 ECH(加密客户端问候)** 是长期的威胁。当目标协商 ECH 时(Cloudflare 和 Firefox 自 2024 年以来已逐步推广此功能),SNI 会被加密,解析器会静默失效。请做好这在未来 2-3 年内成为默认设置的打算。 - **分片的 ClientHello。** 解析器仅检查承载 ClientHello 的第一个 TCP 段中的线性部分。实际上,每个常见客户端都能将 SNI/ALPN 扩展放入第一个段中(通常约 512 字节,远在 MTU 范围内)。如果病态的客户端发送 16 KiB 的预共享密钥扩展导致其跨段分割 —— 这些将会漏网。 - **按 cgroup 的作用域划定。** 概念验证阶段的 TLS 映射表仅使用主机名/ALPN 字符串的 FNV-64 哈希作为键。因此,SNI 拒绝规则是全局性的:无论周围策略文档的选择器如何,每个 cgroup 都受其约束。在这种情况下,策略级别的选择器字段仅作说明用途。要实现按 cgroup 的 TLS 规则需要使用 `(cgroup_id, hash)` 作为键 —— 这是一个合理的后续开发方向。 - **仅限第一个 ALPN 条目。** 遍历器仅检查 ALPN 列表中的第一个协议。这足以捕获单一用途的信标(仅限 `h2`);如果恶意客户端发送 `["h2", "x-evil"]`,排在第二的 `x-evil` 就会漏网。 - **一刀切拦截 ALPN `h2` 是个危险操作。** 几乎所有现代 HTTPS 客户端都会广播 `h2`。请将 `alpnDeny` 用于范围更窄的协议禁令(如 `imap`,`smtp`,自定义标识符),或者用于协议白名单较短的物理隔离部署中。 ## 配置方案 涵盖最常见工作站安全加固场景的八个详细示例。前六个是您可以放入部署 flake 的 `services.microsegebpf.policies` 片段;最后两个(集中式日志传送到 OpenSearch 和 syslog)配置了代理外围的运维管道。 ### 方案 1 —— 强制使用企业 DNS 解析器 **用例。** 您运行着一个企业 DNS 解析器(带有日志记录、恶意软件拦截列表、内部区域记录)。您不希望用户的浏览器、包管理器或受感染的二进制文件通过与 Cloudflare 的 `1.1.1.1` 直接通信来绕过它,或者更糟的是,通过 DoH(`https://1.1.1.1/dns-query`)或 DoT(`tcp/853 to 8.8.8.8`)进行隧道传输。 **为什么这很重要。** 直接连接公共解析器会绕过所有企业的过滤、日志记录和检测 —— 这既涉及日常的策略违规,也涉及恶意软件利用 DNS 建立的 C2 通信。 ``` services.microsegebpf.policies = with microsegebpf.lib.policies; [ # Drop classic DNS, DoT, DoH to the well-known public resolvers. # The baseline ships a curated IP+port list for Cloudflare, Google, # Quad9, OpenDNS, AdGuard. (baselines.deny-public-dns { }) # Belt-and-suspenders: also block via SNI any host pretending to # be a DoH provider on a different IP (CDN re-routing, new IPs # not yet in the IP feed). The wildcard catches cdn-hosted # variants like resolver-dot1.dnscrypt.example.com. (mkPolicy { name = "deny-doh-providers-by-sni"; selector = { cgroupPath = "/"; }; sniDeny = [ "cloudflare-dns.com" "*.cloudflare-dns.com" "dns.google" "*.dns.google" "dns.quad9.net" "*.quad9.net" "doh.opendns.com" "*.dnscrypt.org" ]; }) ]; ``` **工作原理。** - `baselines.deny-public-dns {}` 在端口 `53`、`443` 和 `853` 上**阻止 TCP 和 UDP** 流量流向主要公共解析器的 IP(覆盖了普通 DNS、DoH、DoT)。默认情况下,它以 `/user.slice` 选择器为匹配依据;传入 `cgroupPath = "/"` 也可将其扩展到系统服务。 - 自定义的 `mkPolicy` 添加了一个 **TLS SNI 拒绝列表**,这样即使目标 IP 不在我们的列表中,在 TLS 握手期间广播已知 DoH 提供商 SNI 的连接也会在 ClientHello 完成之前被丢弃。 - 通配符条目(`*.cloudflare-dns.com`)无需逐一列举每个接入点 即可捕获 CDN 边缘节点变体。 **变体。** - 要仅对**您的**企业解析器允许 DoH,请将其附加到 `deny-public-dns` 的 `extraIPv4` 和 `extraIPv6` 中,以维持基础拦截名单,同时通过来自 `mkPolicy` 的显式 `allow` 条目豁免您的 IP。 ### 方案2 —— 浏览器隔离:无法访问内部网络 **用例。** Firefox / Chromium 每天都在运行不受信任的 JavaScript。被武器化的扩展或 0-day RCE 不应该能够扫描企业网络 `10.0.0.0/8`,攻击位于 80 端口的内部 Confluence,或发起横向的 SMB 攻击。 **为什么这很重要。** 这是单项影响最大的 ANSSI 工作站安全加固措施:它将“浏览器被攻陷”从“攻击者现在可以看到内部网络”转化为“攻击者拥有一个被沙箱化的浏览器进程,但没有可用的 LAN 网络句柄”。 ``` services.microsegebpf.policies = with microsegebpf.lib.policies; [ # The baseline blocks egress from /user.slice to RFC1918 on the # commonly-attacked ports (SSH, HTTP, HTTPS, SMB, RDP, alt-HTTP). (baselines.deny-rfc1918-from-user-session { }) # Carve out a per-unit allow for IT helpdesk SSH (the user # legitimately needs to ssh to the bastion). (mkPolicy { name = "allow-bastion-ssh-from-user"; selector = { cgroupPath = "/user.slice"; }; egress = [ (allow { cidr = "10.0.0.42/32"; # bastion IP ports = [ "22" ]; protocol = "tcp"; }) ]; }) ]; ``` **工作原理。** - 基线策略在所有三个 RFC1918 范围内丢弃六个常见端口。浏览器标签页、邮件客户端以及 `/user.slice` 下的任何其他程序都无法访问这些端口上的内部服务。 - 切出部分是一个优先级更高的 `allow`,它重新启用了唯一合法的路径。**规则优先级**:LPM 字典树为每个 `(cgroup, port, proto)` 元组选择最长前缀匹配,因此 `/32` 条目胜过针对 `10.0.0.42:22` 的 `/8` 拒绝规则。 - 到公共互联网(`0.0.0.0/0` 减去 RFC1918)的浏览器网络 IO 不受影响 —— 没有引入隐含的出站防火墙。 **变体。** - 扩展到特定 systemd 单元的所有子 cgroup:`selector = { systemdUnit = "app-firefox-*.scope"; }`。 - 对于不在按标签页进程隔离范围内的 Chromium,请将 `cgroupPath = "/user.slice"` 切换为针对 Chromium 特定作用域名称的更严格选择器。 ### 方案 3 —— 仅将 SSH 限制于堡垒机 **用例。** 生产环境工作站暴露了 `sshd` 以便进行事件响应,但只应允许位于 `10.0.0.42` 的企业堡垒机访问它。暴露在互联网上的 `sshd` 是凭证填充攻击的磁铁,即使配置不当的防火墙意外放行了数据包,工作站也应拒绝来自任何其他来源的 SSH 连接。 ``` services.microsegebpf.policies = with microsegebpf.lib.policies; [ (baselines.sshd-restrict { allowFrom = "10.0.0.42/32"; }) ]; ``` **工作原理。** - `selector = { systemdUnit = "sshd.service"; }`(在基线策略内部设置)目标是 systemd 为 `sshd` 创建的 cgroup。 - 基线策略发出一个单一的 `ingress` 规则:`allow { cidr = allowFrom; ports = [ "22" ]; protocol = "tcp"; }`。由于没有列出 `drop` 规则,且在策略模块上设置了 `defaultIngress = "drop"`,因此默认拒绝所有其他来源的连接。 - **您必须在模块级别设置 `services.microsegebpf.defaultIngress = "drop"` 才能使其生效** —— 否则未匹配的流量将回退到默认允许。 **变体。** - 用于多个堡垒机 IP:传入一个 `/24`(`"10.0.0.0/24"`)或堆叠多个 `mkPolicy` 调用,每个调用添加一个 IP。 - 对于位于不同端口上的高可用堡垒机对,请去掉基线策略,直接使用带有两个 `ingress` 规则的 `mkPolicy`。 ### 方案 4 —— 仅通过企业中继的 SMTP 出站 **用例。** 试图通过直接 SMTP 连接到硬编码邮件服务器进行数据外泄的受感染二进制文件应当操作失败。合法的路径是通过企业 MTA(通常是 `smtp-relay.corp:25`)。 ``` services.microsegebpf.policies = with microsegebpf.lib.policies; [ (baselines.smtp-relay-only { relayCIDR = "10.0.1.10/32"; port = "25"; }) ]; ``` **工作原理。** - `selector = { cgroupPath = "/"; }` —— 应用于主机上的每个 cgroup(包括系统服务和用户进程)。 - 按优先级(LPM,最长匹配胜出)排列的两个规则: 1. 允许(`allow`)到 `10.0.1.10/32` 的端口 25 流量(`/32` = 32 位前缀) 2. 拒绝(`drop`)到 `0.0.0.0/0` 的端口 25 流量(`/0` = 0 位前缀) - 中继的 `/32` 始终胜过全捕获的 `/0`,因此合法邮件得以发送;端口 25 上的其他一切流量均被拒绝。 **变体。** - 对于端口 465 上的 SMTPS 或 587 上的提交端口,传入 `port = "465"` 或 `port = "587"` 并堆叠两个策略。 - 要豁免特定的 systemd 单元(例如 `postfix.service`)不受此拒绝规则限制,请添加一个 `mkPolicy`,使用 `selector = { systemdUnit = "postfix.service"; }` 以及显式 `allow` 到 `0.0.0.0/0:25` —— 其 `/32` 样式的 cgroup 匹配具有更高优先级。 ### 方案 5 —— 通过 SNI 通配符屏蔽社交媒体 **用例。** 合规/可接受使用策略禁止在工作时间访问 TikTok、Facebook、Instagram。基于 IP 的拦截是徒劳的(托管在 CDN 上,IP 不断轮换),但 SNI 匹配可以捕获服务于该品牌的每个 CDN 边缘。 ``` services.microsegebpf.policies = with microsegebpf.lib.policies; [ (mkPolicy { name = "deny-social-media-sni"; selector = { cgroupPath = "/user.slice"; }; sniDeny = [ "facebook.com" "*.facebook.com" "fbcdn.net" "*.fbcdn.net" "instagram.com" "*.instagram.com" "tiktok.com" "*.tiktok.com" "*.tiktokcdn.com" "x.com" "*.x.com" "twitter.com" # legacy redirect "*.twitter.com" ]; }) # Force QUIC fallback so the SNI matcher actually fires. Without # this knob, browsers happily fetch tiktok.com over HTTP/3 (UDP) # and our TCP-only parser never sees the SNI. ]; services.microsegebpf.blockQuic = true; ``` **工作原理。** - `*.facebook.com` 匹配每个子域(`m.facebook.com`、`web.facebook.com`、`static.xx.fbcdn.net` 等)。将其与裸域名 `facebook.com` 配对,以同时捕获顶点域。 - 一个策略中的多个站点只是一个更长的列表 —— LPM 字典树可以扩展到数千个条目,查找开销为 O(字符串长度)。 - `services.microsegebpf.blockQuic = true` 在这里**至关重要**。HTTP/3 的 TLS ClientHello 是加密的;我们无法查看 UDP/443 上的 SNI。迫使 QUIC 失败会让浏览器在 TCP/443 上重试,此时 SNI 解析器即可发挥作用。 **变体。** - 对于白名单方法(只允许用户访问 `*.corporate.com`),反转思路:设置 `defaultEgress = "drop"` 并编写带有 `egress` `allow` 规则的 `mkPolicy`,仅允许您想要放行的目的地。SNI 匹配本身是一项*仅拒绝*功能(在 SNI 侧没有允许覆盖;IP 级别的判决才是事实来源)。 ### 方案 6 —— 结合 TLS 端口加固 + 威胁情报订阅集成 **用例。** 您使用每日威胁情报订阅(一个已知通过 HTTPS 或异常 TLS 端口提供 C2 服务的恶意 IP 列表),并希望 microsegebpf 执行该策略而无需重新造轮子部署管道。 ``` let # Pulled at deploy time by the CI step that builds the workstation # closure. IO must happen at *build* time (Nix is hermetic at # eval), so a separate fetcher derivation feeds the list in. threatFeed = builtins.fromJSON (builtins.readFile ./threat-ips.json); in { services.microsegebpf = { enable = true; enforce = true; # Treat 443, 8443, and a custom corporate VPN port as TLS-bearing. # The SNI parser fires on TCP egress to any of these. tlsPorts = [ 443 8443 4443 ]; # Drop QUIC blanket so SNI-based enforcement isn't bypassed # via HTTP/3. blockQuic = true; policies = with microsegebpf.lib.policies; [ # Drop egress to every IP in the feed, on the same TLS-bearing # ports. The IP feed is the precise blocker; the SNI check # below catches re-hosted infrastructure. (baselines.deny-threat-feed { ips = map (ip: "${ip}/32") threatFeed.ips; ports = [ "443" "8443" "4443" ]; }) # Domain-side feed (different vendor, different threat surface). (mkPolicy { name = "deny-threat-feed-sni"; selector = { cgroupPath = "/"; }; sniDeny = threatFeed.domains; # plain + wildcard mix }) ]; hubble.ui.enable = true; # see what gets dropped, in real time }; } ``` **工作原理。** - `tlsPorts = [ 443 8443 4443 ]` 将 SNI 解析器扩展到企业 VPN 恰好使用的非标准端口上触发。SNI 匹配和 `blockQuic` 都遵循此列表。 - 威胁情报 IP 被放入标准的 L3/L4 LPM(由 `deny-threat-feed` 覆盖);其主机名被放入 SNI LPM(由自定义的 `mkPolicy` 覆盖)。单独一层就能捕获大多数信标;结合在一起则分别覆盖了 IP 轮换和域名轮换。 - `enforce = true` 开启丢弃。结合 `emitAllowEvents = false`(生产环境设置)以保持 Hubble 的信噪比。 **变体。** - 对于更新频率高于每次 NixOS 重建的情报订阅,通过 systemd 定时器将其抓取到 `/etc/microsegebpf/threat.yaml`,并设置 `services.microsegebpf.policies = [ (builtins.readFile "/etc/microsegebpf/threat.yaml") ]`。代理的 inotify 监视器会在约 250 毫秒内检测到更改。 - 构建一个最小的 nix derivation,以便在构建时抓取情报(使用带有哈希值的 `pkgs.fetchurl`),从而使闭包完全可复现 —— 代价是每次情报更新都需要重新构建。 ### 方案 7 —— 集中式日志传送到 OpenSearch **用例。** 一台在本地时间 03:14 丢弃了恶意流量的工作站,不应该要求分析师通过 SSH 登录并使用 grep 检索 journald 来查明情况。将每个流事件和每个控制平面日志传送到全机群范围的 OpenSearch 集群 —— SOC 已经查看的同一个地方 —— 这样第二天早上的调查只需一次 Kibana / OpenSearch Dashboards 查询,而不是一场繁琐的取证之旅。 **为什么这很重要。** 本地的 journald 适用于单台主机,但在机群规模下会崩溃:无跨主机关联,保留期限受限于主机的磁盘预算,无告警钩子。Hubble UI 非常适合交互式流量探索,但它同样短暂且局限于单台主机。集中的日志存储解决了这三个问题:跨主机搜索、数周至数月的长期保留,以及在 5 分钟内同一 C2 SNI 在三台不同主机上被拦截时触发 Sigma / Wazuh / OSSEC 规则的告警。 ``` services.microsegebpf = { enable = true; # ... your usual policy + observability bits ... logs.opensearch = { enable = true; # Any node of the cluster; Vector handles the bulk endpoint # routing internally. endpoint = "https://opensearch.corp.local:9200"; # Daily indices — the OpenSearch idiom for time-series data. # Strftime tokens are expanded by Vector at write time. indexFlows = "microseg-flows-%Y.%m.%d"; indexAgent = "microseg-agent-%Y.%m.%d"; # Basic auth (mandatory in production). The password is read # by systemd from the file at start time and passed to Vector # via LoadCredential — never on the command line, never in # the unit's environment as plaintext. auth.user = "microseg-shipper"; auth.passwordFile = "/run/keys/opensearch-microseg.pwd"; # TLS pinning to the corporate CA. Set verifyCertificate=false # only in a lab — the warning in the journald sink is loud # for a reason. tls.caFile = "/etc/ssl/certs/corp-internal-ca.pem"; }; }; ``` **工作原理。** - 代理**不直接与 OpenSearch 通信。** 它将结构化 JSON 写入 stdout(每个流事件一行)和 stderr(slog 控制平面记录)。systemd 使用 `_SYSTEMD_UNIT=microsegebpf-agent.service` 将两者都捕获到 journald 中。 - 该模块启用了第二个 systemd 单元(`microsegebpf-log-shipper.service`),在 `DynamicUser=true` 下运行 [Vector](https://vector.dev)。Vector 配置由 Nix 作为 JSON 文件在存储中生成,因此它作为 NixOS 闭包差异的一部分,是可复现且可审查的。 - Vector 管道由四个节点组成: 1. `sources.microseg_journal` —— 仅过滤到代理单元的 `journald` 源(`include_units` = `[ "microsegebpf-agent.service" ]`),`current_boot_only = true`。 2. `transforms.microseg_parse` —— VRL `remap`,将 `.message` 解码为 JSON,并将解析后的字段合并到事件根目录中。非 JSON 行原样通过。 3. `transforms.microseg_filter_{flows,agent}` —— 两个基于 `exists(.verdict)` 拆分的 `filter` 转换,因此流事件和 slog 记录会落入不同的索引中。 4. `sinks.opensearch_{flows,agent}` —— 两个 `elasticsearch` 接收器(Elasticsearch 线协议与 OpenSearch 相同),使用批量模式写入配置的索引。 - 转发器单元受到沙箱保护:`DynamicUser=true`,`ProtectSystem=strict`,`RestrictAddressFamilies` 仅限于 `AF_INET/AF_INET6/AF_UNIX`,系统调用过滤器为 `@system-service` + `@network-io`。它只需要到 OpenSearch 的网络出站权限和对 journald 的读取权限(通过 `SupplementaryGroups = [ "systemd-journal" ]` 授予)。 - **解耦非常重要。** 如果 OpenSearch 集群宕机,Vector 会以指数退避方式重试 —— 代理及其 eBPF 数据路径继续运行。如果转发器单元崩溃,journald 会继续缓冲,Vector 在重启时会从游标离开的地方继续处理。日志管道的宕机绝不会导致策略执行被中断。 **变体。** - **在源头添加字段**(例如,用工作站的主机名和 ANSSI 区域标记每个事件)—— 使用 `extraSettings` 在 `microseg_parse` 和过滤器之间插入另一个 `remap`: services.microsegebpf.logs.opensearch.extraSettings = { transforms.add_zone = { type = "remap"; inputs = [ "microseg_parse" ]; source = '' .anssi_zone = "poste-admin" .hostname = get_hostname!() ''; }; transforms.microseg_filter_flows.inputs = [ "add_zone" ]; transforms.microseg_filter_agent.inputs = [ "add_zone" ]; }; - **每个流使用不同的集群**(冷归档与热 SOC)—— 通过 `extraSettings` 覆盖其中一个接收器,使其指向带有不同认证的第二个端点。 - **针对不可靠 WAN 链路的磁盘支持缓冲** —— 设置 `extraSettings.sinks.opensearch_flows.buffer = { type = "disk"; max_size = 268435456; }`(上限为 256 MiB)。`data_dir = /var/lib/vector` 已连接完毕(带有 `StateDirectory = "vector"`,因此 `DynamicUser` 仍然有效)。 - **通过使用主机名模板化索引名称来为每个工作站保留一个 OpenSearch 索引**:`indexFlows = "microseg-flows-\${HOSTNAME}-%Y.%m.%d";`(Vector 在索引模板中扩展环境变量systemd 已经将 HOSTNAME 注入到单元的环境中)。 ### 方案 8 —— 集中式 syslog 转发(基于 TLS 的 RFC 5424) **用例。** 您的 SOC 拥有一台 SIEM(Splunk、Wazuh、ELK、IBM QRadar、Microsoft Sentinel 等),通过 syslog 接收日志。您希望每个流事件和每个代理控制平面日志都能以正确的设施代码存入其中,以便 SIEM 现有的解析和告警管道从第一天起就能触发。 **为什么这很重要。** OpenSearch 非常适合临时查询,但您 SOC 的事件响应工作流可能运行在 SIEM 上:关联规则、工单系统集成、MITRE ATT&CK 映射、待命呼叫。一台已经知道如何处理来自 NixOS 工作站的 `local4.warning` 的 SIEM,比一个没人值班待命的全新 OpenSearch 集群更容易上手。 **为什么使用 TLS。** 流事件包含丢弃流量的工作站名称、特定目的地和特定端口 —— 这正是已经渗透进内部的攻击者想要的资产清单。明文 UDP/514 syslog 会将所有这些信息泄露给路径上的任何被动窃听者。基于 RFC 5425 的 syslog-over-TLS(端口 6514)是现代默认选择;该模块默认设置 `mode = "tcp+tls"`,如果您降级到 UDP 或明文 TCP,会在部署时发出警告。 ``` services.microsegebpf = { enable = true; # ... your usual policy + observability bits ... logs.syslog = { enable = true; # SIEM collector. Port 6514 is the IANA assignment for # syslog-over-TLS (RFC 5425). Vector connects directly — # no rsyslog or syslog-ng relay in between. endpoint = "siem.corp.local:6514"; # Default; spell it out so the intent is reviewable. mode = "tcp+tls"; # APP-NAME field of the RFC 5424 header. SIEMs route on # this — keep it short (<= 48 ASCII chars) and stable. appName = "microsegebpf"; # Facilities. `local4` is a common SIEM convention for # security-relevant network logs; `daemon` is the # canonical service control-plane bucket. facilityFlows = "local4"; facilityAgent = "daemon"; # Pin the SIEM's CA. For mTLS, also set certFile + keyFile; # the key is loaded via systemd LoadCredential so it can # live on a path the dynamic user can't read directly # (e.g. /etc/ssl/private mode 0640 root:ssl-cert). tls = { caFile = "/etc/ssl/certs/corp-internal-ca.pem"; certFile = "/etc/ssl/certs/microseg-client.pem"; # mTLS, optional keyFile = "/etc/ssl/private/microseg-client.key"; # mTLS, optional # keyPassFile = "/run/keys/microseg-key-pass"; # if encrypted verifyCertificate = true; verifyHostname = true; }; }; }; ``` **工作原理。** - 该模块连接了一个与 OpenSearch 并行(或替代)的 Vector 管道 —— 同样的 `microsegebpf-log-shipper.service`,同样的 journald 来源,同样的解析 + 过滤转换。两个额外的 `remap` 转换将每个流格式化为 RFC 5424:1 TIMESTAMP HOSTNAME APP-NAME - - - JSON-BODY
`PRI = facility * 8 + severity`。严重性根据事件从 slog 的 `.level`(代理流)或 `.verdict`(流数据流)计算得出:drop → 4 (warning),log → 5 (notice),allow → 6 (info);ERROR → 3,WARN → 4,INFO → 6,DEBUG → 7。
- 两个 `socket` 接收器通过 TCP+TLS 和以换行符分隔的帧写入配置的端点(兼容 rsyslog 的 `imtcp`,syslog-ng 的 `network()`,Splunk HEC syslog,Wazuh 的 6514 端口监听器)。
- 在网络线路上,三个示例事件如下所示:
<164>1 2026-04-20T07:53:10.457337Z host microsegebpf - - - {"verdict":"drop","src":"10.0.0.1:443","dst":"1.1.1.1:443","unit":"firefox.service",...}
<165>1 2026-04-20T07:53:10.457355Z host microsegebpf - - - {"verdict":"log","src":"10.0.0.1:53","dst":"9.9.9.9:53","unit":"dnsmasq.service",...}
<166>1 2026-04-20T07:53:10.457362Z host microsegebpf - - - {"verdict":"allow","src":"10.0.0.1:80","dst":"8.8.8.8:80","unit":"sshd.service",...}
164 = local4(20) * 8 + warning(4); 165 = local4 + notice;
166 = local4 + info。仅根据 PRI 进行路由的 SIEM 会将丢弃事件直接推送到更高关注度的处理桶,而无需任何自定义解析。
- **解耦机制与 OpenSearch 转发器相同。** TLS 握手失败或 SIEM 宕机 → Vector 带退避重试,代理和 eBPF 数据路径继续执行。journald 继续缓冲,直到游标(位于 `/var/lib/vector/`)赶上进度。
**变体。**
- **同时使用 OpenSearch 和 syslog**(典型的 SIEM 部署):启用两个选项块。它们共享单个 `microsegebpf-log-shipper.service` Vector 实例 —— 一个进程,四个接收器(两个 ES 批量 + 两个 syslog 套接字)。
- **mTLS**(SIEM 对工作站进行身份验证):设置 `tls.certFile`,`tls.keyFile`(如果密钥已加密,还包括 `tls.keyPassFile`)。私钥通过 systemd 的 `LoadCredential` 从您使用的任何密钥管理路径(SOPS, agenix, vault-agent template)绑定挂载到单元中。
- **每个流使用不同的 SIEM**(一个用于判决,另一个由不同供应商用于审计日志):使用 `extraSettings` 覆盖 `sinks.syslog_flows.address`,同时保持默认的 `sinks.syslog_agent` 指向 `endpoint`。
- **严格的 RFC 5425 字节计数帧**(某些 IBM / 传统企业收集器要求此格式):设置 `framing = "bytes"` 并通过 `extraSettings` 添加一个 VRL 转换,在每个 `.message` 前添加 ASCII 十进制长度 + 空格。大多数现代收集器(rsyslog, syslog-ng, Splunk, Wazuh)都接受默认的 `newline_delimited`,因此您很少需要用到这个。
- **传统明文 TCP / UDP**(实验室,本地信任段或过渡期):设置 `mode = "tcp"` 或 `mode = "udp"`。该模块在评估时发出 NixOS 警告,因此该选择是显式的,并且可以在重建日志中进行审查。
## Hubble 集成
Hubble UI 是一个连接到使用 [`observer.proto`](https://github.com/cilium/cilium/blob/main/api/v1/observer/observer.proto) 的 gRPC 端点的 React 应用。
它在启动时调用四个 RPC:
| RPC | nixos-microsegebpf 返回的内容 |
|---|---|
| `ServerStatus` | 缓冲流的数量,“1 个已连接节点”(此主机),运行时间 |
| `GetNodes` | 一个带有本地主机名和 `NODE_CONNECTED` 状态的 `Node` 条目 |
| `GetFlows(stream)` | 先回放最近的流环,然后永远实时跟踪 |
| `GetNamespaces` | 空(我们不对 K8s 命名空间进行建模)|
每个流都是一个真实的 `flow.Flow` protobuf,包含:
- **IP**:源,目标,IPv4 或 IPv6 协议族
- **Layer4**:TCP 或 UDP 源/目标端口
- **源 / 目标端点**:当本地侧是源(出站)时,`Source` 带有 `cluster_name=host`,systemd 单元作为 `pod_name`,以及诸如 `microseg.unit=firefox.service`,`microseg.cgroup_id=12345` 的标签。远端成为 `world` 端点。在入站时,角色互换。
- **判决**:`FORWARDED`,`DROPPED`(带有 `DropReason=POLICY_DENIED`),或 `AUDIT`
- **流量方向**:`INGRESS` 或 `EGRESS`
结果:未修改的上游 Hubble UI 完全像渲染 Cilium Pod 那样,渲染我们工作站的流图。服务映射、流日志、丢弃可视化 —— 开箱即用。
一个小型的配套 CLI,`microseg-probe`,从命令行调用相同的 RPC 以进行无头检查:
```
$ microseg-probe -limit=10
=== ServerStatus ===
Version: nixos-microsegebpf/0.1 (hubble-compat)
NumFlows: 40 / 4096
ConnectedNodes: 1
=== GetFlows (limit=10) ===
DROPPED EGRESS 10.0.2.15:52606 -> 1.1.1.1:443 src=host/session-50.scope dst=world/ policy=1
FORWARDED EGRESS 10.0.2.15:22 -> 10.0.2.2:47861 src=host/sshd.service dst=world/ policy=0
...
```
### 使用 TLS / mTLS 保护 gRPC 观察器
默认监听器(`unix:/run/microseg/hubble.sock`,通过 `RuntimeDirectoryMode` 设置模式为 0750)被内核限制为仅限 root 访问 —— 无需传输身份验证。**TCP 监听器则是另一回事:** 代理观察到的每个流事件(五元组 + SNI)都会流式传输给任何能连接的人。如果您需要从工作站外部消费流数据,请配置 TLS —— 在生产环境中则需使用 mTLS。
```
services.microsegebpf.hubble = {
listen = "0.0.0.0:50051"; # or 127.0.0.1:50051 + SSH tunnel
tls = {
certFile = "/etc/ssl/certs/microseg-server.pem";
keyFile = "/etc/ssl/private/microseg-server.key";
clientCAFile = "/etc/ssl/certs/microseg-clients-ca.pem"; # mTLS
requireClientCert = true; # mTLS hard-on
};
};
```
如果没有设置 `certFile` + `keyFile`,该模块会发出一个评估期警告,指出明文暴露的风险(并且代理在启动时会发出一个运行时的 WARN slog 行,以呼应 Nix 评估期的消息)。`microseg-probe` CLI 镜像了相同的 TLS 选项(`-tls-ca`,`-tls-cert`,`-tls-key`,`-tls-server-name`,`-tls-insecure`),以便操作员可以进行端到端验证:
```
microseg-probe -addr=corp-host:50051 \
-tls-ca=/etc/ssl/certs/corp-ca.pem \
-tls-cert=/etc/ssl/certs/operator.pem \
-tls-key=/etc/ssl/private/operator.key \
-tls-server-name=corp-host \
-limit=10 -follow
```
### FQDN 解析缓存
`host:` 规则在每次 Apply 周期重新解析 DNS 名称。为了限制解析器投毒攻击窗口 —— 恶意 DNS 响应在 Apply 周期之间翻转 `/32` LPM 条目 —— 代理将结果缓存 `services.microsegebpf.dnsCacheTTL`(默认为 `60s`)。失败的重解析会回退到上次已知的正确答案,因此暂时的解析器故障不会丢弃之前已验证通过的 FQDN 规则。有关威胁模型,请参见 [SECURITY-AUDIT.md §F-3](SECURITY-AUDIT.md)。
## 构建
### 开发工作流
```
cd nixos-microsegebpf
nix-shell --run 'make build'
sudo ./bin/microseg-agent -policy=examples/policy.yaml
```
`nix-shell` 引入了 Go 1.25、clang 21、llvm 21、bpftool 7、libbpf、protoc、rsync。`make build` 运行 `bpftool` 将正在运行的内核的 BTF 转储到 `bpf/vmlinux.h` 中,调用 `bpf2go` 编译 `bpf/microseg.c` 并生成 Go 绑定,然后 `go build` 生成 `bin/microseg-agent` 静态二进制文件。
### 可复现的 Nix 构建
```
nix-build
sudo ./result/bin/microseg-agent -policy=examples/policy.yaml
```
`vendorHash` 固定在 `nix/package.nix` 中;当 `go.mod` 更改时重新计算:
```
nix-build 2>&1 | grep "got:" | awk '{print $2}'
# 粘贴到 nix/package.nix
```
Nix 构建期望预先生成的 `bpf/microseg_bpfel.{go,o}` 和 `bpf/vmlinux.h`(在 `nix-build` 之前,在 Nix 沙箱外运行一次 `make generate`)。这是因为沙箱无法访问 `/sys/kernel/btf/vmlinux`,并且为每个目标内核提供一个附带的 vmlinux.h 是不切实际的。
## NixOS 模块 + flake(推荐的 GitOps 工作流)
仓库提供了一个 `flake.nix`,它暴露了 `nixosModules.default`、`packages.default`、一个可组合的 `lib.policies` 库,以及一个启动 NixOS 虚拟机并断言数据路径确实丢弃匹配流的 `checks.vm-test`。预期的使用模式是在您现有的基础设施仓库中建立一个部署 flake:
```
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
microsegebpf.url = "github:aambert/nixos-microsegebpf";
};
outputs = { self, nixpkgs, microsegebpf, ... }: {
nixosConfigurations.workstation = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
microsegebpf.nixosModules.default
({ ... }: {
services.microsegebpf = {
enable = true;
policies = with microsegebpf.lib.policies.baselines; [
(deny-public-dns {})
(sshd-restrict { allowFrom = "10.0.0.0/24"; })
(deny-rfc1918-from-user-session {})
];
hubble.ui.enable = true;
};
})
];
};
# Re-export the upstream VM test so `nix flake check` in your
# infra repo gates deployments on the same end-to-end assertion.
checks = microsegebpf.checks;
};
}
```
### GitOps 工作流
1. 在您的基础设施仓库中将策略作为 Nix 表达式进行编辑(可组合,而非原始 YAML)。
2. 推送到 git。CI 运行 `nix flake check`。组合后的 `checks.vm-test` 启动一个 NixOS 虚拟机,应用新策略,并断言内核内的丢弃判决 —— 错误的策略在到达任何主机之前就会导致 CI 失败。
3. CI 通过您现有的 `nixos-rebuild switch --flake .`、`deploy-rs`、`colmena` 或 `morph` 步骤进行部署。
4. systemd 注意到新的 `ExecStart` 哈希值(`/nix/store` 中的策略文件路径发生了更改),重新启动 `microsegebpf-agent`,并且 eBPF 映射表会在 <1 秒内重新填充。
5. 随时可以使用 `nixos-rebuild --rollback` 回滚。策略的上一代版本仍然保留在存储中。
一个完整的部署 flake 位于 [`examples/deployment/flake.nix`](examples/deployment/flake.nix) 中。
### 可用的策略基线
`microsegebpf.lib.policies.baselines` 开箱即提供了以下功能:
| 函数 | 效果 |
|---|---|
| `deny-public-dns { cgroupPath, extraIPv4, extraIPv6 }` | 阻止从选定的 cgroup 树直接连接到 Cloudflare、Google、Quad9、OpenDNS、AdGuard 的 TCP+UDP/53, /443, /853。强制通过企业解析器进行解析。 |
| `sshd-restrict { allowFrom, port }` | 将 `sshd.service` 入站限制为单个 CIDR。 |
| `deny-rfc1918-from-user-session { cgroupPath, ports }` | 阻止从用户会话到 RFC1918 地址的横向移动。 |
| `smtp-relay-only { relayCIDR, port }` | TCP/25 上的出站流量仅限访问指定的中继;其余全部被丢弃。 |
| `deny-threat-feed { ips, cgroupPath, ports }` | 阻止显式列出的 C2/接收坑 IP。由调用方提供,通常在部署时从威胁情报订阅生成。 |
| `deny-host { hostnames, ports, protocol, cgroupPath }` | 通过 FQDN 进行 L3/L4 拒绝。代理在每次 Apply 时解析每个主机名,并为每个 A/AAAA 记录安装一个 /32 (v4) 或 /128 (v6) 条目。随着目标 DNS 记录的轮换而动态适应 —— 适用于静态 CIDR 会过期的 CDN 前端服务。 |
| `deny-sni { hostnames }` | 通过 SNI 进行 TLS 查看拒绝。接受精确模式(`facebook.com`)和单标签通配符(`*.facebook.com`)。由反转主机名上的 LPM 字典树提供支持,参见 ARCHITECTURE.md §9.2。 |
| `deny-alpn { protocols }` | 通过 ALPN 标识符(`h2`,`http/1.1`,`imap`,`smtp` 等)进行 TLS 查看拒绝。谨慎使用:一刀切拦截 `h2` 会切断几乎所有现代 HTTPS 客户端。 |
对于一次性规则,可直接使用 `microsegebpf.lib.policies.mkPolicy`、`drop` 和 `allow` —— 详见 [`nix/policies/default.nix`](nix/policies/default.nix)。`mkPolicy` 也接受内联的 `sniDeny` / `alpnDeny` 列表,用于绕过基线策略进行临时的 TLS 匹配。
### 直接导入模块(无 flake)
如果您不使用 flake:
```
{ ... }: {
imports = [ /path/to/nixos-microsegebpf/nix/microsegebpf.nix ];
services.microsegebpf = {
enable = true;
enforce = false; # observe-only until policies are validated
emitAllowEvents = true; # see all traffic in Hubble during bake-in
defaultEgress = "allow";
defaultIngress = "allow";
resolveInterval = "60s"; # safety-net; inotify handles real-time updates
policies = [
''
apiVersion: microseg.local/v1
kind: Policy
metadata: { name: deny-public-dns }
spec:
selector: { cgroupPath: /user.slice }
egress:
- { action: drop, cidr: 1.1.1.0/24, ports: ["443", "853"], protocol: tcp }
- { action: drop, cidr: 2001:4860::/32, ports: ["443", "853"], protocol: tcp }
''
];
hubble.ui.enable = true; # co-located UI on http://localhost:12000
};
}
```
该模块附带符合 ANSSI 工作站指导原则的 systemd 加固措施:
`CapabilityBoundingSet = [ CAP_BPF CAP_NET_ADMIN CAP_PERFMON CAP_SYS_RESOURCE ]`,`NoNewPrivileges`,`ProtectSystem=strict`,`SystemCallFilter` 限制为 `@system-service @network-io bpf`,`ReadWritePaths` 仅限于 `/sys/fs/bpf`。代理永远不需要完全的 root 权限。
## 仓库布局
```
bpf/microseg.c Kernel-side datapath (cgroup_skb, LPM trie, IPv4+IPv6, TLS SNI/ALPN)
bpf/microseg_bpfel.{go,o} bpf2go output (committed; regenerated via `make generate`)
bpf/vmlinux.h BTF dump for CO-RE (committed; regenerated via `make generate`)
pkg/loader/ cilium/ebpf-based loader: load .o, attach to cgroupv2, ring buffer reader
pkg/policy/ YAML schema, selector resolution, BPF map sync (Apply/Resolve)
pkg/identity/ cgroup walker (Snapshot) + inotify watcher with pub/sub Subscribe()
pkg/observer/ Hubble observer.proto gRPC server, flow protobuf converter
cmd/microseg-agent/ Daemon entry point
cmd/microseg-probe/ CLI Hubble client for headless inspection
nix/microsegebpf.nix NixOS module (services.microsegebpf)
nix/package.nix buildGoModule derivation with vendorHash and BPF preBuild
nix/policies/ Composable policy library (mkPolicy + 8 baselines: deny-public-dns, sshd-restrict, deny-rfc1918-from-user-session, smtp-relay-only, deny-threat-feed, deny-host, deny-sni, deny-alpn)
nix/tests/vm-test.nix nixosTest: in-kernel drop verdict (L3/L4 + FQDN), SNI exact + wildcard (v4 + v6)
flake.nix Flake outputs (packages, nixosModules, lib, checks)
default.nix, shell.nix Non-flake entry points
.github/workflows/ GitHub Actions: nix-build (fast), vm-test (slow), security (govulncheck + reuse + SBOM drift + grype hubble-ui, nightly cron)
examples/policy.yaml Sample raw-YAML policy bundle
examples/tls-policy.yaml Sample TLS-aware policy (SNI/ALPN deny lists)
examples/fqdn-policy.yaml Sample FQDN-by-hostname policy (host: example.com)
examples/deployment/ Sample consumer flake (the GitOps target shape)
LICENSES/ SPDX license texts (MIT, GPL-2.0-only — bpf/microseg.c is dual-licensed for the BPF subsystem GPL-only helpers)
REUSE.toml REUSE-spec annotations for files without inline SPDX header
ARCHITECTURE.md / .fr.md Deep dive on the eBPF datapath, LPM key layout, identity model, TLS-aware peeking, log-shipping pipeline (EN + FR)
SECURITY-AUDIT.md / .fr.md Structured security audit (CVSS 3.1 scoring, manual code-review findings, per-CVE reachability matrix for upstream dependencies, remediation roadmap; EN + FR)
sbom/ CycloneDX 1.5/1.6 + SPDX 2.3 + CSV SBOMs for the source tree, Go modules, agent runtime closure, and Vector closure (regenerable via `sbom/README.md` recipe)
```
## 限制与路线图
本项目刻意**不**涉及的内容:
- **无 L7 *内容*解析。** 没有 HTTP 路径匹配,没有 gRPC 方法过滤,没有 Kafka topic 感知,没有 TLS 拦截。TLS 解析器是*仅限查看的* —— 它只检查明文的 SNI/ALPN 扩展且从不解密。添加感知负载的 L7 将需要 Envoy 风格的 sidecar;那是 Cilium 的领域。
- **无分片重组。** 第一个分片携带 L4 头部并被管制;后续分片不会被分类。工作站流量在此层几乎从不会分片。
- **没有针对未解析主机名的感知 DNS 的策略。** “拦截 `doh.example.com`”在 TLS 阶段通过 `sniDeny` 起作用(SNI 是客户端输入的主机名)。它对普通 DNS **不**起作用 —— 如果您需要更早的执行点,请配合使用 DNS 策略工具。
- **无 SAN 匹配。** 主题备用名称 存在于服务器的证书(在 ServerHello/Certificate 中发送),而不在客户端的 ClientHello 中。我们的解析器只能看到客户端侧的元数据。SAN 匹配对于*审计*有用,但对于*预防*则无用。
- **TLS 1.3 ECH(加密客户端问候)** 是 SNI 匹配的长期威胁。当目标协商 ECH 时(Cloudflare 和 Firefox 自 2024 年以来已逐步推广此功能),内部 SNI 会被加密,我们将静默失效。请做好这在未来 2-3 年内成为默认设置的打算。
- **按 cgroup 的 TLS 作用域划定** 尚未建模:SNI / ALPN 拒绝列表对主机是全局的。在这种情况下,策略文档级别的选择器仅作说明用途。以 `(cgroup_id, hash)` 为键的映射表是一个合理的后续开发方向。
路线图上的内容:
- 一个 `audit` 动作,类似于 `LOG`,但会为流打上额外的取证元数据(二进制路径,命令行)标记
- 按 cgroup 的 TLS 拒绝作用域划定(去除上面提到的仅作说明用途的注意事项)
- 一旦内核中的 AES 辅助程序或可行的用户空间往返路径落地,将支持 HTTP/3 / QUIC SNI 提取
## 许可证
所有源文件均使用 MIT 许可证。`bpf/microseg.c` 中的内核侧 eBPF 程序还通过双 SPDX 表达式标记为 GPL-2.0-only,并声明运行时 LICENSE 字符串为 `"Dual MIT/GPL"`,以便 BPF 子系统允许其使用仅限 GPL 的辅助函数(`bpf_loop`,`bpf_skb_cgroup_id` 等)。
`bpf/microseg.c` 上的确切 SPDX 标头是 `SPDX-License-Identifier: (MIT AND GPL-2.0-only)`。否则 `reuse lint` 工具会尝试将包裹它的 markdown 语句解析为真正的许可证声明;IgnoreStart/End 注释告诉它跳过该段落。
符合 REUSE 规范:每个文件都有文件内 SPDX 标头或 [`REUSE.toml`](REUSE.toml) 中的 glob 匹配。运行 `reuse lint` 进行验证。有关完整的逐文件细分,请参见 [LICENSE](LICENSE);规范的 MIT 文本请参见 [`LICENSES/MIT.txt`](LICENSES/MIT.txt);BPF 子系统所要求的双许可证文本包含在 [`LICENSES/GPL-2.0-only.txt`](LICENSES/GPL-2.0-only.txt) 中。
## 致谢
如果没有以下上游工作,这个项目就不可能存在:
- [Cilium](https://cilium.io/) 和 [`cilium/ebpf`](https://github.com/cilium/ebpf) Go 库
- [Hubble](https://github.com/cilium/hubble) 及其 `observer.proto`
- [Tetragon](https://github.com/cilium/tetragon) —— 证明了 Cilium 风格的 eBPF 基础设施在 Kubernetes 之外也具有合理性,即使 Tetragon 本身解决的是一个不同的问题
every outbound packet"] K2["cgroup_skb/ingress
every inbound packet"] K3[("LPM tries
egress_v4/v6, ingress_v4/v6
key: cgroup_id, port, proto, ip")] K4[("tls_sni_lpm + tls_alpn_deny")] K5["TLS ClientHello peeker
SNI + ALPN via bpf_loop
per-CPU 256-byte scratch"] K6{"Verdict
SK_PASS / SK_DROP"} K7[("Ring buffer 1 MiB")] K8[("default_cfg map
enforce, tlsPorts, blockQuic")] K1 -- lookup --> K3 K2 -- lookup --> K3 K5 -- "reversed-LPM" --> K4 K3 --> K6 K4 --> K6 K6 -- "flow event" --> K7 end subgraph AG["microsegebpf-agent.service - CAP_BPF, NET_ADMIN, PERFMON"] direction LR A1["pkg/loader (cilium/ebpf)
load .o, attach cgroupv2"] A2["pkg/policy
delta Map.Update
DNS cache 60s + stale-while-error
16 MiB file cap"] A3["pkg/identity
cgroup walker
inotify pub/sub Subscribe"] A4["pkg/observer (Hubble gRPC)
unix socket or TCP+TLS or mTLS"] A6(["Static Go binary - 4-component runtime closure
iana-etc, mailcap, agent, tzdata
NoNewPrivileges + ProtectSystem strict + SystemCallFilter"]) A3 -- "cgroup events" --> A2 A3 -- "cgroup events" --> A4 end A1 -- "attach + load" --> K1 A1 -- "ring read" --> K7 A2 -- "delta write" --> K3 A2 -- "delta write" --> K4 subgraph OPT["Optional co-located services - each opt-in via NixOS module"] direction LR O1["microsegebpf-log-shipper.service - Vector 0.52, DynamicUser
journald to parse_json to split to 4 sinks"] O2["hubble-ui OCI v0.13.5 (podman)
volume /run/microseg
binds 127.0.0.1:12000 only"] O3["systemd-journald
buffers stdout/stderr per boot
cursor in /var/lib/vector"] O4["microseg-probe CLI
-tls-ca, -tls-cert, -tls-key, -tls-server-name"] O3 --> O1 end A4 -- "gRPC unix or TCP+TLS" --> O2 A4 -- "gRPC" --> O4 AG -. "stdout/stderr" .-> O3 subgraph EXT["Configuration plane and external endpoints"] direction LR E1["GitOps flake + NixOS module
services.microsegebpf - enable, enforce,
policies, hubble.tls, dnsCacheTTL,
logs.opensearch, logs.syslog"] E2["Policy YAML
rules: cidr or host
selector: cgroupPath or systemdUnit
tls.sniDeny, tls.alpnDeny
8 baselines"] E3(["Operator"]) E4["DNS resolver
system /etc/resolv.conf
ideally local DNSSEC validating"] E5[("OpenSearch / SIEM
flows index + agent index
Vector elasticsearch sink")] E6[("Corp syslog SIEM
rsyslog, syslog-ng, Splunk, Wazuh
port 6514 RFC 5425 TLS")] end E1 -- "render flags" --> AG E2 -. "-policy=..." .-> A2 E3 -- "ssh -L 12000" --> O2 E3 -- "CLI inspect" --> O4 A2 -. "host: re-resolve" .-> E4 O1 -. "_bulk HTTPS" .-> E5 O1 -. "RFC 5425 TLS" .-> E6 style KER fill:#DBEAFE,stroke:#1E3A8A,stroke-width:2px style AG fill:#D1FAE5,stroke:#065F46,stroke-width:2px style OPT fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px style EXT fill:#FEF3C7,stroke:#92400E,stroke-width:2px ``` ## 解决了什么问题 ### 本地防火墙过滤与 eBPF 微隔离 —— 关键差异所在 传统的**本地防火墙**(`iptables`、`nftables` 以及工作站内置的主机防火墙)通过**网络身份**进行过滤:源 IP、目标 IP、端口、协议。它的心智模型是一个包含区域和区域间规则的网络图:“允许 `10.0.0.0/24` 访问 `10.0.0.5:443`”。在工作站上,这种机制非常粗糙 —— 用户运行的每个进程共享同一个 IP,因此每个进程都继承相同的策略。受损的浏览器标签页和合法的 `apt update` 在防火墙看来是一模一样的。更糟的是,如果本地防火墙没有明确关闭某些端口,同一内部子网上的两台工作站可以在所有端口上相互访问,这就是单台主机被攻陷后发生横向移动的经典前提条件。 **基于 eBPF 的微隔离**通过**工作负载身份**进行过滤:哪个进程、哪个用户、哪个 systemd 单元、哪个 cgroup。其心智模型是按应用程序制定策略:“Firefox 只能访问 `*.corporate.com:443`,其他一律禁止”。相同 IP 背后的相同目标会根据*谁*在发起请求而获得不同的判决结果。位于相同 `/24` 网段内的两台工作站不再默认互信 —— 每台工作站的代理都在内核级别执行自己的最小权限入站和出站控制,即使底层的网络结构允许它们通信。 `nixos-microsegebpf` 在单个 Linux 设备上为您提供第二种模型,使用自然的工作站身份标识(cgroupv2 id、systemd 单元、uid),而不是 Cilium 所需的 Kubernetes Pod 标签。 #### 横向移动与按应用出站控制示意图 同一机群的两个互补视图。`OK` = 规则集允许;`DROP` = 数据包离开工作站(或入站时在送达之前)被 `cgroup_skb` 钩子丢弃。 **视图 1 —— 扁平 /24 LAN 上的横向移动。** 位于 `10.0.0.0/24` 网段的三台 NixOS 工作站、一个企业堡垒机,以及相同的部署每台工作站的 GitOps flake。传统防火墙将允许每台工作站通过所有端口访问其他工作站;每台工作站上的 eBPF 代理将 LAN 变成了按主机划分的策略区域。 ``` %%{init: {'theme':'base','themeVariables':{'primaryColor':'#FFFFFF','primaryTextColor':'#0F172A','primaryBorderColor':'#475569','lineColor':'#475569','fontFamily':'monospace','fontSize':'13px'}}}%% flowchart LR GIT["Central GitOps flake
services.microsegebpf
+ policy bundle"] subgraph LAN["Corporate LAN 10.0.0.0/24 - flat L2, no firewall between workstations"] direction TB WSA["NixOS WS-A 10.0.0.10
+ microsegebpf-agent"] WSB["NixOS WS-B 10.0.0.20
+ microsegebpf-agent"] WSC["NixOS WS-C 10.0.0.30
+ microsegebpf-agent"] BAS["Bastion 10.0.0.42
any OS, no agent required"] end GIT -- "nixos-rebuild switch
(deploy-rs / colmena / morph)" --> WSA GIT --> WSB GIT --> WSC WSA -- "DROP SMB:445" --> WSB WSB -- "DROP http:80" --> WSC WSA -- "DROP ssh from peer" --> WSC BAS -- "OK ssh:22 (whitelisted)" --> WSA BAS --> WSB BAS --> WSC style GIT fill:#FCE7F3,stroke:#9D174D,stroke-width:2px style LAN fill:#FEF3C7,stroke:#92400E,stroke-width:2px style WSA fill:#D1FAE5,stroke:#065F46,stroke-width:1.5px style WSB fill:#D1FAE5,stroke:#065F46,stroke-width:1.5px style WSC fill:#D1FAE5,stroke:#065F46,stroke-width:1.5px style BAS fill:#E5E7EB,stroke:#475569,stroke-width:1.5px,stroke-dasharray: 4 2 ``` **视图 2 —— 单个工作站上按 cgroup 的出站控制。** 放大 WS-A,展示相同的互联网网关如何根据发起数据包的 cgroup 产生不同的判决结果。基于(源 IP,目标 IP,端口)进行过滤的传统防火墙要么允许来自 `10.0.0.10` 的所有流量,要么阻止所有流量 —— 无法实现按应用的细粒度控制。 ``` %%{init: {'theme':'base','themeVariables':{'primaryColor':'#FFFFFF','primaryTextColor':'#0F172A','primaryBorderColor':'#475569','lineColor':'#475569','fontFamily':'monospace','fontSize':'13px'}}}%% flowchart LR subgraph WSA["NixOS WS-A 10.0.0.10 + microsegebpf-agent"] direction TB A_FF["Firefox
app-firefox.scope"] A_APT["apt
system.slice"] end GW["Internet gateway / NAT"] subgraph NET["Internet"] direction TB CORP["corporate.com:443"] REPO["repo.corp.com:443"] DOH["1.1.1.1 DoH:443"] FBC["fbcdn.net:443
Facebook CDN"] end A_FF -- "OK corp.com:443" --> GW A_FF -- "DROP DoH 1.1.1.1" --> GW A_FF -- "DROP fbcdn" --> GW A_APT -- "OK repo.corp.com:443" --> GW GW --> CORP GW --> REPO GW -. dropped at cgroup_skb .-> DOH GW -. dropped at cgroup_skb .-> FBC style WSA fill:#D1FAE5,stroke:#065F46,stroke-width:1.5px style GW fill:#EDE9FE,stroke:#5B21B6,stroke-width:1.5px style NET fill:#F3F4F6,stroke:#475569,stroke-width:1.5px ``` 这些视图说明了: * **每台工作站都是 NixOS。** 相同的 flake 在 WS-A、WS-B、WS-C 上声明了 `services.microsegebpf` 模块 + 相同的策略包;`nixos-rebuild switch`(或 `deploy-rs` / `colmena` / `morph`)可以在一次推送中将策略变更分发到整个机群。堡垒机可以是任何操作系统,不需要安装此代理 —— 它只是允许的入站流量的*来源*,而不是需要被监管的对等方。 * **横向移动被遏制。** WS-A 尝试对 WS-B 进行 SMB 扫描,或对 WS-C 发起 SSH 会话,会分别被 WS-B 和 WS-C 自身的入站钩子丢弃 —— 即使 LAN 结构中没有任何东西阻止数据包到达。而传统防火墙会将其放行。 * **仅允许来自堡垒机的 SSH。** 每台工作站的 `sshd-restrict` 基线策略仅允许来自 `10.0.0.42/32` 的 22 端口入站连接,并丢弃其他所有流量,因此公司的跳板机仍可用于应急响应 (IR),而受感染的对等主机则无法连接。 * **按应用控制的出站。** WS-A 上的 Firefox 被允许访问 `corporate.com:443`(企业 Web 应用),但被禁止访问 `1.1.1.1`(会绕过企业 DNS)和 `*.fbcdn.net`(合规使用策略)。在 `system.slice` 中运行的 `apt` 被允许在同一端口上访问企业仓库,因为策略的匹配依据是 cgroup,而不是 IP。同一工作站上的两个进程,通过相同的网关,会获得不同的判决结果。 ### 通过 Nix 进行集中化管理及大规模部署 将此项目作为 NixOS 模块 + flake 提供的全部意义在于,运维人员的工作流与管理工作站任何其他配置的工作流完全一致: 1. 机群中每台工作站的微隔离策略包都**位于一个 git 仓库中**,用 Nix 表达。无需在目标主机上编辑逐个主机的 YAML。 2. 策略的变更会经过**与任何其他配置变更相同的审查和 CI 关卡**:`nix flake check` 会启动一个 NixOS 虚拟机,应用新策略,并在变更触及真实工作站之前断言内核内的丢弃判决。 3. 推送部署通过机群已经在使用的任何 NixOS 部署工具(`nixos-rebuild switch`、`deploy-rs`、`colmena`、`morph`)进行。systemd 会注意到 `/nix/store` 中策略文件的路径发生了改变,重新启动 `microsegebpf-agent`,eBPF 映射表会在不到一秒钟内在每台主机上重新加载。 4. 回滚只需执行 `nixos-rebuild --rollback` —— 策略的上一代版本仍然保留在存储中。 这在 **ANSSI 工作站安全加固背景下** 非常重要,对 *poste admin*(管理员工作站)进行微隔离的理由是阻止攻击者在扁平的内部子网上轻易进行横向移动。如果没有这种隔离,您只有两个毫无吸引力的选择: * **网络侧的微隔离**(每台主机独享私有 VLAN,带有基于 MAC 策略的 NAC,内部防火墙网格)—— 运维成本高昂,需要更改交换机/路由器/设备,这通常超出了小型运维团队的能力范围。 * **逐台单独编辑的主机防火墙规则** —— 缺乏一致性,没有审查记录,且一旦某台主机的配置发生偏离,整个机群就会退回到“信任所有内部流量”的状态。 `nixos-microsegebpf` 降低了这些成本:在每台工作站的内核中执行策略(无需购买或运维新设备),并且管理平面是一个 git 仓库,其形态和工具链与团队已经用于其余 NixOS 配置的完全相同。达到 ANSSI 级别的横向移动遏制仅仅是一项配置变更,而不再是一个庞大的基础设施项目。 ### 具体用例 | 目标 | 策略形式 | 防御对象 | |---|---|---| | **遏制受损的浏览器** | `selector: { systemdUnit: "app-firefox-*.scope" }` + 拒绝到 RFC1918 地址的出站流量 | 试图扫描内网主机或以此为跳板的恶意浏览器扩展 | | **强制使用企业 DNS** | `selector: { cgroupPath: /user.slice }` + 拒绝到公共解析器的 TCP/UDP/53, /443, /853 流量 | DNS 隧道数据外泄,DoH/DoT 绕过企业过滤器 | | **将 SMTP 限制为仅限 MTA** | `selector: { cgroupPath: / }` + 仅允许 TCP/25 到中继 CIDR | 使用硬编码 SMTP 服务器进行数据外泄的恶意二进制文件 | | **锁定 sshd 入站连接** | `selector: { systemdUnit: sshd.service }` + 仅允许来自堡垒机 CIDR 的入站流量 | 暴露在互联网上的 `sshd` 遭到凭证填充攻击 | | **拦截已知的 C2 IP** | `selector: { cgroupPath: / }` + 拒绝到威胁情报 IP 列表的出站流量 | 磁盘上已有的恶意二进制文件的信标通信 (Beaconing) | | **在 Hubble 中审计一切** | `enforce = false` + 仅观察模式 | 在编写任何丢弃规则之前,映射工作站的流量表面 | ### 与已有方案的差异 | 已有方案... | 在上述用例中缺失的部分 | microseg-poste 新增的功能 | |---|---|---| | `nftables` / `iptables` | 按进程的规则需要 `cgroup` 匹配扩展,且原生不支持 systemd 单元名 | 开箱即用的按 systemd 单元的规则;提供用于可视化的 Hubble UI | | AppArmor / SELinux | 没有*网络目的地*策略的概念;它们仅限制 syscall 参数和文件访问 | 在数据包边界处的网络层策略执行 | | Tetragon | 执行方式为 `SIGKILL` 或 syscall 覆盖 → 会杀死进程。在桌面环境中具有破坏性(浏览器会话丢失) | 数据包级别的 `SK_DROP` → 连接干净地失败,应用程序继续运行 | | Cilium | 需要 Kubernetes;使用 Pod 标签作为身份标识 | 无需集群,无需 K8s;以 cgroup id + systemd 单元作为身份标识 | | OpenSnitch / Little Snitch | 交互式的逐连接提示;适合个人使用,但不适用于 ANSSI 风格的策略执行 | 声明式 YAML/Nix 策略,对 GitOps 友好,无用户提示 | ### 性能预算 针对工作站而非 10 GbE 路由器进行了规模调整。下表中的每个数据都是基于 `bpf/microseg.c` 中的 eBPF 结构定义以及已发布的 cgroup_skb 基准测试得出的分析最坏情况;有关逐行推导,请参见 [ARCHITECTURE.md §11](ARCHITECTURE.md)。 | 层级 | 开销 | 时机 | |---|---|---| | `cgroup_skb` 每包钩子(查找 + 判决) | **100-250 ns** | 每个数据包,双向 | | TLS ClientHello 探测(通过 `bpf_loop` 解析 SNI + ALPN) | **1-6 μs** | 在 `tlsPorts` 上的每个新 TCP 连接一次 | | BPF 映射表内存(最坏情况,所有容量全满) | **约 12 MB** | 始终;实际策略通常使用 <200 KB | | 代理用户空间 RSS(静态 Go 二进制文件) | **25-40 MB** | 始终 | | 代理稳态 CPU 占用 | **<单核的 0.1 %** | 始终;在增量 Apply 期间峰值约为 5-15 % | | Vector 日志转发器 RSS | 50-150 MB | 仅当 `logs.{opensearch,syslog}.enable = true` 时 | | `hubble-ui` OCI 容器 | 约 80 MB | 仅当 `hubble.ui.enable = true` 且连接了浏览器时 | | **全量开启、所有映射表全满时的总体开销** | **约 280 MB RSS,<单核的 2 %** | 极端的病理部署情况 | 具体来说,在一条饱和的 1 Gbit/s 线路上(约 8 万包/秒),cgroup_skb 的开销在主机上每个流量的双向总计**约占单核的 2%**。实际的办公室工作负载测量值通常在 1% 以下。 ### 何时不适合使用此项目 - **具有高带宽网络吞吐量的服务器。** `cgroup_skb` 每个数据包的开销约为 100-250 ns(参见 [§11.1](ARCHITECTURE.md));适用于工作站,但不适用于 10bE+ 服务器 —— 请在这些场景下使用带有 XDP 的原生 Cilium。 - **您希望按主机名进行过滤**(`*.facebook.com`)。本项目基于已解析的 IP 和(即将支持)TLS SNI 进行工作。要进行纯粹基于主机名的过滤,请配合使用 DNS 策略工具。 - **您需要 L7 检查**(阻止特定的 HTTP 路径,解析 JWT,按 API 端点进行速率限制)。这是 L7 代理(如 Envoy, Traefik, NGINX)的工作。Microseg-poste 刻意保持在 L3/L4 层。 - **您无法运行 ≥ 5.10 的内核。** cgroup_skb 挂载点和 LPM_TRIE 映射类型在更早的版本中就已存在,但 BTF / CO-RE 的可靠性是从 5.10 才真正开始有保障的。本项目在 6.12 上进行了测试。 ## 为什么会有这个项目 Cilium 和 Hubble 是为 Kubernetes 集群设计的。它们的身份模型围绕 Pod 标签构建,其数据路径挂载到 Pod 的 veth 接口,且 Hubble UI 期望流量来自由按节点的 `cilium-agent` 实例提供的 `hubble-relay`。在工作站上没有 Pod、没有 API 服务器,也没有标签 —— 因此 Cilium 并不适用。 [Tetragon](https://github.com/cilium/tetragon),Isovalent 对 Cilium 的裸金属提取版本,是最接近的匹配:它能在主机上加载 eBPF,提供 TracingPolicy CRD,并且无需集群即可运行。但是 Tetragon 有意将其范围限定在**运行时安全可观察性 + syscall 级别的策略执行**(kprobe + `SIGKILL` / 覆盖返回值)。它不提供网络数据路径:Tetragon 仓库中没有与 `bpf_lxc.c` / `bpf_host.c` 对等的组件,没有基于 LPM 的 CIDR 匹配,也没有在数据包级别按流执行的丢弃判决。 `nixos-microsegebpf` 填补了这一空白。它执行了 Cilium 在 Kubernetes 节点上所做的工作 —— 加载掌管数据包路径的 eBPF 程序,评估身份感知策略,发出 Hubble 流量 —— 但使用的是工作站自有的身份原语: - 本地端点的 **cgroupv2 id**(由 `bpf_get_current_cgroup_id` 原生返回) - 其 **systemd 单元名**,从 cgroup 路径派生(`/user.slice/user-1000.slice/app.slice/firefox.service` → `firefox.service`) - 其 **所属用户**,可通过相同的路径遍历获得 这意味着策略可以像 Cilium 策略针对 Pod 标签一样,精确瞄准“由 Firefox 启动的任何程序”或“`user.slice` 下的所有进程”。 ## 它的实际工作原理 代理运行后,每个数据包都会触发以下四件事: 1. **eBPF 钩子触发。** 挂载在 cgroupv2 根节点的 `cgroup_skb/egress`(或 `/ingress`)在数据包即将离开网卡(或刚刚到达时)将其拦截。处理程序读取 IP/L4 头部,向内核查询本地进程所属的 cgroup,并构建一个策略查找键。 2. **LPM 查找。** 代理维护四个 `BPF_MAP_TYPE_LPM_TRIE` 映射 —— `egress_v4`、`ingress_v4`、`egress_v6`、`ingress_v6`。键是一个紧凑的 `(cgroup_id, peer_port, protocol, peer_ip)` 元组,LPM 的 `prefix_len` 被设置为使 cgroup/端口/协议精确匹配,而 IP 匹配到配置的 CIDR 前缀。如果未匹配到,则回退到可配置的默认判决。 3. **应用判决。** eBPF 程序返回 `SK_DROP`(内核丢弃数据包,syscall 看到 `EPERM`)或 `SK_PASS`(正常转发)。无用户空间往返,无代理。 4. **发出流事件。** 与判决无关,程序在一个 1 MiB 的环形缓冲区上保留一个记录,包含五元组、判决、匹配的策略 id 和本地 cgroup。代理排空环形缓冲区,通过定期刷新的缓存用 systemd 单元名装饰每条记录,将其转换为 Cilium `flow.Flow` protobuf,并发布给每个连接的 Hubble 客户端。 ## 策略外观示例 ``` apiVersion: microseg.local/v1 kind: Policy metadata: name: deny-public-dns-from-user-session spec: selector: cgroupPath: /user.slice # every cgroup under this prefix egress: - action: drop cidr: 1.1.1.0/24 # full CIDR, LPM-matched ports: ["53", "443", "853"] # exact ports protocol: tcp - action: drop cidr: 2001:4860::/32 # IPv6 supported natively ports: ["443", "853"] protocol: tcp - action: drop cidr: 127.0.0.0/8 ports: ["8000-8099"] # ranges expanded server-side protocol: tcp ``` 一个策略可简化为“对于每个与选择器匹配的 cgroup,在每个方向上将 N 个条目推入 LPM 映射表。” 选择器可以通过 **systemd 单元 glob**(`firefox.service`,`app-firefox-*.scope`)或 **cgroup 路径前缀**(`/user.slice/user-1000.slice`)进行匹配。 ## 感知 TLS 的 SNI / ALPN 匹配(仅限查看) 基于 IP 的过滤在 CDN 面前会遇到瓶颈:数千个站点共享相同的 Cloudflare / Fastly / Akamai IP,仅靠 IP 规则要么会过度拦截(使合法目标瘫痪),要么会完全失效(如果目标的 IP 在策略写入时和运行时之间发生了变化)。microsegebpf 通过仅查看的 TLS 解析器增强了 L3/L4 数据路径,该解析器从 TLS ClientHello 中读取明文 SNI 主机名和第一个 ALPN 协议标识符,对其进行哈希处理,并应用优先于 IP 级别允许的拒绝判决。 **无解密。** SNI 和 ALPN 在 ClientHello(最初的 TLS 握手消息)中以明文传输。eBPF 解析器仅检查这两个扩展,不涉及其他内容;连接的其余部分对它是不可见的。 ### 模式 ``` apiVersion: microseg.local/v1 kind: Policy metadata: name: ban-doh-providers spec: selector: cgroupPath: / # documentary; see "Limits" below tls: sniDeny: - "1.1.1.1" - "cloudflare-dns.com" - "dns.google" alpnDeny: [] # see warning below ``` 在 Nix 中: ``` services.microsegebpf.policies = with microsegebpf.lib.policies.baselines; [ (deny-sni { hostnames = [ "facebook.com" "tiktok.com" "x.com" ]; }) (deny-alpn { protocols = [ "imap" "smtp" ]; }) # niche, see below ]; ``` ### 为什么这很重要:SNI 与 IP 演示 ``` $ curl https://cloudflare.com # IPv4 path A exit=28 (DROP — SNI matched 'cloudflare.com') $ curl --resolve cloudflare.com:443:1.1.1.1 https://cloudflare.com # IPv4 path B exit=28 (DROP — SNI still 'cloudflare.com', different peer IP) $ curl https://example.com # unrelated exit=0 (ALLOW) ``` 隐藏在不同 IP 背后的相同主机名,或者隐藏在我们无法预测的 CDN 背后的主机名,同样会被拦截。 ### 覆盖范围矩阵 SNI/ALPN 解析器能看到和看不到的内容明细,以免您感到意外: | 协议 / 上下文 | 已覆盖? | 原因 | |---|---|---| | 基于 TLS 的 HTTP/1.1,基于 TLS 的 HTTP/2,基于 TLS 的 gRPC | ✅ | 无论上层承载的是何种 L7 协议,TLS ClientHello 都是相同的。 | | **HTTP/3 / QUIC** | ⚠ 仅支持全面拦截 | QUIC 的 TLS ClientHello 使用从目标 Connection ID 派生的密钥进行加密;在内核中派生它们需要 AES-128-CTR + AES-128-GCM,这超出了 eBPF 的执行能力。设置 `services.microsegebpf.blockQuic = true`(CLI 标志 `-block-quic`)可丢弃所有发往您配置的 `tlsPorts` 的 UDP 出站流量。浏览器会因此回退到 TCP/TLS,此时 SNI 解析器即可发挥作用。 | | **STARTTLS**(SMTP 提交/587,IMAP/143,XMPP) | ❌ | TLS 握手发生在明文交换(`STARTTLS\n`)之后,并在流中间到达。我们的解析器仅检查新 TCP 连接上的第一个数据包。 | | 非标准端口上的 TLS | ✅ 通过配置支持 | 设置 `services.microsegebpf.tlsPorts = [ 443 8443 4443 ];`(或 `-tls-ports=443,8443,4443`)。最多支持 8 个端口。SNI 解析器会在发往这些端口的 TCP 出站流量上被触发。 | | **通配符 SNI**(`*.example.com`) | ✅ | 通过在反转主机名上构建 LPM 字典树来实现(类似于 Cilium 的 FQDN 方法)。模式存储了 `.example.com` 反转后的字节,并以一个点作为终止符;查找时会反转在线传输的 SNI,字典树将选择最长匹配的前缀。仅支持最左侧标签的单级通配符(即 `*.foo.com`,不支持 `evil*.foo.com` 或 `foo.*.com`)。 | | **通过 FQDN 主机名实现的 L3/L4**(`host: api.corp.example.com`) | ✅ | 在任何出站/入站规则中使用 `host:` 而不是 `cidr:`。代理通过系统解析器将 FQDN 解析为 A 和 AAAA 记录,并为每个解析出的地址安装一个 `/32`(v4)或 `/128`(v6)的条目。每次 Apply 时都会重新解析(由 cgroup 事件驱动或由后备定时器触发),因此当 DNS 记录发生变化时,规则也会随之适应。解析失败会记录警告并在该轮操作中跳过该规则。 | ### 限制 - **TLS 1.3 ECH(加密客户端问候)** 是长期的威胁。当目标协商 ECH 时(Cloudflare 和 Firefox 自 2024 年以来已逐步推广此功能),SNI 会被加密,解析器会静默失效。请做好这在未来 2-3 年内成为默认设置的打算。 - **分片的 ClientHello。** 解析器仅检查承载 ClientHello 的第一个 TCP 段中的线性部分。实际上,每个常见客户端都能将 SNI/ALPN 扩展放入第一个段中(通常约 512 字节,远在 MTU 范围内)。如果病态的客户端发送 16 KiB 的预共享密钥扩展导致其跨段分割 —— 这些将会漏网。 - **按 cgroup 的作用域划定。** 概念验证阶段的 TLS 映射表仅使用主机名/ALPN 字符串的 FNV-64 哈希作为键。因此,SNI 拒绝规则是全局性的:无论周围策略文档的选择器如何,每个 cgroup 都受其约束。在这种情况下,策略级别的选择器字段仅作说明用途。要实现按 cgroup 的 TLS 规则需要使用 `(cgroup_id, hash)` 作为键 —— 这是一个合理的后续开发方向。 - **仅限第一个 ALPN 条目。** 遍历器仅检查 ALPN 列表中的第一个协议。这足以捕获单一用途的信标(仅限 `h2`);如果恶意客户端发送 `["h2", "x-evil"]`,排在第二的 `x-evil` 就会漏网。 - **一刀切拦截 ALPN `h2` 是个危险操作。** 几乎所有现代 HTTPS 客户端都会广播 `h2`。请将 `alpnDeny` 用于范围更窄的协议禁令(如 `imap`,`smtp`,自定义标识符),或者用于协议白名单较短的物理隔离部署中。 ## 配置方案 涵盖最常见工作站安全加固场景的八个详细示例。前六个是您可以放入部署 flake 的 `services.microsegebpf.policies` 片段;最后两个(集中式日志传送到 OpenSearch 和 syslog)配置了代理外围的运维管道。 ### 方案 1 —— 强制使用企业 DNS 解析器 **用例。** 您运行着一个企业 DNS 解析器(带有日志记录、恶意软件拦截列表、内部区域记录)。您不希望用户的浏览器、包管理器或受感染的二进制文件通过与 Cloudflare 的 `1.1.1.1` 直接通信来绕过它,或者更糟的是,通过 DoH(`https://1.1.1.1/dns-query`)或 DoT(`tcp/853 to 8.8.8.8`)进行隧道传输。 **为什么这很重要。** 直接连接公共解析器会绕过所有企业的过滤、日志记录和检测 —— 这既涉及日常的策略违规,也涉及恶意软件利用 DNS 建立的 C2 通信。 ``` services.microsegebpf.policies = with microsegebpf.lib.policies; [ # Drop classic DNS, DoT, DoH to the well-known public resolvers. # The baseline ships a curated IP+port list for Cloudflare, Google, # Quad9, OpenDNS, AdGuard. (baselines.deny-public-dns { }) # Belt-and-suspenders: also block via SNI any host pretending to # be a DoH provider on a different IP (CDN re-routing, new IPs # not yet in the IP feed). The wildcard catches cdn-hosted # variants like resolver-dot1.dnscrypt.example.com. (mkPolicy { name = "deny-doh-providers-by-sni"; selector = { cgroupPath = "/"; }; sniDeny = [ "cloudflare-dns.com" "*.cloudflare-dns.com" "dns.google" "*.dns.google" "dns.quad9.net" "*.quad9.net" "doh.opendns.com" "*.dnscrypt.org" ]; }) ]; ``` **工作原理。** - `baselines.deny-public-dns {}` 在端口 `53`、`443` 和 `853` 上**阻止 TCP 和 UDP** 流量流向主要公共解析器的 IP(覆盖了普通 DNS、DoH、DoT)。默认情况下,它以 `/user.slice` 选择器为匹配依据;传入 `cgroupPath = "/"` 也可将其扩展到系统服务。 - 自定义的 `mkPolicy` 添加了一个 **TLS SNI 拒绝列表**,这样即使目标 IP 不在我们的列表中,在 TLS 握手期间广播已知 DoH 提供商 SNI 的连接也会在 ClientHello 完成之前被丢弃。 - 通配符条目(`*.cloudflare-dns.com`)无需逐一列举每个接入点 即可捕获 CDN 边缘节点变体。 **变体。** - 要仅对**您的**企业解析器允许 DoH,请将其附加到 `deny-public-dns` 的 `extraIPv4` 和 `extraIPv6` 中,以维持基础拦截名单,同时通过来自 `mkPolicy` 的显式 `allow` 条目豁免您的 IP。 ### 方案2 —— 浏览器隔离:无法访问内部网络 **用例。** Firefox / Chromium 每天都在运行不受信任的 JavaScript。被武器化的扩展或 0-day RCE 不应该能够扫描企业网络 `10.0.0.0/8`,攻击位于 80 端口的内部 Confluence,或发起横向的 SMB 攻击。 **为什么这很重要。** 这是单项影响最大的 ANSSI 工作站安全加固措施:它将“浏览器被攻陷”从“攻击者现在可以看到内部网络”转化为“攻击者拥有一个被沙箱化的浏览器进程,但没有可用的 LAN 网络句柄”。 ``` services.microsegebpf.policies = with microsegebpf.lib.policies; [ # The baseline blocks egress from /user.slice to RFC1918 on the # commonly-attacked ports (SSH, HTTP, HTTPS, SMB, RDP, alt-HTTP). (baselines.deny-rfc1918-from-user-session { }) # Carve out a per-unit allow for IT helpdesk SSH (the user # legitimately needs to ssh to the bastion). (mkPolicy { name = "allow-bastion-ssh-from-user"; selector = { cgroupPath = "/user.slice"; }; egress = [ (allow { cidr = "10.0.0.42/32"; # bastion IP ports = [ "22" ]; protocol = "tcp"; }) ]; }) ]; ``` **工作原理。** - 基线策略在所有三个 RFC1918 范围内丢弃六个常见端口。浏览器标签页、邮件客户端以及 `/user.slice` 下的任何其他程序都无法访问这些端口上的内部服务。 - 切出部分是一个优先级更高的 `allow`,它重新启用了唯一合法的路径。**规则优先级**:LPM 字典树为每个 `(cgroup, port, proto)` 元组选择最长前缀匹配,因此 `/32` 条目胜过针对 `10.0.0.42:22` 的 `/8` 拒绝规则。 - 到公共互联网(`0.0.0.0/0` 减去 RFC1918)的浏览器网络 IO 不受影响 —— 没有引入隐含的出站防火墙。 **变体。** - 扩展到特定 systemd 单元的所有子 cgroup:`selector = { systemdUnit = "app-firefox-*.scope"; }`。 - 对于不在按标签页进程隔离范围内的 Chromium,请将 `cgroupPath = "/user.slice"` 切换为针对 Chromium 特定作用域名称的更严格选择器。 ### 方案 3 —— 仅将 SSH 限制于堡垒机 **用例。** 生产环境工作站暴露了 `sshd` 以便进行事件响应,但只应允许位于 `10.0.0.42` 的企业堡垒机访问它。暴露在互联网上的 `sshd` 是凭证填充攻击的磁铁,即使配置不当的防火墙意外放行了数据包,工作站也应拒绝来自任何其他来源的 SSH 连接。 ``` services.microsegebpf.policies = with microsegebpf.lib.policies; [ (baselines.sshd-restrict { allowFrom = "10.0.0.42/32"; }) ]; ``` **工作原理。** - `selector = { systemdUnit = "sshd.service"; }`(在基线策略内部设置)目标是 systemd 为 `sshd` 创建的 cgroup。 - 基线策略发出一个单一的 `ingress` 规则:`allow { cidr = allowFrom; ports = [ "22" ]; protocol = "tcp"; }`。由于没有列出 `drop` 规则,且在策略模块上设置了 `defaultIngress = "drop"`,因此默认拒绝所有其他来源的连接。 - **您必须在模块级别设置 `services.microsegebpf.defaultIngress = "drop"` 才能使其生效** —— 否则未匹配的流量将回退到默认允许。 **变体。** - 用于多个堡垒机 IP:传入一个 `/24`(`"10.0.0.0/24"`)或堆叠多个 `mkPolicy` 调用,每个调用添加一个 IP。 - 对于位于不同端口上的高可用堡垒机对,请去掉基线策略,直接使用带有两个 `ingress` 规则的 `mkPolicy`。 ### 方案 4 —— 仅通过企业中继的 SMTP 出站 **用例。** 试图通过直接 SMTP 连接到硬编码邮件服务器进行数据外泄的受感染二进制文件应当操作失败。合法的路径是通过企业 MTA(通常是 `smtp-relay.corp:25`)。 ``` services.microsegebpf.policies = with microsegebpf.lib.policies; [ (baselines.smtp-relay-only { relayCIDR = "10.0.1.10/32"; port = "25"; }) ]; ``` **工作原理。** - `selector = { cgroupPath = "/"; }` —— 应用于主机上的每个 cgroup(包括系统服务和用户进程)。 - 按优先级(LPM,最长匹配胜出)排列的两个规则: 1. 允许(`allow`)到 `10.0.1.10/32` 的端口 25 流量(`/32` = 32 位前缀) 2. 拒绝(`drop`)到 `0.0.0.0/0` 的端口 25 流量(`/0` = 0 位前缀) - 中继的 `/32` 始终胜过全捕获的 `/0`,因此合法邮件得以发送;端口 25 上的其他一切流量均被拒绝。 **变体。** - 对于端口 465 上的 SMTPS 或 587 上的提交端口,传入 `port = "465"` 或 `port = "587"` 并堆叠两个策略。 - 要豁免特定的 systemd 单元(例如 `postfix.service`)不受此拒绝规则限制,请添加一个 `mkPolicy`,使用 `selector = { systemdUnit = "postfix.service"; }` 以及显式 `allow` 到 `0.0.0.0/0:25` —— 其 `/32` 样式的 cgroup 匹配具有更高优先级。 ### 方案 5 —— 通过 SNI 通配符屏蔽社交媒体 **用例。** 合规/可接受使用策略禁止在工作时间访问 TikTok、Facebook、Instagram。基于 IP 的拦截是徒劳的(托管在 CDN 上,IP 不断轮换),但 SNI 匹配可以捕获服务于该品牌的每个 CDN 边缘。 ``` services.microsegebpf.policies = with microsegebpf.lib.policies; [ (mkPolicy { name = "deny-social-media-sni"; selector = { cgroupPath = "/user.slice"; }; sniDeny = [ "facebook.com" "*.facebook.com" "fbcdn.net" "*.fbcdn.net" "instagram.com" "*.instagram.com" "tiktok.com" "*.tiktok.com" "*.tiktokcdn.com" "x.com" "*.x.com" "twitter.com" # legacy redirect "*.twitter.com" ]; }) # Force QUIC fallback so the SNI matcher actually fires. Without # this knob, browsers happily fetch tiktok.com over HTTP/3 (UDP) # and our TCP-only parser never sees the SNI. ]; services.microsegebpf.blockQuic = true; ``` **工作原理。** - `*.facebook.com` 匹配每个子域(`m.facebook.com`、`web.facebook.com`、`static.xx.fbcdn.net` 等)。将其与裸域名 `facebook.com` 配对,以同时捕获顶点域。 - 一个策略中的多个站点只是一个更长的列表 —— LPM 字典树可以扩展到数千个条目,查找开销为 O(字符串长度)。 - `services.microsegebpf.blockQuic = true` 在这里**至关重要**。HTTP/3 的 TLS ClientHello 是加密的;我们无法查看 UDP/443 上的 SNI。迫使 QUIC 失败会让浏览器在 TCP/443 上重试,此时 SNI 解析器即可发挥作用。 **变体。** - 对于白名单方法(只允许用户访问 `*.corporate.com`),反转思路:设置 `defaultEgress = "drop"` 并编写带有 `egress` `allow` 规则的 `mkPolicy`,仅允许您想要放行的目的地。SNI 匹配本身是一项*仅拒绝*功能(在 SNI 侧没有允许覆盖;IP 级别的判决才是事实来源)。 ### 方案 6 —— 结合 TLS 端口加固 + 威胁情报订阅集成 **用例。** 您使用每日威胁情报订阅(一个已知通过 HTTPS 或异常 TLS 端口提供 C2 服务的恶意 IP 列表),并希望 microsegebpf 执行该策略而无需重新造轮子部署管道。 ``` let # Pulled at deploy time by the CI step that builds the workstation # closure. IO must happen at *build* time (Nix is hermetic at # eval), so a separate fetcher derivation feeds the list in. threatFeed = builtins.fromJSON (builtins.readFile ./threat-ips.json); in { services.microsegebpf = { enable = true; enforce = true; # Treat 443, 8443, and a custom corporate VPN port as TLS-bearing. # The SNI parser fires on TCP egress to any of these. tlsPorts = [ 443 8443 4443 ]; # Drop QUIC blanket so SNI-based enforcement isn't bypassed # via HTTP/3. blockQuic = true; policies = with microsegebpf.lib.policies; [ # Drop egress to every IP in the feed, on the same TLS-bearing # ports. The IP feed is the precise blocker; the SNI check # below catches re-hosted infrastructure. (baselines.deny-threat-feed { ips = map (ip: "${ip}/32") threatFeed.ips; ports = [ "443" "8443" "4443" ]; }) # Domain-side feed (different vendor, different threat surface). (mkPolicy { name = "deny-threat-feed-sni"; selector = { cgroupPath = "/"; }; sniDeny = threatFeed.domains; # plain + wildcard mix }) ]; hubble.ui.enable = true; # see what gets dropped, in real time }; } ``` **工作原理。** - `tlsPorts = [ 443 8443 4443 ]` 将 SNI 解析器扩展到企业 VPN 恰好使用的非标准端口上触发。SNI 匹配和 `blockQuic` 都遵循此列表。 - 威胁情报 IP 被放入标准的 L3/L4 LPM(由 `deny-threat-feed` 覆盖);其主机名被放入 SNI LPM(由自定义的 `mkPolicy` 覆盖)。单独一层就能捕获大多数信标;结合在一起则分别覆盖了 IP 轮换和域名轮换。 - `enforce = true` 开启丢弃。结合 `emitAllowEvents = false`(生产环境设置)以保持 Hubble 的信噪比。 **变体。** - 对于更新频率高于每次 NixOS 重建的情报订阅,通过 systemd 定时器将其抓取到 `/etc/microsegebpf/threat.yaml`,并设置 `services.microsegebpf.policies = [ (builtins.readFile "/etc/microsegebpf/threat.yaml") ]`。代理的 inotify 监视器会在约 250 毫秒内检测到更改。 - 构建一个最小的 nix derivation,以便在构建时抓取情报(使用带有哈希值的 `pkgs.fetchurl`),从而使闭包完全可复现 —— 代价是每次情报更新都需要重新构建。 ### 方案 7 —— 集中式日志传送到 OpenSearch **用例。** 一台在本地时间 03:14 丢弃了恶意流量的工作站,不应该要求分析师通过 SSH 登录并使用 grep 检索 journald 来查明情况。将每个流事件和每个控制平面日志传送到全机群范围的 OpenSearch 集群 —— SOC 已经查看的同一个地方 —— 这样第二天早上的调查只需一次 Kibana / OpenSearch Dashboards 查询,而不是一场繁琐的取证之旅。 **为什么这很重要。** 本地的 journald 适用于单台主机,但在机群规模下会崩溃:无跨主机关联,保留期限受限于主机的磁盘预算,无告警钩子。Hubble UI 非常适合交互式流量探索,但它同样短暂且局限于单台主机。集中的日志存储解决了这三个问题:跨主机搜索、数周至数月的长期保留,以及在 5 分钟内同一 C2 SNI 在三台不同主机上被拦截时触发 Sigma / Wazuh / OSSEC 规则的告警。 ``` services.microsegebpf = { enable = true; # ... your usual policy + observability bits ... logs.opensearch = { enable = true; # Any node of the cluster; Vector handles the bulk endpoint # routing internally. endpoint = "https://opensearch.corp.local:9200"; # Daily indices — the OpenSearch idiom for time-series data. # Strftime tokens are expanded by Vector at write time. indexFlows = "microseg-flows-%Y.%m.%d"; indexAgent = "microseg-agent-%Y.%m.%d"; # Basic auth (mandatory in production). The password is read # by systemd from the file at start time and passed to Vector # via LoadCredential — never on the command line, never in # the unit's environment as plaintext. auth.user = "microseg-shipper"; auth.passwordFile = "/run/keys/opensearch-microseg.pwd"; # TLS pinning to the corporate CA. Set verifyCertificate=false # only in a lab — the warning in the journald sink is loud # for a reason. tls.caFile = "/etc/ssl/certs/corp-internal-ca.pem"; }; }; ``` **工作原理。** - 代理**不直接与 OpenSearch 通信。** 它将结构化 JSON 写入 stdout(每个流事件一行)和 stderr(slog 控制平面记录)。systemd 使用 `_SYSTEMD_UNIT=microsegebpf-agent.service` 将两者都捕获到 journald 中。 - 该模块启用了第二个 systemd 单元(`microsegebpf-log-shipper.service`),在 `DynamicUser=true` 下运行 [Vector](https://vector.dev)。Vector 配置由 Nix 作为 JSON 文件在存储中生成,因此它作为 NixOS 闭包差异的一部分,是可复现且可审查的。 - Vector 管道由四个节点组成: 1. `sources.microseg_journal` —— 仅过滤到代理单元的 `journald` 源(`include_units` = `[ "microsegebpf-agent.service" ]`),`current_boot_only = true`。 2. `transforms.microseg_parse` —— VRL `remap`,将 `.message` 解码为 JSON,并将解析后的字段合并到事件根目录中。非 JSON 行原样通过。 3. `transforms.microseg_filter_{flows,agent}` —— 两个基于 `exists(.verdict)` 拆分的 `filter` 转换,因此流事件和 slog 记录会落入不同的索引中。 4. `sinks.opensearch_{flows,agent}` —— 两个 `elasticsearch` 接收器(Elasticsearch 线协议与 OpenSearch 相同),使用批量模式写入配置的索引。 - 转发器单元受到沙箱保护:`DynamicUser=true`,`ProtectSystem=strict`,`RestrictAddressFamilies` 仅限于 `AF_INET/AF_INET6/AF_UNIX`,系统调用过滤器为 `@system-service` + `@network-io`。它只需要到 OpenSearch 的网络出站权限和对 journald 的读取权限(通过 `SupplementaryGroups = [ "systemd-journal" ]` 授予)。 - **解耦非常重要。** 如果 OpenSearch 集群宕机,Vector 会以指数退避方式重试 —— 代理及其 eBPF 数据路径继续运行。如果转发器单元崩溃,journald 会继续缓冲,Vector 在重启时会从游标离开的地方继续处理。日志管道的宕机绝不会导致策略执行被中断。 **变体。** - **在源头添加字段**(例如,用工作站的主机名和 ANSSI 区域标记每个事件)—— 使用 `extraSettings` 在 `microseg_parse` 和过滤器之间插入另一个 `remap`: services.microsegebpf.logs.opensearch.extraSettings = { transforms.add_zone = { type = "remap"; inputs = [ "microseg_parse" ]; source = '' .anssi_zone = "poste-admin" .hostname = get_hostname!() ''; }; transforms.microseg_filter_flows.inputs = [ "add_zone" ]; transforms.microseg_filter_agent.inputs = [ "add_zone" ]; }; - **每个流使用不同的集群**(冷归档与热 SOC)—— 通过 `extraSettings` 覆盖其中一个接收器,使其指向带有不同认证的第二个端点。 - **针对不可靠 WAN 链路的磁盘支持缓冲** —— 设置 `extraSettings.sinks.opensearch_flows.buffer = { type = "disk"; max_size = 268435456; }`(上限为 256 MiB)。`data_dir = /var/lib/vector` 已连接完毕(带有 `StateDirectory = "vector"`,因此 `DynamicUser` 仍然有效)。 - **通过使用主机名模板化索引名称来为每个工作站保留一个 OpenSearch 索引**:`indexFlows = "microseg-flows-\${HOSTNAME}-%Y.%m.%d";`(Vector 在索引模板中扩展环境变量systemd 已经将 HOSTNAME 注入到单元的环境中)。 ### 方案 8 —— 集中式 syslog 转发(基于 TLS 的 RFC 5424) **用例。** 您的 SOC 拥有一台 SIEM(Splunk、Wazuh、ELK、IBM QRadar、Microsoft Sentinel 等),通过 syslog 接收日志。您希望每个流事件和每个代理控制平面日志都能以正确的设施代码存入其中,以便 SIEM 现有的解析和告警管道从第一天起就能触发。 **为什么这很重要。** OpenSearch 非常适合临时查询,但您 SOC 的事件响应工作流可能运行在 SIEM 上:关联规则、工单系统集成、MITRE ATT&CK 映射、待命呼叫。一台已经知道如何处理来自 NixOS 工作站的 `local4.warning` 的 SIEM,比一个没人值班待命的全新 OpenSearch 集群更容易上手。 **为什么使用 TLS。** 流事件包含丢弃流量的工作站名称、特定目的地和特定端口 —— 这正是已经渗透进内部的攻击者想要的资产清单。明文 UDP/514 syslog 会将所有这些信息泄露给路径上的任何被动窃听者。基于 RFC 5425 的 syslog-over-TLS(端口 6514)是现代默认选择;该模块默认设置 `mode = "tcp+tls"`,如果您降级到 UDP 或明文 TCP,会在部署时发出警告。 ``` services.microsegebpf = { enable = true; # ... your usual policy + observability bits ... logs.syslog = { enable = true; # SIEM collector. Port 6514 is the IANA assignment for # syslog-over-TLS (RFC 5425). Vector connects directly — # no rsyslog or syslog-ng relay in between. endpoint = "siem.corp.local:6514"; # Default; spell it out so the intent is reviewable. mode = "tcp+tls"; # APP-NAME field of the RFC 5424 header. SIEMs route on # this — keep it short (<= 48 ASCII chars) and stable. appName = "microsegebpf"; # Facilities. `local4` is a common SIEM convention for # security-relevant network logs; `daemon` is the # canonical service control-plane bucket. facilityFlows = "local4"; facilityAgent = "daemon"; # Pin the SIEM's CA. For mTLS, also set certFile + keyFile; # the key is loaded via systemd LoadCredential so it can # live on a path the dynamic user can't read directly # (e.g. /etc/ssl/private mode 0640 root:ssl-cert). tls = { caFile = "/etc/ssl/certs/corp-internal-ca.pem"; certFile = "/etc/ssl/certs/microseg-client.pem"; # mTLS, optional keyFile = "/etc/ssl/private/microseg-client.key"; # mTLS, optional # keyPassFile = "/run/keys/microseg-key-pass"; # if encrypted verifyCertificate = true; verifyHostname = true; }; }; }; ``` **工作原理。** - 该模块连接了一个与 OpenSearch 并行(或替代)的 Vector 管道 —— 同样的 `microsegebpf-log-shipper.service`,同样的 journald 来源,同样的解析 + 过滤转换。两个额外的 `remap` 转换将每个流格式化为 RFC 5424:
标签:0day挖掘, API集成, cgroupv2, Cilium, Docker镜像, gRPC, Hubble, Linux工作站, NixOS, NPM, Python工具, TLS SNI过滤, 内核安全, 可观测性, 客户端加密, 微隔离, 日志审计, 流量丢弃, 流量转发, 端点安全, 网络安全, 网络微分段, 网络访问控制, 补丁管理, 身份感知策略, 防火墙策略, 隐私保护, 零信任网络