Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion palworld/module.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: gameplane.local/module/v1
name: palworld
displayName: Palworld (Dedicated)
version: 1.1.0
version: 1.1.1
game: palworld
summary: Palworld dedicated server (thijsvanloef/palworld-server-docker) with RCON console, save-aware graceful stop, and a .pak mod volume with archive install. First boot downloads the server via steamcmd.
homepage: https://www.pocketpair.jp/palworld
Expand Down
13 changes: 12 additions & 1 deletion palworld/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ spec:
displayName: Palworld (Dedicated)
game: palworld
# Kept in lockstep with module.yaml's bundle version.
version: 1.1.0
version: 1.1.1
# Brand color the dashboard tints this game's icon/accents with.
accentColor: "#3d9dd9"
description: |
Expand All @@ -42,6 +42,17 @@ spec:
# authoritative source for the wizard's picker.
image: thijsvanloef/palworld-server-docker:latest

# REQUIRED. The image starts as root but its entrypoint DROPS privileges to
# PUID/PGID (1000). The data PVC is root-owned, so the dropped-privilege
# user cannot chmod the files SteamCMD just installed, and the server dies:
# ./PalServer.sh is not executable.
# It then exits 0 and restart-loops (92 times on the test cluster before
# SteamCMD's resume finally clawed through). fsGroup makes the kubelet chown
# the volume to gid 1000 so the dropped user owns its own install.
# Do NOT set runAsUser here — the image must START as root in order to drop.
security:
fsGroup: 1000

env:
- name: RCON_ENABLED
value: "true"
Expand Down
64 changes: 64 additions & 0 deletions validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,69 @@ def rule_mods_loaders_requires_versions(spec: dict) -> list[Finding]:
return []


def rule_puid_image_needs_fsgroup(spec: dict, cfg: dict, image_ref: str) -> list[Finding]:
"""Rule 8: images with PUID/PGID privilege drop need fsGroup.

Historical bug: thijsvanloef/palworld-server-docker has User: "" (starts as root)
but Env declares PUID=1000/PGID=1000, dropping privileges at runtime. Without
spec.security.fsGroup, the data PVC is root-owned and the dropped-privilege
user can't write — leading to "PalServer.sh is not executable" and restart loops.

ERROR: image declares PUID/PGID in Env but template has no fsGroup.
WARN: template sets runAsUser on a PUID-style image (image needs to start as
root in order to drop privileges).
"""
env_list = cfg.get("Env") or []
has_puid = any(e.split("=", 1)[0] == "PUID" for e in env_list)
has_pgid = any(e.split("=", 1)[0] == "PGID" for e in env_list)

if not (has_puid or has_pgid):
return []

findings: list[Finding] = []

# Check if fsGroup is set
fs_group = get_path(spec, "security.fsGroup")
if fs_group is None:
findings.append(
Finding(
# WARN, not ERROR. A PUID/PGID image is a *risk* signal, not proof of
# breakage: many such images chown the volume themselves while still
# root, before dropping. factorio, rust and terraria all do this and
# run fine with no fsGroup. But palworld does NOT — once dropped it
# could not chmod +x the files SteamCMD had just installed
# ("./PalServer.sh is not executable."), exited 0, and restart-looped
# 92 times. So: flag it for a human, don't fail the build.
WARN,
"puid-image-needs-fsgroup",
f"image {image_ref!r} declares PUID/PGID in Env (its entrypoint drops "
"privileges at runtime) but spec.security.fsGroup is unset. If the image "
"does not chown the data volume before dropping, the dropped user cannot "
"write its own install — palworld hit exactly this ('./PalServer.sh is not "
"executable.', exit 0, 92 restarts). Verify, and set spec.security.fsGroup "
"to the PUID if needed. Do NOT set runAsUser: the image must start as root "
"in order to drop.",
)
)

# Warn if template incorrectly sets runAsUser on a PUID image
run_as_user = get_path(spec, "security.runAsUser")
if run_as_user is not None:
findings.append(
Finding(
WARN,
"puid-image-with-runasuser",
f"image {image_ref!r} declares PUID/PGID (needs to start as root "
"to drop privileges), but spec.security.runAsUser={run_as_user} — "
"this forces the container to start as non-root, preventing the "
"privilege-drop mechanism from working. Remove spec.security.runAsUser "
"and rely on fsGroup for permissions.",
)
)

return findings


# --------------------------------------------------------------------------
# Orchestration
# --------------------------------------------------------------------------
Expand Down Expand Up @@ -701,6 +764,7 @@ def validate_module(spec: dict, cache: dict[str, dict]) -> list[Finding]:
findings += rule_nonroot_requires_runasuser(spec, cfg, ref)
findings += rule_runasuser_requires_home(spec, cfg, ref)
findings += rule_mount_shadowing(spec, cfg, ref)
findings += rule_puid_image_needs_fsgroup(spec, cfg, ref)

findings += rule_rcon_protocol(spec)
findings += rule_mods_loaders_requires_versions(spec)
Expand Down
Loading