From 6cff570022d2d0650f33ab068d1c48b1c4d22d87 Mon Sep 17 00:00:00 2001 From: jnfrati Date: Tue, 8 Sep 2026 10:49:10 +0200 Subject: [PATCH] [client] Verify rootless state reuse with a stable UID Persisted profiles remain scoped to the creating UID. Verify same-UID container recreation without broadening application permissions, and document the Kubernetes volume permission behavior observed on OpenShift. Remove unused synthetic-user home metadata. --- client/Dockerfile-rootless | 5 ++ .../internal/profilemanager/invoking_user.go | 1 - .../profilemanager/invoking_user_test.go | 34 ------------- client/test/rootless-arbitrary-uid-docker.sh | 51 ++++++++++++++++++- 4 files changed, 54 insertions(+), 37 deletions(-) diff --git a/client/Dockerfile-rootless b/client/Dockerfile-rootless index 0169bedc3..36ae24009 100644 --- a/client/Dockerfile-rootless +++ b/client/Dockerfile-rootless @@ -3,6 +3,11 @@ # CGO_ENABLED=0 go build -o netbird ./client # podman build -t localhost/netbird:latest -f client/Dockerfile-rootless --ignorefile .dockerignore-client . # podman run --rm -it --user 1001230000:0 --cap-drop=ALL --security-opt=no-new-privileges localhost/netbird:latest +# +# Reuse /var/lib/netbird volumes with the same runtime UID. +# Config/profile files start at 0600; Kubernetes fsGroup may widen their modes. +# Where supported, pod securityContext.fsGroupChangePolicy: OnRootMismatch +# preserves modes when the volume root's ownership and permissions match. FROM alpine:3.24 diff --git a/client/internal/profilemanager/invoking_user.go b/client/internal/profilemanager/invoking_user.go index 8c7bc1ab6..7ba612ffb 100644 --- a/client/internal/profilemanager/invoking_user.go +++ b/client/internal/profilemanager/invoking_user.go @@ -56,7 +56,6 @@ func InvokingUser() (*user.User, error) { Username: uidString, Uid: uidString, Gid: strconv.Itoa(getegid()), - HomeDir: os.Getenv("HOME"), }, nil } diff --git a/client/internal/profilemanager/invoking_user_test.go b/client/internal/profilemanager/invoking_user_test.go index 2fd56117c..401975cf4 100644 --- a/client/internal/profilemanager/invoking_user_test.go +++ b/client/internal/profilemanager/invoking_user_test.go @@ -14,39 +14,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestInvokingUserReturnsResolvedCurrentUser(t *testing.T) { - t.Setenv(envSudoUser, "") - - want := &user.User{ - Username: "misha", - Uid: "1234", - Gid: "1234", - HomeDir: filepath.Join("/home", "misha"), - } - origCurrentUser := currentUser - currentUser = func() (*user.User, error) { return want, nil } - t.Cleanup(func() { currentUser = origCurrentUser }) - - got, err := InvokingUser() - require.NoError(t, err) - assert.Same(t, want, got, "resolved process user should be returned unchanged") -} - -func TestInvokingUserUsesNumericIdentityForUnmappedNonRoot(t *testing.T) { - t.Setenv(envSudoUser, "") - t.Setenv("HOME", "/var/lib/netbird") - fakeUnmappedUser(t, 1001230000, 0, errors.New("user: unknown userid 1001230000")) - - got, err := InvokingUser() - require.NoError(t, err) - assert.Equal(t, &user.User{ - Username: "1001230000", - Uid: "1001230000", - Gid: "0", - HomeDir: "/var/lib/netbird", - }, got, "unmapped non-root identity should use kernel credentials") -} - func TestInvokingUserFailsClosedWithoutPositiveUID(t *testing.T) { for _, uid := range []int{0, -1} { t.Run(fmt.Sprintf("UID%d", uid), func(t *testing.T) { @@ -63,7 +30,6 @@ func TestInvokingUserFailsClosedWithoutPositiveUID(t *testing.T) { func TestProfileFilePathUsesNumericIdentityForUnmappedNonRoot(t *testing.T) { t.Setenv(envSudoUser, "") - t.Setenv("HOME", "/var/lib/netbird") fakeUnmappedUser(t, 1001230000, 0, errors.New("user: unknown userid 1001230000")) profilesRoot := t.TempDir() diff --git a/client/test/rootless-arbitrary-uid-docker.sh b/client/test/rootless-arbitrary-uid-docker.sh index 4a5cee2b3..97555bc05 100755 --- a/client/test/rootless-arbitrary-uid-docker.sh +++ b/client/test/rootless-arbitrary-uid-docker.sh @@ -28,10 +28,15 @@ PLATFORM="${PLATFORM:-linux/${TARGETARCH}}" WAIT_TIMEOUT="${WAIT_TIMEOUT:-30}" TMP_DIR="$(mktemp -d)" CONTAINER="netbird-rootless-uid-${RANDOM}-$$" +VOLUME="" cleanup() { local status=$? "${RUNTIME}" rm -f "${CONTAINER}" >/dev/null 2>&1 || true + if [[ -n "${VOLUME}" ]] && ! "${RUNTIME}" volume rm "${VOLUME}" >/dev/null; then + echo "failed to remove test volume ${VOLUME}" >&2 + status=1 + fi rm -rf "${TMP_DIR}" exit "${status}" } @@ -66,9 +71,10 @@ build_image() { start_container() { echo "==> Starting ${CONTAINER} as unmapped UID 1001230000" - "${RUNTIME}" run --rm -d \ + "${RUNTIME}" run -d \ --name "${CONTAINER}" \ --user 1001230000:0 \ + --volume "${VOLUME}:/var/lib/netbird" \ --cap-drop=ALL \ --security-opt=no-new-privileges \ --entrypoint /usr/local/bin/netbird \ @@ -112,14 +118,55 @@ assert_arbitrary_uid_contract() { touch /var/lib/netbird/.uid-smoke rm /var/lib/netbird/.uid-smoke test -S /var/lib/netbird/netbird.sock + test "$(stat -c %a /var/lib/netbird/config.json)" = 600 + test "$(stat -c %a /var/lib/netbird/active_profile.json)" = 600 ' "${RUNTIME}" exec "${CONTAINER}" \ /usr/local/bin/netbird profile list >/dev/null } +assert_same_uid_restart() { + echo "==> Verifying persistent profiles with the same runtime UID" + local profile_name="rootless-restart" profiles_before profiles_after + + "${RUNTIME}" exec "${CONTAINER}" \ + /usr/local/bin/netbird profile add "${profile_name}" >/dev/null + profiles_before="$("${RUNTIME}" exec "${CONTAINER}" \ + /usr/local/bin/netbird profile list --show-id)" + if [[ "${profiles_before}" != *"${profile_name}"* ]]; then + echo "created profile is missing before restart" >&2 + return 1 + fi + + "${RUNTIME}" stop "${CONTAINER}" >/dev/null + "${RUNTIME}" rm "${CONTAINER}" >/dev/null + start_container + wait_until_live + assert_arbitrary_uid_contract + + profiles_after="$("${RUNTIME}" exec "${CONTAINER}" \ + /usr/local/bin/netbird profile list --show-id)" + if [[ "${profiles_after}" != "${profiles_before}" ]]; then + echo "profiles changed after recreating the container with the same volume and UID" >&2 + container_logs + return 1 + fi + + "${RUNTIME}" exec "${CONTAINER}" \ + /usr/local/bin/netbird profile rename "${profile_name}" "${profile_name}-renamed" >/dev/null + profiles_after="$("${RUNTIME}" exec "${CONTAINER}" \ + /usr/local/bin/netbird profile list --show-id)" + if [[ "${profiles_after}" != *"${profile_name}-renamed"* ]]; then + echo "persisted profile could not be updated after restart" >&2 + return 1 + fi +} + build_image +VOLUME="$("${RUNTIME}" volume create "${CONTAINER}-state")" start_container wait_until_live assert_arbitrary_uid_contract +assert_same_uid_restart -echo "==> Rootless arbitrary UID validation passed" +echo "==> Rootless arbitrary UID and same-UID persistence validation passed"