| name | sanity-validators |
| description | Use this skill BEFORE writing or modifying any input validation in this repo — e.g. "validate this CLI flag", "sanitize this filename", "check this username", "add a new sanity helper", any edit that touches pkg/sanity/sanity.go or any caller that runs user-supplied / env-derived / config-derived strings through a `sanity.*` helper. Captures the Sanitize* vs Validate* naming pattern, the per-validator character sets, and the decision matrix for picking the right helper. Read this BEFORE relaxing or tightening any validator — the rules are scoped and the wrong helper is what produced issue |
pkg/sanity validators — naming, scope, and decision matrix
pkg/sanity is the single chokepoint for validating untrusted strings before they
reach the OS, the shell, Helm, kubectl, file paths, or NSS lookups. Every helper
in this package follows one of two patterns; using the wrong one (or reusing a
helper scoped to a different domain) is how validation bugs slip in.
The two patterns
| Prefix | Signature | Behavior |
|---|
Sanitize* | (s string) (string, error) | Strips invalid characters and returns the cleaned form. Errors only if nothing valid remains (returns ErrInvalidName). The caller is expected to use the returned value, not the input. |
Validate* | (s string) error | Rejects any input containing invalid characters. The caller uses the input string as-is when the error is nil. |
Signature is destiny. A (string, error) return means "I might rewrite your
input" — if the caller doesn't use the returned value, the validation is wasted
or wrong. An error return means "I won't touch it" — the input is safe to use.
Default to Validate* for anything sourced from a user or environment. Use
Sanitize* only when the codebase itself is constructing a string from
trusted-but-messy inputs (e.g. deriving a filename from a label).
Function catalog
Sanitizers (return (string, error))
| Function | Charset | Use for |
|---|
SanitizeIdentifier | [A-Za-z0-9_-] | Generic identifiers when no more specific helper fits. |
SanitizeFilename | [A-Za-z0-9_-] (today, same as identifier) | Derived filenames. Standalone so future relaxation (e.g. permit .) can land without touching identifier or module-name callers. |
SanitizeModuleName | [A-Za-z0-9_-] (today, same as identifier) | Linux kernel module names. Standalone so future tightening (drop -, since kernel modules use _) can land without touching identifier or filename callers. |
SanitizePath | path semantics | File-system paths. Cleans, makes absolute, rejects .. segments and shell metacharacters. |
Alphanumeric | [A-Za-z0-9] | Pure transformer (string return only, no error). For display-string normalisation, not security validation. |
All sanitizers return ErrInvalidName when filtering leaves nothing.
Validators (return error)
| Function | Charset / contract | Use for |
|---|
ValidateIdentifier | [A-Za-z0-9_-], non-empty | Namespace / release / profile names; anything that flows into Kubernetes object names. |
ValidateUsername | [A-Za-z0-9_.-], non-empty, no .., no shell metachars | POSIX/Linux usernames (SUDO_USER, owner accounts). Permits . for firstname.lastname accounts. |
ValidateOperationID | [A-Za-z0-9_.-], non-empty, no .., no shell metachars | Network-upgrade operationId (CR spec.operationId). Flows into .bak archive paths written as root. Permits . for dotted version strings (e.g. upgrade-v0.76.0-20060102T150405Z). |
ValidateDNSName | [A-Za-z0-9.-] matching RFC 952/1123 host pattern | Hostnames, FQDNs, cluster names derived from DNS. |
ValidateHexToken | [0-9a-fA-F], length ≤ 4096 | Teleport join tokens and similar hex secrets. |
ValidateHostPort | hostname or hostname:port; no path components, no shell metachars | host:port style endpoints. Use ValidateURL for full URLs. |
ValidateChartReference | OCI URL / classic repo/chart / local path | Helm chart references. Critical for the block-node Chart field. |
ValidateInputFile | path semantics + file exists + within allowed roots | User-supplied file paths from CLI flags. |
Decision matrix — "which one do I use for X?"
Match by what the string represents in the real world, not by what it looks like:
- Kubernetes namespace / release / profile name →
ValidateIdentifier
- Linux user account (SUDO_USER, file owner, sudoers principal) →
ValidateUsername
- Network-upgrade operationId (CR
spec.operationId, embedded in .bak paths) → ValidateOperationID
- Kernel module name →
SanitizeModuleName (current callers want the sanitized form for the modprobe call)
- Helm chart reference →
ValidateChartReference
- Hostname / FQDN / cluster DNS name →
ValidateDNSName
- host:port endpoint →
ValidateHostPort
- Hex token / shared secret →
ValidateHexToken
- Filesystem path from a user →
SanitizePath (when you'll use the cleaned form) or ValidateInputFile (when you need the file to exist and be in an allowed root)
- Derived filename from an internal label →
SanitizeFilename
- Display-only string normalisation →
Alphanumeric
- Nothing on this list matches → do not repurpose a near-fit; add a new validator scoped to the actual domain.
Hard rules
1. Do not alias one validator to another
Before issue #602, Filename and ModuleName were literal aliases of Identifier.
That made it impossible to relax the username charset (issue #600) without
implicitly broadening filenames and kernel-module names too. Each domain gets its
own function even when the charset is currently identical — future divergence
should not require touching unrelated callers.
2. Validate at the boundary, not in the middle
The right call site is the first point a string enters the program from an
untrusted source:
- CLI flag parsing in
cmd/cli/commands/**/init.go
- Config-file unmarshal in
pkg/config/global.go
- Env-var reads (e.g.
os.Getenv("SUDO_USER") in internal/kube/admin.go)
- Public API entry points
Do not assume an upstream caller already validated. If you're about to use a
string in a Helm/kubectl/shell/path/NSS operation and the validator hasn't run
in the current function or its immediate caller, run it.
3. Don't reuse Validate* against persisted state
Validators are designed for fresh user input. When code loads a previously
persisted value (e.g. BlockNodeState.ReleaseInfo.ChartRef from disk) and the
value will be used as authoritative without re-validation, the validator's
guarantees no longer hold — a corrupted or pre-relaxation-era state file can
smuggle in values that the current validator would reject. If you add new
persisted fields, also add a validation pass at the load boundary.
4. The .. substring check runs before the per-char check
ValidateUsername (and any future validator that permits .) rejects .. as
a substring before iterating characters. Don't reorder these.
5. Shell metachars are a separate, smaller gate than the full charset check
shellMetachars = [;&|$\x60<>(){}[\]*?~] is intentionally checked separately so
the error message can distinguish "contains shell metacharacter" from "contains
invalid character". This set is narrower than what the per-validator charset
rejects: many non-shell characters are still invalid there. Both checks produce
safe behavior; this split only affects error specificity. Do not collapse them
into a single regex unless you also change the error messages.
When adding a new validator
- Name it for the domain, not the charset.
ValidateUsername, not
ValidateAlnumDotUnderscoreHyphen.
- Pick the right prefix:
Validate* (strict) if any non-empty caller will
pass an env/CLI/config string; Sanitize* only if every call site wants the
cleaned form.
- Don't reuse
isValidIdentifierChar if your domain might diverge. Add a
domain-specific predicate (isValidUsernameChar, isValidFooBarChar) even
if it returns the same set today.
- Reject empty first. Every validator should return
"<name> cannot be empty"
on "".
- Reject
.. and shell metachars next, before the per-char loop, when the
permitted charset includes any of . / -.
- Add tests for: empty input, valid happy path, every shell metachar, every
control byte (
\x00, \n, \r, \t, \x07, \x1b), a .. traversal
attempt, a SQL/command-injection attempt, and a path-traversal attempt. Mirror
the structure of TestSanity_ValidateUsername in pkg/sanity/sanity_test.go.
- Update this skill with the new validator and its decision-matrix row.
History (so the conventions don't drift)
- Issue #600:
Username rejected firstname.lastname accounts (e.g.
nana.ec). Fixed by permitting . in the username charset and reaffirming
the existing .. traversal check.
- Issue #602: split the package into
Sanitize* vs Validate* by behavior;
dropped the Filename/ModuleName-as-aliases-of-Identifier shortcut so each
domain can evolve independently; renamed Username → ValidateUsername to
match its actual semantics (returns error, not a sanitized form).