Running Falco with gVisor is a powerful way to layer security for your containers, but it introduces a new set of interactions you need to understand.

Here’s how it looks in practice. Let’s say you have a simple Nginx deployment running under gVisor. You want Falco to monitor this container.

First, you’ll need to run your container with gVisor. This is typically done by specifying the runtimeClassName in your Kubernetes pod spec:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-gvisor
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      runtimeClassName: gvisor
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80

Now, how do you get Falco to see what’s happening inside that gVisor sandbox? Falco typically hooks into the kernel’s syscall auditing mechanisms (like auditd or bpf). gVisor, by design, intercepts syscalls and emulates them in userspace, meaning they don’t directly hit the host kernel in the same way.

This is where the magic (and complexity) lies: gVisor provides a mechanism to forward selected syscalls to the host kernel, allowing tools like Falco to observe them. This is achieved through a specific gVisor configuration.

To enable this, you need to configure gVisor to enable "syscall forwarding" and specifically allow Falco’s necessary syscalls. This is done by modifying the gVisor configuration file, often located at /etc/gvisor/config.toml.

Here’s a snippet of a config.toml that would enable this:

# Enable syscall interception and forwarding
intercept_syscalls = true

# Define rules for which syscalls to forward to the host kernel
# This is a minimal set that Falco often relies on for basic operations.
# You might need to adjust this based on your specific Falco rules.
syscall_allowlist = [
  "read",
  "write",
  "open",
  "close",
  "stat",
  "lstat",
  "fstat",
  "access",
  "execve",
  "fork",
  "vfork",
  "clone",
  "wait4",
  "connect",
  "sendto",
  "recvfrom",
  "socket",
  "bind",
  "listen",
  "accept",
  "getdents",
  "pread64",
  "pwrite64",
  "readv",
  "writev",
  "select",
  "poll",
  "epoll_wait",
  "epoll_create1",
  "newfstatat",
  "fcntl",
  "ioctl",
  "pipe",
  "dup",
  "dup2",
  "nanosleep",
  "gettimeofday",
  "getpid",
  "getppid",
  "setsid",
  "setuid",
  "setgid",
  "setreuid",
  "setregid",
  "setgroups",
  "setpgid",
  "uname",
  "sysinfo",
  "mount",
  "umount2",
  "pivot_root",
  "chroot",
  "fchdir",
  "chdir",
  "getcwd",
  "rename",
  "mkdir",
  "rmdir",
  "creat",
  "link",
  "unlink",
  "symlink",
  "readlink",
  "chmod",
  "chown",
  "lchown",
  "setxattr",
  "lsetxattr",
  "fsetxattr",
  "getxattr",
  "lgetxattr",
  "fgetxattr",
  "listxattr",
  "llistxattr",
  "flistxattr",
  "removexattr",
  "lremovexattr",
  "fremovexattr",
  "mknod",
  "mkfifo",
  "utimes",
  "add_key",
  "request_key",
  "keyctl",
  "readahead",
  "sync",
  "fsync",
  "fdatasync",
  "fallocate",
  "fadvise64",
  "splice",
  "tee",
  "vmsplice",
  "prlimit64",
  "fanotify_init",
  "fanotify_mark",
  "prctl",
  "perf_event_open",
  "personality",
  "capget",
  "capset",
  "ptrace",
  "set_tid_address",
  "reboot",
  "syslog",
  "setpriority",
  "sched_setparam",
  "sched_getparam",
  "sched_setscheduler",
  "sched_getscheduler",
  "sched_get_priority_max",
  "sched_get_priority_min",
  "sched_rr_get_interval",
  "sched_yield",
  "sched_exit",
  "sched_getaffinity",
  "sched_setaffinity",
  "timerfd_create",
  "timerfd_settime",
  "timerfd_gettime",
  "eventfd",
  "eventfd_read",
  "eventfd_write",
  "signalfd",
  "signalfd4",
  "futex",
  "epoll_ctl",
  "epoll_create",
  "epoll_ctl_old",
  "sigaction",
  "sigprocmask",
  "sigreturn",
  "sigsuspend",
  "sigpending",
  "sigtimedwait",
  "sigqueueinfo",
  "kexec_load",
  "memfd_create",
  "shmget",
  "shmctl",
  "shmat",
  "shmdt",
  "msgget",
  "msgsnd",
  "msgrcv",
  "msgctl",
  "semget",
  "semop",
  "semctl",
  "mq_open",
  "mq_send",
  "mq_receive",
  "mq_notify",
  "mq_getsetattr",
  "mq_unlink",
  "epoll_pwait",
  "utimensat",
  "signalfd",
  "pidfd_send_signal",
  "io_uring_setup",
  "io_uring_enter",
  "io_uring_register",
  "io_uring_unregister",
  "openat",
  "mkdirat",
  "mknodat",
  "fchownat",
  "futimesat",
  "newfstatat",
  "unlinkat",
  "renameat",
  "linkat",
  "symlinkat",
  "readlinkat",
  "fchmodat",
  "faccessat",
  "pselect6",
  "ppoll",
  "unshare",
  "setns",
  "getcpu",
  "epoll_wait2",
  "epoll_pwait2",
  "mount", # Ensure mount/umount are often needed for file system events
  "umount2",
  "pivot_root",
  "chroot",
  "fchdir",
  "chdir",
  "getcwd",
  "rename",
  "mkdir",
  "rmdir",
  "creat",
  "link",
  "unlink",
  "symlink",
  "readlink",
  "chmod",
  "chown",
  "lchown",
  "setxattr",
  "lsetxattr",
  "fsetxattr",
  "getxattr",
  "lgetxattr",
  "fgetxattr",
  "listxattr",
  "llistxattr",
  "flistxattr",
  "removexattr",
  "lremovexattr",
  "fremovexattr",
  "mknod",
  "mkfifo",
  "utimes",
  "add_key",
  "request_key",
  "keyctl",
  "readahead",
  "sync",
  "fsync",
  "fdatasync",
  "fallocate",
  "fadvise64",
  "splice",
  "tee",
  "vmsplice",
  "prlimit64",
  "fanotify_init",
  "fanotify_mark",
  "prctl",
  "perf_event_open",
  "personality",
  "capget",
  "capset",
  "ptrace",
  "set_tid_address",
  "reboot",
  "syslog",
  "setpriority",
  "sched_setparam",
  "sched_getparam",
  "sched_setscheduler",
  "sched_getscheduler",
  "sched_get_priority_max",
  "sched_get_priority_min",
  "sched_rr_get_interval",
  "sched_yield",
  "sched_exit",
  "sched_getaffinity",
  "sched_setaffinity",
  "timerfd_create",
  "timerfd_settime",
  "timerfd_gettime",
  "eventfd",
  "eventfd_read",
  "eventfd_write",
  "signalfd",
  "signalfd4",
  "futex",
  "epoll_ctl",
  "epoll_create",
  "epoll_ctl_old",
  "sigaction",
  "sigprocmask",
  "sigreturn",
  "sigsuspend",
  "sigpending",
  "sigtimedwait",
  "sigqueueinfo",
  "kexec_load",
  "memfd_create",
  "shmget",
  "shmctl",
  "shmat",
  "shmdt",
  "msgget",
  "msgsnd",
  "msgrcv",
  "msgctl",
  "semget",
  "semop",
  "semctl",
  "mq_open",
  "mq_send",
  "mq_receive",
  "mq_notify",
  "mq_getsetattr",
  "mq_unlink",
  "epoll_pwait",
  "utimensat",
  "signalfd",
  "pidfd_send_signal",
  "io_uring_setup",
  "io_uring_enter",
  "io_uring_register",
  "io_uring_unregister",
  "openat",
  "mkdirat",
  "mknodat",
  "fchownat",
  "futimesat",
  "newfstatat",
  "unlinkat",
  "renameat",
  "linkat",
  "symlinkat",
  "readlinkat",
  "fchmodat",
  "faccessat",
  "pselect6",
  "ppoll",
  "unshare",
  "setns",
  "getcpu",
  "epoll_wait2",
  "epoll_pwait2",
]

This syscall_allowlist tells gVisor which syscalls, when invoked by a sandboxed process, should be passed through to the host kernel for actual execution. Falco then hooks into the host kernel’s auditing mechanisms to capture these forwarded syscalls.

The key insight here is that gVisor doesn’t block Falco; it acts as an intermediary. By carefully configuring gVisor’s syscall_allowlist, you decide which events are "visible" to the host kernel and thus to Falco. A broad allowlist provides more visibility for Falco but slightly reduces the isolation gVisor offers. A very narrow list might prevent Falco from detecting critical events.

The challenge when running Falco with gVisor is that the default Falco rules are often written assuming direct kernel access. When syscalls are intercepted and emulated by gVisor, their behavior might differ, or certain syscalls might not even reach the kernel if they are not in the syscall_allowlist.

For example, a Falco rule like container_proc_exec might trigger on a execve syscall. If execve is in the syscall_allowlist, Falco will see it. However, if gVisor itself performs an action related to that execve (e.g., preparing the execution environment), Falco might not see the exact same sequence of events as it would on a non-gVisor container.

You’ll likely need to tune your Falco rules. This often means:

  1. Identifying missing events: Observe what Falco isn’t detecting that you expect it to.
  2. Checking the syscall_allowlist: Ensure the relevant syscalls are indeed being forwarded.
  3. Adjusting gVisor configuration: Add missing syscalls to the syscall_allowlist in config.toml and restart the gVisor runtime.
  4. Modifying Falco rules: Sometimes, a rule needs to be adapted to account for the indirect path of the syscall or to look for slightly different patterns that emerge from the gVisor emulation.

The most common pitfall is a missing syscall in the syscall_allowlist. If Falco is reporting that it’s not seeing certain file access events within a gVisor container, double-check if open, openat, read, write, stat, newfstatat are present. If network connections aren’t being monitored, verify connect, bind, socket.

The next hurdle you’ll likely face is understanding how gVisor’s network emulation interacts with Falco’s network-related rules, as network events can be particularly tricky to trace across the sandbox boundary.

Want structured learning?

Take the full Falco course →