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 cs2/module.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: gameplane.local/module/v1
name: cs2
displayName: Counter-Strike 2
version: 1.0.1
version: 1.0.2
game: cs2
summary: Counter-Strike 2 dedicated server (joedwards32/CS2) with Source RCON and a Metamod/CS# plugins volume.
homepage: https://www.counter-strike.net/cs2
Expand Down
7 changes: 5 additions & 2 deletions cs2/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ metadata:
spec:
displayName: Counter-Strike 2
game: cs2
version: 1.0.1
version: 1.0.2
accentColor: "#f4a900"
description: |
Counter-Strike 2 dedicated server using
Expand Down Expand Up @@ -121,7 +121,10 @@ spec:
- name: SRCDS_TOKEN
displayName: GSLT (Steam Login Token)
description: Game Server Login Token from https://steamcommunity.com/dev/managegameservers.
type: string
# A Steam Game Server Login Token is a CREDENTIAL. type: password stores
# it in a per-GameServer Secret and injects it via SecretKeyRef — it never
# lands inline in the GameServer CR or the pod spec.
type: password
required: true
default: ""
- name: CS2_HOST_WORKSHOP_COLLECTION
Expand Down
65 changes: 65 additions & 0 deletions validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,70 @@ def rule_puid_image_needs_fsgroup(spec: dict, cfg: dict, image_ref: str) -> list
return findings


def rule_credential_fields_must_be_password(spec: dict) -> list[Finding]:
"""Rule 9: credential-shaped config fields must have type: password.

Config fields whose names look like credentials (PASSWORD, TOKEN, SECRET,
etc.) are stored in plaintext in the GameServer CR and etcd if type is not
'password'. Only 'type: password' stores values in a per-GameServer Secret
and injects them via SecretKeyRef — the value never lands inline in the CR,
pod spec, or etcd.

Historical bug (caught in cs2): SRCDS_TOKEN (a Steam Game Server Login Token)
was declared as 'type: string', so it would have been stored in plaintext in
the CR and visible to anyone with kubectl read access.

ERROR: name looks credential-shaped but type is not 'password'.
Skip: type is 'bool' or 'int' (a boolean flag named 'PASSWORD_ENABLED' is not
a credential even if the name matches).
"""
config_schema = spec.get("configSchema") or []
findings: list[Finding] = []

# Credential name patterns to match (case-insensitive)
credential_substrings = {
"PASSWORD",
"PASSWD",
"PASS",
"TOKEN",
"SECRET",
"APIKEY",
"API_KEY",
"AUTHKEY",
"GSLT",
"CREDENTIAL",
}

for entry in config_schema:
if not isinstance(entry, dict):
continue

name = entry.get("name", "")
field_type = entry.get("type", "")

# Skip non-credential types (bool/int are never credentials)
if field_type in ("bool", "int"):
continue

# Check if name contains any credential substring (case-insensitive)
name_upper = name.upper()
if any(substring in name_upper for substring in credential_substrings):
if field_type != "password":
findings.append(
Finding(
ERROR,
"credential-field-must-be-password",
f"config field {name!r} looks credential-shaped but "
f"type={field_type!r} (not 'password') — the value will be "
"stored in plaintext in the GameServer CR, etcd, and visible "
"in 'kubectl get gameserver -o yaml'. Set type: password so "
"the operator stores this in a per-GameServer Secret instead.",
)
)

return findings


# --------------------------------------------------------------------------
# Orchestration
# --------------------------------------------------------------------------
Expand Down Expand Up @@ -768,6 +832,7 @@ def validate_module(spec: dict, cache: dict[str, dict]) -> list[Finding]:

findings += rule_rcon_protocol(spec)
findings += rule_mods_loaders_requires_versions(spec)
findings += rule_credential_fields_must_be_password(spec)
return findings


Expand Down
Loading