Tenant Isolation
This commit is contained in:
@@ -139,7 +139,7 @@
|
|||||||
ngx_memzero(&of, sizeof(ngx_open_file_info_t));
|
ngx_memzero(&of, sizeof(ngx_open_file_info_t));
|
||||||
|
|
||||||
of.read_ahead = clcf->read_ahead;
|
of.read_ahead = clcf->read_ahead;
|
||||||
@@ -278,6 +384,242 @@
|
@@ -278,6 +384,231 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -213,14 +213,9 @@
|
|||||||
+ croot = slcf->tenant_isolation_root;
|
+ croot = slcf->tenant_isolation_root;
|
||||||
+
|
+
|
||||||
+ /*
|
+ /*
|
||||||
+ * Require a path-separator boundary immediately after the prefix, so a
|
+ * Require '/' (or end) right after the prefix so root "/home/u1" is not
|
||||||
+ * docroot that merely shares a string prefix with the confinement root
|
+ * treated as a parent of "/home/u10/..." (would fail open). Length checked
|
||||||
+ * (root "/home/u1" vs docroot "/home/u10/...") is not mistaken for being
|
+ * first to keep the read in bounds; a root ending in '/' implies it.
|
||||||
+ * inside it -- otherwise `rel` would be computed against the wrong root
|
|
||||||
+ * and the check would silently no-op (fail open). If the configured root
|
|
||||||
+ * itself ends in '/', that boundary is already implied. The length guard
|
|
||||||
+ * is evaluated first so the path->data[croot.len] read stays in bounds
|
|
||||||
+ * (path->data is NUL-terminated at path->len).
|
|
||||||
+ */
|
+ */
|
||||||
+ if (path->len < croot.len
|
+ if (path->len < croot.len
|
||||||
+ || ngx_strncmp(path->data, croot.data, croot.len) != 0
|
+ || ngx_strncmp(path->data, croot.data, croot.len) != 0
|
||||||
@@ -301,10 +296,8 @@
|
|||||||
+ case ENFILE:
|
+ case ENFILE:
|
||||||
+ case ENOMEM:
|
+ case ENOMEM:
|
||||||
+ case ESTALE:
|
+ case ESTALE:
|
||||||
+ /* Not an escape verdict: a permission, fd/memory-exhaustion or stale-NFS
|
+ /* Not an escape: perm / fd exhaustion / stale NFS. Let nginx's own open
|
||||||
+ * condition that stopped resolution before it could conclude. Let nginx's
|
+ * surface the real 403/500/503 instead of masking it as a 403. */
|
||||||
+ * own open run so it surfaces the correct 403/500/503 rather than masking
|
|
||||||
+ * it (or a transient hiccup) as a tenant_isolation 403. */
|
|
||||||
+ return NGX_OK;
|
+ return NGX_OK;
|
||||||
+
|
+
|
||||||
+ case EXDEV: /* RESOLVE_BENEATH: resolution would escape the root */
|
+ case EXDEV: /* RESOLVE_BENEATH: resolution would escape the root */
|
||||||
@@ -318,11 +311,9 @@
|
|||||||
+ case EOPNOTSUPP:
|
+ case EOPNOTSUPP:
|
||||||
+ case EPERM:
|
+ case EPERM:
|
||||||
+ case EINVAL:
|
+ case EINVAL:
|
||||||
+ /* openat2 unavailable or rejecting our resolve flags (old kernel, a
|
+ /* openat2 unavailable or rejecting our flags (old kernel / seccomp, e.g.
|
||||||
+ * seccomp filter that ENOSYS/EPERMs it -- e.g. a hardened systemd unit --
|
+ * RestrictSUIDSGID). Latch to the realpath fallback instead of failing
|
||||||
+ * or a kernel that predates RESOLVE_BENEATH). Latch and use the realpath
|
+ * open or 403-ing every request. */
|
||||||
+ * containment check from now on rather than failing open or 403-ing every
|
|
||||||
+ * request. */
|
|
||||||
+ if (!openat2_unavailable) {
|
+ if (!openat2_unavailable) {
|
||||||
+ openat2_unavailable = 1;
|
+ openat2_unavailable = 1;
|
||||||
+ ngx_log_error(NGX_LOG_WARN, r->connection->log, err,
|
+ ngx_log_error(NGX_LOG_WARN, r->connection->log, err,
|
||||||
@@ -334,10 +325,8 @@
|
|||||||
+ return ngx_http_static_tenant_check_realpath(r, root_buf, path);
|
+ return ngx_http_static_tenant_check_realpath(r, root_buf, path);
|
||||||
+
|
+
|
||||||
+ default:
|
+ default:
|
||||||
+ /* Unexpected errno from openat2. Fail CLOSED: an escape we have no named
|
+ /* Unexpected errno: fail closed. Benign non-escape errnos are handled
|
||||||
+ * case for must not be served. Benign resource/permission errnos that
|
+ * above, so treat anything here as a possible escape. */
|
||||||
+ * genuinely mean "not an escape" are enumerated above, so reaching here
|
|
||||||
+ * is anomalous -- deny and log rather than fall through to nginx. */
|
|
||||||
+ ngx_log_error(NGX_LOG_ERR, r->connection->log, err,
|
+ ngx_log_error(NGX_LOG_ERR, r->connection->log, err,
|
||||||
+ "tenant_isolation: openat2 failed with an unexpected error "
|
+ "tenant_isolation: openat2 failed with an unexpected error "
|
||||||
+ "for \"%V\", denying", path);
|
+ "for \"%V\", denying", path);
|
||||||
|
|||||||
+13
-12
@@ -244,18 +244,19 @@ function clean_install() {
|
|||||||
cd /opt/mod/; git clone --recurse-submodules https://github.com/wargio/naxsi.git naxsi
|
cd /opt/mod/; git clone --recurse-submodules https://github.com/wargio/naxsi.git naxsi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# NGX_MOD_ZSTD — Zstandard compression module from eilandert (maintained
|
# zstd module from https://github.com/theraw/zstd-nginx-module (NGX_MOD_ZSTD =
|
||||||
# fork of tokers/zstd-nginx-module, which has been abandoned since 0.1.1 in
|
# git tag). Extract with --strip-components=1 into a fixed local dir name so the
|
||||||
# 2023-10-23). Pinned via NGX_MOD_ZSTD; tarball pattern (dir name embeds
|
# --add-module path never depends on the archive's top folder; guard on config
|
||||||
# version → cache invalidates automatically when the pin moves).
|
# so a partial extract re-fetches.
|
||||||
#
|
if [ ! -f /opt/mod/zstd-nginx-module-${NGX_MOD_ZSTD}/config ]; then
|
||||||
# The fork ships the flush-loop CPU-spin fix we previously carried as
|
cd /opt/mod/
|
||||||
# build/patches/zstd-nginx-module-0.1.1-flush-cpu-spin.patch — see PR #23,
|
rm -rf zstd-nginx-module-${NGX_MOD_ZSTD}
|
||||||
# PR #49, and the "content-less flush completed" branch in
|
wget -O zstd-nginx-module-${NGX_MOD_ZSTD}.tar.gz \
|
||||||
# filter/ngx_http_zstd_filter_module.c. No local patching needed.
|
https://github.com/theraw/zstd-nginx-module/archive/refs/tags/${NGX_MOD_ZSTD}.tar.gz
|
||||||
if [ ! -d /opt/mod/zstd-nginx-module-${NGX_MOD_ZSTD} ]; then
|
mkdir -p zstd-nginx-module-${NGX_MOD_ZSTD}
|
||||||
cd /opt/mod/; wget https://github.com/eilandert/zstd-nginx-module/archive/refs/tags/${NGX_MOD_ZSTD}.tar.gz
|
tar xf zstd-nginx-module-${NGX_MOD_ZSTD}.tar.gz \
|
||||||
cd /opt/mod/; tar xf ${NGX_MOD_ZSTD}.tar.gz; rm -Rf ${NGX_MOD_ZSTD}.tar.gz
|
-C zstd-nginx-module-${NGX_MOD_ZSTD} --strip-components=1
|
||||||
|
rm -Rf zstd-nginx-module-${NGX_MOD_ZSTD}.tar.gz
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# END OF NGINX MODULES
|
# END OF NGINX MODULES
|
||||||
|
|||||||
+13
-12
@@ -244,18 +244,19 @@ function clean_install() {
|
|||||||
cd /opt/mod/; git clone --recurse-submodules https://github.com/wargio/naxsi.git naxsi
|
cd /opt/mod/; git clone --recurse-submodules https://github.com/wargio/naxsi.git naxsi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# NGX_MOD_ZSTD — Zstandard compression module from eilandert (maintained
|
# zstd module from https://github.com/theraw/zstd-nginx-module (NGX_MOD_ZSTD =
|
||||||
# fork of tokers/zstd-nginx-module, which has been abandoned since 0.1.1 in
|
# git tag). Extract with --strip-components=1 into a fixed local dir name so the
|
||||||
# 2023-10-23). Pinned via NGX_MOD_ZSTD; tarball pattern (dir name embeds
|
# --add-module path never depends on the archive's top folder; guard on config
|
||||||
# version → cache invalidates automatically when the pin moves).
|
# so a partial extract re-fetches.
|
||||||
#
|
if [ ! -f /opt/mod/zstd-nginx-module-${NGX_MOD_ZSTD}/config ]; then
|
||||||
# The fork ships the flush-loop CPU-spin fix we previously carried as
|
cd /opt/mod/
|
||||||
# build/patches/zstd-nginx-module-0.1.1-flush-cpu-spin.patch — see PR #23,
|
rm -rf zstd-nginx-module-${NGX_MOD_ZSTD}
|
||||||
# PR #49, and the "content-less flush completed" branch in
|
wget -O zstd-nginx-module-${NGX_MOD_ZSTD}.tar.gz \
|
||||||
# filter/ngx_http_zstd_filter_module.c. No local patching needed.
|
https://github.com/theraw/zstd-nginx-module/archive/refs/tags/${NGX_MOD_ZSTD}.tar.gz
|
||||||
if [ ! -d /opt/mod/zstd-nginx-module-${NGX_MOD_ZSTD} ]; then
|
mkdir -p zstd-nginx-module-${NGX_MOD_ZSTD}
|
||||||
cd /opt/mod/; wget https://github.com/eilandert/zstd-nginx-module/archive/refs/tags/${NGX_MOD_ZSTD}.tar.gz
|
tar xf zstd-nginx-module-${NGX_MOD_ZSTD}.tar.gz \
|
||||||
cd /opt/mod/; tar xf ${NGX_MOD_ZSTD}.tar.gz; rm -Rf ${NGX_MOD_ZSTD}.tar.gz
|
-C zstd-nginx-module-${NGX_MOD_ZSTD} --strip-components=1
|
||||||
|
rm -Rf zstd-nginx-module-${NGX_MOD_ZSTD}.tar.gz
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# END OF NGINX MODULES
|
# END OF NGINX MODULES
|
||||||
|
|||||||
+4
-13
@@ -59,19 +59,10 @@ http {
|
|||||||
open_file_cache_min_uses 2;
|
open_file_cache_min_uses 2;
|
||||||
open_file_cache_errors on;
|
open_file_cache_errors on;
|
||||||
|
|
||||||
# Symlink backstop for tenant static files. Enforced by nginx at the SAME
|
# Symlink backstop for tenant_isolation, checked at the serving open(): catches
|
||||||
# open() that serves the file, so it complements the tenant_isolation patch
|
# the probe->open race and the paths the patch skips (gzip_static, autoindex,
|
||||||
# (docs/tenant-isolation.md) on the axes that patch cannot cover from its
|
# dav, mp4/flv). if_not_owner allows a tenant's own symlinks, blocks ones whose
|
||||||
# separate openat2 probe:
|
# target has a different owner (docroot/x -> /etc/passwd => 403).
|
||||||
# * the probe->open TOCTOU window — a tenant that swaps a benign file for an
|
|
||||||
# escaping symlink after the probe is still caught here, at serve time;
|
|
||||||
# * the open paths the patch does not intercept — gzip_static, autoindex,
|
|
||||||
# dav, mp4/flv — which all funnel through this same disable_symlinks check.
|
|
||||||
# `if_not_owner` (not `on`) is deliberate: it permits a tenant's OWN in-tree
|
|
||||||
# symlinks (e.g. Laravel's public/storage) while refusing a symlink whose
|
|
||||||
# target has a different owner (docroot/x -> /etc/passwd, root-owned => 403),
|
|
||||||
# which matches the tenant_isolation semantics. `from=$document_root` skips
|
|
||||||
# symlink checks on the docroot prefix itself (owned by the platform).
|
|
||||||
disable_symlinks if_not_owner from=$document_root;
|
disable_symlinks if_not_owner from=$document_root;
|
||||||
|
|
||||||
# ===================== TLS ============================ #
|
# ===================== TLS ============================ #
|
||||||
|
|||||||
@@ -55,15 +55,7 @@ export NGX_MOD_LUA_LOCK="0.09"
|
|||||||
export NGX_MOD_LUA_SRCACHE="0.33"
|
export NGX_MOD_LUA_SRCACHE="0.33"
|
||||||
|
|
||||||
# https://github.com/eilandert/zstd-nginx-module/tags
|
# https://github.com/eilandert/zstd-nginx-module/tags
|
||||||
# Zstandard compression module. Chrome 123+ and Firefox 126+ send
|
export NGX_MOD_ZSTD="v1.1"
|
||||||
# `Accept-Encoding: zstd`; older clients fall back to brotli/gzip.
|
|
||||||
# Switched from tokers/zstd-nginx-module (unmaintained since 0.1.1, 2023-10-23)
|
|
||||||
# to eilandert/zstd-nginx-module, an actively maintained fork that has already
|
|
||||||
# absorbed the flush-loop CPU-spin fix we previously carried as a local patch
|
|
||||||
# (PR #23 + PR #49 + follow-up work — see filter/ngx_http_zstd_filter_module.c
|
|
||||||
# in the fork for the "content-less flush completed" branch that supersedes
|
|
||||||
# our patch).
|
|
||||||
export NGX_MOD_ZSTD="0.90.6"
|
|
||||||
|
|
||||||
# https://github.com/zlib-ng/zlib-ng/releases
|
# https://github.com/zlib-ng/zlib-ng/releases
|
||||||
# Drop-in libz replacement with SIMD-accelerated DEFLATE. Built in --zlib-compat
|
# Drop-in libz replacement with SIMD-accelerated DEFLATE. Built in --zlib-compat
|
||||||
|
|||||||
Reference in New Issue
Block a user