Skip to content

chore(scripts): generate-federation-iac.go iacData has drifted out of sync with the templates it renders #153

Open

Description

@cristim

scripts/generate-federation-iac.go is a standalone dev tool for rendering
the same templates the runtime API (GET /api/federation/iac,
internal/api/handler_federation.go) renders. Its own doc comment says:

iacData mirrors internal/api/handler_federation.go:federationIaCData.
Keep in sync with any template variable changes.

It has not been kept in sync. iacData is missing several fields that
internal/iacfiles/templates/aws-wif.tfvars.tmpl and
aws-cfn-deploy.sh.tmpl already reference:

  • CUDlyAPIURL — referenced by the {{if .CUDlyAPIURL}} auto-registration
    block at the bottom of aws-wif.tfvars.tmpl (and other tfvars templates).
  • ContactEmail — referenced inside that same block.
  • OIDCIssuerHost — referenced by aws-cfn-deploy.sh.tmpl's
    --parameter-overrides.

Reproduce (pre-existing on main, unrelated to any in-flight PR):

go run scripts/generate-federation-iac.go \
  --target aws --source gcp \
  --account-name "prod-aws" --account-id "123456789012" \
  --output -
Error: render internal/iacfiles/templates/aws-wif.tfvars.tmpl: template: iac:24:19: executing "iac" at <.CUDlyAPIURL>: can't evaluate field CUDlyAPIURL in type main.iacData

The default (tfvars) and --format bundle output for --target aws (any
non-aws --source) are currently unusable — text/template errors out
executing the struct field reference. --format cf-params still works
(that template doesn't reference the missing fields).

Not a security issue — this is a developer-facing tool
(//go:build ignore, run via go run, never reachable from customer
traffic), and its output is unusable rather than silently wrong. Filing so
it doesn't stay silently broken.

Suggested fix

Superseded in part by LeanerCloud/cloud-commitments-cli#1709. That issue tracks the missing iacData
fields as a current breakage on main, with a wider field list
(SourceAccountID as well) and an end-to-end render test. Add the fields
there, not here.

What remains for this issue once LeanerCloud/cloud-commitments-cli#1709 lands: fix the doc comment. "Keep in
sync with any template variable changes" claims an invariant nothing
enforces, which is how the drift went unnoticed in the first place. Say what
is true instead, and point at whatever coverage LeanerCloud/cloud-commitments-cli#1709 adds as the thing that
actually holds the two in step.

Not doing:

Remedy simplified (2026-08-03): the two new flags and the reflection sync
test were dropped, leaving the doc-comment correction. Note also corrected
the same day: an earlier revision of this section claimed no test could cover
the script because it is //go:build ignore. That is true of a test that
imports the struct, but LeanerCloud/cloud-commitments-cli#1709 shows an end-to-end test that executes
the script covers the same drift, so the coverage half of the original
remedy was right and this section was wrong to drop it outright.

Found while working on LeanerCloud/cloud-commitments-cli#1640 (internal/iacfiles/templates/aws-wif.tfvars.tmpl
now also references {{.OIDCSubjectClaim}}, which I added to iacData in
that PR — but the pre-existing missing fields above are unrelated to that
change and out of its scope).

Findings from the 2026-09-02 codebase audit

Added by an automated audit of 3c0f8ac94048a2c36fce5ccddee54e6c4849a5cd (tip of origin/main). Each item below was reported by one reviewer and independently confirmed by a second that did not write it. Full report: docs/audits/codebase-audit-2026-09-02.md.

A14-028 (low)

This drift is also load-bearing on a test's rationale, so the two should be closed together. TestGenerator_CrossAccountDoesNotRequireSubjectClaim (scripts/generate_federation_iac_test.go:434-446) asserts only on stderr, with a comment explaining that the invocation "currently fails for an unrelated pre-existing reason: iacData is missing the SourceAccountID / CUDlyAPIURL / ContactEmail / OIDCIssuerHost fields ... so no tfvars path of this script renders on main today". Three of those four now exist (SourceAccountID at generate-federation-iac.go:119, CUDlyAPIURL and ContactEmail at :141-142) and generate-federation-iac_test.go:40 renders tfvars successfully, so the justification is stale and the test currently passes whatever exit code the aws-to-aws path returns, including a regression that breaks it outright. Once the remaining OIDCIssuerHost gap is closed here, the test should assert res.exitCode == 0 and lose the rationale. (audit finding A14-028)

