Skip to content

Commit facc8bb

Browse files
Merge pull request #289 from overmindtech/implement-opa-policy-enforcement
Implement opa policy enforcement
2 parents 9e0f49a + b2f133d commit facc8bb

File tree

4 files changed

+414
-0
lines changed

4 files changed

+414
-0
lines changed

.github/workflows/automatic.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,42 @@ jobs:
1616
id: fmt
1717
run: terraform fmt -check -diff
1818

19+
policy-checks:
20+
runs-on: ubuntu-latest
21+
if: github.event.action != 'closed'
22+
needs: execute
23+
permissions:
24+
contents: read
25+
id-token: write
26+
pull-requests: write
27+
steps:
28+
- uses: actions/checkout@v4
29+
30+
- name: Terraform Init (for policies)
31+
uses: ./.github/actions/terraform_init/
32+
with:
33+
terraform_deploy_role: ${{ vars.TERRAFORM_DEPLOY_ROLE }}
34+
35+
- name: Download terraform plan
36+
uses: actions/download-artifact@v4
37+
with:
38+
name: tfplan
39+
40+
- name: Convert plan to JSON
41+
run: |
42+
terraform show -json tfplan > tfplan.json
43+
44+
- uses: overmindtech/policy-signals-action@v1
45+
with:
46+
policies-path: './policies'
47+
terraform-plan-json: './tfplan.json'
48+
overmind-api-key: ${{ secrets.OVM_API_KEY }}
49+
ticket-link: ${{ needs.execute.outputs.run-url }}
50+
1951
execute:
2052
runs-on: ubuntu-latest
53+
outputs:
54+
run-url: ${{ steps.submit-plan.outputs.run-url }}
2155
permissions:
2256
contents: read # required for checkout
2357
id-token: write # mint AWS credentials through OIDC

policies/cost-control.rego

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package main
2+
3+
# Cost Control Policy
4+
# Checks for expensive instance types and configurations
5+
6+
# Get all EC2 instances from terraform plan
7+
ec2_instances[instance] {
8+
instance := input.resource_changes[_]
9+
instance.type == "aws_instance"
10+
}
11+
12+
# Get all RDS instances from terraform plan
13+
rds_instances[instance] {
14+
instance := input.resource_changes[_]
15+
instance.type == "aws_db_instance"
16+
}
17+
18+
# Get all RDS clusters from terraform plan
19+
rds_clusters[cluster] {
20+
cluster := input.resource_changes[_]
21+
cluster.type == "aws_rds_cluster"
22+
}
23+
24+
# List of expensive EC2 instance types
25+
expensive_ec2_types := {
26+
"m5.24xlarge", "m5.16xlarge", "m5.12xlarge",
27+
"r5.24xlarge", "r5.16xlarge", "r5.12xlarge",
28+
"c5.24xlarge", "c5.18xlarge", "c5.12xlarge",
29+
"x1.32xlarge", "x1.16xlarge",
30+
"r4.16xlarge", "r4.8xlarge",
31+
"m4.16xlarge", "m4.10xlarge",
32+
"c4.8xlarge",
33+
"p3.16xlarge", "p3.8xlarge", "p3.2xlarge",
34+
"p2.16xlarge", "p2.8xlarge",
35+
"g3.16xlarge", "g3.8xlarge"
36+
}
37+
38+
# List of expensive RDS instance types
39+
expensive_rds_types := {
40+
"db.r5.24xlarge", "db.r5.16xlarge", "db.r5.12xlarge",
41+
"db.r4.16xlarge", "db.r4.8xlarge",
42+
"db.m5.24xlarge", "db.m5.16xlarge", "db.m5.12xlarge",
43+
"db.m4.16xlarge", "db.m4.10xlarge",
44+
"db.x1.32xlarge", "db.x1.16xlarge"
45+
}
46+
47+
# High-cost regions (typically more expensive than us-east-1)
48+
high_cost_regions := {
49+
"ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2",
50+
"eu-central-1", "eu-west-1", "eu-west-2", "eu-west-3",
51+
"sa-east-1"
52+
}
53+
54+
# Deny expensive EC2 instance types
55+
deny[msg] {
56+
instance := ec2_instances[_]
57+
expensive_ec2_types[instance.change.after.instance_type]
58+
msg := sprintf("EC2 instance '%s' uses expensive instance type '%s' - consider using a smaller instance type", [instance.address, instance.change.after.instance_type])
59+
}
60+
61+
# Deny expensive RDS instance types
62+
deny[msg] {
63+
instance := rds_instances[_]
64+
expensive_rds_types[instance.change.after.instance_class]
65+
msg := sprintf("RDS instance '%s' uses expensive instance type '%s' - consider using a smaller instance type", [instance.address, instance.change.after.instance_class])
66+
}
67+
68+
# Deny RDS clusters without deletion protection in production
69+
deny[msg] {
70+
cluster := rds_clusters[_]
71+
cluster.change.after.tags.Environment == "prod"
72+
not cluster.change.after.deletion_protection
73+
msg := sprintf("RDS cluster '%s' in production does not have deletion protection enabled", [cluster.address])
74+
}
75+
76+
deny[msg] {
77+
cluster := rds_clusters[_]
78+
cluster.change.after.tags.Environment == "production"
79+
not cluster.change.after.deletion_protection
80+
msg := sprintf("RDS cluster '%s' in production does not have deletion protection enabled", [cluster.address])
81+
}
82+
83+
# Warn about missing cost tracking tags
84+
warn[msg] {
85+
instance := ec2_instances[_]
86+
not instance.change.after.tags.CostCenter
87+
msg := sprintf("EC2 instance '%s' is missing 'CostCenter' tag for cost tracking", [instance.address])
88+
}
89+
90+
warn[msg] {
91+
instance := rds_instances[_]
92+
not instance.change.after.tags.CostCenter
93+
msg := sprintf("RDS instance '%s' is missing 'CostCenter' tag for cost tracking", [instance.address])
94+
}
95+
96+
# Warn about instances in high-cost regions for production workloads
97+
warn[msg] {
98+
instance := ec2_instances[_]
99+
instance.change.after.tags.Environment == "prod"
100+
provider_region := input.configuration.provider_config.aws.expressions.region.constant_value
101+
high_cost_regions[provider_region]
102+
msg := sprintf("Production EC2 instance '%s' - ensure you're using the most cost-effective region", [instance.address])
103+
}
104+
105+
warn[msg] {
106+
instance := ec2_instances[_]
107+
instance.change.after.tags.Environment == "production"
108+
provider_region := input.configuration.provider_config.aws.expressions.region.constant_value
109+
high_cost_regions[provider_region]
110+
msg := sprintf("Production EC2 instance '%s' - ensure you're using the most cost-effective region", [instance.address])
111+
}
112+
113+
# Warn about dev instances without auto-shutdown
114+
warn[msg] {
115+
instance := ec2_instances[_]
116+
instance.change.after.tags.Environment == "dev"
117+
not instance.change.after.tags.AutoShutdown
118+
msg := sprintf("Development EC2 instance '%s' should have 'AutoShutdown' tag to reduce costs", [instance.address])
119+
}
120+
121+
warn[msg] {
122+
instance := ec2_instances[_]
123+
instance.change.after.tags.Environment == "development"
124+
not instance.change.after.tags.AutoShutdown
125+
msg := sprintf("Development EC2 instance '%s' should have 'AutoShutdown' tag to reduce costs", [instance.address])
126+
}

