Skip to content

Commit d2983bf

Browse files
authored
Merge pull request hashicorp#173 from hashicorp/restict-assumedrole-by-workspace
add restrict-assumed-role-by-workspace
2 parents f2c297d + 33f4e00 commit d2983bf

File tree

55 files changed

+6509
-52
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+6509
-52
lines changed
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# This policy restricts accounts that can be assumed by the AWS provider
2+
# It includes a map that maps roles to lists of regex expressions
3+
# that match one or more workspace names
4+
# It assumes that the role_arn is either a hard-coded value or a reference
5+
# to a Terraform variable
6+
7+
#### Imports #####
8+
import "tfconfig"
9+
import "tfplan"
10+
import "tfrun"
11+
import "strings"
12+
13+
##### Functions #####
14+
15+
# Find all providers aliases of given type using the tfconfig import
16+
find_provider_aliases = func(type) {
17+
18+
# We will find all provider aliases og given type from tfconfig,
19+
# meaning providers.TYPE.alias.ALIAS
20+
providers = {}
21+
22+
# Iterate over all modules in the tfconfig import
23+
for tfconfig.module_paths as path {
24+
# Iterate over providers of given type in module
25+
aliases = tfconfig.module(path).providers[type]["alias"] else {}
26+
for aliases as alias, data {
27+
# Change default alias ("") to "default"
28+
if alias is "" {
29+
alias = "default"
30+
}
31+
32+
# Get the address of the provider alias
33+
if length(path) == 0 {
34+
# root module
35+
address = type + "." + alias
36+
} else {
37+
# non-root module
38+
address = "module." + strings.join(path, ".module.") + "." +
39+
type + "." + alias
40+
}
41+
42+
providers[address] = data
43+
44+
} // end aliases loop
45+
} // end module_paths loop
46+
47+
return providers
48+
}
49+
50+
51+
# Determine role_arn of a provider from its data
52+
determine_role_arn = func(data) {
53+
54+
# Return empty string if provider does not assume a role
55+
role_arn_value = ""
56+
57+
# Check for role_arn in config
58+
if (length(data["config"]) else 0) > 0 and
59+
(length(data["config"]["assume_role"]) else 0) > 0 {
60+
config_assume_role = data["config"]["assume_role"]
61+
if config_assume_role[0]["role_arn"] else null is not null {
62+
role_arn = config_assume_role[0]["role_arn"]
63+
# This would only happen for Terraform 0.11 since a reference
64+
# to a variable in Terraform 0.12 would end up in
65+
# the references value
66+
if role_arn matches "\\$\\{var\\.(.*)\\}" {
67+
# role_arn of AWS provider was a Terraform 0.11 style variable
68+
role_arn_variable = strings.trim_suffix(strings.trim_prefix(role_arn, "${var."), "}")
69+
role_arn_value = tfplan.variables[role_arn_variable]
70+
} else {
71+
# role_arn of AWS provider was hard-coded role_arn
72+
role_arn_value = role_arn
73+
} // end determination of role_arn type
74+
} // end role_arn in config test
75+
} // end config test
76+
77+
# Check for role_arn in references
78+
if (length(data["references"]) else 0) > 0 and
79+
(length(data["references"]["assume_role"]) else 0) > 0 {
80+
references_assume_role = data["references"]["assume_role"]
81+
if references_assume_role[0]["role_arn"] else null is not null and
82+
length(references_assume_role[0]["role_arn"]) > 0 {
83+
role_arn = references_assume_role[0]["role_arn"][0]
84+
if role_arn matches "\\$\\{var\\.(.*)\\}" {
85+
# role_arn of AWS provider was a Terraform 0.11 style variable
86+
role_arn_variable = strings.trim_suffix(strings.trim_prefix(role_arn, "${var."), "}")
87+
role_arn_value = tfplan.variables[role_arn_variable]
88+
} else if role_arn matches "var\\.(.*)" {
89+
# role_arn of AWS provider was a Terraform 0.12 style variable
90+
role_arn_variable = strings.trim_prefix(role_arn, "var.")
91+
role_arn_value = tfplan.variables[role_arn_variable]
92+
} // end determination of role_arn type
93+
} // end role_arn in references test
94+
} // end references test
95+
96+
return role_arn_value
97+
}
98+
99+
# Get assumed roles from all AWS providers
100+
get_assumed_roles = func() {
101+
102+
# Initialize empty map of roles indexed by aliases
103+
assumed_roles = {}
104+
105+
# Get all AWS provider aliases
106+
aws_providers = find_provider_aliases("aws")
107+
108+
# Iterate through all AWS provider aliases
109+
for aws_providers as alias, data {
110+
assumed_roles[alias] = determine_role_arn(data)
111+
} // end aws_providers
112+
113+
return assumed_roles
114+
115+
}
116+
117+
# Validate that all assumed roles are allowed
118+
validate_assumed_roles = func(allowed_roles_map) {
119+
120+
validated = true
121+
122+
assumed_roles = get_assumed_roles()
123+
124+
# Iterate over all assumed roles used by providers
125+
for assumed_roles as alias, role {
126+
# Validate that each assumed role is in map
127+
if role is not "" {
128+
if role not in keys(allowed_roles_map) {
129+
print("AWS provider with alias", alias, "has assumed role",
130+
role, "that is not allowed.")
131+
validated = false
132+
} else {
133+
134+
# Get workspace name
135+
workspace_name = tfrun.workspace.name
136+
137+
# Validate that role is allowed for current workspace
138+
matched = false
139+
for allowed_roles_map[role] as workspace_regex {
140+
if workspace_name matches workspace_regex {
141+
matched = true
142+
}
143+
} // end for workspace_regex
144+
if not matched {
145+
print("Workspace", workspace_name, "is not allowed to use role", role)
146+
print("It used that role in the AWS provider with alias", alias)
147+
validated = false
148+
} // end matched check
149+
} // end else role in allowed_roles_map
150+
} // end if role is not ""
151+
} // end assumed_roles loop
152+
return validated
153+
}
154+
155+
###### Allowed Roles #####
156+
allowed_roles_map = {
157+
"arn:aws:iam::123412341234:role/role-dev": [
158+
"(.*)-dev$",
159+
"^dev-(.*)",
160+
],
161+
"arn:aws:iam::567856785678:role/role-qa": [
162+
"(.*)-qa$",
163+
"^qa-(.*)",
164+
],
165+
"arn:aws:iam::909012349090:role/role-prod": [
166+
"(.*)-prod$",
167+
"^prod-(.*)",
168+
],
169+
}
170+
171+
##### Rules #####
172+
roles_validated = validate_assumed_roles(allowed_roles_map)
173+
main = rule {
174+
roles_validated
175+
}

governance/second-generation/aws/restrict-assumed-role.sentinel

Lines changed: 19 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,22 @@ find_provider_aliases = func(type) {
2121
# Iterate over providers of given type in module
2222
aliases = tfconfig.module(path).providers[type]["alias"] else {}
2323
for aliases as alias, data {
24-
#print("alias:", alias)
25-
# Change default alias ("") to "default"
26-
if alias is "" {
27-
#print("found default alias")
28-
alias = "default"
29-
}
30-
31-
# Get the address of the provider alias
32-
if length(path) == 0 {
33-
# root module
34-
address = type + "." + alias
35-
} else {
36-
# non-root module
37-
address = "module." + strings.join(path, ".module.") + "." +
38-
type + "." + alias
39-
}
40-
#print("address:", address)
41-
42-
providers[address] = data
24+
# Change default alias ("") to "default"
25+
if alias is "" {
26+
alias = "default"
27+
}
28+
29+
# Get the address of the provider alias
30+
if length(path) == 0 {
31+
# root module
32+
address = type + "." + alias
33+
} else {
34+
# non-root module
35+
address = "module." + strings.join(path, ".module.") + "." +
36+
type + "." + alias
37+
}
38+
39+
providers[address] = data
4340

4441
} // end aliases loop
4542
} // end module_paths loop
@@ -51,30 +48,24 @@ find_provider_aliases = func(type) {
5148
# Determine role_arn of a provider from its data
5249
determine_role_arn = func(data) {
5350

51+
# Return empty string if provider does not assume a role
5452
role_arn_value = ""
5553

5654
# Check for role_arn in config
5755
if (length(data["config"]) else 0) > 0 and
5856
(length(data["config"]["assume_role"]) else 0) > 0 {
5957
config_assume_role = data["config"]["assume_role"]
60-
#print ( "config_assume_role is: ", config_assume_role )
6158
if config_assume_role[0]["role_arn"] else null is not null {
6259
role_arn = config_assume_role[0]["role_arn"]
6360
# This would only happen for Terraform 0.11 since a reference
6461
# to a variable in Terraform 0.12 would end up in
6562
# the references value
66-
#print("role_arn in config:", role_arn)
6763
if role_arn matches "\\$\\{var\\.(.*)\\}" {
6864
# role_arn of AWS provider was a Terraform 0.11 style variable
69-
#print ( "role_arn is a Terraform 0.11 style variable" )
7065
role_arn_variable = strings.trim_suffix(strings.trim_prefix(role_arn, "${var."), "}")
71-
#print ( "role_arn variable is: ", role_arn_variable )
72-
#print ( "Value of role_arn is: ", tfplan.variables[role_arn_variable] )
7366
role_arn_value = tfplan.variables[role_arn_variable]
7467
} else {
7568
# role_arn of AWS provider was hard-coded role_arn
76-
#print ( "role_arn is a hard-coded value" )
77-
#print ( "Value of role_arn is: ", role_arn )
7869
role_arn_value = role_arn
7970
} // end determination of role_arn type
8071
} // end role_arn in config test
@@ -84,24 +75,16 @@ determine_role_arn = func(data) {
8475
if (length(data["references"]) else 0) > 0 and
8576
(length(data["references"]["assume_role"]) else 0) > 0 {
8677
references_assume_role = data["references"]["assume_role"]
87-
#print ( "references_assume_role is: ", references_assume_role )
8878
if references_assume_role[0]["role_arn"] else null is not null and
8979
length(references_assume_role[0]["role_arn"]) > 0 {
9080
role_arn = references_assume_role[0]["role_arn"][0]
91-
#print("role_arn in references:", role_arn)
9281
if role_arn matches "\\$\\{var\\.(.*)\\}" {
9382
# role_arn of AWS provider was a Terraform 0.11 style variable
94-
#print ( "role_arn is a Terraform 0.11 style variable" )
9583
role_arn_variable = strings.trim_suffix(strings.trim_prefix(role_arn, "${var."), "}")
96-
#print ( "role_arn variable is: ", role_arn_variable )
97-
#print ( "Value of role_arn is: ", tfplan.variables[role_arn_variable] )
9884
role_arn_value = tfplan.variables[role_arn_variable]
9985
} else if role_arn matches "var\\.(.*)" {
10086
# role_arn of AWS provider was a Terraform 0.12 style variable
101-
#print ( "role_arn is a Terraform 0.12 style variable" )
10287
role_arn_variable = strings.trim_prefix(role_arn, "var.")
103-
#print ( "role_arn variable is: ", role_arn_variable )
104-
#print ( "Value of role_arn is: ", tfplan.variables[role_arn_variable] )
10588
role_arn_value = tfplan.variables[role_arn_variable]
10689
} // end determination of role_arn type
10790
} // end role_arn in references test
@@ -118,12 +101,10 @@ get_assumed_roles = func() {
118101

119102
# Get all AWS provider aliases
120103
aws_providers = find_provider_aliases("aws")
121-
#print("aws providers:", aws_providers)
122104

123105
# Iterate through all AWS provider aliases
124106
for aws_providers as alias, data {
125-
#print("alias:", alias)
126-
assumed_roles[alias] = determine_role_arn(data)
107+
assumed_roles[alias] = determine_role_arn(data)
127108
} // end aws_providers
128109

129110
return assumed_roles
@@ -136,7 +117,6 @@ validate_assumed_roles = func(allowed_roles) {
136117
validated = true
137118

138119
assumed_roles = get_assumed_roles()
139-
#print("assumed roles:", assumed_roles)
140120

141121
for assumed_roles as alias, role {
142122
if role is not "" and role not in allowed_roles {
@@ -151,7 +131,7 @@ validate_assumed_roles = func(allowed_roles) {
151131

152132
###### Allowed Roles #####
153133
allowed_roles = [
154-
"arn:aws:iam::753646501470:role/roger-terraform-assumed-role",
134+
"arn:aws:iam::123412341234:role/terraform-assumed-role",
155135
]
156136

157137
##### Rules #####
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"mock": {
3+
"tfconfig": "mock-tfconfig-fail-bad-role-0.11.sentinel",
4+
"tfplan": "mock-tfplan-fail-bad-role-0.11.sentinel",
5+
"tfrun": "mock-tfrun-dev.sentinel"
6+
},
7+
"test": {
8+
"main": false
9+
}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"mock": {
3+
"tfconfig": "mock-tfconfig-fail-bad-role-0.12.sentinel",
4+
"tfplan": "mock-tfplan-fail-bad-role-0.12.sentinel",
5+
"tfrun": "mock-tfrun-dev.sentinel"
6+
},
7+
"test": {
8+
"main": false
9+
}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"mock": {
3+
"tfconfig": "mock-tfconfig-fail-dev-0.11.sentinel",
4+
"tfplan": "mock-tfplan-fail-dev-0.11.sentinel",
5+
"tfrun": "mock-tfrun-dev.sentinel"
6+
},
7+
"test": {
8+
"main": false
9+
}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"mock": {
3+
"tfconfig": "mock-tfconfig-fail-dev-0.12.sentinel",
4+
"tfplan": "mock-tfplan-fail-dev-0.12.sentinel",
5+
"tfrun": "mock-tfrun-dev.sentinel"
6+
},
7+
"test": {
8+
"main": false
9+
}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"mock": {
3+
"tfconfig": "mock-tfconfig-fail-prod-0.11.sentinel",
4+
"tfplan": "mock-tfplan-fail-prod-0.11.sentinel",
5+
"tfrun": "mock-tfrun-prod.sentinel"
6+
},
7+
"test": {
8+
"main": false
9+
}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"mock": {
3+
"tfconfig": "mock-tfconfig-fail-prod-0.12.sentinel",
4+
"tfplan": "mock-tfplan-fail-prod-0.12.sentinel",
5+
"tfrun": "mock-tfrun-prod.sentinel"
6+
},
7+
"test": {
8+
"main": false
9+
}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"mock": {
3+
"tfconfig": "mock-tfconfig-fail-qa-0.11.sentinel",
4+
"tfplan": "mock-tfplan-fail-qa-0.11.sentinel",
5+
"tfrun": "mock-tfrun-qa.sentinel"
6+
},
7+
"test": {
8+
"main": false
9+
}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"mock": {
3+
"tfconfig": "mock-tfconfig-fail-qa-0.12.sentinel",
4+
"tfplan": "mock-tfplan-fail-qa-0.12.sentinel",
5+
"tfrun": "mock-tfrun-qa.sentinel"
6+
},
7+
"test": {
8+
"main": false
9+
}
10+
}

0 commit comments

Comments
 (0)