From 99e75aaf4de4165b3de3898e783f2e8f76c97cc4 Mon Sep 17 00:00:00 2001 From: theraw Date: Fri, 26 Jun 2026 03:13:19 +0000 Subject: [PATCH] isolation --- .../nginx-1.31.2-tenant-isolation.patch | 351 ++++++++++++++++++ build/raccoon.sh | 17 +- build/trixie.sh | 17 +- static/Raccoon/nginx-raweb.service | 12 +- static/Trixie/nginx-raweb.service | 12 +- version | 16 + 6 files changed, 415 insertions(+), 10 deletions(-) create mode 100644 build/patches/nginx-1.31.2-tenant-isolation.patch diff --git a/build/patches/nginx-1.31.2-tenant-isolation.patch b/build/patches/nginx-1.31.2-tenant-isolation.patch new file mode 100644 index 0000000..cc68102 --- /dev/null +++ b/build/patches/nginx-1.31.2-tenant-isolation.patch @@ -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 ++#if defined(SYS_openat2) ++#define NGX_TI_OPENAT2 1 ++#endif ++#endif ++ ++#if (NGX_TI_OPENAT2) ++#include ++#include ++#include ++#include ++#include ++#if defined(__has_include) ++# if __has_include() ++# include ++# 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) + { diff --git a/build/raccoon.sh b/build/raccoon.sh index 8440004..b130846 100644 --- a/build/raccoon.sh +++ b/build/raccoon.sh @@ -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 diff --git a/build/trixie.sh b/build/trixie.sh index 3e4bec4..6676c2d 100644 --- a/build/trixie.sh +++ b/build/trixie.sh @@ -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 diff --git a/static/Raccoon/nginx-raweb.service b/static/Raccoon/nginx-raweb.service index 721189b..a655158 100644 --- a/static/Raccoon/nginx-raweb.service +++ b/static/Raccoon/nginx-raweb.service @@ -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] diff --git a/static/Trixie/nginx-raweb.service b/static/Trixie/nginx-raweb.service index 721189b..a655158 100644 --- a/static/Trixie/nginx-raweb.service +++ b/static/Trixie/nginx-raweb.service @@ -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] diff --git a/version b/version index 6837d5a..1c4ff8f 100644 --- a/version +++ b/version @@ -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