Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0876578
conf: better handling of set
regit Jul 23, 2026
cf54099
conf: add an util to iterate on sequence
regit Jul 29, 2026
0815f7d
landlock: expose path grant helpers as public API
regit Aug 9, 2026
5a97f3a
landlock: add network access sandboxing
regit Aug 9, 2026
9921b05
landlock: support new kernel filesystem access rights
regit Aug 9, 2026
320675c
landlock: allow plugin to declare their needs
regit Aug 9, 2026
43bdcb0
landlock: allow output modules to declare their needs
regit Aug 9, 2026
2eed5e9
landlock: add SCLandlockForEachOutput helper
regit Aug 6, 2026
183a0ec
landlock: add YAML option to override network lock
regit Jun 12, 2026
b85cf68
landlock: add some needed access to sandbox
regit Jun 14, 2026
d1eb439
landlock: sandbox earlier
regit Jun 14, 2026
c9fac6e
landlock: allow disabling landlock setup in plugin
regit Jun 14, 2026
72d7d46
output/pcap: implement landlock enabler
regit Jun 11, 2026
bf69d1a
eve: implement landlock enabler
regit Jun 11, 2026
8fcce21
landlock: handle truncation of files
regit Jul 23, 2026
a8e337c
landlock: authorization for some files
regit Jul 23, 2026
822420a
landlock: handle renaming in filestore
regit Jul 23, 2026
4ccf591
landlock: fix dataset state keyword
regit Jul 24, 2026
c24c58b
landlock: remove removal permission when sandboxing
regit Jul 24, 2026
8807e2c
fastlog: add landlock support
regit Jul 28, 2026
ad04674
docs: document landlock update
regit Jun 12, 2026
b17e715
landlock: read permission for rules files
regit Jul 28, 2026
0e970d7
landlock: add an option to rewrite a dir content
regit Jul 29, 2026
e4632bf
github: add landlock workflow
regit Jul 29, 2026
cc6e4bf
landlock: support older kernel headers
regit Jul 29, 2026
7b35750
landlock: absent config dir is not an error
regit Jul 29, 2026
b50a3b2
landlock: be stricter on write permissions
regit Jul 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 179 additions & 0 deletions .github/workflows/landlock.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
name: landlock

on:
push:
paths-ignore:
# Don't run this workflow if only files under doc/ have been
# modified.
- "doc/**"
- "etc/schema.json"
pull_request:
paths-ignore:
# Don't run this workflow if only files under doc/ have been
# modified.
- "doc/**"
- "etc/schema.json"
workflow_dispatch:
inputs:
SU_REPO:
SU_BRANCH:
SV_REPO:
SV_BRANCH:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions: read-all

env:
DEFAULT_CFLAGS: "-Wall -Wextra -Werror -Wno-unused-parameter -Wno-unused-function"

# Apt sometimes likes to ask for user input, this will prevent that.
DEBIAN_FRONTEND: "noninteractive"

# A recent version of stable Rust that is known to pass build, test and other
# verification steps in this workflow. This was added because using "stable"
# could cause some steps to fail.
RUST_VERSION_KNOWN: "1.95.0"

jobs:

prepare-deps:
name: Prepare dependencies
uses: ./.github/workflows/prepare-deps.yml

# Run suricata-verify with the Landlock sandbox enabled.
#
# This job deliberately runs directly on the runner instead of in a
# container: Landlock is a kernel LSM, so enforcement depends on the host
# kernel rather than on the image. The Ubuntu runners ship a kernel with
# Landlock available (5.13+, ABI 1+), which is all Suricata needs.
landlock:
name: Landlock sandbox (suricata-verify)
runs-on: ubuntu-24.04
needs: [prepare-deps]
steps:
- name: Determine number of CPUs
run: echo CPUS=$(nproc --all) >> $GITHUB_ENV

- name: Cache ~/.cargo
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
with:
path: ~/.cargo/registry
key: cargo-registry

- name: Install dependencies
run: |
sudo apt update
sudo apt -y install \
autoconf \
automake \
build-essential \
cargo \
cbindgen \
git \
jq \
libcap-ng-dev \
libevent-dev \
libhiredis-dev \
libjansson-dev \
liblz4-dev \
liblua5.1-dev \
libmagic-dev \
libnet1-dev \
libnetfilter-queue-dev \
libnfnetlink-dev \
libpcap-dev \
libpcre2-dev \
libtool \
libyaml-dev \
make \
python3-yaml \
rustc \
zlib1g-dev

- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0

# Landlock is only compiled in when linux/landlock.h is present, and it
# is only enforced when the running kernel supports it. If either is
# missing Suricata logs an error and carries on unsandboxed, which would
# make every test below pass without exercising anything. Fail loudly
# instead.
- name: Check kernel Landlock support
run: |
echo "kernel: $(uname -r)"
if [ ! -e /sys/kernel/security/lsm ]; then
echo "securityfs not mounted, cannot confirm Landlock is active" >&2
exit 1
fi
cat /sys/kernel/security/lsm
if ! grep -q landlock /sys/kernel/security/lsm; then
echo "Landlock is not among the active LSMs on this kernel" >&2
exit 1
fi

