isolation
build-and-publish / build (Raccoon, ubuntu:26.04, NEXUS_PASS_RACCOON, NEXUS_REPO_RACCOON, NEXUS_USER_RACCOON, raccoon) (push) Successful in 3m51s
build-and-publish / build (Trixie, debian:13, NEXUS_PASS_TRIXIE, NEXUS_REPO_TRIXIE, NEXUS_USER_TRIXIE, trixie) (push) Successful in 3m35s

This commit is contained in:
theraw
2026-06-26 03:13:19 +00:00
parent 2f6102e8aa
commit 99e75aaf4d
6 changed files with 415 additions and 10 deletions
@@ -0,0 +1,351 @@
--- a/src/http/modules/ngx_http_static_module.c 2026-06-17 14:40:35.000000000 +0000
+++ b/src/http/modules/ngx_http_static_module.c 2026-06-26 00:59:37.635956019 +0000
@@ -14,6 +14,97 @@
static ngx_int_t ngx_http_static_init(ngx_conf_t *cf);
+/*
+ * twiy tenant isolation
+ * ---------------------
+ * Optional, kernel-enforced confinement of static file resolution to the
+ * tenant's document root (or an explicit tenant_isolation_root). Every static
+ * open is validated with openat2(RESOLVE_BENEATH|RESOLVE_NO_MAGICLINKS) anchored
+ * at the confinement root, so a symlink, ".." or magic-link that would escape
+ * the tenant tree is refused (403) at the syscall level -- independent of config
+ * correctness and free of the TOCTOU window of disable_symlinks.
+ *
+ * Compiled in only when the build applies the tenant-isolation patch. Per-vhost
+ * default is off unless NGX_TENANT_ISOLATION_DEFAULT_ON is defined at compile
+ * time: the twiy-raweb build defines it (always-on, override with
+ * `tenant_isolation off;`); a plain build leaves it off and opt-in via
+ * `tenant_isolation on;`. Runtime is inert on kernels without openat2 -- nginx's
+ * own disable_symlinks still applies.
+ */
+
+#ifndef NGX_TENANT_ISOLATION_DEFAULT_ON
+#define NGX_TENANT_ISOLATION_DEFAULT_ON 0
+#endif
+
+#if defined(__linux__)
+#include <sys/syscall.h>
+#if defined(SYS_openat2)
+#define NGX_TI_OPENAT2 1
+#endif
+#endif
+
+#if (NGX_TI_OPENAT2)
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <limits.h>
+#if defined(__has_include)
+# if __has_include(<linux/openat2.h>)
+# include <linux/openat2.h>
+# define NGX_TI_HAVE_OPENAT2_H 1
+# endif
+#endif
+#ifndef NGX_TI_HAVE_OPENAT2_H
+struct open_how { uint64_t flags; uint64_t mode; uint64_t resolve; };
+#endif
+#ifndef RESOLVE_BENEATH
+#define RESOLVE_BENEATH 0x08
+#endif
+#ifndef RESOLVE_NO_MAGICLINKS
+#define RESOLVE_NO_MAGICLINKS 0x02
+#endif
+#ifndef O_PATH
+#define O_PATH 010000000
+#endif
+#endif
+
+
+typedef struct {
+ ngx_flag_t tenant_isolation;
+ ngx_str_t tenant_isolation_root;
+} ngx_http_static_loc_conf_t;
+
+
+static void *ngx_http_static_create_loc_conf(ngx_conf_t *cf);
+static char *ngx_http_static_merge_loc_conf(ngx_conf_t *cf, void *parent,
+ void *child);
+#if (NGX_TI_OPENAT2)
+static ngx_int_t ngx_http_static_tenant_check(ngx_http_request_t *r,
+ ngx_http_static_loc_conf_t *slcf, ngx_str_t *path, size_t root);
+#endif
+
+
+static ngx_command_t ngx_http_static_commands[] = {
+
+ { ngx_string("tenant_isolation"),
+ NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
+ ngx_conf_set_flag_slot,
+ NGX_HTTP_LOC_CONF_OFFSET,
+ offsetof(ngx_http_static_loc_conf_t, tenant_isolation),
+ NULL },
+
+ { ngx_string("tenant_isolation_root"),
+ NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
+ ngx_conf_set_str_slot,
+ NGX_HTTP_LOC_CONF_OFFSET,
+ offsetof(ngx_http_static_loc_conf_t, tenant_isolation_root),
+ NULL },
+
+ ngx_null_command
+};
+
+
static ngx_http_module_t ngx_http_static_module_ctx = {
NULL, /* preconfiguration */
ngx_http_static_init, /* postconfiguration */
@@ -24,15 +115,15 @@
NULL, /* create server configuration */
NULL, /* merge server configuration */
- NULL, /* create location configuration */
- NULL /* merge location configuration */
+ ngx_http_static_create_loc_conf, /* create location configuration */
+ ngx_http_static_merge_loc_conf /* merge location configuration */
};
ngx_module_t ngx_http_static_module = {
NGX_MODULE_V1,
&ngx_http_static_module_ctx, /* module context */
- NULL, /* module directives */
+ ngx_http_static_commands, /* module directives */
NGX_HTTP_MODULE, /* module type */
NULL, /* init master */
NULL, /* init module */
@@ -87,6 +178,21 @@
clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
+#if (NGX_TI_OPENAT2)
+ {
+ ngx_http_static_loc_conf_t *slcf;
+
+ slcf = ngx_http_get_module_loc_conf(r, ngx_http_static_module);
+
+ if (slcf->tenant_isolation == 1) {
+ rc = ngx_http_static_tenant_check(r, slcf, &path, root);
+ if (rc != NGX_OK) {
+ return rc;
+ }
+ }
+ }
+#endif
+
ngx_memzero(&of, sizeof(ngx_open_file_info_t));
of.read_ahead = clcf->read_ahead;
@@ -278,6 +384,209 @@
}
+#if (NGX_TI_OPENAT2)
+
+/*
+ * Fallback for kernels/seccomp where openat2() is unavailable: canonicalize the
+ * mapped target with realpath() (which resolves symlinks via readlink/lstat --
+ * all permitted by the default service syscall set) and confirm it stays within
+ * the canonical confinement root. Blocks escaping symlinks/".."; carries the
+ * same TOCTOU caveat as nginx's own disable_symlinks (the openat2 path is
+ * race-free and is preferred whenever the kernel permits it).
+ */
+
+static ngx_int_t
+ngx_http_static_tenant_check_realpath(ngx_http_request_t *r, u_char *root_buf,
+ ngx_str_t *path)
+{
+ char real[PATH_MAX], rroot[PATH_MAX];
+ size_t rl;
+
+ if (realpath((const char *) path->data, real) == NULL) {
+ /* nonexistent / dangling / unreadable: let nginx report 404/403 */
+ return NGX_OK;
+ }
+ if (realpath((const char *) root_buf, rroot) == NULL) {
+ return NGX_OK;
+ }
+
+ rl = ngx_strlen(rroot);
+ if (ngx_strncmp(real, rroot, rl) == 0
+ && (real[rl] == '\0' || real[rl] == '/'))
+ {
+ return NGX_OK; /* canonical target is in-tree */
+ }
+
+ ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
+ "tenant_isolation: blocked escape for \"%V\" "
+ "(resolves outside \"%s\")", path, rroot);
+ return NGX_HTTP_FORBIDDEN;
+}
+
+
+/*
+ * Validate that `path` (already mapped from the request URI) resolves to a
+ * target beneath the tenant's confinement root, using openat2() so the kernel
+ * itself rejects escapes via symlink, "..", magic-link or absolute symlink.
+ *
+ * Confinement root = tenant_isolation_root if configured, else the document
+ * root portion of `path` (its first `root` bytes). Returns NGX_OK to let nginx
+ * proceed (in-tree target, or genuinely missing file -> nginx makes the 404),
+ * NGX_HTTP_FORBIDDEN on a blocked escape. If openat2 is unavailable (old kernel
+ * or a seccomp filter that ENOSYS/EPERMs it), it latches and uses the realpath
+ * fallback for the life of the worker instead of failing open.
+ */
+
+static ngx_int_t
+ngx_http_static_tenant_check(ngx_http_request_t *r,
+ ngx_http_static_loc_conf_t *slcf, ngx_str_t *path, size_t root)
+{
+ int dirfd;
+ long fd;
+ char *rel;
+ u_char *root_buf;
+ ngx_str_t croot;
+ ngx_err_t err;
+ struct open_how how;
+ static ngx_uint_t openat2_unavailable = 0; /* per-worker latch */
+
+ if (slcf->tenant_isolation_root.len) {
+ croot = slcf->tenant_isolation_root;
+
+ if (path->len < croot.len
+ || ngx_strncmp(path->data, croot.data, croot.len) != 0)
+ {
+ ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
+ "tenant_isolation: \"%V\" is outside root \"%V\"",
+ path, &croot);
+ return NGX_HTTP_FORBIDDEN;
+ }
+
+ rel = (char *) path->data + croot.len;
+
+ } else {
+ if (root == 0 || root >= path->len) {
+ /* cannot determine the document root: leave it to nginx */
+ return NGX_OK;
+ }
+
+ croot.data = path->data;
+ croot.len = root;
+ rel = (char *) path->data + root;
+ }
+
+ while (*rel == '/') {
+ rel++;
+ }
+
+ if (*rel == '\0') {
+ /* request maps to the confinement root itself: nothing to confine */
+ return NGX_OK;
+ }
+
+ root_buf = ngx_pnalloc(r->pool, croot.len + 1);
+ if (root_buf == NULL) {
+ return NGX_HTTP_INTERNAL_SERVER_ERROR;
+ }
+ ngx_memcpy(root_buf, croot.data, croot.len);
+ root_buf[croot.len] = '\0';
+
+ if (openat2_unavailable) {
+ return ngx_http_static_tenant_check_realpath(r, root_buf, path);
+ }
+
+ dirfd = open((const char *) root_buf, O_PATH|O_DIRECTORY|O_CLOEXEC);
+ if (dirfd < 0) {
+ /* root dir missing/unsearchable: let nginx's own open report it */
+ return NGX_OK;
+ }
+
+ ngx_memzero(&how, sizeof(how));
+ how.flags = (uint64_t) (O_PATH|O_CLOEXEC);
+ how.resolve = (uint64_t) (RESOLVE_BENEATH|RESOLVE_NO_MAGICLINKS);
+
+ fd = syscall(SYS_openat2, dirfd, rel, &how, sizeof(how));
+
+ if (fd >= 0) {
+ (void) close((int) fd);
+ (void) close(dirfd);
+ return NGX_OK;
+ }
+
+ err = ngx_errno;
+ (void) close(dirfd);
+
+ switch (err) {
+
+ case NGX_ENOENT:
+ case NGX_ENOTDIR:
+ case NGX_ENAMETOOLONG:
+ /* genuinely-missing target: let nginx produce its normal 404 */
+ return NGX_OK;
+
+ case EXDEV: /* RESOLVE_BENEATH: resolution would escape the root */
+ case NGX_ELOOP: /* magic-link / symlink loop refused */
+ case EAGAIN: /* lost a resolution race: fail closed */
+ ngx_log_error(NGX_LOG_ERR, r->connection->log, err,
+ "tenant_isolation: blocked escape for \"%V\"", path);
+ return NGX_HTTP_FORBIDDEN;
+
+ case NGX_ENOSYS:
+ case EOPNOTSUPP:
+ case EPERM:
+ /* openat2 unavailable (old kernel, or a seccomp filter that
+ * ENOSYS/EPERMs it -- e.g. a hardened systemd unit). Latch and use the
+ * realpath containment check from now on rather than failing open. */
+ if (!openat2_unavailable) {
+ openat2_unavailable = 1;
+ ngx_log_error(NGX_LOG_WARN, r->connection->log, err,
+ "tenant_isolation: openat2 unavailable (an old kernel, "
+ "or the unit's RestrictSUIDSGID= seccomp ENOSYSes it), "
+ "using realpath fallback -- drop RestrictSUIDSGID for "
+ "race-free enforcement");
+ }
+ return ngx_http_static_tenant_check_realpath(r, root_buf, path);
+
+ default:
+ /* unexpected: let nginx's own open path decide */
+ return NGX_OK;
+ }
+}
+
+#endif
+
+
+static void *
+ngx_http_static_create_loc_conf(ngx_conf_t *cf)
+{
+ ngx_http_static_loc_conf_t *conf;
+
+ conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_static_loc_conf_t));
+ if (conf == NULL) {
+ return NULL;
+ }
+
+ conf->tenant_isolation = NGX_CONF_UNSET;
+
+ return conf;
+}
+
+
+static char *
+ngx_http_static_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
+{
+ ngx_http_static_loc_conf_t *prev = parent;
+ ngx_http_static_loc_conf_t *conf = child;
+
+ ngx_conf_merge_value(conf->tenant_isolation, prev->tenant_isolation,
+ NGX_TENANT_ISOLATION_DEFAULT_ON);
+ ngx_conf_merge_str_value(conf->tenant_isolation_root,
+ prev->tenant_isolation_root, "");
+
+ return NGX_CONF_OK;
+}
+
+
static ngx_int_t
ngx_http_static_init(ngx_conf_t *cf)
{
+15 -2
View File
@@ -31,6 +31,7 @@ function apply_patches() {
apply_one "${APPLY_PATCH_SYSTEMD_NOTIFY:-0}" "nginx-${NGINX}-systemd-notify.patch"
apply_one "${APPLY_PATCH_DYNAMIC_TLS_RECORDS:-0}" "nginx-${NGINX}-dynamic-tls-records.patch"
apply_one "${APPLY_PATCH_HTTP2_HPACK_ENC:-0}" "nginx-${NGINX}-http2-hpack-enc.patch"
apply_one "${APPLY_PATCH_TENANT_ISOLATION:-0}" "nginx-${NGINX}-tenant-isolation.patch"
touch "${nginx_src}/.patches_applied"
}
@@ -256,6 +257,12 @@ function clean_install() {
}
test_nginx() {
# Tenant-isolation directive default: ON only when the patch is applied AND
# TENANT_ISOLATION_DEFAULT_ON=1 (the twiy-raweb posture). Injected into cc-opt.
local TI_CC_OPT=""
if [ "${APPLY_PATCH_TENANT_ISOLATION:-0}" = "1" ] && [ "${TENANT_ISOLATION_DEFAULT_ON:-0}" = "1" ]; then
TI_CC_OPT="-DNGX_TENANT_ISOLATION_DEFAULT_ON=1"
fi
cd /opt/nginx-${NGINX} && LUAJIT_LIB="/usr/local/LuaJIT/lib" LUAJIT_INC="/usr/local/LuaJIT/include/luajit-2.1/" CFLAGS=-fPIC CXXFLAGS=-fPIC ./configure --with-compat \
--user=nginx \
--group=nginx \
@@ -314,11 +321,17 @@ test_nginx() {
--add-module=/opt/mod/ngx_brotli \
--add-module=/opt/mod/zstd-nginx-module-${NGX_MOD_ZSTD} \
--add-module=/opt/mod/testcookie \
--with-cc-opt="-g -O2 -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC -I/usr/local/aws-lc/include -I/usr/local/zlib-ng/include -DNGX_HAVE_SYSTEMD" \
--with-cc-opt="-g -O2 -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC -I/usr/local/aws-lc/include -I/usr/local/zlib-ng/include -DNGX_HAVE_SYSTEMD ${TI_CC_OPT}" \
--with-ld-opt="-Wl,-rpath,/usr/local/LuaJIT/lib -Wl,-rpath,/usr/local/lib -Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie -L/opt/mod/pcre2-${SYSTEM_PCRE}/.libs -lpcre2-8 -L/usr/local/aws-lc/lib -lssl -lcrypto -Wl,-rpath,/usr/local/aws-lc/lib -L/usr/local/zlib-ng/lib -lz -Wl,-rpath,/usr/local/zlib-ng/lib -lsystemd"
make clean
}
function build() {
# Tenant-isolation directive default: ON only when the patch is applied AND
# TENANT_ISOLATION_DEFAULT_ON=1 (the twiy-raweb posture). Injected into cc-opt.
local TI_CC_OPT=""
if [ "${APPLY_PATCH_TENANT_ISOLATION:-0}" = "1" ] && [ "${TENANT_ISOLATION_DEFAULT_ON:-0}" = "1" ]; then
TI_CC_OPT="-DNGX_TENANT_ISOLATION_DEFAULT_ON=1"
fi
cd /opt/nginx-${NGINX} && LUAJIT_LIB="/usr/local/LuaJIT/lib" LUAJIT_INC="/usr/local/LuaJIT/include/luajit-2.1/" CFLAGS=-fPIC CXXFLAGS=-fPIC ./configure --with-compat \
--user=nginx \
--group=nginx \
@@ -377,7 +390,7 @@ function build() {
--add-module=/opt/mod/ngx_brotli \
--add-module=/opt/mod/zstd-nginx-module-${NGX_MOD_ZSTD} \
--add-module=/opt/mod/testcookie \
--with-cc-opt="-g -O2 -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC -I/usr/local/aws-lc/include -I/usr/local/zlib-ng/include -DNGX_HAVE_SYSTEMD" \
--with-cc-opt="-g -O2 -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC -I/usr/local/aws-lc/include -I/usr/local/zlib-ng/include -DNGX_HAVE_SYSTEMD ${TI_CC_OPT}" \
--with-ld-opt="-Wl,-rpath,/usr/local/LuaJIT/lib -Wl,-rpath,/usr/local/lib -Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie -L/opt/mod/pcre2-${SYSTEM_PCRE}/.libs -lpcre2-8 -L/usr/local/aws-lc/lib -lssl -lcrypto -Wl,-rpath,/usr/local/aws-lc/lib -L/usr/local/zlib-ng/lib -lz -Wl,-rpath,/usr/local/zlib-ng/lib -lsystemd"
# NOTE: kept as separate statements (not `make && make install && make clean`)
# so `set -e` actually fires on a make failure. The && chain hides left-side
+15 -2
View File
@@ -31,6 +31,7 @@ function apply_patches() {
apply_one "${APPLY_PATCH_SYSTEMD_NOTIFY:-0}" "nginx-${NGINX}-systemd-notify.patch"
apply_one "${APPLY_PATCH_DYNAMIC_TLS_RECORDS:-0}" "nginx-${NGINX}-dynamic-tls-records.patch"
apply_one "${APPLY_PATCH_HTTP2_HPACK_ENC:-0}" "nginx-${NGINX}-http2-hpack-enc.patch"
apply_one "${APPLY_PATCH_TENANT_ISOLATION:-0}" "nginx-${NGINX}-tenant-isolation.patch"
touch "${nginx_src}/.patches_applied"
}
@@ -256,6 +257,12 @@ function clean_install() {
}
test_nginx() {
# Tenant-isolation directive default: ON only when the patch is applied AND
# TENANT_ISOLATION_DEFAULT_ON=1 (the twiy-raweb posture). Injected into cc-opt.
local TI_CC_OPT=""
if [ "${APPLY_PATCH_TENANT_ISOLATION:-0}" = "1" ] && [ "${TENANT_ISOLATION_DEFAULT_ON:-0}" = "1" ]; then
TI_CC_OPT="-DNGX_TENANT_ISOLATION_DEFAULT_ON=1"
fi
cd /opt/nginx-${NGINX} && LUAJIT_LIB="/usr/local/LuaJIT/lib" LUAJIT_INC="/usr/local/LuaJIT/include/luajit-2.1/" CFLAGS=-fPIC CXXFLAGS=-fPIC ./configure --with-compat \
--user=nginx \
--group=nginx \
@@ -314,11 +321,17 @@ test_nginx() {
--add-module=/opt/mod/ngx_brotli \
--add-module=/opt/mod/zstd-nginx-module-${NGX_MOD_ZSTD} \
--add-module=/opt/mod/testcookie \
--with-cc-opt="-g -O2 -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC -I/usr/local/aws-lc/include -I/usr/local/zlib-ng/include -DNGX_HAVE_SYSTEMD" \
--with-cc-opt="-g -O2 -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC -I/usr/local/aws-lc/include -I/usr/local/zlib-ng/include -DNGX_HAVE_SYSTEMD ${TI_CC_OPT}" \
--with-ld-opt="-Wl,-rpath,/usr/local/LuaJIT/lib -Wl,-rpath,/usr/local/lib -Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie -L/opt/mod/pcre2-${SYSTEM_PCRE}/.libs -lpcre2-8 -L/usr/local/aws-lc/lib -lssl -lcrypto -Wl,-rpath,/usr/local/aws-lc/lib -L/usr/local/zlib-ng/lib -lz -Wl,-rpath,/usr/local/zlib-ng/lib -lsystemd"
make clean
}
function build() {
# Tenant-isolation directive default: ON only when the patch is applied AND
# TENANT_ISOLATION_DEFAULT_ON=1 (the twiy-raweb posture). Injected into cc-opt.
local TI_CC_OPT=""
if [ "${APPLY_PATCH_TENANT_ISOLATION:-0}" = "1" ] && [ "${TENANT_ISOLATION_DEFAULT_ON:-0}" = "1" ]; then
TI_CC_OPT="-DNGX_TENANT_ISOLATION_DEFAULT_ON=1"
fi
cd /opt/nginx-${NGINX} && LUAJIT_LIB="/usr/local/LuaJIT/lib" LUAJIT_INC="/usr/local/LuaJIT/include/luajit-2.1/" CFLAGS=-fPIC CXXFLAGS=-fPIC ./configure --with-compat \
--user=nginx \
--group=nginx \
@@ -377,7 +390,7 @@ function build() {
--add-module=/opt/mod/ngx_brotli \
--add-module=/opt/mod/zstd-nginx-module-${NGX_MOD_ZSTD} \
--add-module=/opt/mod/testcookie \
--with-cc-opt="-g -O2 -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC -I/usr/local/aws-lc/include -I/usr/local/zlib-ng/include -DNGX_HAVE_SYSTEMD" \
--with-cc-opt="-g -O2 -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC -I/usr/local/aws-lc/include -I/usr/local/zlib-ng/include -DNGX_HAVE_SYSTEMD ${TI_CC_OPT}" \
--with-ld-opt="-Wl,-rpath,/usr/local/LuaJIT/lib -Wl,-rpath,/usr/local/lib -Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie -L/opt/mod/pcre2-${SYSTEM_PCRE}/.libs -lpcre2-8 -L/usr/local/aws-lc/lib -lssl -lcrypto -Wl,-rpath,/usr/local/aws-lc/lib -L/usr/local/zlib-ng/lib -lz -Wl,-rpath,/usr/local/zlib-ng/lib -lsystemd"
# NOTE: kept as separate statements (not `make && make install && make clean`)
# so `set -e` actually fires on a make failure. The && chain hides left-side
+9 -3
View File
@@ -38,8 +38,6 @@ BindReadOnlyPaths=/etc
BindPaths=/run
BindPaths=/var/log/nginx
BindPaths=/nginx
BindPaths=/var/cache/nginx
BindPaths=/srv
NoNewPrivileges=true
ProtectKernelTunables=true
@@ -53,7 +51,7 @@ PrivateTmp=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
# RestrictSUIDSGID=true <- deliberately OFF (see NOTE): it breaks openat2(2)
LockPersonality=true
SystemCallArchitectures=native
SystemCallFilter=@system-service
@@ -62,6 +60,14 @@ SystemCallErrorNumber=EPERM
# NOTE deliberately OFF:
# MemoryDenyWriteExecute=true breaks LuaJIT (JIT writable+executable pages)
# SystemCallFilter=~@resources breaks nginx workers' prlimit64()
# RestrictSUIDSGID=true its seccomp filter makes openat2(2) return
# ENOSYS (cannot be re-allowed via
# SystemCallFilter), which disables the
# tenant_isolation race-free openat2 path and
# forces the realpath() fallback. Workers are
# unprivileged + NoNewPrivileges=true, so a
# worker-created setuid/setgid file is owned by
# nginx (not root) and grants no escalation.
# ProtectSystem and ProtectHome are redundant under TemporaryFileSystem=/.
[Install]
+9 -3
View File
@@ -38,8 +38,6 @@ BindReadOnlyPaths=/etc
BindPaths=/run
BindPaths=/var/log/nginx
BindPaths=/nginx
BindPaths=/var/cache/nginx
BindPaths=/srv
NoNewPrivileges=true
ProtectKernelTunables=true
@@ -53,7 +51,7 @@ PrivateTmp=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
# RestrictSUIDSGID=true <- deliberately OFF (see NOTE): it breaks openat2(2)
LockPersonality=true
SystemCallArchitectures=native
SystemCallFilter=@system-service
@@ -62,6 +60,14 @@ SystemCallErrorNumber=EPERM
# NOTE deliberately OFF:
# MemoryDenyWriteExecute=true breaks LuaJIT (JIT writable+executable pages)
# SystemCallFilter=~@resources breaks nginx workers' prlimit64()
# RestrictSUIDSGID=true its seccomp filter makes openat2(2) return
# ENOSYS (cannot be re-allowed via
# SystemCallFilter), which disables the
# tenant_isolation race-free openat2 path and
# forces the realpath() fallback. Workers are
# unprivileged + NoNewPrivileges=true, so a
# worker-created setuid/setgid file is owned by
# nginx (not root) and grants no escalation.
# ProtectSystem and ProtectHome are redundant under TemporaryFileSystem=/.
[Install]
+16
View File
@@ -86,3 +86,19 @@ export APPLY_PATCH_DYNAMIC_TLS_RECORDS=1
# hunks reference internals that have drifted). Patch NOT yet shipped — would
# need a full rebase. Keep toggle here for the day someone ports it.
export APPLY_PATCH_HTTP2_HPACK_ENC=0
# Tenant isolation: kernel-enforced (openat2 RESOLVE_BENEATH|NO_MAGICLINKS)
# confinement of static-file resolution to each tenant's document root, so a
# symlink/".."/magic-link that would escape the tenant tree is refused with 403
# at the syscall level (no TOCTOU, unlike disable_symlinks). Patch shipped at
# build/patches/nginx-${NGINX}-tenant-isolation.patch; self-contained in
# ngx_http_static_module.c (adds the `tenant_isolation` + `tenant_isolation_root`
# directives). Optional for a plain nginx build — set =0 to exclude entirely.
export APPLY_PATCH_TENANT_ISOLATION=1
# When the patch is applied, also compile the `tenant_isolation` directive
# default-ON (-DNGX_TENANT_ISOLATION_DEFAULT_ON=1) so EVERY vhost is confined
# unless it sets `tenant_isolation off;`. This is the twiy-raweb posture. Set =0
# to ship the patch but leave the directive opt-in per vhost (`tenant_isolation
# on;`) — the "default nginx" posture. Inert at runtime on kernels w/o openat2.
export TENANT_ISOLATION_DEFAULT_ON=1