Skip to content

[NeverLiteral] parameter validator — refuse committed-literal values for env-sourced fields #252

Description

@ChrisonSimtian

Motivation

In the ErpForFactoryGames CD bring-up, the deploy target's address (a private-range LXC IP) was committed in deploy/erp-deploy.json as a string literal — "Host": "10.10.107.175". Not a secret (RFC1918, not externally routable), but it leaked the homelab's internal topology to a public repo. We fixed it in ErpForFactoryGames#266 by swapping the IP for an erp-lxc SSH-config alias resolved at runtime.

Catch was: nothing in the framework warned us. The field is plumbed through [Parameter] and RemoteOptions.Host and SshConnectionResolver cleanly — but the literal value sitting in a JSON config file passed every other check. Gitleaks didn't flag it (it's not credential-shaped). GitHub Secret Scanning didn't flag it. Code review didn't catch it because the IP looked like config, not a secret.

This is a generic CD smell: fields that should resolve from environment / secret store / operator config sometimes get pinned as literals in committed files, and the framework silently allows it. ADR-0002's [Secret] story handles credential-shaped fields well; the gap is the non-credential fields where the literal is the leak.

Proposal

Two attributes that share a validator:

// "This field must resolve from environment / prompt / explicit override at run-time.
// Refuse to start if it resolves to a literal in a committed config file."
[NeverLiteral]

// Stronger form: this field must come from a [SecretSource]-shaped lookup
// (env-var named after the field, secret store, prompt). Refuses parameter
// flags AND config-file values. Implied for [Secret].
[FromEnvironment]

Behaviour:

  • At parameter-resolution time, the validator looks at how the value was resolved.
  • If [NeverLiteral] and the resolution source is "committed config file", fail with "Field {Name} must not be a literal in a committed file — set ${ENV_VAR_NAME} instead."
  • Local override files (e.g. *.local.json that are gitignored) pass — committed status is the discriminator, checkable via git check-ignore.

The validator runs on every ./build.sh invocation, not just Doctor. Lifts the "we forgot to use the env-var" failure to the very first frame instead of letting it ship.

Why this fits Fallout (not gitleaks / secret-scanning)

  • Gitleaks detects patterns in the file. It can't tell whether a string is "supposed to be env-sourced" — that's a schema concern, only knowable from the C# parameter declaration.
  • GitHub Secret Scanning is partner-pattern-driven. Non-credential leaks (IPs, internal hostnames, identifiers) aren't covered.
  • Fallout already owns the parameter schema. The cost is O(LOC for one validator) rather than maintaining an external regex set.

Open questions

  • Granularity. Field-level (this string) vs object-level (this whole RemoteOptions block must be env-resolved). Probably both, with object-level implemented as "every field marked [NeverLiteral]".
  • CI vs local enforcement. Should the validator fail the build hard, or just warn locally and fail in CI? [Secret] fails hard already; [NeverLiteral] should probably match.
  • Backwards compat for migrations. A repo retrofitting this on existing fields needs a transition mode (warn but don't fail) — otherwise the first [NeverLiteral] lands and every old PR breaks.
  • Detection of "committed file" without shelling to git. Caller might pass a --config <path> not yet under source control. Cheapest probe: git check-ignore --quiet <path> exit code; fallback to "is the file inside the repo tree" + "is it tracked at HEAD" via libgit2.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesttarget/vNextTargets the next calendar-version

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions