#7678·checkov

`CKV_GCP_17` crashes on nested dynamic `default_key_specs`

Author: dogmatic69Created Sep 9, 2026Updated Sep 9, 2026
Labelscrash

Checkov version

3.3.16

The affected implementation is also present on the current main branch:

https://github.com/bridgecrewio/checkov/blob/main/checkov/terraform/checks/resource/gcp/GoogleCloudDNSKeySpecsRSASHA1.py

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:

hcl
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:

bash
checkov --directory . --check CKV_GCP_17

Actual behavior

The current check contains this logic:

python
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.FAILED

For 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 FAILED if any resolved key specification definitely uses rsasha1.
  • Return UNKNOWN if no definite violation is found but one or more relevant values cannot be resolved or have an unexpected shape.
  • Return PASSED only when all relevant values can be inspected and none uses rsasha1.

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

An unresolved Terraform value appeared as a string among dictionary entries. The merged fix added a policy-level guard and a Terraform regression fixture:

python
for statement in statements:
    if not isinstance(statement, dict):
        continue

For 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:

python
if not isinstance(statement, list) or not isinstance(statement[0], dict):
    return CheckResult.UNKNOWN

Another skips non-dictionary elements before calling .get():

python
for statement in statements:
    if not isinstance(statement, dict):
        continue

Returning 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:

python
if "dynamic" in conf:
    return CheckResult.UNKNOWN

CKV_GCP_17 can evaluate resolved entries first, then return UNKNOWN only when unresolved entries prevent a definitive result.

Issue #1337 and 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:

python
dynamic_conf = conf.get("dynamic", {})

if not isinstance(dynamic_conf, list):
    return False

This 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:

  1. Validate that dnssec_config is a dictionary before inspecting it.
  2. Validate that default_key_specs is a list before iterating it.
  3. Track whether any entry is unresolved or has an unexpected type.
  4. Continue inspecting all dictionary entries so a definite rsasha1 value still fails.
  5. Return UNKNOWN if no definite failure exists but at least one entry could not be evaluated.
  6. Return PASSED only when all relevant entries are inspectable and none use rsasha1.

A suitable implementation shape is:

python
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.PASSED

An 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:

python
if not isinstance(key_specs, dict):
    continue

For 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:

  1. Literal rsasha1 key specification returns FAILED.
  2. Literal rsasha256 key specification returns PASSED.
  3. Missing default_key_specs retains the existing PASSED behavior because the provider default is RSASHA256.
  4. A dynamic default_key_specs block with an unresolved algorithm does not crash and returns UNKNOWN.
  5. A list containing a resolved rsasha1 dictionary and an unresolved placeholder returns FAILED.
  6. A list containing a safe dictionary and an unresolved placeholder returns UNKNOWN.
  7. An unexpected non-list default_key_specs representation returns UNKNOWN.
  8. An unexpected non-dictionary dnssec_config representation returns UNKNOWN.

The Terraform fixture should use the synthetic nested dynamic configuration above so the test covers Checkov's parser output.