[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.
This commit is contained in:
jnfrati
2026-09-08 10:49:10 +02:00
parent e9bea322fa
commit 6cff570022
4 changed files with 54 additions and 37 deletions
+5
View File
@@ -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
@@ -56,7 +56,6 @@ func InvokingUser() (*user.User, error) {
Username: uidString,
Uid: uidString,
Gid: strconv.Itoa(getegid()),
HomeDir: os.Getenv("HOME"),
}, nil
}
@@ -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()
+49 -2
View File
@@ -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"