]> git.ipfire.org Git - thirdparty/suricata.git/commitdiff
landlock: basic implementation
authorEric Leblond <el@stamus-networks.com>
Wed, 20 Jul 2022 09:05:57 +0000 (11:05 +0200)
committerVictor Julien <vjulien@oisf.net>
Tue, 13 Sep 2022 08:36:00 +0000 (10:36 +0200)
This patch is adding support for Landlock, a Linux
Security Module available since Linux 5.13.

The concept is to prevent any file operation on directories where
Suricata is not supposed to access.

Landlock support is built by default if the header is present. The
feature is disabled by default and need to be activated in the YAML
to be active.

Landlock documentation: https://docs.kernel.org/userspace-api/landlock.html

Feature: #5479

configure.ac
src/Makefile.am
src/suricata.c
src/util-landlock.c [new file with mode: 0644]
src/util-landlock.h [new file with mode: 0644]
suricata.yaml.in

index 92c55d0cc8aaca1e734b0741e8ac99d5963f3c9e..05a18ee92af61f413f304d52e6256a77e046cefe 100644 (file)
         AC_SUBST(SECLDFLAGS)
     ])
 
+    #check for Landlock support
+    AC_CHECK_HEADERS([linux/landlock.h])
+    enable_landlock="no"
+    if test "$ac_cv_header_linux_landlock_h" = "yes"; then
+        enable_landlock="yes"
+    fi
+
     #check for plugin support
     AC_CHECK_HEADERS([dlfcn.h])
     AC_MSG_CHECKING([for plugin support])
@@ -2513,6 +2520,7 @@ SURICATA_BUILD_CONF="Suricata Configuration:
   Hyperscan support:                       ${enable_hyperscan}
   Libnet support:                          ${enable_libnet}
   liblz4 support:                          ${enable_liblz4}
+  Landlock support:                        ${enable_landlock}
 
   Rust support:                            ${enable_rust}
   Rust strict mode:                        ${enable_rust_strict}
index d2f23447d0c03c4c81bc45b0d26773cac7189ce8..2760dfca7793c4d13faef44dd1df0b6387feff4a 100755 (executable)
@@ -546,6 +546,7 @@ noinst_HEADERS = \
        util-ioctl.h \
        util-ip.h \
        util-ja3.h \
+       util-landlock.h \
        util-logopenfile.h \
        util-log-redis.h \
        util-lua-common.h \
@@ -1132,6 +1133,7 @@ libsuricata_c_a_SOURCES = \
        util-ioctl.c \
        util-ip.c \
        util-ja3.c \
+       util-landlock.c \
        util-logopenfile.c \
        util-log-redis.c \
        util-lua.c \
index 2842ff6b5e6c732ac956e6a1e0624d2a623e9862..3f2cdc1051646a8671dc4885e40070bd308b433e 100644 (file)
 #include "util-daemon.h"
 #include "util-byte.h"
 #include "util-luajit.h"
+#include "util-landlock.h"
+
 #include "reputation.h"
 
 #include "output.h"
@@ -2904,6 +2906,8 @@ int SuricataMain(int argc, char **argv)
         exit(EXIT_FAILURE);
     }
 
+    LandlockSandboxing(&suricata);
+
     SCDropMainThreadCaps(suricata.userid, suricata.groupid);
 
     /* Re-enable coredumps after privileges are dropped. */
