diff --git a/configure.ac b/configure.ac index 92c55d0cc8..05a18ee92a 100644 --- a/configure.ac +++ b/configure.ac @@ -377,6 +377,13 @@ 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} diff --git a/src/Makefile.am b/src/Makefile.am index d2f23447d0..2760dfca77 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -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 \ diff --git a/src/suricata.c b/src/suricata.c index 2842ff6b5e..3f2cdc1051 100644 --- a/src/suricata.c +++ b/src/suricata.c @@ -162,6 +162,8 @@ #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 index 0000000000..cc1fadeac2 --- /dev/null +++ b/src/util-landlock.c @@ -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 + */ + +#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 + +#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 index 0000000000..5c090435c0 --- /dev/null +++ b/src/util-landlock.h @@ -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 + */ + +#ifndef __UTIL_LANDLOCK_H__ +#define __UTIL_LANDLOCK_H__ + +#include "suricata.h" + +void LandlockSandboxing(SCInstance *suri); + +#endif /* __UTIL_LANDLOCK_H__ */ diff --git a/suricata.yaml.in b/suricata.yaml.in index 7f3ad5b5ca..bb8712cd20 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -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