# cfn-lint E1020: catch CloudFormation reference errors early

> **TL;DR:** cfn-lint catches a broken CloudFormation `Ref` before deployment: it reports rule E1020 and exits with code 2. You can reproduce it from two public commits without an AWS account, fix the reference, and run cfn-lint on every pull request. A clean lint run proves the references resolve, not that the stack is safe or will deploy.

*By Prateek Singh, founder of Ganak AI Labs. Updated 6 October 2026.*

A one-line change to an Auto Scaling group swaps `!Ref PrivateSubnetB` for `!Ref MissingPrivateSubnetB`. The YAML is valid, the diff looks harmless, and in a busy review it gets approved.

The mistake only shows up later, when CloudFormation tries to resolve a resource that doesn't exist.

This kind of error is cheap to catch before anything touches AWS. Here's how to reproduce it with cfn-lint from two public commits, fix it, gate pull requests on it, and see what lint can't tell you.

*Disclosure: I'm the founder of Cloudeval.*

**Key terms used below**

- **CloudFormation:** AWS's infrastructure-as-code service, using JSON or YAML templates.
- **`Ref`:** the intrinsic function that returns the value of a parameter or resource declared in the same template.
- **cfn-lint:** the AWS CloudFormation Linter. It validates templates locally against CloudFormation's resource specifications.
- **E1020:** the cfn-lint rule for `Ref` validation.

## What is a CloudFormation reference error?

A reference error is a `Ref` (or a similar intrinsic) that names something the template doesn't declare.

In the example below, the Auto Scaling group's `VPCZoneIdentifier` points to `MissingPrivateSubnetB`. That isn't a resource or a parameter in the file.

This is different from an architecture concern. Whether two private subnets give you enough availability, or a security group exposes the workload, are review questions. A broken `Ref` is simply wrong, and a linter can prove it.

## How do I catch a broken Ref before deployment?

1. Install a pinned cfn-lint version.
2. Lint the known-good baseline and the regression.
3. Fix the reference and lint again.
4. Run cfn-lint on every pull request so it fails the check.
5. Add a review step for what lint doesn't judge.

## Step 1: Install a pinned cfn-lint

Pin the version so teammates and CI get the same result you do:

```bash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install cfn-lint==1.52.1
cfn-lint --version   # cfn-lint 1.52.1
```

On Windows, activate with `.venv\Scripts\Activate.ps1`. The commands below use a POSIX shell.

## Step 2: Lint the baseline and the regression

