Private, on-device Terraform security review for Chrome
Generated from the scanner rule registry (src/lib/scanner/rules.ts) by
scripts/generate-rules-doc.ts. Do not edit by hand: a test fails whenever this
file is out of sync with the registry, and npm run docs:rules regenerates it.
52 rules: 9 critical, 17 high, 19 medium, 7 low.
| Rule | Severity | Title |
|---|---|---|
| FR-AWS-APIGW-001 | low | API Gateway stage has no access logging |
| FR-AWS-CLOUDFRONT-001 | medium | CloudFront distribution accepts TLS below 1.2 |
| FR-AWS-CLOUDTRAIL-001 | medium | CloudTrail trail does not cover every region |
| FR-AWS-CLOUDWATCH-001 | low | CloudWatch log group has no retention period |
| FR-AWS-CRED-001 | critical | Hardcoded credentials in provider block |
| FR-AWS-DYNAMODB-001 | medium | DynamoDB table has no point-in-time recovery |
| FR-AWS-EBS-001 | high | EBS volume is not encrypted at rest |
| FR-AWS-EC2-001 | medium | EC2 instance is given a public IP address |
| FR-AWS-EC2-002 | high | EC2 instance metadata service still answers unauthenticated (IMDSv1) requests |
| FR-AWS-ECR-001 | medium | ECR repository does not scan images on push |
| FR-AWS-EFS-001 | high | EFS file system is not encrypted at rest |
| FR-AWS-EKS-001 | high | EKS cluster API endpoint is open to the internet |
| FR-AWS-ELB-001 | high | Load balancer listener accepts plaintext HTTP |
| FR-AWS-ELB-002 | low | Load balancer has no access logging |
| FR-AWS-IAM-001 | critical | IAM policy grants wildcard action on wildcard resource |
| FR-AWS-IAM-002 | high | IAM policy grants wildcard actions |
| FR-AWS-IAM-003 | critical | IAM role trust policy lets any AWS account assume the role |
| FR-AWS-IAM-004 | high | Long-lived IAM access key is created in Terraform |
| FR-AWS-KMS-001 | medium | KMS key does not have automatic rotation enabled |
| FR-AWS-LAMBDA-001 | high | Lambda function URL requires no authentication |
| FR-AWS-RDS-001 | critical | RDS instance is publicly accessible |
| FR-AWS-RDS-002 | high | RDS storage is not encrypted at rest |
| FR-AWS-RDS-003 | medium | RDS instance has no backup retention configured |
| FR-AWS-RDS-004 | medium | Database has no deletion protection |
| FR-AWS-S3-001 | critical | S3 bucket ACL grants public access |
| FR-AWS-S3-002 | medium | S3 bucket has no versioning configured |
| FR-AWS-S3-003 | low | S3 bucket has no access logging configured |
| FR-AWS-S3-004 | medium | S3 bucket has no server-side encryption configuration |
| FR-AWS-S3-005 | high | S3 bucket has no effective public access block |
| FR-AWS-S3-006 | low | Versioned S3 bucket never expires noncurrent versions |
| FR-AWS-SG-001 | high | Security group allows ingress from the entire internet |
| FR-AWS-SG-002 | medium | Security group allows unrestricted egress to the internet |
| FR-AWS-SG-003 | medium | Default security group is left with traffic rules |
| FR-AWS-SNS-001 | medium | SNS topic is not encrypted at rest |
| FR-AWS-SQS-001 | medium | SQS queue has no server-side encryption configured |
| FR-AZURE-KV-001 | medium | Key vault can be permanently deleted |
| FR-AZURE-NSG-001 | critical | Network security group exposes an admin port to the internet |
| FR-AZURE-SQL-001 | high | Azure database server is reachable from the public internet |
| FR-AZURE-STORAGE-001 | critical | Azure storage container allows anonymous public access |
| FR-AZURE-STORAGE-002 | high | Azure storage account accepts plaintext HTTP |
| FR-AZURE-VM-001 | medium | Virtual machine’s managed disks are not encrypted at host |
| FR-GCP-COMPUTE-001 | medium | Compute Engine instance is given a public IP address |
| FR-GCP-FIREWALL-001 | critical | Firewall rule exposes an admin port to the internet |
| FR-GCP-IAM-001 | high | Service account key is created in Terraform |
| FR-GCP-SQL-001 | high | Cloud SQL instance has a public IP address |
| FR-GCP-STORAGE-001 | high | GCS bucket IAM grants public access (allUsers/allAuthenticatedUsers) |
| FR-GCP-STORAGE-002 | medium | GCS bucket still allows per-object ACLs |
| FR-SECRET-001 | critical | Plaintext secret embedded in resource configuration |
| FR-TF-001 | low | Terraform block does not pin a required_version |
| FR-TF-002 | low | Provider version is not constrained |
| FR-TF-003 | medium | local-exec provisioner runs shell commands on whoever applies the plan |
| FR-TF-004 | high | S3 state backend does not ask for encryption |
Severity: low
A stage without access logs keeps no record of which caller hit which route with what result, so abuse of the API is invisible and rate disputes come down to guesswork.
Recommendation: Add an access_log_settings block with a destination_arn and format so every request is recorded to CloudWatch.
References:
Severity: medium
TLS 1.0 and 1.1 are formally deprecated and accept ciphers with practical attacks, so a viewer connection can be negotiated down to a protocol the industry has retired.
Recommendation: Set minimum_protocol_version = “TLSv1.2_2021” in viewer_certificate, which requires a dedicated certificate rather than the default CloudFront one.
References:
Severity: medium
Activity in the regions the trail does not cover is invisible, and an unused region is exactly where an intruder prefers to work.
Recommendation: Set is_multi_region_trail = true so the trail follows every region, including ones the account does not otherwise use.
References:
Severity: low
Logs kept forever are paid for forever, and an unbounded group usually means nobody decided how far back an investigation should be able to reach.
Recommendation: Set retention_in_days to how long the logs are actually needed; unset and 0 both mean “never expire”.
References:
Severity: critical
Credentials written into a provider block are committed to version control, copied into every clone, and shared with everyone who can read the repository.
Recommendation: Remove access_key/secret_key; use environment variables, IAM roles, or a secrets manager.
References:
Severity: medium
Without point-in-time recovery a corrupting write or an accidental delete can only be undone from whatever on-demand backup happens to exist.
Recommendation: Add a point_in_time_recovery block with enabled = true so the table can be restored to any second in the retention window.
References:
Severity: high
Encryption can only be chosen when the volume is created, so an unencrypted volume stays unencrypted — as does every snapshot and image taken from it.
Recommendation: Set encrypted = true (ideally with a customer-managed KMS key).
References:
Severity: medium
A public IP exposes every port the instance’s security groups allow directly to the internet, with no load balancer or NAT gateway in between.
Recommendation: Set associate_public_ip_address = false and reach the instance through a load balancer, NAT gateway or SSM Session Manager instead.
References:
Severity: high
IMDSv1 answers any process that can make the instance issue a plain GET, so one SSRF bug in the application hands out the instance role’s credentials.
Recommendation: Add metadata_options { http_tokens = “required” }, which makes IMDS answer only a request that first obtained a token by PUT.
References:
Severity: medium
Without scan-on-push, images land in the registry carrying whatever known vulnerabilities their layers hold, and nothing ever looks at them again.
Recommendation: Add image_scanning_configuration { scan_on_push = true }, or move scanning to the registry level with an aws_ecr_registry_scanning_configuration.
References:
Severity: high
Encryption at rest can only be chosen at creation, so fixing this later means creating a second file system and copying the data across.
Recommendation: Set encrypted = true (ideally with a customer-managed KMS key).
References:
Severity: high
A public API endpoint with unrestricted CIDRs puts the cluster control plane in front of the whole internet, one credential or authentication flaw away from cluster admin.
Recommendation: Set endpoint_public_access = false and use the private endpoint, or at least restrict public_access_cidrs to trusted ranges.
References:
Severity: high
A plaintext listener hands session cookies, tokens and credentials to anyone on the path between the client and the load balancer.
Recommendation: Use protocol = “HTTPS” with an ACM certificate, or make the HTTP listener redirect to HTTPS.
References:
Severity: low
Without access logs there is no record of who sent what through the load balancer, so reconstructing an attack or an outage starts from nothing.
Recommendation: Add an access_logs block with a bucket and enabled = true so every request is recorded to S3.
References:
Severity: critical
Action “” on Resource “” is account administrator: whoever holds it can read every store, delete every log, and grant themselves more.
Recommendation: Scope Action and Resource to the minimum set the principal requires.
References:
Severity: high
A service wildcard grants every action that service has today plus every one AWS adds to it later, which is far more than the workload was reviewed for.
Recommendation: List the specific actions the principal calls instead of a service wildcard (s3:, iam:) or an Allow on NotAction; scoping Resource does not limit what may be done to it — s3:* still includes DeleteBucket and PutBucketPolicy.
References:
Severity: critical
A trust policy open to “*” lets any AWS account in the world assume the role and use every permission attached to it.
Recommendation: Name the accounts, services or federated identities that may assume this role. If cross-account access is intended, pair the principal with a Condition on sts:ExternalId or aws:PrincipalOrgID.
References:
Severity: high
A static key never expires, and its secret is written to Terraform state in plaintext — which makes the state file itself a credential store.
Recommendation: Delete the aws_iam_access_key resource and give the workload a role instead (instance profile, IRSA, or OIDC federation).
References:
Severity: medium
Without rotation one key protects the data for the whole life of the key, so a single disclosure exposes everything ever encrypted under it.
Recommendation: Set enable_key_rotation = true.
References:
Severity: high
With authorization_type “NONE” anyone who finds the URL can invoke the function: it runs with its execution role and bills to the account, unauthenticated, from anywhere.
Recommendation: Set authorization_type = “AWS_IAM”, or put the function behind an API Gateway or CloudFront layer that authenticates callers.
References:
Severity: critical
A publicly accessible instance is reachable from the whole internet, leaving the database’s own login as the only thing between it and an attacker.
Recommendation: Set publicly_accessible = false and place the instance in private subnets.
References:
Severity: high
Storage encryption cannot be turned on after creation, and it is inherited by the instance’s snapshots and read replicas — so the gap propagates.
Recommendation: Set storage_encrypted = true.
References:
Severity: medium
At zero retention there are no automated backups and no point-in-time restore, so a bad migration or a ransom event has nothing to roll back to.
Recommendation: Set backup_retention_period to at least 7 days so point-in-time recovery is possible.
References:
Severity: medium
Without deletion protection one terraform destroy, one console click or one bad automation run drops the database, and recovery becomes a restore instead of a non-event.
Recommendation: Set deletion_protection = true on databases whose data would be missed.
References:
Severity: critical
A public ACL serves every object in the bucket to anonymous callers, and bucket names are guessable enough to be found by scanners.
Recommendation: Set acl to “private” and add an aws_s3_bucket_public_access_block.
References:
Severity: medium
Without versioning an overwrite or a delete is final, including one made by ransomware or by a mistaken sync from a developer’s laptop.
Recommendation: Add an aws_s3_bucket_versioning resource with status = “Enabled”.
References:
Severity: low
With no access log there is no way to answer what was read from the bucket, which is the first question asked after an exposure.
Recommendation: Add an aws_s3_bucket_logging resource pointing at a log-archive bucket.
References:
Severity: medium
S3 applies SSE-S3 to new objects by default, but only an explicit configuration pins the algorithm and lets a customer-managed key gate the data.
Recommendation: Add an aws_s3_bucket_server_side_encryption_configuration resource; use aws:kms with a customer-managed key where the data warrants it.
References:
Severity: high
Unless all four flags are on, one future ACL or bucket policy — set by anyone with write access, or by a module — can make the bucket public.
Recommendation: Add an aws_s3_bucket_public_access_block for the bucket with block_public_acls, block_public_policy, ignore_public_acls and restrict_public_buckets all set to true.
References:
Severity: low
On a versioned bucket every overwrite and delete keeps the old copy forever, so storage grows without bound until someone purges it by hand.
Recommendation: Add an aws_s3_bucket_lifecycle_configuration rule with a noncurrent_version_expiration (or transition to a colder class) matching how long old versions are actually needed.
References:
Severity: high
A rule open to 0.0.0.0/0 puts the port in front of the internet’s continuous background scanning, which finds it within minutes of the apply.
Recommendation: Restrict cidr_blocks to specific trusted ranges; never use 0.0.0.0/0 for admin ports.
References:
Severity: medium
Unrestricted egress is what turns a foothold into a breach: it is the path a dropper downloads over and the path the data leaves by.
Recommendation: Restrict egress cidr_blocks to the destinations the workload actually needs.
References:
Severity: medium
Every network interface that names no security group lands in the default group, so rules left on it apply to resources nobody chose them for.
Recommendation: Declare aws_default_security_group with no ingress or egress blocks so the default group denies all traffic, and attach purpose-built groups instead.
References:
Severity: medium
Unlike SQS, SNS applies no encryption by default, so a topic without a KMS key buffers every message it handles in plaintext.
Recommendation: Set kms_master_key_id — alias/aws/sns at minimum, a customer-managed key where the messages warrant it.
References:
Severity: medium
SQS encrypts new queues with its own keys by default, but only an explicit setting pins that on, and only a customer-managed KMS key lets access to the messages be gated and audited.
Recommendation: Set kms_master_key_id to a customer-managed key where the messages warrant it, or sqs_managed_sse_enabled = true to pin SQS-owned-key encryption.
References:
Severity: medium
Without purge protection a deleted vault can be purged on the spot, and everything its keys encrypted is unrecoverable the moment they are gone.
Recommendation: Set purge_protection_enabled = true (with soft_delete_retention_days) so a deleted vault or key survives its retention window instead of being purged on the spot.
References:
Severity: critical
SSH and RDP reachable from the internet are the two ports credential-stuffing bots scan for first, and a VM answers them with a password prompt.
Recommendation: Set source_address_prefix to the specific ranges that need SSH/RDP, or drop the rule entirely and reach the VM through Azure Bastion or a just-in-time access policy.
References:
Severity: high
A public endpoint makes the server reachable from any network, leaving its firewall rules and its login as the only defence.
Recommendation: Set public_network_access_enabled = false and reach the server over a private endpoint or a VNet service endpoint; the setting defaults to true, so leaving it out publishes the server.
References:
Severity: critical
A container set to blob or container access serves its objects to anonymous callers over the internet, with no key or SAS token required.
Recommendation: Set container_access_type = “private”.
References:
Severity: high
With HTTPS-only off, the blobs themselves, any SAS token in the query string, and a replayable signed request all travel in cleartext.
Recommendation: Remove the flag or set it to true (https_traffic_only_enabled on azurerm 4.x, enable_https_traffic_only before it) so blobs, files and queues can only be read over TLS.
References:
Severity: medium
Server-side encryption covers the managed disk itself but not the VM host, so without encryption at host the temp disk and the disks’ caches hold plaintext on shared hardware.
Recommendation: Set encryption_at_host_enabled = true (the subscription must have the EncryptionAtHost feature registered, and the VM size must support it).
References:
Severity: medium
An access_config block is the external IP: with one, the VM is on the internet and only its firewall rules stand between it and a scanner.
Recommendation: Drop the access_config block so the instance has no external IP, and reach it through Cloud NAT for egress, an HTTPS load balancer for ingress, and IAP TCP forwarding for SSH.
References:
Severity: critical
An ingress rule reaching 22 or 3389 from 0.0.0.0/0 puts SSH or RDP in front of the internet’s continuous scanning for exactly those ports.
Recommendation: Restrict source_ranges to the ranges that need SSH/RDP, or remove the rule and use IAP TCP forwarding (which needs only 35.235.240.0/20) so the port is never reachable from the internet.
References:
Severity: high
A service account key never expires, and one generated by Terraform has its private key written into state in plaintext — so whoever can read state holds the identity for good.
Recommendation: Delete the google_service_account_key resource and use workload identity federation or an attached service account; if an external system truly needs a key, create it outside Terraform so the private key never touches state.
References:
Severity: high
A public IP makes the database reachable from any network, leaving the authorized-networks list and the password as the only defence.
Recommendation: Set settings.ip_configuration.ipv4_enabled = false and give the instance a private_network instead; ipv4_enabled defaults to true, so leaving it out puts the database on a public address.
References:
Severity: high
allUsers publishes the bucket to the internet, and allAuthenticatedUsers means any Google account in existence — not any account in this organization.
Recommendation: Remove allUsers/allAuthenticatedUsers members from the bucket IAM binding.
References:
Severity: medium
While object ACLs are live, a single object can be shared publicly without the bucket’s IAM policy ever saying so, which makes access unauditable.
Recommendation: Set uniform_bucket_level_access = true so access is decided by IAM alone.
References:
Severity: critical
A literal secret in configuration is in version control, in every clone of it, and in Terraform state — so rotating it is the only real fix.
Recommendation: Store secrets in AWS Secrets Manager / Vault and reference them; never inline literals.
References:
Severity: low
Unpinned, one configuration can be applied by a laptop, a colleague and CI with three different Terraform versions — one of which upgrades the state format.
Recommendation: Add required_version (e.g. “>= 1.9.0, < 2.0.0”) to the terraform block; any one block in the directory carrying it is enough.
References:
Severity: low
Unconstrained, terraform init takes whatever the registry serves that day, so a provider release can change what an unmodified configuration does to production.
Recommendation: Declare the provider in required_providers with a version constraint (e.g. version = “~> 5.0”), and commit the resulting lock file.
References:
Severity: medium
local-exec runs on the applying machine with its credentials and filesystem, is invisible in terraform plan, and logs its command line — secrets included.
Recommendation: Replace the provisioner with a real resource, user_data / cloud-init, or an explicit pipeline step — a failing local-exec also leaves the resource tainted.
References:
Severity: high
State records every attribute of every resource in plaintext — database passwords, generated private keys, certificate bodies — and the backend is what protects it.
Recommendation: Set encrypt = true on the backend “s3” block, or name a kms_key_id, so the state object is at least as protected as the resources it describes.
References: