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