The [public example repository](https://github.com/ganakailabs/cloudeval-aws-cloudformation-review-example) keeps both versions at fixed commits, so this works even if branches move:

```bash
curl -fsSLo baseline.yaml https://raw.githubusercontent.com/ganakailabs/cloudeval-aws-cloudformation-review-example/c3c867bb6d587482a47be1dc6bd8eae798a37844/templates/webapp.yaml
curl -fsSLo regression.yaml https://raw.githubusercontent.com/ganakailabs/cloudeval-aws-cloudformation-review-example/e18247c84c34944428141841f54dd3075e657b30/templates/webapp.yaml

cfn-lint baseline.yaml;   echo "exit=$?"
cfn-lint regression.yaml; echo "exit=$?"
```

Here's the output, with the list of valid names shortened:

```text
exit=0
E1020 'MissingPrivateSubnetB' is not one of ['EnvironmentName', 'LatestAmiId', ..., 'PrivateSubnetA', 'PrivateSubnetB', ..., 'AWS::StackName']
regression.yaml:338:7

exit=2
```

The baseline passes. The regression fails with E1020 at line 338.

cfn-lint lists every name the `Ref` could have used, which makes the typo obvious. The [source diff](https://github.com/ganakailabs/cloudeval-aws-cloudformation-review-example/pull/3/files) is a single line:

```diff
 VPCZoneIdentifier:
   - !Ref PrivateSubnetA
-  - !Ref PrivateSubnetB
+  - !Ref MissingPrivateSubnetB
```

## Step 3: Fix the reference and lint again

```bash
sed 's/!Ref MissingPrivateSubnetB/!Ref PrivateSubnetB/' regression.yaml > corrected.yaml
cfn-lint corrected.yaml; echo "exit=$?"   # exit=0
diff baseline.yaml corrected.yaml && echo "identical to baseline"
```

The corrected file passes and matches the baseline byte for byte. That closes this error. It doesn't close the review.

| Input | cfn-lint result | What it shows |
|---|---|---|
| Baseline commit `c3c867bb6d58` | Exit 0, no findings | The baseline passes this pinned checker |
| Regression commit `e18247c84c34` | Exit 2, E1020 | The changed `Ref` doesn't name a declared resource or parameter |
| Corrected regression | Exit 0, no findings | Restoring `PrivateSubnetB` removes the error |

## Step 4: Run cfn-lint on every pull request

cfn-lint's exit codes are built for CI. 0 means no issues, 2 an error, 4 a warning and 8 an informational finding, and combinations add up bitwise. Any non-zero exit fails the job.

A minimal GitHub Actions job:

```yaml
name: cfn-lint
on: pull_request
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install cfn-lint==1.52.1
      - run: cfn-lint "templates/**/*.yaml"
```

Quoting the glob lets cfn-lint expand it. If only errors should fail the build, `--non-zero-exit-code error` changes the threshold.

Want to catch it before the push? cfn-lint ships a [pre-commit](https://github.com/aws-cloudformation/cfn-lint#pre-commit) hook, and its README links a GitHub Action and plugins for VS Code and IntelliJ.

Make the job a required status check in branch protection, and a broken `Ref` can't be merged.

## Step 5: Add a review step for what lint doesn't judge

A passing template can still have broad ingress, public access or availability assumptions that don't fit the workload. That's the job of a review step.

The example repository is wired for Cloudeval, which reviews CloudFormation JSON and YAML statically, as a beta. It doesn't connect to a live AWS account.

To try it:

1. Fork the example and install the Cloudeval GitHub App on your fork.
2. Create a Cloudeval project from the repository.
3. Add a project-scoped CI key as the `CLOUDEVAL_ACCESS_KEY` secret, plus `CLOUDEVAL_PROJECT_ID`.

You don't need AWS credentials. The [CloudFormation review walkthrough](https://docs.cloudeval.ai/guides/review/cloudformation-before-deployment?utm_source=hashnode&utm_medium=referral&utm_campaign=cfn-lint-reference-errors&utm_content=docs-walkthrough) covers the full setup.

The two screenshots below come from an Azure demo project. A CloudFormation beta review covers fewer areas.

![Cloudeval Cloud Evaluation report Action Plan tab with ranked actions, critical risk count and evidence-backed findings, shown on an Azure Playground project](https://mintcdn.com/ganakailabs-db727e50/7G2QBGQbVygbrIBs/assets/images/reports/report-tab-action-plan.png)
*The Action Plan tab ranks findings by effort and evidence.*

Three habits keep the beta review honest:

- **Check that it actually ran.** The example workflow skips the review when its secrets are missing, so a green workflow isn't proof. Open the review step and confirm there's a real result.
- **Keep sources separate.** cfn-lint findings, extra Checkov findings and Cloudeval's own AWS mappings should stay distinguishable.
- **Read "Not assessed" literally.** It means no result was established, not that the area passed.

![Cloudeval report breakdown view tracing findings across project, pillar and score health, shown on an Azure Playground project](https://mintcdn.com/ganakailabs-db727e50/7G2QBGQbVygbrIBs/assets/images/reports/report-current-breakdown.png)
*Breakdown traces a summary score back through each pillar.*

## Does a clean cfn-lint run mean the template is safe to deploy?

No. It means the template passes cfn-lint's checks, including that every `Ref` resolves.

Account permissions, service quotas, live dependencies and runtime behaviour can still break a deployment. None of them is visible in the file.

Each tool answers a different question:

| Check | Answers | Needs AWS credentials? |
|---|---|---|
| cfn-lint | Are resources, properties and references valid against the CloudFormation specs? | No |
| cfn-guard or Checkov | Does the template follow the policies your team maintains? | No |
| `aws cloudformation validate-template` | Is the template format valid, and what parameters does it take? | Yes |
| Change sets | What will change in this stack before execution? | Yes |
| Pull request review (for example, Cloudeval static beta) | Does the change add exposure, networking or deployment-quality findings, where checks apply? | No, for static review |

Run cfn-lint first because it's fast and local. Add a policy tool, review the change in the PR and create a change set before you execute.

## Frequently asked questions

### What is cfn-lint?

cfn-lint is the AWS CloudFormation Linter. It validates JSON and YAML templates locally against CloudFormation's resource specifications and reports errors, warnings and informational findings, such as E1020 for an invalid `Ref`.

### How do I use cfn-lint?

Install it with `pip install cfn-lint`, then run `cfn-lint template.yaml` or a quoted glob such as `cfn-lint "templates/**/*.yaml"`. Any non-zero exit code means it found something. Pin the version in CI so results are repeatable.

### What is the difference between validate-template and cfn-lint?

`validate-template` checks the template format and parameters through the AWS API, so it needs credentials. cfn-lint checks resources, properties and references locally against its schemas, with no AWS account.

### Do I need an AWS account for static validation?

No. A local cfn-lint check needs no AWS credentials, and neither does the example in this post. Cloudeval's static review needs a Cloudeval account and project access, but not AWS credentials.

### Does Cloudeval connect to AWS or estimate AWS cost?

No. Cloudeval's CloudFormation support is a static beta. It reviews the supplied template and doesn't read live AWS state or estimate AWS cost.

## Takeaway

A broken `Ref` is the cheapest CloudFormation bug you'll ever fix, as long as you catch it before deploy. Pin cfn-lint, make it a required check, and read a pass as "the references resolve", not "the change is safe".

For the wider comparison of linting, policy checks, PR review and change sets, read [How do I review CloudFormation before deployment?](https://cloudeval.ai/answers/review-cloudformation-before-deployment?utm_source=hashnode&utm_medium=referral&utm_campaign=cfn-lint-reference-errors&utm_content=canonical)

**Related on cloudeval.ai**

- [What is IaC review?](https://cloudeval.ai/glossary/iac-review?utm_source=hashnode&utm_medium=referral&utm_campaign=cfn-lint-reference-errors&utm_content=related-glossary)
- [How do I export a cloud review with its evidence?](https://cloudeval.ai/answers/export-cloud-review-evidence?utm_source=hashnode&utm_medium=referral&utm_campaign=cfn-lint-reference-errors&utm_content=related-export)
- [Cloudeval AI sample reports](https://cloudeval.ai/sample-reports?utm_source=hashnode&utm_medium=referral&utm_campaign=cfn-lint-reference-errors&utm_content=related-samples)

> **Try it**
>
> Run the two `curl` commands and `cfn-lint` above. It takes about a minute. Then fork the example repository and follow the [CloudFormation review walkthrough](https://docs.cloudeval.ai/guides/review/cloudformation-before-deployment?utm_source=hashnode&utm_medium=referral&utm_campaign=cfn-lint-reference-errors&utm_content=try-docs) to add the static beta review in the [Cloudeval app](https://cloudeval.ai/app?utm_source=hashnode&utm_medium=referral&utm_campaign=cfn-lint-reference-errors&utm_content=try-app).

> **About the author**
>
> **Prateek Singh** is the founder of Ganak AI Labs and builds Cloudeval AI.
>
> [cloudeval.ai](https://cloudeval.ai/?utm_source=hashnode&utm_medium=referral&utm_campaign=cfn-lint-reference-errors&utm_content=author) | [Docs](https://docs.cloudeval.ai/?utm_source=hashnode&utm_medium=referral&utm_campaign=cfn-lint-reference-errors&utm_content=author) | [YouTube](https://www.youtube.com/@GanakAILabs) | [GitHub](https://github.com/ganakailabs) | [LinkedIn](https://www.linkedin.com/in/prateekkumarsingh/)