diff --git a/src/util-landlock.c b/src/util-landlock.c
new file mode 100644 (file)
index 0000000..cc1fade
--- /dev/null
@@ -0,0 +1,275 @@
+/* Copyright (C) 2022 Open Information Security Foundation
+ *
+ * You can copy, redistribute or modify this Program under the terms of
+ * the GNU General Public License version 2 as published by the Free
+ * Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * version 2 along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+/**
+ * \file
+ *
+ * \author Eric Leblond <el@stamus-networks.com>
+ */
+
+#include "suricata.h"
+#include "util-conf.h"
+#include "util-landlock.h"
+#include "util-mem.h"
+
+#ifndef HAVE_LINUX_LANDLOCK_H
+
+void LandlockSandboxing(SCInstance *suri)
+{
+    return;
+}
+
+#else /* HAVE_LINUX_LANDLOCK_H */
+
+#include <linux/landlock.h>
+
+#ifndef landlock_create_ruleset
+static inline int landlock_create_ruleset(
+        const struct landlock_ruleset_attr *const attr, const size_t size, const __u32 flags)
+{
+    return syscall(__NR_landlock_create_ruleset, attr, size, flags);
+}
+#endif
+
+#ifndef landlock_add_rule
+static inline int landlock_add_rule(const int ruleset_fd, const enum landlock_rule_type rule_type,
+        const void *const rule_attr, const __u32 flags)
+{
+    return syscall(__NR_landlock_add_rule, ruleset_fd, rule_type, rule_attr, flags);
+}
+#endif
+
+#ifndef landlock_restrict_self
+static inline int landlock_restrict_self(const int ruleset_fd, const __u32 flags)
+{
+    return syscall(__NR_landlock_restrict_self, ruleset_fd, flags);
+}
+#endif
+
+#ifndef LANDLOCK_ACCESS_FS_REFER
+#define LANDLOCK_ACCESS_FS_REFER (1ULL << 13)
+#endif
+
+#define _LANDLOCK_ACCESS_FS_WRITE                                                                  \
+    (LANDLOCK_ACCESS_FS_WRITE_FILE | LANDLOCK_ACCESS_FS_REMOVE_DIR |                               \
+            LANDLOCK_ACCESS_FS_REMOVE_FILE | LANDLOCK_ACCESS_FS_MAKE_CHAR |                        \
+            LANDLOCK_ACCESS_FS_MAKE_DIR | LANDLOCK_ACCESS_FS_MAKE_REG |                            \
+            LANDLOCK_ACCESS_FS_MAKE_SOCK | LANDLOCK_ACCESS_FS_MAKE_FIFO |                          \
+            LANDLOCK_ACCESS_FS_MAKE_BLOCK | LANDLOCK_ACCESS_FS_MAKE_SYM |                          \
+            LANDLOCK_ACCESS_FS_REFER)
+
+#define _LANDLOCK_ACCESS_FS_READ (LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR)
+
+#define _LANDLOCK_SURI_ACCESS_FS_WRITE                                                             \
+    (LANDLOCK_ACCESS_FS_WRITE_FILE | LANDLOCK_ACCESS_FS_MAKE_DIR | LANDLOCK_ACCESS_FS_MAKE_REG |   \
+            LANDLOCK_ACCESS_FS_REMOVE_FILE | LANDLOCK_ACCESS_FS_MAKE_SOCK)
+
+struct landlock_ruleset {
+    int fd;
+    struct landlock_ruleset_attr attr;
+};
+
+static inline struct landlock_ruleset *LandlockCreateRuleset(void)
+{
+    struct landlock_ruleset *ruleset = SCCalloc(1, sizeof(struct landlock_ruleset));
+    if (ruleset == NULL) {
+        SCLogError(SC_ERR_MEM_ALLOC, "Can't alloc landlock ruleset");
+        return NULL;
+    }
+
+    ruleset->attr.handled_access_fs =
+            _LANDLOCK_ACCESS_FS_READ | _LANDLOCK_ACCESS_FS_WRITE | LANDLOCK_ACCESS_FS_EXECUTE;
+
+    int abi = landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
+    if (abi < 0) {
+        SCFree(ruleset);
+        return NULL;
+    }
+    if (abi < 2) {
+        ruleset->attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_REFER;
+    }
+
+    ruleset->fd = landlock_create_ruleset(&ruleset->attr, sizeof(ruleset->attr), 0);
+    if (ruleset->fd < 0) {
+        SCFree(ruleset);
+        SCLogError(SC_ERR_CONF_YAML_ERROR, "Can't create landlock ruleset");
+        return NULL;
+    }
+    return ruleset;
+}
+
+static inline void LandlockEnforceRuleset(struct landlock_ruleset *ruleset)
+{
+    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) {
+        SCLogError(
+                SC_ERR_CONF_YAML_ERROR, "Can't self restrict (prctl phase): %s", strerror(errno));
+        return;
+    }
+    if (landlock_restrict_self(ruleset->fd, 0)) {
+        SCLogError(SC_ERR_CONF_YAML_ERROR, "Can't self restrict (landlock phase): %s",
+                strerror(errno));
+    }
+}
+
+static int LandlockSandboxingAddRule(
+        struct landlock_ruleset *ruleset, const char *directory, uint64_t permission)
+{
+    struct landlock_path_beneath_attr path_beneath = {
+        .allowed_access = permission & ruleset->attr.handled_access_fs,
+    };
+
+    int dir_fd = open(directory, O_PATH | O_CLOEXEC | O_DIRECTORY);
+    if (dir_fd == -1) {
+        SCLogError(SC_ERR_CONF_YAML_ERROR, "Can't open %s", directory);
+        return -1;
+    }
+    path_beneath.parent_fd = dir_fd;
+
+    if (landlock_add_rule(ruleset->fd, LANDLOCK_RULE_PATH_BENEATH, &path_beneath, 0)) {
+        SCLogError(SC_ERR_CONF_YAML_ERROR, "Can't add write rule: %s", strerror(errno));
+        close(dir_fd);
+        return -1;
+    }
+
+    close(dir_fd);
+    return 0;
+}
+
+static inline void LandlockSandboxingWritePath(
+        struct landlock_ruleset *ruleset, const char *directory)
+{
+    if (LandlockSandboxingAddRule(ruleset, directory, _LANDLOCK_SURI_ACCESS_FS_WRITE) == 0) {
+        SCLogConfig("Added write permission to '%s'", directory);
+    }
+}
+
+static inline void LandlockSandboxingReadPath(
+        struct landlock_ruleset *ruleset, const char *directory)
+{
+    if (LandlockSandboxingAddRule(ruleset, directory, _LANDLOCK_ACCESS_FS_READ) == 0) {
+        SCLogConfig("Added read permission to '%s'", directory);
+    }
+}
+
+void LandlockSandboxing(SCInstance *suri)
+{
+    /* Read configuration variable and exit if no enforcement */
+    int conf_status;
+    ConfGetBool("security.landlock.enabled", &conf_status);
+    if (!conf_status) {
+        SCLogConfig("Landlock is not enabled in configuration");
+        return;
+    }
+    struct landlock_ruleset *ruleset = LandlockCreateRuleset();
+    if (ruleset == NULL) {
+        SCLogError(SC_ERR_NOT_SUPPORTED, "Kernel does not support Landlock");
+        return;
+    }
+
+    LandlockSandboxingWritePath(ruleset, ConfigGetLogDirectory());
+    struct stat sb;
+    if (stat(ConfigGetDataDirectory(), &sb) == 0) {
+        LandlockSandboxingAddRule(ruleset, ConfigGetDataDirectory(),
+                _LANDLOCK_SURI_ACCESS_FS_WRITE | _LANDLOCK_ACCESS_FS_READ);
+    }
+    if (suri->run_mode == RUNMODE_PCAP_FILE) {
+        const char *pcap_file;
+        ConfGet("pcap-file.file", &pcap_file);
+        char *file_name = SCStrdup(pcap_file);
+        if (file_name != NULL) {
+            struct stat statbuf;
+            if (stat(file_name, &statbuf) != -1) {
+                if (S_ISDIR(statbuf.st_mode)) {
+                    LandlockSandboxingReadPath(ruleset, file_name);
+                } else {
+                    LandlockSandboxingReadPath(ruleset, dirname(file_name));
+                }
+            } else {
+                SCLogError(SC_ERR_OPENING_FILE, "Can't open pcap file");
+            }
+            SCFree(file_name);
+        }
+    }
+    if (suri->sig_file) {
+        char *file_name = SCStrdup(suri->sig_file);
+        if (file_name != NULL) {
+            LandlockSandboxingReadPath(ruleset, dirname(file_name));
+            SCFree(file_name);
+        }
+    }
+    if (suri->pid_filename) {
+        char *file_name = SCStrdup(suri->pid_filename);
+        if (file_name != NULL) {
+            LandlockSandboxingWritePath(ruleset, dirname(file_name));
+            SCFree(file_name);
+        }
+    }
+    if (ConfUnixSocketIsEnable()) {
+        const char *socketname;
+        if (ConfGet("unix-command.filename", &socketname) == 1) {
+            if (PathIsAbsolute(socketname)) {
+                char *file_name = SCStrdup(socketname);
+                if (file_name != NULL) {
+                    LandlockSandboxingWritePath(ruleset, dirname(file_name));
+                    SCFree(file_name);
+                }
+            } else {
+                LandlockSandboxingWritePath(ruleset, LOCAL_STATE_DIR "/run/suricata/");
+            }
+        } else {
+            LandlockSandboxingWritePath(ruleset, LOCAL_STATE_DIR "/run/suricata/");
+        }
+    }
+    if (suri->sig_file_exclusive == FALSE) {
+        const char *rule_path;
+        ConfGet("default-rule-path", &rule_path);
+        if (rule_path) {
+            LandlockSandboxingReadPath(ruleset, rule_path);
+        }
+    }
+
+    ConfNode *read_dirs = ConfGetNode("security.landlock.directories.read");
+    if (read_dirs) {
+        if (!ConfNodeIsSequence(read_dirs)) {
+            SCLogWarning(SC_ERR_INVALID_ARGUMENT,
+                    "Invalid security.landlock.directories.read configuration section: "
+                    "expected a list of directory names.");
+        } else {
+            ConfNode *directory;
+            TAILQ_FOREACH (directory, &read_dirs->head, next) {
+                LandlockSandboxingReadPath(ruleset, directory->val);
+            }
+        }
+    }
+    ConfNode *write_dirs = ConfGetNode("security.landlock.directories.write");
+    if (write_dirs) {
+        if (!ConfNodeIsSequence(write_dirs)) {
+            SCLogWarning(SC_ERR_INVALID_ARGUMENT,
+                    "Invalid security.landlock.directories.write configuration section: "
+                    "expected a list of directory names.");
+        } else {
+            ConfNode *directory;
+            TAILQ_FOREACH (directory, &write_dirs->head, next) {
+                LandlockSandboxingWritePath(ruleset, directory->val);
+            }
+        }
+    }
+    LandlockEnforceRuleset(ruleset);
+    SCFree(ruleset);
+}
+
+#endif /* HAVE_LINUX_LANDLOCK_H */
diff --git a/src/util-landlock.h b/src/util-landlock.h
new file mode 100644 (file)
index 0000000..5c09043
--- /dev/null
@@ -0,0 +1,31 @@
+/* Copyright (C) 2022 Open Information Security Foundation
+ *
+ * You can copy, redistribute or modify this Program under the terms of
+ * the GNU General Public License version 2 as published by the Free
+ * Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * version 2 along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+/**
+ * \file
+ *
+ * \author Eric Leblond <el@stamus-networks.com>
+ */
+
+#ifndef __UTIL_LANDLOCK_H__
+#define __UTIL_LANDLOCK_H__
+
+#include "suricata.h"
+
+void LandlockSandboxing(SCInstance *suri);
+
+#endif /* __UTIL_LANDLOCK_H__ */
index 7f3ad5b5caba8db214e423db50772ff3aff1e1e8..bb8712cd200c616f8a10b888b3c7fe41d03a5774 100644 (file)
@@ -1097,6 +1097,20 @@ asn1-max-frames: 256
 #  user: suri
 #  group: suri
 
+security:
+  # Use landlock security module under Linux
+  landlock:
+    enabled: no
+    directories:
+      #write:
+      #  - @e_rundir@
+      # /usr and /etc folders are added to read list to allow
+      # file magic to be used.
+      read:
+        - /usr/
+        - /etc/
+        - @e_sysconfdir@
+
 # Some logging modules will use that name in event as identifier. The default
 # value is the hostname
 #sensor-name: suricata