Files
cozystack/hack/console-nginx-header-buffers.bats
T
Aleksei Sviridkin 7d53f7af22 fix(dashboard): raise the console nginx header buffers for large OIDC tokens
The nginx config baked into the console image kept the nginx defaults for
client_header_buffer_size (1k) and large_client_header_buffers (4 8k). An
OIDC access token carrying many group claims arrives as a single Cookie
header of several kilobytes, which no longer fits into one buffer, so
nginx answered 400 "request header or cookie too large" before the
request reached the /apis, /api or /k8s/ upstream.

Raise both to 16k, at the top level of that config rather than inside the
server block: a header field read before the Host header has selected the
virtual server is measured against the default server's buffers, so the
same lines inside a server block cover that server only while it stays
the default one. Measured against the pinned nginx base image, a 12 KB
cookie gets 400 on the old config and 200 on the new one. The ceiling
moves rather than disappears: a 20 KB cookie is still rejected.

This is the console hop alone. The platform ingress-nginx controller in
front of it runs on the same defaults, so where the dashboard is published
through an Ingress a token large enough to exceed them is still rejected
at the edge; where it is published through the gateway, whose edge has no
per-header ceiling of this kind, the console was the hop returning the
400. Raising the controller is a separate change in a separate package.

The accompanying check extracts the config from the Containerfile heredoc
and fails if either directive is missing, below 16k, or no longer ahead of
the first server block: the config lives inside an image build, where no
chart template and no helm-unittest case can reach it.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-22 21:43:23 +03:00

78 lines
4.7 KiB
Bash

#!/usr/bin/env bats
# The console image bakes its nginx config into the Containerfile as an inline
# heredoc, so no chart template and no helm-unittest case can see it. This pins
# the header-buffer pair that image ships. The per-buffer size in
# large_client_header_buffers is what bounds a single Cookie header: on the
# default 4 8k an access token carrying many group claims does not fit one
# buffer, and nginx answers 400 "request header or cookie too large" before the
# request reaches the /apis, /api or /k8s/ upstream. client_header_buffer_size
# is raised alongside it so such a request is read without escalating to a large
# buffer at all. Either line is cheap to drop in an unrelated rewrite of that
# config, and the loss is invisible until a user with a big token logs in.
#
# The check reads the heredoc body, not the whole Containerfile, so a directive
# that moved out of the served config -- into a comment, or into a build stage
# that ships nothing -- does not read as present.
#
# Harness note: the CI path is hack/cozytest.sh, NOT real bats. No `run`,
# `$status`, `$output`, `skip` or setup()/teardown(); each @test is a shell
# function under `set -eu -x`, and paths resolve from the repo root. The two
# runners disagree on negative assertions, so an assertion written for bats can
# be vacuous under the one that gates a merge.
# Run with: hack/cozytest.sh hack/console-nginx-header-buffers.bats
REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")/.." && pwd)"
CONSOLE_CONTAINERFILE="packages/system/dashboard/images/console/Containerfile"
# Prints an nginx size in KiB, or -1 for a spelling this check cannot read.
nginx_size_kib() {
printf '%s\n' "$1" | awk '
/^[0-9]+[kK]$/ { print int(substr($0, 1, length($0) - 1)); next }
/^[0-9]+[mM]$/ { print int(substr($0, 1, length($0) - 1)) * 1024; next }
/^[0-9]+$/ { print int($0 / 1024); next }
{ print -1 }
'
}
@test "console nginx config reads client request headers into buffers larger than the nginx defaults" {
cd "$REPO_ROOT"
conf=$(awk '
/^COPY <<.EOF. \/etc\/nginx\/conf\.d\/default\.conf$/ { inblock = 1; next }
inblock && /^EOF$/ { inblock = 0; next }
inblock { print }
' "$CONSOLE_CONTAINERFILE")
[ -n "$conf" ] || { echo "no /etc/nginx/conf.d/default.conf heredoc found in $CONSOLE_CONTAINERFILE" >&2; exit 1; }
# An extraction that silently grabbed the wrong slice would let every
# assertion below pass on an empty-ish config, so anchor on a line the
# served config is known to carry.
printf '%s\n' "$conf" | grep -q 'proxy_pass https://kubernetes.default.svc:443;' \
|| { echo "extracted heredoc carries no apiserver proxy_pass; the extraction is wrong, not the config" >&2; exit 1; }
header_buffer=$(printf '%s\n' "$conf" | awk '$1 == "client_header_buffer_size" { sub(/;$/, "", $2); print $2 }')
[ -n "$header_buffer" ] || { echo "client_header_buffer_size is absent; the console ships it at 16k next to large_client_header_buffers" >&2; exit 1; }
header_buffer_kib=$(nginx_size_kib "$header_buffer")
[ "$header_buffer_kib" -ge 16 ] \
|| { echo "client_header_buffer_size reads as $header_buffer, and this check wants at least 16k" >&2; exit 1; }
# Third field, because the directive is `large_client_header_buffers <number> <size>`
# and it is the per-buffer size that bounds one Cookie header.
large_buffer=$(printf '%s\n' "$conf" | awk '$1 == "large_client_header_buffers" { sub(/;$/, "", $3); print $3 }')
[ -n "$large_buffer" ] || { echo "large_client_header_buffers is absent: a Cookie header over 8k gets a 400 from the console nginx" >&2; exit 1; }
large_buffer_kib=$(nginx_size_kib "$large_buffer")
[ "$large_buffer_kib" -ge 16 ] \
|| { echo "large_client_header_buffers reads as $large_buffer per buffer, and a large OIDC token needs at least 16k" >&2; exit 1; }
# Placement, not only presence. A header field read before the Host header
# has selected the virtual server is measured against the default server's
# buffers, so the same lines inside a server block cover that server only
# while it stays the default one. The shipped config puts them ahead of the
# first server block, and that is the shape checked here.
prelude=$(printf '%s\n' "$conf" | awk '/^server[[:space:]]*{/ { exit } { print }')
printf '%s\n' "$prelude" | grep -q '^client_header_buffer_size' \
|| { echo "client_header_buffer_size does not stand at the top of the config, ahead of the first server block" >&2; exit 1; }
printf '%s\n' "$prelude" | grep -q '^large_client_header_buffers' \
|| { echo "large_client_header_buffers does not stand at the top of the config, ahead of the first server block" >&2; exit 1; }
}