In a CloudFormation template scanned by cfn-guard, what does a rule see when a property is set by !Ref or !If?
answer
- the template is source, not a deployment
- the tag parses into a data node
- comparing a map to a boolean
- read Default and AllowedValues instead
- Fn::If ships both branches
basics
~10 sIt sees the intrinsic itself, as data: a Ref or Fn::If node, not a resolved value. Nothing resolves parameters or conditions before deployment, so an equality test against a literal never matches.
solid answer
~50 scfn-nag and cfn-guard read the template file, and a template is pre-deployment source: `!Ref EnableEncryption` is parsed as the object `{"Ref": "EnableEncryption"}` and `!If [IsProd, 30, 0]` as `{"Fn::If": ["IsProd", 30, 0]}`. A rule written as `Properties.StorageEncrypted == true` therefore compares a map to a boolean and does not match, which usually means it silently reports nothing rather than failing - a false negative on exactly the property you cared about. The fixes are to decide deliberately: forbid parameterising security-critical properties so the value must be a literal; or inspect the parameter's `Default` and `AllowedValues`, which are in the same file; or require the check to hold in **both** branches of an `Fn::If`, since both branches are physically present in the template. Anything genuinely undecidable here belongs to a detective check on the deployed resource, not to a rule pretending it knows.
code
yaml · 14 linesParameters:
EnableEncryption:
Type: String
Default: "false"
AllowedValues: ["true", "false"]
Conditions:
IsProd: !Equals [!Ref EnvName, "prod"]
Resources:
Db:
Type: AWS::RDS::DBInstance
Properties:
StorageEncrypted: !Ref EnableEncryption
BackupRetentionPeriod: !If [IsProd, 30, 0]
...go deeper
Know that a template is pre-deployment source: parameters and conditions are unresolved, so the rule sees a Ref or Fn::If node rather than a value. That alone explains most surprising results.
Explain how the shorthand tags parse into data, why an equality test then quietly matches nothing, and how the parameter's Default and AllowedValues in the same file let you write a rule that is still meaningful.
Show you decide fail-closed or fail-open deliberately per property, that you require both Fn::If branches to hold, and that you move genuinely undecidable checks to a detective control instead of pretending the template answers them.
Own the scoping call: which small set of properties may never be parameterised, and what you tell teams whose templates are legitimately parameter-driven. The cost of a strict rule here is paid by every stack author.
## The artifact is source, not a plan A CloudFormation template is the input a template scanner has, and it is the *source* of a stack, not a description of what a deployment will do. There is no plan file listing resolved attributes. Parameters have not been supplied. Conditions have not been evaluated. Cross-resource references have not been resolved. Everything a rule reads is therefore either a literal that the author wrote, or an **intrinsic function node** standing in for a value that will exist later. In parsed form the shorthand tags become ordinary data: | in YAML | as parsed data | | --- | --- | | `!Ref EnableEncryption` | `{"Ref": "EnableEncryption"}` | | `!Sub "${AWS::StackName}-db"` | `{"Fn::Sub": "${AWS::StackName}-db"}` | | `!If [IsProd, 30, 0]` | `{"Fn::If": ["IsProd", 30, 0]}` | | `!GetAtt Vpc.CidrBlock` | `{"Fn::GetAtt": ["Vpc", "CidrBlock"]}` | A rule that says "this property must equal `true`" is comparing a map against a boolean. It does not match. In most rule languages a non-match at that position means the rule produces no finding. ## Two directions of wrongness **False negative (the common one).** The template says `StorageEncrypted: !Ref EnableEncryption`, the parameter's `Default` is `"false"`, and the check reports clean because the value was never a literal `true`. The gate has been fully green, all quarter, over a template that can deploy unencrypted. This is worse than having no rule, because the green run is now cited as evidence. **False positive.** The rule is written strictly - "the value must be the literal `true`" - and it blocks a template that resolves correctly in every environment. The developer's experience is a gate that refuses a change for reasons that are not about the change, and that is how a control loses the room. ## What the template still tells you The template is not silent about parameters; it is only silent about the *supplied* value. The same file carries: - **`Parameters`** with `Type`, `Default`, `AllowedValues`, `AllowedPattern`. If a boolean-ish parameter's `AllowedValues` is `["true", "false"]` and its `Default` is `"false"`, a rule can legitimately say: an unconstrained parameter is not an acceptable source for this property. If `AllowedValues` is `["true"]`, the parameterised value is provably safe. - **`Conditions`**, with their definitions. `Fn::If` embeds *both* branches literally, so a rule can require that the property satisfies the check in each branch - a genuinely stronger statement than checking a resolved value, because it holds for every condition outcome. - **`Fn::Sub` / `Fn::Join`** string composition, where a partial check on the literal segments is sometimes enough (a prefix, a suffix) even though the whole string is unknown. ## The design choices, stated plainly 1. **Require a literal.** For a small set of security-critical properties, forbid parameterisation outright: the property must be written in the template. Cheap, decidable, and unpopular - so scope it narrowly and say which properties and why. 2. **Rule over the parameter contract.** Accept an intrinsic, but demand the referenced parameter constrain itself with `AllowedValues` (or a `Default` that satisfies the control). Now the rule is about the template's own promises. 3. **Rule over every branch.** For `Fn::If`, require both branches to pass. This is the technique most authors miss. 4. **Defer.** Some things - a `!GetAtt` from another resource, an `!ImportValue` from another stack, a `TemplateURL` pointing at a nested stack whose body is not in this file at all - simply are not knowable from this document. Say so, and put a detective check on the deployed resource instead of a preventive rule that guesses. ## Fail closed or fail open? That is the decision the whole leaf turns on, and it should be explicit rather than accidental. Today, most rules fail open by construction: an unmatched comparison produces nothing. If a property matters, write the rule so an unresolvable value is a finding in its own right - "this property is set from a parameter with no `AllowedValues`" - rather than letting silence stand for compliance. Whichever you pick, the wrong outcome is the one nobody chose: a rule that is neither preventing nor detecting, whose green result is being read as assurance. cfn-nag and cfn-guard both live at this layer. cfn-nag ships a library of built-in rules and separates failing violations from warnings; cfn-guard evaluates rules you write in its own declarative language against the template's structure. Neither of them deploys anything, and neither of them can resolve a parameter you have not supplied - so both inherit exactly this limitation, and both let you write around it once you know it is there.
- How would you write the encryption rule so the template above is a finding rather than a silent pass?Match the property whatever its shape: if it is the literal `true`, pass; if it is a `Ref`, look up that parameter in the same template and require `AllowedValues` to contain only acceptable values; anything else - another intrinsic, an import - is a finding with a message saying the value is not decidable here. The key change is that an unresolvable value produces output instead of nothing.
- The property is set by Fn::If. What is the correct rule?Require the check to hold on both branches, because both are literally present in the template and either can be the deployed value. That is stronger than testing a resolved value from one environment. If one branch is itself another intrinsic, the same recursion applies and eventually you either reach a literal or declare it undecidable.
- A property comes from Fn::ImportValue or a nested stack's TemplateURL. What do you do?Accept that this document cannot answer it. The exported value lives in another stack and the nested template is not in this file, so a preventive rule here would be guessing. Record it as out of scope for the template gate and cover it with a detective check against the deployed resource.
saying these in an interview costs you the question
- Believes the scanner resolves parameters before evaluating
- Writes equality against a literal and calls the property covered
- Reads a clean report as proof the stack deploys encrypted
- Ignores Parameters Default and AllowedValues in the same file
- Checks only one branch of an Fn::If
- Blocks every parameterised property rather than scoping the rule