# Comes from prepare-deps rather than a plain checkout, so that a pull
# request can point at a matching suricata-verify branch or PR from its
# body (SV_REPO / SV_BRANCH). Landlock changes usually need paired test
# updates, so that override matters here.
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: prep
path: prep
- run: tar xf prep/suricata-verify.tar.gz

- run: ./autogen.sh
- run: ./configure
- run: make -j ${{ env.CPUS }}

- name: Check Landlock was built in
run: |
./src/suricata --build-info | grep -E '^ *Landlock support: +yes$'

# Confirm the sandbox actually engages before relying on the suite: a
# kernel or build without Landlock only logs an error and keeps running,
# so a green suite would otherwise prove nothing.
# Suricata's exit code is deliberately ignored: run from the build tree
# it also complains about the not-yet-installed sysconfdir and rule
# directory. Only the sandbox message matters here, the suite below is
# what checks Suricata actually behaves.
- name: Check the sandbox is enforced
run: |
mkdir -p landlock-check
./src/suricata -c suricata.yaml -l landlock-check \
--set security.landlock.enabled=yes \
--set logging.default-log-level=info \
-r suricata-verify/tests/datasets/datasets-parent-path/one-packet.pcap \
> landlock-check/run.log 2>&1 || true
cat landlock-check/run.log
grep -q "Sandboxing via landlock is active" landlock-check/run.log

# SV_LANDLOCK makes the handful of tests that cannot work under the
# sandbox skip themselves (see their test.yaml).
#
# The magic database has to be granted explicitly. Most tests ship their
# own suricata.yaml, which replaces the top level one entirely and so
# carries no landlock section at all -- the /usr/ read entry from the
# default configuration does not apply to them, and any test using the
# filemagic keyword then dies with "could not find any valid magic
# files".
#
# /usr is granted wholesale, the same thing the default suricata.yaml
# does, rather than guessing at the magic database location: libmagic
# picks its own compiled-in path and distributions disagree about it
# (/usr/share/misc, /usr/share/file, /usr/lib/file, ...).
#
# Index 3 is used because --set on a list replaces the entry at that
# index instead of appending: 0..2 are taken in the default
# suricata.yaml, and overwriting index 0 would silently drop /usr/.
- name: Run suricata-verify with Landlock enabled
run: |
SV_LANDLOCK=1 python3 ./suricata-verify/run.py -q --debug-failed \
--set security.landlock.enabled=yes \
--set security.landlock.directories.read.3=/usr

