sftp-server: add a flag to call unveil on starting directory
s-k2 at caipora.net
s-k2 at caipora.net
Thu Jan 29 13:50:57 AEDT 2026
On Thu, Jan 29, 2026 at 09:35:08AM +1100, Damien Miller wrote:
> We could do something quite similar for linux using landlock
> LANDLOCK_RULE_PATH_BENEATH.
I had hopes to bring that feature into the OpenBSD sftp-server without
having to consider other platforms. But well, I made up a first draft
of a patch for landlock support in portable OpenSSH.
This is rough first version, could you please give me some input:
Does that follow the code guidelines/code organization rules?
Things to consider:
- Landlock has subtile differences. It leaks the information if a file
exists (if the user has access to it) even if restrictions are
enabled. I don't know a way around that. unveil doesn't leak that
information, it just returns EACCES.
- I need to fix the configure.am file, it currently just checks for
the presence of the header. But I have to check if the syscall is
defined as normal function (which landlock.h doesn't do yet, but
this could lead to compile errors in the future)
- I need to do more testing, I just compiled it and tried a few file
operations
- I haven't adjusted the man page yet...
Feel free to comment on the patch below, I will try to incorporate
that. But for now I need to know if that patch is the right direction.
> For FreeBSD, this is potentially possible using Capsicum though there
> might be an API impedence mismatch compared to unveil/landlock.
To be honest, I have no clue how to make that work with Capsicum,
maybe FreeBSD support could be delayed...
Kind regards,
Stefan
diff --git a/Makefile.in b/Makefile.in
index 7f7d2c5..74ac557 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -107,7 +107,7 @@ LIBSSH_OBJS=${LIBOPENSSH_OBJS} \
kexgexc.o kexgexs.o \
kexsntrup761x25519.o kexmlkem768x25519.o sntrup761.o kexgen.o \
sftp-realpath.o platform-pledge.o platform-tracing.o platform-misc.o \
- sshbuf-io.o misc-agent.o
+ platform-restrict-fs.o sshbuf-io.o misc-agent.o
P11OBJS= ssh-pkcs11-client.o
diff --git a/configure.ac b/configure.ac
index 60d4571..4b8f2ad 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1003,6 +1003,7 @@ int main(void) { if (NSVersionOfRunTimeLibrary("System") >= (60 << 16))
])
AC_CHECK_HEADERS([linux/seccomp.h linux/filter.h linux/audit.h], [],
[], [#include <linux/types.h>])
+ AC_CHECK_HEADERS([linux/landlock.h])
# Obtain MIPS ABI
case "$host" in
mips*)
diff --git a/platform-restrict-fs.c b/platform-restrict-fs.c
new file mode 100644
index 0000000..47463f6
--- /dev/null
+++ b/platform-restrict-fs.c
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2026 Stefan Klein. All rights reserved.
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+
+#include "time.h"
+#include "log.h"
+
+#ifndef HAVE_LINUX_LANDLOCK_H
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdint.h>
+#include <string.h>
+#include <unistd.h>
+#include <linux/landlock.h>
+#include <sys/syscall.h>
+#include <sys/prctl.h>
+
+#define KNOWN_RESTRICTIONS ( \
+ LANDLOCK_ACCESS_FS_EXECUTE | \
+ LANDLOCK_ACCESS_FS_WRITE_FILE | \
+ LANDLOCK_ACCESS_FS_READ_FILE | \
+ LANDLOCK_ACCESS_FS_TRUNCATE | \
+ LANDLOCK_ACCESS_FS_READ_DIR | \
+ 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 ALLOWED_ACCESS ( \
+ LANDLOCK_ACCESS_FS_WRITE_FILE | \
+ LANDLOCK_ACCESS_FS_READ_FILE | \
+ LANDLOCK_ACCESS_FS_TRUNCATE | \
+ LANDLOCK_ACCESS_FS_READ_DIR | \
+ LANDLOCK_ACCESS_FS_REMOVE_DIR | \
+ LANDLOCK_ACCESS_FS_REMOVE_FILE | \
+ LANDLOCK_ACCESS_FS_MAKE_DIR | \
+ LANDLOCK_ACCESS_FS_MAKE_REG | \
+ LANDLOCK_ACCESS_FS_MAKE_SYM | \
+ LANDLOCK_ACCESS_FS_REFER )
+
+static inline int
+landlock_create_ruleset(const struct landlock_ruleset_attr *const attr, size_t size, uint32_t flags)
+{
+ return syscall(__NR_landlock_create_ruleset, attr, size, flags);
+}
+
+static inline int
+landlock_add_rule(int ruleset_fd, enum landlock_rule_type rule_type, const void *rule_attr, uint32_t flags)
+{
+ return syscall(__NR_landlock_add_rule, ruleset_fd, rule_type, rule_attr, flags);
+}
+
+static inline int
+landlock_restrict_self(int ruleset_fd, uint32_t flags)
+{
+ return syscall(__NR_landlock_restrict_self, ruleset_fd, flags);
+}
+
+int
+platform_restrict_fs_access(const char *path)
+{
+ if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) {
+ error("Failed to set no new privileges attribute on process: %s", strerror(errno));
+ return -1;
+ }
+
+ struct landlock_ruleset_attr attr = {
+ .handled_access_fs = KNOWN_RESTRICTIONS
+ };
+
+ int ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
+ if (ruleset_fd < 0) {
+ error("Failed to create landlock ruleset: %s", strerror(errno));
+ return -1;
+ }
+
+ struct landlock_path_beneath_attr path_beneath = {
+ .parent_fd = open(path, O_PATH | O_CLOEXEC),
+ .allowed_access = ALLOWED_ACCESS,
+ };
+
+ if (path_beneath.parent_fd < 0) {
+ error("Failed to open current working directory for landlock rule: %s", strerror(errno));
+ close(ruleset_fd);
+ return -1;
+ }
+
+ if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &path_beneath, 0) != 0) {
+ error("Failed to add landlock rule: %s", strerror(errno));
+ close(path_beneath.parent_fd);
+ close(ruleset_fd);
+ return -1;
+ }
+ close(path_beneath.parent_fd);
+
+ if (landlock_restrict_self(ruleset_fd, 0) != 0) {
+ error("Failed to enforce landlock ruleset: %s", strerror(errno));
+ close(ruleset_fd);
+ return -1;
+ }
+
+ close(ruleset_fd);
+
+ return 0;
+}
+
+#else
+
+int
+platform_restrict_fs_access(const char *path)
+{
+ error("Failed to restrict filesystem access, your system does not support this");
+ return -1;
+}
+
+#endif /* HAVE_LINUX_LANDLOCK_H */
diff --git a/platform.h b/platform.h
index 08cbd22..e6066ae 100644
--- a/platform.h
+++ b/platform.h
@@ -38,3 +38,6 @@ void platform_disable_tracing(int);
void platform_pledge_agent(void);
void platform_pledge_sftp_server(void);
void platform_pledge_mux(void);
+
+/* in platform-restrict-fs.c */
+int platform_restrict_fs_access(const char *);
diff --git a/sftp-server.c b/sftp-server.c
index b98c3cd..9e27c9a 100644
--- a/sftp-server.c
+++ b/sftp-server.c
@@ -1888,7 +1888,7 @@ sftp_server_usage(void)
extern char *__progname;
fprintf(stderr,
- "usage: %s [-ehR] [-d start_directory] [-f log_facility] "
+ "usage: %s [-ehRU] [-d start_directory] [-f log_facility] "
"[-l log_level]\n\t[-P denied_requests] "
"[-p allowed_requests] [-u umask]\n"
" %s -Q protocol_feature\n",
@@ -1899,7 +1899,7 @@ sftp_server_usage(void)
int
sftp_server_main(int argc, char **argv, struct passwd *user_pw)
{
- int i, r, in, out, ch, skipargs = 0, log_stderr = 0;
+ int i, r, in, out, ch, skipargs = 0, log_stderr = 0, unveil_cwd = 0;
ssize_t len, olen;
SyslogFacility log_facility = SYSLOG_FACILITY_AUTH;
char *cp, *homedir = NULL, uidstr[32], buf[4*4096];
@@ -1914,7 +1914,7 @@ sftp_server_main(int argc, char **argv, struct passwd *user_pw)
pw = pwcopy(user_pw);
while (!skipargs && (ch = getopt(argc, argv,
- "d:f:l:P:p:Q:u:cehR")) != -1) {
+ "d:f:l:P:p:Q:u:cehRU")) != -1) {
switch (ch) {
case 'Q':
if (strcasecmp(optarg, "requests") != 0) {
@@ -1976,6 +1976,9 @@ sftp_server_main(int argc, char **argv, struct passwd *user_pw)
fatal("Invalid umask \"%s\"", optarg);
(void)umask((mode_t)mask);
break;
+ case 'U':
+ unveil_cwd = 1;
+ break;
case 'h':
default:
sftp_server_usage();
@@ -2029,6 +2032,11 @@ sftp_server_main(int argc, char **argv, struct passwd *user_pw)
}
}
+ if (unveil_cwd) {
+ if (platform_restrict_fs_access(".") != 0)
+ sftp_server_cleanup_exit(255);
+ }
+
for (;;) {
struct pollfd pfd[2];
More information about the openssh-unix-dev
mailing list