Skip to content

Commit 3138e40

Browse files
authored
feat: add multi-role onboarding configuration (#23)
* feat: add multi-role onboarding * docs: add multi-role onboarding examples * test: discover OpenTofu test configurations
1 parent 324a8d1 commit 3138e40

20 files changed

Lines changed: 525 additions & 251 deletions

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Breaking
11+
- Replace the singleton root role and assignment inputs with keyed `roles` and `subjects` collections. Roles can independently scope workflow groups, cloud connectors, VCS connectors, and templates; each subject assignment now carries its complete roles list.
12+
1013
### Added
1114
- Add root-level AWS, Azure, and GCP identity wiring so Terraform creates the selected cloud identity and registers its generated identifiers with StackGuardian.
1215
- Add CLI-authenticated AWS and Azure onboarding and guarded destroy tasks with temporary targeted plans and StackGuardian token prompting.

‎README.md‎

Lines changed: 36 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# StackGuardian Terraform Modules v2
1+
# StackGuardian Terraform Modules
22

3-
Terraform modules for StackGuardian onboarding and cloud identity configuration. The root module creates workflow groups, cloud/VCS connectors, a role, and one assignment. `stackguardian_private_runner/` is explicitly outside the v2 upgrade scope.
3+
Terraform modules for StackGuardian onboarding and cloud identity configuration. The root module creates workflow groups, cloud/VCS connectors, keyed roles, and one multi-role assignment per subject.
44

55
## Prerequisites
66

@@ -9,23 +9,31 @@ Terraform modules for StackGuardian onboarding and cloud identity configuration.
99
- AWS provider `>= 6.58.0, < 7.0.0`, AzureRM `>= 5.0.1, < 6.0.0`, AzureAD `>= 3.9.0, < 4.0.0`, and Google `>= 7.44.0, < 8.0.0` in their applicable modules.
1010
- Credentials authorized to create the selected cloud identities. Azure management needs Microsoft Graph application-management and subscription role-assignment privileges.
1111

12-
Use `terraform.tfvars.example` as a schema reference. Put secret values in `TF_VAR_*` variables or a secret manager, not version control.
12+
Use `terraform.tfvars.example` as a configuration reference. Put secret values in `TF_VAR_*` variables or a secret manager, not version control.
1313

14-
## Static Credential Deprecation
14+
## How To Use
1515

16-
`aws_static`, `azure_static`, and `AWS_STATIC` or `AZURE_STATIC` cloud connectors are deprecated. Static secrets are retained in Terraform state and may appear in plan artifacts, so protect both as sensitive data. Prefer `aws_rbac`, `aws_oidc`, `azure_oidc`, or the corresponding non-static connector kind.
16+
1. Copy `terraform.tfvars.example` to an ignored `terraform.tfvars` file and replace its placeholders.
17+
2. Set `TF_VAR_stackguardian_api_key` through a secret manager or environment variable.
18+
3. Authenticate to each cloud used by `cloud_connectors`.
19+
4. Run `terraform init`, review `terraform plan`, then run `terraform apply`.
20+
21+
For AWS and Azure OIDC onboarding, customize the matching file in `examples/` and use `task onboard:aws` or `task onboard:azure`. Those tasks authenticate through the local cloud CLI, create the selected cloud identity, and apply a reviewed plan.
22+
23+
## Static Credentials
24+
25+
`AWS_STATIC` and `AZURE_STATIC` are supported only when `allow_static_credentials = true` is set on that connector. Static secrets are retained in Terraform state and may appear in plan artifacts. Prefer `AWS_RBAC`, `AWS_OIDC`, `AZURE_OIDC`, or `GCP_OIDC` whenever possible.
1726

1827
Static authentication requires an explicit acknowledgement. The root acknowledgement is per connector and cannot enable another connector:
1928

2029
```hcl
21-
cloud_connectors = [{
22-
name = "legacy-aws"
23-
kind = "AWS_STATIC"
24-
allow_static_credentials = true
25-
aws_access_key_id = var.legacy_aws_access_key_id
26-
aws_secret_access_key = var.legacy_aws_secret_access_key
27-
aws_region = "eu-central-1"
28-
}]
30+
cloud_connectors = {
31+
legacy-aws = {
32+
kind = "AWS_STATIC"
33+
allow_static_credentials = true
34+
aws_region = "eu-central-1"
35+
}
36+
}
2937
```
3038

3139
Standalone `aws_static` and `azure_static` modules also require `allow_static_credentials = true`. Terraform emits a deprecation warning during apply after acknowledgement.
@@ -42,25 +50,21 @@ Both onboarding tasks use `TF_VAR_stackguardian_api_key` when it is set. Otherwi
4250

4351
Use `task destroy:aws` or `task destroy:azure` to remove a connector trial. Each asks you to type the connector name before it destroys resources. AWS teardown retains any account-level `api.app.stackguardian.io` OIDC provider because it may be shared by multiple StackGuardian roles.
4452

45-
## V2 Inputs
46-
47-
| v1 input | v2 input |
48-
| --- | --- |
49-
| `api_key` | `stackguardian_api_key` |
50-
| `org_name` | `stackguardian_org_name` |
51-
| `user_or_group` | `subject` |
52-
| `connector_type` | `kind` in `cloud_connectors` |
53-
| `cloud_connector_name` | `name` in `cloud_connectors` |
54-
| `aws_default_region` | `aws_region` in the AWS connector |
55-
| `armTenantId`, `armSubscriptionId`, `armClientId`, `armClientSecret` | `azure_tenant_id`, `azure_subscription_id`, `azure_client_id`, `azure_client_secret` in the Azure connector |
56-
| `role_arn`, `role_external_id` | `aws_role_arn`, `aws_external_id` |
57-
| `aws_oidc.region`, `role_name`, `account_number`, `aws_policy` | `aws_region`, `iam_role_name`, `aws_account_id`, `policy_arn` |
58-
| `aws_rbac.aws_role_name`, `aws_policy` | `iam_role_name`, `policy_arn` |
59-
| `azure_static.AD_name` | `application_display_name` |
60-
| `azure_oidc.sg_org_name` | `stackguardian_org_name` |
61-
| `gcp_oidc.project`, `sg-org-id` | `gcp_project_id` in the GCP connector, `stackguardian_org_name` |
62-
63-
Legacy aliases are intentionally unavailable. The root configures StackGuardian once; standalone StackGuardian leaf modules inherit provider configuration from their caller and do not accept API credentials.
53+
## Onboarding Configuration
54+
55+
`terraform.tfvars.example` is a complete multi-role configuration. The root accepts these top-level collections:
56+
57+
- `workflow_groups`: unique names of workflow groups to create.
58+
- `cloud_connectors`: a map keyed by connector name. Each value selects one cloud `kind` and supplies the corresponding identity settings. The root creates the selected identity and registers it with StackGuardian.
59+
- `vcs_connectors`: a map keyed by connector name. Each value supplies one GitHub, GitLab, or Bitbucket credential configuration. Its `name` must match the map key.
60+
- `roles`: a map keyed by StackGuardian role name. Each role references one or more workflow groups, cloud connectors, VCS connectors, or templates.
61+
- `subjects`: a map keyed by a local email or qualified SSO subject. Each subject receives one or more role names in one assignment resource.
62+
63+
Role references use the corresponding collection keys. A role must include at least one non-empty scope. Empty scope categories produce no permissions.
64+
65+
Subjects can be local emails such as `developer@example.invalid`, qualified SSO emails such as `okta/developer@example.invalid`, or SSO groups such as `okta/platform-engineers`. `entity_type` defaults to `EMAIL`; set it to `GROUP` for a group. Role lists must be non-empty and duplicate-free.
66+
67+
VCS credentials remain sensitive and are stored in Terraform state. Keep real values in an ignored secret vars file, a generated `terraform.tfvars.json`, or JSON-encoded `TF_VAR_vcs_connectors`; examples use literal placeholders.
6468

6569
## Permissions And Defaults
6670

@@ -71,20 +75,6 @@ Legacy aliases are intentionally unavailable. The root configures StackGuardian
7175
- Cloud and VCS connector modules require access to create StackGuardian connectors. They reject missing, mismatched, or conflicting credentials.
7276
- Role, assignment, and workflow-group modules require StackGuardian role-management permission.
7377

74-
## Role v4 State Migration
75-
76-
The role resource changes from `stackguardian_role` to `stackguardian_rolev4`; this cannot use a `moved` block or `terraform state mv`. During a maintenance window with exclusive backend locking, back up state and record the existing permissions, then run:
77-
78-
```bash
79-
terraform state rm 'module.stackguardian_role.stackguardian_role.role'
80-
terraform import 'module.stackguardian_role.stackguardian_rolev4.role' '<role-name>'
81-
terraform plan
82-
```
83-
84-
For standalone role usage, omit `module.stackguardian_role.`. Review the v4 `allowed_permissions`, apply the reviewed update, then run a second plan and test an allowed and denied path. Do not use `state mv`; roll back only by restoring the backed-up state/configuration.
85-
86-
Before replacing the former authoritative GCP IAM policy, add and import `google_service_account_iam_member.self_workload_identity`, then remove only the old policy state binding. Likewise, import the existing AWS role-policy attachment when converting from the legacy global attachment. Export existing IAM bindings first and verify the plan cannot remove unrelated principals.
87-
8878
## Module Usage
8979

9080
Each module has a short usage and outputs reference in its directory README. Run `terraform init -upgrade`, `terraform validate`, and a reviewed plan from the specific module directory. Lock files are deliberately not committed because callers initialize independently.

‎Taskfile.yml‎

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ tasks:
9191
terraform -chdir="$dir" fmt -check
9292
done
9393
test:
94-
desc: Runs isolated OpenTofu native tests with mocked providers
94+
desc: Runs isolated OpenTofu tests discovered under tests directories
9595
deps: [preflight:tofu]
9696
cmds:
9797
- |
@@ -100,14 +100,52 @@ tasks:
100100
root="$(pwd)"
101101
temp="$(mktemp -d)"
102102
trap 'rm -rf "$temp"' EXIT
103-
for dir in stackguardian_role stackguardian_connector_cloud; do
104-
tar --exclude='.terraform' --exclude='.terraform.lock.hcl' -cf - -C "$root" "$dir" | tar -xf - -C "$temp"
105-
target="$temp/$dir"
106-
perl -0pi -e 's/required_version = "= 1\.5\.7"/required_version = ">= 1.6.0"/' "$target/provider.tf"
107-
perl -0pi -e 's/source\s*=\s*"StackGuardian\/stackguardian"/source = "registry.terraform.io\/StackGuardian\/stackguardian"/' "$target/provider.tf"
103+
export AWS_ACCESS_KEY_ID="test"
104+
export AWS_SECRET_ACCESS_KEY="test"
105+
export AWS_SESSION_TOKEN="test"
106+
export AWS_EC2_METADATA_DISABLED="true"
107+
export ARM_CLIENT_ID="00000000-0000-0000-0000-000000000000"
108+
export ARM_CLIENT_SECRET="test"
109+
export ARM_TENANT_ID="00000000-0000-0000-0000-000000000000"
110+
export ARM_SUBSCRIPTION_ID="00000000-0000-0000-0000-000000000000"
111+
export AZUREAD_CLIENT_ID="$ARM_CLIENT_ID"
112+
export AZUREAD_CLIENT_SECRET="$ARM_CLIENT_SECRET"
113+
export AZUREAD_TENANT_ID="$ARM_TENANT_ID"
114+
while IFS= read -r config_dir; do
115+
if [ "$config_dir" = "$root" ]; then
116+
target="$temp/root"
117+
else
118+
target="$temp/${config_dir#"$root"/}"
119+
fi
120+
mkdir -p "$target"
121+
tar --exclude='.git' --exclude='.terraform' --exclude='.terraform.lock.hcl' -cf - -C "$config_dir" . | tar -xf - -C "$target"
122+
if [ "$config_dir" = "$root" ]; then
123+
: > "$target/provider.tf"
124+
printf '%s\n' \
125+
'terraform {' \
126+
' required_version = "= 1.5.7"' \
127+
'' \
128+
' required_providers {' \
129+
' stackguardian = {' \
130+
' source = "StackGuardian/stackguardian"' \
131+
' version = ">= 1.12.0, < 2.0.0"' \
132+
' }' \
133+
' }' \
134+
'}' \
135+
'' \
136+
'provider "stackguardian" {' \
137+
' api_key = var.stackguardian_api_key' \
138+
' org_name = var.stackguardian_org_name' \
139+
' api_uri = var.stackguardian_api_uri' \
140+
'}' >> "$target/provider.tf"
141+
fi
142+
while IFS= read -r provider_file; do
143+
perl -0pi -e 's/required_version = "= 1\.5\.7"/required_version = ">= 1.6.0"/' "$provider_file"
144+
perl -0pi -e 's/source\s*=\s*"StackGuardian\/stackguardian"/source = "registry.terraform.io\/StackGuardian\/stackguardian"/' "$provider_file"
145+
done < <(find "$target" -name provider.tf -type f)
108146
tofu -chdir="$target" init -backend=false
109147
tofu -chdir="$target" test
110-
done
148+
done < <(find "$root" \( -name .git -o -name .terraform \) -type d -prune -o -path '*/tests/*.tofutest.hcl' -type f -print | while IFS= read -r test_file; do dirname "$(dirname "$test_file")"; done | sort -u)
111149
onboard:aws:
112150
desc: Creates AWS OIDC resources and registers the connector with StackGuardian
113151
deps: [preflight:terraform]
@@ -291,7 +329,7 @@ tasks:
291329
target="$temp/${dir#./}"
292330
mkdir -p "$target"
293331
if [ "$dir" = "." ]; then
294-
tar --exclude='.git' --exclude='.kilo' --exclude='.terraform' --exclude='.terraform.lock.hcl' --exclude='terraform.tfvars' --exclude='*.auto.tfvars' --exclude='role_response.json' --exclude='stackguardian_private_runner' -cf - -C "$root" . | tar -xf - -C "$target"
332+
tar --exclude='.git' --exclude='.terraform' --exclude='.terraform.lock.hcl' --exclude='terraform.tfvars' --exclude='*.auto.tfvars' --exclude='role_response.json' --exclude='stackguardian_private_runner' -cf - -C "$root" . | tar -xf - -C "$target"
295333
else
296334
tar --exclude='.terraform' --exclude='.terraform.lock.hcl' -cf - -C "$root" "$dir" | tar -xf - -C "$temp"
297335
fi

‎examples/aws-oidc.tfvars.example‎

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,36 @@
33
stackguardian_org_name = "wicked-hop"
44

55
workflow_groups = ["engineering"]
6-
cloud_connectors = [{
7-
name = "aws-oidc"
8-
kind = "AWS_OIDC"
9-
aws_region = "eu-central-1"
10-
iam_role_name = "StackGuardianOidcRole"
11-
policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
12-
}]
6+
cloud_connectors = {
7+
aws-oidc = {
8+
kind = "AWS_OIDC"
9+
aws_region = "eu-central-1"
10+
iam_role_name = "StackGuardianOidcRole"
11+
policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
12+
}
13+
}
1314

14-
vcs_connectors = {}
15-
role_name = "engineering-developer"
16-
template_list = ["terraform-aws-vpc"]
17-
subject = "developer@example.invalid"
18-
entity_type = "EMAIL"
15+
roles = {
16+
engineering-developer = {
17+
workflow_groups = ["engineering"]
18+
cloud_connectors = ["aws-oidc"]
19+
template_list = ["terraform-aws-vpc"]
20+
}
21+
}
22+
23+
subjects = {
24+
# Local user
25+
"developer@example.invalid" = {
26+
roles = ["engineering-developer"]
27+
}
28+
# Qualified SSO user
29+
"okta/engineering.user@example.invalid" = {
30+
entity_type = "EMAIL"
31+
roles = ["engineering-developer"]
32+
}
33+
# SSO group
34+
"okta/platform-engineers" = {
35+
entity_type = "GROUP"
36+
roles = ["engineering-developer"]
37+
}
38+
}

‎examples/azure-oidc.tfvars.example‎

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,37 @@
33
stackguardian_org_name = "example-org"
44

55
workflow_groups = ["engineering"]
6-
cloud_connectors = [{
7-
name = "azure-oidc"
8-
kind = "AZURE_OIDC"
9-
azure_subscription_id = "00000000-0000-0000-0000-000000000000"
10-
azure_tenant_id = "00000000-0000-0000-0000-000000000000"
11-
application_display_name = "stackguardian-oidc"
12-
role_definition_name = "Reader"
13-
}]
6+
cloud_connectors = {
7+
azure-oidc = {
8+
kind = "AZURE_OIDC"
9+
azure_subscription_id = "00000000-0000-0000-0000-000000000000"
10+
azure_tenant_id = "00000000-0000-0000-0000-000000000000"
11+
application_display_name = "stackguardian-oidc"
12+
role_definition_name = "Reader"
13+
}
14+
}
1415

15-
vcs_connectors = {}
16-
role_name = "engineering-developer"
17-
template_list = ["terraform-azurerm-resource-group"]
18-
subject = "developer@example.invalid"
19-
entity_type = "EMAIL"
16+
roles = {
17+
engineering-developer = {
18+
workflow_groups = ["engineering"]
19+
cloud_connectors = ["azure-oidc"]
20+
template_list = ["terraform-azurerm-resource-group"]
21+
}
22+
}
23+
24+
subjects = {
25+
# Local user
26+
"developer@example.invalid" = {
27+
roles = ["engineering-developer"]
28+
}
29+
# Qualified SSO user
30+
"okta/engineering.user@example.invalid" = {
31+
entity_type = "EMAIL"
32+
roles = ["engineering-developer"]
33+
}
34+
# SSO group
35+
"okta/platform-engineers" = {
36+
entity_type = "GROUP"
37+
roles = ["engineering-developer"]
38+
}
39+
}

0 commit comments

Comments
 (0)