# A run without the sandbox, to make sure a failure above is attributable
# to Landlock rather than to something unrelated.
- name: Run suricata-verify without Landlock (control)
run: python3 ./suricata-verify/run.py -q --debug-failed
17 changes: 17 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,23 @@
enable_landlock="no"
if test "$ac_cv_header_linux_landlock_h" = "yes"; then
enable_landlock="yes"
# struct landlock_ruleset_attr gained the scoped field in Linux 6.12
# (Landlock ABI 6). Missing constants can be defined by hand, a
# struct member cannot, so it needs its own check.
AC_CHECK_MEMBER([struct landlock_ruleset_attr.scoped],
[AC_DEFINE([HAVE_LANDLOCK_RULESET_ATTR_SCOPED], [1],
[Set if struct landlock_ruleset_attr has the scoped field])],
[],
[[#include <linux/landlock.h>]])
# struct landlock_ruleset_attr gained the handled_access_net field in
# Linux 6.7 (Landlock ABI 4), together with struct
# landlock_net_port_attr and LANDLOCK_RULE_NET_PORT. As with scoped, a
# struct member cannot be defined by hand, so it needs its own check.
AC_CHECK_MEMBER([struct landlock_ruleset_attr.handled_access_net],
[AC_DEFINE([HAVE_LANDLOCK_RULESET_ATTR_HANDLED_ACCESS_NET], [1],
[Set if struct landlock_ruleset_attr has the handled_access_net field])],
[],
[[#include <linux/landlock.h>]])
fi

#check for plugin support
Expand Down
153 changes: 151 additions & 2 deletions doc/userguide/configuration/landlock.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ To enable Landlock, edit the YAML and set ``enabled`` to ``yes``:

landlock:
enabled: yes
plugin-setup: false
directories:
write:
- /var/log/suricata/
Expand All @@ -35,8 +36,156 @@ To enable Landlock, edit the YAML and set ``enabled`` to ``yes``:
- /etc/suricata/

Following your running configuration you may have to add some directories.
There are two lists you can use, ``write`` to add directories where write is needed
and ``read`` for directories where read access is needed.
There are three lists you can use, ``write`` to add directories where write is needed,
``read`` for directories where read access is needed and ``rewrite`` for
directories holding files that are rewritten in place.

``rewrite`` grants read, write *and* truncate on the directory. It is needed
for files that are replaced in place each time they are updated, the previous
content being discarded first -- dataset ``save``/``state`` files are the
typical case. Plain ``write`` deliberately leaves truncation out, since
emptying a file is a classic way to erase traces, so a file that already has
content cannot be rewritten with ``write`` alone::

landlock:
enabled: yes
directories:
rewrite:
- /var/lib/mysets/

Built-in outputs (``pcap-log``, ``fast``, ``eve-log`` with ``redis``, ``unix_*``
and custom ``filename`` paths, ...) declare the filesystem and network access
they need on their own. In particular, an absolute ``filename`` on the ``fast``
output is granted per-file, so the common ``filename: /dev/null`` idiom (enable
the module but discard its output) works without opening up write access to the
whole ``/dev`` directory.
Plugins can do the same by implementing the ``LandlockEnable``
callback on ``SCPlugin`` (see :ref:`libsuricata`). The lists above only need to
contain directories that are not covered by these declarations. If ever letting
the plugin set up landlock is not wanted, one can set the `plugin-setup` option
to `false`.

A handful of system pseudo-files are also granted read access automatically:
``/sys/devices/system/cpu`` (online-CPU detection via ``sysconf``), ``/proc/stat``,
``/proc/sys/vm/overcommit_memory`` (allocator tuning) and ``/dev/urandom`` (RNG
seeding fallback). These are probed by glibc, the system allocator and the Rust
standard library during normal startup; granting them avoids spurious ``EACCES``
errors and Landlock audit noise without meaningfully widening the sandbox.
Missing paths are silently skipped.

Rule files passed on the command line are handled too: ``-s``/``-S`` get read
access on the directory holding the rule file, and
``--firewall-rules-exclusive`` gets a per-file read grant. A relative path is
resolved against ``default-rule-path`` (``firewall.rule-path`` for the firewall
rule file), which is granted as a directory.

Lua scripts writing their own files
-----------------------------------

Suricata cannot know in advance which files a Lua output script will open:
the path is chosen by the script at runtime, often built from per-flow data
such as addresses and ports. Such writes are therefore *not* granted
automatically and will fail with ``Permission denied`` once the sandbox is
active, for example::

Info: output-lua: failed to run script: ./streaming-tcp.lua:25:
/var/log/suricata/6-10.0.0.1-10.0.0.2-1234-80: Permission denied

When using a Lua script that writes files on its own, add the target
directory to ``security.landlock.directories.write``::

landlock:
enabled: yes
directories:
write:
- /var/log/suricata/

Scripts that only write through Suricata's own logging facilities do not
need any extra permission.

Datasets using an absolute path
-------------------------------

Suricata's data directory (``$localstatedir/lib/suricata``, where datasets are
kept by default) is granted read and write access automatically, so rules
whose ``load``, ``save`` or ``state`` file is a plain relative name work out
of the box.

A rule that points at an *absolute* path is different: the file lives outside
the data directory and Suricata cannot guess it, so nothing grants it. Note
that absolute paths are refused outright unless
``datasets.rules.allow-absolute-filenames`` is enabled -- once it is, the
sandbox becomes the next thing in the way and the access has to be declared by
hand.

Which list to use depends on the dataset keyword used in the signature:

``load``
add its directory to ``security.landlock.directories.read``, otherwise the
rule fails to load.

``save``
add its directory to ``security.landlock.directories.rewrite``. Plain
``write`` creates the file the first time but cannot replace the content of
an existing one, so the set would silently stop being updated from the second
run on. Note that ``rewrite`` also grants read, which a ``save``-only set
does not need.

``state``
a shorthand for ``load`` plus ``save`` on the same file, so it needs the
same ``rewrite`` grant as ``save``.

Note that a denied dataset save is quiet: the set is simply not written and no
error is logged. If a ``save``/``state`` file stops being updated after
enabling Landlock, a missing ``rewrite`` entry is the first thing to check.

For example, with a rule such as::

alert dns any any -> any any (dns.query; \
dataset:isnotset,dns-seen,type string,state /var/lib/mysets/dns-seen.txt; \
sid:1; rev:1;)

the matching configuration is::

landlock:
enabled: yes
directories:
rewrite:
- /var/lib/mysets/

The same directories can be passed on the command line instead::

suricata --set security.landlock.directories.rewrite.0=/var/lib/mysets/

Beware that ``--set`` on a list *replaces* the entry at that index: if the
YAML already defines entries, use the next free index (or add the directory to
the YAML) rather than overwriting index ``0``.

Granting access to network ports
--------------------------------

When a module or plugin cannot declare its needs (for example a third-party
filetype that opens an unknown TCP service), TCP ports can be granted manually
under ``security.landlock.network``. There is no default value: ports listed
here are *added* to whatever the modules and plugins have already declared.

::

landlock:
enabled: yes
network:
connect:
tcp:
- 6379
- 9092
bind:
tcp:
- 8080

``connect.tcp`` lists ports the process is allowed to connect to (e.g. a Redis
or Kafka broker). ``bind.tcp`` lists ports it is allowed to bind/listen on.
Both options are silently ignored on kernels whose Landlock ABI does not
support network rules (ABI < 4).

Landlock is not active in some distributions and you may need to activate it
at boot by adding ``lsm=landock`` to the Linux command line. For example,
Expand Down
Loading
Loading