`CKV_GCP_17` crashes on nested dynamic `default_key_specs`
Checkov version
3.3.16
The affected implementation is also present on the current main branch:
Summary
Checkov CKV_GCP_17 can crash when scanning a google_dns_managed_zone whose dnssec_config.default_key_specs is generated by a nested Terraform dynamic block.
The check is intended to reject Cloud DNS DNSSEC key specifications that use the insecure rsasha1 algorithm. The Terraform configuration expresses dnssec_config and its nested default_key_specs block dynamically. Checkov partially expands that configuration, but at least one resulting default_key_specs entry is a scalar placeholder rather than a dictionary. CKV_GCP_17 assumes every entry is a dictionary and indexes it with default_key_specs["algorithm"], raising:
TypeError: string indices must be integers, not 'str'Checkov logs the policy exception but can still return a successful process status, leaving the affected resource without a result from CKV_GCP_17.
Affected Check
- Check ID:
CKV_GCP_17 - Name:
Ensure that RSASHA1 is not used for the zone-signing and key-signing keys in Cloud DNS DNSSEC - Public implementation:
GoogleCloudDNSKeySpecsRSASHA1.py
Create main.tf with this minimal synthetic reproduction:
variable "dnssec_key_spec" {
type = object({
algorithm = optional(string)
key_length = optional(number)
key_type = optional(string)
})
}
resource "google_dns_managed_zone" "example" {
name = "example-zone"
dns_name = "example.com."
dynamic "dnssec_config" {
for_each = [var.dnssec_key_spec]
content {
dynamic "default_key_specs" {
for_each = [dnssec_config.value]
content {
algorithm = default_key_specs.value.algorithm
key_length = default_key_specs.value.key_length
key_type = default_key_specs.value.key_type
}
}
}
}
}Run:
checkov --directory . --check CKV_GCP_17Actual behavior
The current check contains this logic:
if "default_key_specs" in dnssec_config:
for default_key_specs in dnssec_config["default_key_specs"]:
if "algorithm" in default_key_specs and default_key_specs["algorithm"] == ["rsasha1"]:
return CheckResult.FAILEDFor nested dynamic content, Checkov can supply a string placeholder in dnssec_config["default_key_specs"]. The membership test succeeds when that string contains algorithm, after which the dictionary lookup raises TypeError: string indices must be integers, not 'str'.
The check therefore produces no PASSED, FAILED, or UNKNOWN result for the resource.
Expected behavior
The check must not raise an exception.
- Return
FAILEDif any resolved key specification definitely usesrsasha1. - Return
UNKNOWNif no definite violation is found but one or more relevant values cannot be resolved or have an unexpected shape. - Return
PASSEDonly when all relevant values can be inspected and none usesrsasha1.
Impact
The policy exception can leave the resource unevaluated while the Checkov process exits successfully, creating a false-negative risk.
Similar public issues and fixes
Issue #7571 and PR #7581
- Issue:
CKV_AWS_70crashes on mixedconcat([dict], var.list)Statement shape - Fix: PR #7581
- Exception:
TypeError: string indices must be integers, not 'str'
An unresolved Terraform value appeared as a string among dictionary entries. The merged fix added a policy-level guard and a Terraform regression fixture:
for statement in statements:
if not isinstance(statement, dict):
continueFor CKV_GCP_17, skipping unresolved entries could return PASSED without evaluating every possible algorithm.
Issue #6571 and commit 70a1306
Dynamic WAF blocks produced placeholder values of unexpected types. The affected checks added policy-level shape validation. One returns UNKNOWN when it cannot inspect the statement:
if not isinstance(statement, list) or not isinstance(statement[0], dict):
return CheckResult.UNKNOWNAnother skips non-dictionary elements before calling .get():
for statement in statements:
if not isinstance(statement, dict):
continueReturning UNKNOWN distinguishes an unresolved result from a confirmed pass.
Issue #488 and PR #489
The affected Azure policy conservatively returned UNKNOWN whenever dynamic content was present:
if "dynamic" in conf:
return CheckResult.UNKNOWNCKV_GCP_17 can evaluate resolved entries first, then return UNKNOWN only when unresolved entries prevent a definitive result.
Issue #1337 and PR #1712
- Issue: GKE checks can crash with dynamic content blocks
- Related fix: PR #1712
PR #1712 recursively processes nested dynamic blocks but does not guarantee the final value shape expected by each policy.
Issue #5641 and PR #5642
The parser previously assumed any dynamic field was an HCL dynamic-block list. The fix verifies the field type before iterating it:
dynamic_conf = conf.get("dynamic", {})
if not isinstance(dynamic_conf, list):
return FalseThis guard does not address values inside an expanded default_key_specs list.
Suggested fix
Add shape validation to GoogleCloudDNSKeySpecsRSASHA1.scan_resource_conf():
The implementation should:
- Validate that
dnssec_configis a dictionary before inspecting it. - Validate that
default_key_specsis a list before iterating it. - Track whether any entry is unresolved or has an unexpected type.
- Continue inspecting all dictionary entries so a definite
rsasha1value still fails. - Return
UNKNOWNif no definite failure exists but at least one entry could not be evaluated. - Return
PASSEDonly when all relevant entries are inspectable and none usersasha1.
A suitable implementation shape is:
def scan_resource_conf(self, conf):
dnssec_configs = conf.get("dnssec_config")
if not isinstance(dnssec_configs, list) or not dnssec_configs:
return CheckResult.PASSED
dnssec_config = dnssec_configs[0]
self.evaluated_keys = ["dnssec_config"]
if not isinstance(dnssec_config, dict):
return CheckResult.UNKNOWN
default_key_specs = dnssec_config.get("default_key_specs")
if default_key_specs is None:
return CheckResult.PASSED
if not isinstance(default_key_specs, list):
return CheckResult.UNKNOWN
has_unknown_key_specs = False
for index, key_specs in enumerate(default_key_specs):
if not isinstance(key_specs, dict):
has_unknown_key_specs = True
continue
algorithm = key_specs.get("algorithm")
if algorithm == ["rsasha1"]:
self.evaluated_keys = [
f"dnssec_config/[0]/default_key_specs/[{index}]/algorithm"
]
return CheckResult.FAILED
if algorithm is not None and not isinstance(algorithm, list):
has_unknown_key_specs = True
self.evaluated_keys = ["dnssec_config/[0]/default_key_specs"]
if has_unknown_key_specs:
return CheckResult.UNKNOWN
return CheckResult.PASSEDAn absent algorithm preserves the existing PASSED behavior because Google Cloud DNS defaults to RSASHA256. An unexpected representation of a present algorithm returns UNKNOWN.
Result semantics
The PR #7581 guard prevents the exception:
if not isinstance(key_specs, dict):
continueFor this check, an unresolved input may evaluate to rsasha1. Return UNKNOWN instead of skipping it and reporting PASSED. A resolved rsasha1 entry still takes precedence and returns FAILED.
Regression coverage
Add unit and Terraform-fixture coverage for:
- Literal
rsasha1key specification returnsFAILED. - Literal
rsasha256key specification returnsPASSED. - Missing
default_key_specsretains the existingPASSEDbehavior because the provider default is RSASHA256. - A dynamic
default_key_specsblock with an unresolved algorithm does not crash and returnsUNKNOWN. - A list containing a resolved
rsasha1dictionary and an unresolved placeholder returnsFAILED. - A list containing a safe dictionary and an unresolved placeholder returns
UNKNOWN. - An unexpected non-list
default_key_specsrepresentation returnsUNKNOWN. - An unexpected non-dictionary
dnssec_configrepresentation returnsUNKNOWN.
The Terraform fixture should use the synthetic nested dynamic configuration above so the test covers Checkov's parser output.
Source: bridgecrewio/checkov