Activity

  1. cristim commented on Aug 3, 2026

    @cristim
    MemberAuthor

    Three follow-ups found while implementing the CodeRabbit fixes on LeanerCloud/cloud-commitments-cli#1691 (which closes LeanerCloud/cloud-commitments-cli#1640). All are pre-existing on origin/main and all were deliberately left out of LeanerCloud/cloud-commitments-cli#1691 to keep that PR scoped to the --oidc-subject-claim path. Filing here since this issue already tracks the drift between scripts/generate-federation-iac.go and the templates.


    1. iacData is also missing SourceAccountID, so the aws -> aws cross-account path is broken too

    The issue body lists CUDlyAPIURL, ContactEmail and OIDCIssuerHost. SourceAccountID is missing as well: internal/iacfiles/templates/aws-cross-account.tfvars.tmpl references it, so the cross-account combination fails the same way the WIF one does.

    Reproduced against origin/main's copy of the script (34d85bc1b):

    $ go run scripts/generate-federation-iac.go \
        --target aws --source aws \
        --account-name "tgt" --account-id "999888777666" --output -
    Error: render internal/iacfiles/templates/aws-cross-account.tfvars.tmpl: template: iac:16:23: executing "iac" at <.SourceAccountID>: can't evaluate field SourceAccountID in type main.iacData
    exit status 1
    

    Full set of fields the templates reference but iacData lacks: CUDlyAPIURL, ContactEmail, SourceAccountID, OIDCIssuerHost. Net effect: every .tfvars path of this script is unusable, plus --format bundle for all targets. --format cf-params is the only working mode.

    This is why LeanerCloud/cloud-commitments-cli#1691's TestGenerator_CrossAccountDoesNotRequireSubjectClaim asserts on stderr rather than on exitCode == 0; there is a comment in that test pointing back here.


    2. shellEscape does not escape }

    internal/api/handler_federation.go:399

    r := strings.NewReplacer(`\`, `\\`, `"`, `\"`, "`", "\\`", `$`, `\$`)

    It covers \ " $but not</code>}<code>. Several CLI templates interpolate values inside a ${VAR:-{{.Field}}}default, e.g.aws-wif-cli.sh.tmpl:21and:139, aws-cfn-deploy.sh.tmpl:56. A value containing }terminates the parameter expansion early, so the remainder of the value lands in the script as bare shell tokens rather than as part of the string. Escaping$alone does not prevent that, because the}` is structural to the surrounding expansion, not to the value.

    Not currently exploitable: every field that reaches those expansions is built by buildGenericIaCData from the validated target/source/config URL, or by populateSourceAccountID from the database. It is a latent gap in a helper whose stated contract is "safe use inside a double-quoted bash argument", which } breaks.


    3. Two render paths that emit shell/HCL bypass escaping entirely

    shellEscapeData is applied at only two call sites (handler_federation.go:274 for single-file cli, and :640 for cloudformation/deploy-cfn.sh). Two other renders take the raw struct:

    • handler_federation.go:532 renders templates/azure-wif-bicep-deploy.sh.tmpl with raw data. This is a shell script written into the Azure bicep/ARM zip, and it is the one deploy script in the codebase that does not get the shellEscapeData treatment its siblings get. Most likely an oversight rather than a decision.
    • handler_federation.go:592 renders the .auto.tfvars templates with raw data. Here escaping is not the right fix: shell escaping is wrong for HCL. HCL string values need their own treatment (", \, and ${/%{ interpolation sequences), or the values need validating at the boundary the way sec(iac/aws): require OIDC subject claim in the federation bundle generator cloud-commitments-cli#1691 now validates --oidc-subject-claim.

    Same caveat as LeanerCloud/cloud-commitments-cli#2: the fields are not attacker-controlled from the request today, so this is hardening, not an open hole.


    Suggested shape for whoever picks this up

    1. Add the four missing fields to iacData and wire them from flags (--cudly-api-url, --contact-email, --source-account-id); derive OIDCIssuerHost from OIDCIssuerURL as the server does.
    2. Add a test asserting iacData and federationIaCData stay field-compatible (reflection over field names is enough) so this cannot drift again silently. Better still, render every template in internal/iacfiles/templates with Option("missingkey=error") against iacData in a test, which catches a missing field the moment a template starts using it.
    3. Decide per sink rather than per struct: shell templates get shellEscapeData (with } added to shellEscape), HCL templates get an HCL-safe path or boundary validation, JSON templates go through encoding/json rather than string interpolation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions