Skip to main content
Security misconfigurations in cloud infrastructure are the most common source of breaches — public S3 buckets, unencrypted RDS instances, IAM policies with wildcard actions, security groups that expose SSH or RDP to the internet. ubx ships with 8 built-in security rules that check for exactly these patterns, evaluated at ubx scan time without cloud credentials, without Checkov, without a separate security scanner. Each rule checks a specific pattern in your .iac resource attributes — the same attributes you already declared for provisioning. This tutorial shows a stack deliberately written to pass all 8 rules, with inline comments explaining what each rule checks and why the attribute values shown satisfy it. Use it as a reference when your own ubx scan surfaces violations.

What you’ll learn

  • The 8 built-in security rules and what each checks
  • How to write .iac resources that satisfy each rule
  • How ubx scan differs from ubx validate — and when to run each

Why this matters

ubx scan runs at validate time — no cloud credentials, no network, no apply. Every security rule violation is caught before CI even gets to plan. A stack that doesn’t pass ubx scan is a stack with known security misconfigurations, not a hypothetical risk.

The source code

# S3_PUBLIC_BUCKET: no public-read or public-read-write acl → PASS
# S3_VERSIONING_DISABLED: versioning.enabled = true → PASS
unit "aws_s3_bucket_v2" "assets" {
  bucket = "myapp-${input.env}-assets"
  versioning = {
    enabled = true
  }
}

unit "aws_s3_bucket_v2" "logs" {
  bucket = "myapp-${input.env}-logs"
  versioning = {
    enabled = true
  }
}

# RDS_ENCRYPTION_DISABLED: storage_encrypted = true → PASS
# RDS_BACKUP_RETENTION:    backup_retention_period >= 7 → PASS
# RDS_PUBLICLY_ACCESSIBLE: publicly_accessible not set (defaults false) → PASS
unit "aws_rds_instance" "db" {
  engine                  = "postgres"
  instance_class          = "db.t3.micro"
  identifier              = "myapp-${input.env}-db"
  storage_encrypted       = true
  backup_retention_period = 7
}

# IAM_WILDCARD_ACTION: actions are scoped ("s3:GetObject"), not "*" → PASS
unit "aws_iam_policy" "app_policy" {
  name   = "myapp-${input.env}-policy"
  policy = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":[\"s3:GetObject\",\"s3:PutObject\"],\"Resource\":\"*\"}]}"
}

# SG_OPEN_INGRESS: ingress on port 443 only (not 22/SSH or 3389/RDP) → PASS
unit "aws_ec2_security_group" "web" {
  name        = "myapp-${input.env}-web-sg"
  description = "Allow HTTPS inbound"
  ingress = {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# EKS_PUBLIC_ENDPOINT: public access enabled BUT private access also enabled → PASS
# (both must be true to pass; public-only endpoint fails this rule)
unit "aws_eks_cluster" "main" {
  name     = "myapp-${input.env}-cluster"
  role_arn = "arn:aws:iam::123456789012:role/eks-cluster"
  vpc_config = {
    subnet_ids              = ["subnet-aaa", "subnet-bbb"]
    endpoint_public_access  = true
    endpoint_private_access = true
  }
}

The 8 built-in security rules

Rule IDResourceWhat it checksFailing condition
S3_PUBLIC_BUCKETaws_s3_bucket_v2ACL is not publicacl = "public-read" or "public-read-write"
S3_VERSIONING_DISABLEDaws_s3_bucket_v2Versioning is enabledversioning.enabled != true
RDS_ENCRYPTION_DISABLEDaws_rds_instanceStorage is encryptedstorage_encrypted != true
RDS_BACKUP_RETENTIONaws_rds_instanceBackup retention ≥ 7 daysbackup_retention_period < 7
RDS_PUBLICLY_ACCESSIBLEaws_rds_instanceNot publicly accessiblepublicly_accessible = true
IAM_WILDCARD_ACTIONaws_iam_policyNo wildcard * actionAction = "*" in policy JSON
SG_OPEN_INGRESSaws_ec2_security_groupNo open SSH/RDP ingressPort 22 or 3389 open to 0.0.0.0/0
EKS_PUBLIC_ENDPOINTaws_eks_clusterPrivate endpoint enabledendpoint_public_access = true AND endpoint_private_access = false

How it works

1

ubx scan evaluates rules against resolved attribute values

Each rule is evaluated against the .iac attribute map — the same values used for type-checking. Rules check literal values only; Pending<T> attributes (resource outputs) are excluded from scan evaluation since their values aren’t known at compile time.
2

Severity filtering controls which violations are shown

severity = "high" in the scan { } config means only high-severity rules are checked. The 8 built-in rules are all high severity. Lower-severity rules (informational, best-practice) are checked when severity = "medium" or "low".
3

A clean scan produces no output

When all rules pass, ubx scan outputs ✓ scan OK with no violation messages. This is the expected state in CI — a clean scan means zero known misconfigurations at the attribute level.

Common mistakes

backup_retention_period = 0 disables automated backups entirely and fails RDS_BACKUP_RETENTION. The minimum passing value is 7. Setting backup_retention_period = 1 for dev is common but will fail the scan unless you lower the scan severity or add an exception for dev stacks.
Run ubx scan separately from ubx validate in CI — ubx validate checks types and syntax, ubx scan checks security. Both should pass before a PR is mergeable. Add both as separate CI steps with different failure messages so developers know immediately whether they have a type error or a security misconfiguration.

Run it

ubx validate           # type-checks only — scan rules not evaluated
ubx scan               # evaluates all 8 built-in security rules
ubx scan --severity low  # includes lower-severity rules

What you learned

ubx ships with 8 built-in security rules covering S3, RDS, IAM, security groups, and EKS
ubx scan runs at validate time — no cloud credentials, no network access required
A clean scan outputs ✓ scan OK — zero violations means zero known misconfigurations

Next steps

Shared apply history

Track every apply across your team with ubx history

policy as code

Write custom security rules beyond the 8 built-ins