policies/s3-security.rego

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package main
2+
3+
# S3 Security Policy
4+
# Checks for S3 buckets without proper security configurations
5+
6+
# Get all S3 bucket resources from terraform plan
7+
s3_buckets[bucket] {
8+
bucket := input.resource_changes[_]
9+
bucket.type == "aws_s3_bucket"
10+
}
11+
12+
s3_bucket_public_access_blocks[block] {
13+
block := input.resource_changes[_]
14+
block.type == "aws_s3_bucket_public_access_block"
15+
}
16+
17+
s3_bucket_encryption[encrypt] {
18+
encrypt := input.resource_changes[_]
19+
encrypt.type == "aws_s3_bucket_server_side_encryption_configuration"
20+
}
21+
22+
# Deny S3 buckets without Environment tag
23+
deny[msg] {
24+
bucket := s3_buckets[_]
25+
not bucket.change.after.tags.Environment
26+
msg := sprintf("S3 bucket '%s' is missing the required 'Environment' tag", [bucket.address])
27+
}
28+
29+
# Deny S3 buckets without Name tag
30+
deny[msg] {
31+
bucket := s3_buckets[_]
32+
not bucket.change.after.tags.Name
33+
msg := sprintf("S3 bucket '%s' is missing the required 'Name' tag", [bucket.address])
34+
}
35+
36+
# Deny S3 buckets without Owner tag
37+
deny[msg] {
38+
bucket := s3_buckets[_]
39+
not bucket.change.after.tags.Owner
40+
msg := sprintf("S3 bucket '%s' is missing the required 'Owner' tag", [bucket.address])
41+
}
42+
43+
# Deny S3 buckets without Project tag
44+
deny[msg] {
45+
bucket := s3_buckets[_]
46+
not bucket.change.after.tags.Project
47+
msg := sprintf("S3 bucket '%s' is missing the required 'Project' tag", [bucket.address])
48+
}
49+
50+
# Deny S3 buckets without encryption
51+
deny[msg] {
52+
bucket := s3_buckets[_]
53+
not has_bucket_encryption(bucket.change.after.bucket)
54+
msg := sprintf("S3 bucket '%s' does not have server-side encryption configured", [bucket.address])
55+
}
56+
57+
# Warn about S3 buckets without public access block
58+
warn[msg] {
59+
bucket := s3_buckets[_]
60+
not has_public_access_block(bucket.change.after.bucket)
61+
msg := sprintf("S3 bucket '%s' does not have public access block configured - consider adding one for security", [bucket.address])
62+
}
63+
64+
# Warn about S3 buckets that might be publicly accessible
65+
warn[msg] {
66+
bucket := s3_buckets[_]
67+
bucket.change.after.acl == "public-read"
68+
msg := sprintf("S3 bucket '%s' has public-read ACL - ensure this is intentional", [bucket.address])
69+
}
70+
71+
warn[msg] {
72+
bucket := s3_buckets[_]
73+
bucket.change.after.acl == "public-read-write"
74+
msg := sprintf("S3 bucket '%s' has public-read-write ACL - this is a security risk", [bucket.address])
75+
}
76+
77+
# Helper function to check if bucket has encryption configuration
78+
has_bucket_encryption(bucket_name) {
79+
encryption := s3_bucket_encryption[_]
80+
encryption.change.after.bucket == bucket_name
81+
}
82+
83+
# Helper function to check if bucket has public access block
84+
has_public_access_block(bucket_name) {
85+
block := s3_bucket_public_access_blocks[_]
86+
block.change.after.bucket == bucket_name
87+
}

0 commit comments

Comments
 (0)