Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Breaking
- 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.

### Added
- Add root-level AWS, Azure, and GCP identity wiring so Terraform creates the selected cloud identity and registers its generated identifiers with StackGuardian.
- Add CLI-authenticated AWS and Azure onboarding and guarded destroy tasks with temporary targeted plans and StackGuardian token prompting.
Expand Down
82 changes: 36 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# StackGuardian Terraform Modules v2
# StackGuardian Terraform Modules

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.
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.

## Prerequisites

Expand All @@ -9,23 +9,31 @@ Terraform modules for StackGuardian onboarding and cloud identity configuration.
- 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.
- Credentials authorized to create the selected cloud identities. Azure management needs Microsoft Graph application-management and subscription role-assignment privileges.

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

## Static Credential Deprecation
## How To Use

`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.
1. Copy `terraform.tfvars.example` to an ignored `terraform.tfvars` file and replace its placeholders.
2. Set `TF_VAR_stackguardian_api_key` through a secret manager or environment variable.
3. Authenticate to each cloud used by `cloud_connectors`.
4. Run `terraform init`, review `terraform plan`, then run `terraform apply`.

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.

## Static Credentials

`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.

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

```hcl
cloud_connectors = [{
name = "legacy-aws"
kind = "AWS_STATIC"
allow_static_credentials = true
aws_access_key_id = var.legacy_aws_access_key_id
aws_secret_access_key = var.legacy_aws_secret_access_key
aws_region = "eu-central-1"
}]
cloud_connectors = {
legacy-aws = {
kind = "AWS_STATIC"
allow_static_credentials = true
aws_region = "eu-central-1"
}
}
```

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

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.

## V2 Inputs

| v1 input | v2 input |
| --- | --- |
| `api_key` | `stackguardian_api_key` |
| `org_name` | `stackguardian_org_name` |
| `user_or_group` | `subject` |
| `connector_type` | `kind` in `cloud_connectors` |
| `cloud_connector_name` | `name` in `cloud_connectors` |
| `aws_default_region` | `aws_region` in the AWS connector |
| `armTenantId`, `armSubscriptionId`, `armClientId`, `armClientSecret` | `azure_tenant_id`, `azure_subscription_id`, `azure_client_id`, `azure_client_secret` in the Azure connector |
| `role_arn`, `role_external_id` | `aws_role_arn`, `aws_external_id` |
| `aws_oidc.region`, `role_name`, `account_number`, `aws_policy` | `aws_region`, `iam_role_name`, `aws_account_id`, `policy_arn` |
| `aws_rbac.aws_role_name`, `aws_policy` | `iam_role_name`, `policy_arn` |
| `azure_static.AD_name` | `application_display_name` |
| `azure_oidc.sg_org_name` | `stackguardian_org_name` |
| `gcp_oidc.project`, `sg-org-id` | `gcp_project_id` in the GCP connector, `stackguardian_org_name` |

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.
## Onboarding Configuration

`terraform.tfvars.example` is a complete multi-role configuration. The root accepts these top-level collections:

- `workflow_groups`: unique names of workflow groups to create.
- `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.
- `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.
- `roles`: a map keyed by StackGuardian role name. Each role references one or more workflow groups, cloud connectors, VCS connectors, or templates.
- `subjects`: a map keyed by a local email or qualified SSO subject. Each subject receives one or more role names in one assignment resource.

Role references use the corresponding collection keys. A role must include at least one non-empty scope. Empty scope categories produce no permissions.

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.

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.

## Permissions And Defaults

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

## Role v4 State Migration

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:

```bash
terraform state rm 'module.stackguardian_role.stackguardian_role.role'
terraform import 'module.stackguardian_role.stackguardian_rolev4.role' '<role-name>'
terraform plan
```

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.

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.

## Module Usage

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.
Expand Down
54 changes: 46 additions & 8 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ tasks:
terraform -chdir="$dir" fmt -check
done
test:
desc: Runs isolated OpenTofu native tests with mocked providers
desc: Runs isolated OpenTofu tests discovered under tests directories
deps: [preflight:tofu]
cmds:
- |
Expand All @@ -100,14 +100,52 @@ tasks:
root="$(pwd)"
temp="$(mktemp -d)"
trap 'rm -rf "$temp"' EXIT
for dir in stackguardian_role stackguardian_connector_cloud; do
tar --exclude='.terraform' --exclude='.terraform.lock.hcl' -cf - -C "$root" "$dir" | tar -xf - -C "$temp"
target="$temp/$dir"
perl -0pi -e 's/required_version = "= 1\.5\.7"/required_version = ">= 1.6.0"/' "$target/provider.tf"
perl -0pi -e 's/source\s*=\s*"StackGuardian\/stackguardian"/source = "registry.terraform.io\/StackGuardian\/stackguardian"/' "$target/provider.tf"
export AWS_ACCESS_KEY_ID="test"
export AWS_SECRET_ACCESS_KEY="test"
export AWS_SESSION_TOKEN="test"
export AWS_EC2_METADATA_DISABLED="true"
export ARM_CLIENT_ID="00000000-0000-0000-0000-000000000000"
export ARM_CLIENT_SECRET="test"
export ARM_TENANT_ID="00000000-0000-0000-0000-000000000000"
export ARM_SUBSCRIPTION_ID="00000000-0000-0000-0000-000000000000"
export AZUREAD_CLIENT_ID="$ARM_CLIENT_ID"
export AZUREAD_CLIENT_SECRET="$ARM_CLIENT_SECRET"
export AZUREAD_TENANT_ID="$ARM_TENANT_ID"
while IFS= read -r config_dir; do
if [ "$config_dir" = "$root" ]; then
target="$temp/root"
else
target="$temp/${config_dir#"$root"/}"
fi
mkdir -p "$target"
tar --exclude='.git' --exclude='.terraform' --exclude='.terraform.lock.hcl' -cf - -C "$config_dir" . | tar -xf - -C "$target"
if [ "$config_dir" = "$root" ]; then
: > "$target/provider.tf"
printf '%s\n' \
'terraform {' \
' required_version = "= 1.5.7"' \
'' \
' required_providers {' \
' stackguardian = {' \
' source = "StackGuardian/stackguardian"' \
' version = ">= 1.12.0, < 2.0.0"' \
' }' \
' }' \
'}' \
'' \
'provider "stackguardian" {' \
' api_key = var.stackguardian_api_key' \
' org_name = var.stackguardian_org_name' \
' api_uri = var.stackguardian_api_uri' \
'}' >> "$target/provider.tf"
fi
while IFS= read -r provider_file; do
perl -0pi -e 's/required_version = "= 1\.5\.7"/required_version = ">= 1.6.0"/' "$provider_file"
perl -0pi -e 's/source\s*=\s*"StackGuardian\/stackguardian"/source = "registry.terraform.io\/StackGuardian\/stackguardian"/' "$provider_file"
done < <(find "$target" -name provider.tf -type f)
tofu -chdir="$target" init -backend=false
tofu -chdir="$target" test
done
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)
onboard:aws:
desc: Creates AWS OIDC resources and registers the connector with StackGuardian
deps: [preflight:terraform]
Expand Down Expand Up @@ -291,7 +329,7 @@ tasks:
target="$temp/${dir#./}"
mkdir -p "$target"
if [ "$dir" = "." ]; then
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"
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"
else
tar --exclude='.terraform' --exclude='.terraform.lock.hcl' -cf - -C "$root" "$dir" | tar -xf - -C "$temp"
fi
Expand Down
44 changes: 32 additions & 12 deletions examples/aws-oidc.tfvars.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,36 @@
stackguardian_org_name = "wicked-hop"

workflow_groups = ["engineering"]
cloud_connectors = [{
name = "aws-oidc"
kind = "AWS_OIDC"
aws_region = "eu-central-1"
iam_role_name = "StackGuardianOidcRole"
policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
}]
cloud_connectors = {
aws-oidc = {
kind = "AWS_OIDC"
aws_region = "eu-central-1"
iam_role_name = "StackGuardianOidcRole"
policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
}
}

vcs_connectors = {}
role_name = "engineering-developer"
template_list = ["terraform-aws-vpc"]
subject = "developer@example.invalid"
entity_type = "EMAIL"
roles = {
engineering-developer = {
workflow_groups = ["engineering"]
cloud_connectors = ["aws-oidc"]
template_list = ["terraform-aws-vpc"]
}
}

subjects = {
# Local user
"developer@example.invalid" = {
roles = ["engineering-developer"]
}
# Qualified SSO user
"okta/engineering.user@example.invalid" = {
entity_type = "EMAIL"
roles = ["engineering-developer"]
}
# SSO group
"okta/platform-engineers" = {
entity_type = "GROUP"
roles = ["engineering-developer"]
}
}
46 changes: 33 additions & 13 deletions examples/azure-oidc.tfvars.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,37 @@
stackguardian_org_name = "example-org"

workflow_groups = ["engineering"]
cloud_connectors = [{
name = "azure-oidc"
kind = "AZURE_OIDC"
azure_subscription_id = "00000000-0000-0000-0000-000000000000"
azure_tenant_id = "00000000-0000-0000-0000-000000000000"
application_display_name = "stackguardian-oidc"
role_definition_name = "Reader"
}]
cloud_connectors = {
azure-oidc = {
kind = "AZURE_OIDC"
azure_subscription_id = "00000000-0000-0000-0000-000000000000"
azure_tenant_id = "00000000-0000-0000-0000-000000000000"
application_display_name = "stackguardian-oidc"
role_definition_name = "Reader"
}
}

vcs_connectors = {}
role_name = "engineering-developer"
template_list = ["terraform-azurerm-resource-group"]
subject = "developer@example.invalid"
entity_type = "EMAIL"
roles = {
engineering-developer = {
workflow_groups = ["engineering"]
cloud_connectors = ["azure-oidc"]
template_list = ["terraform-azurerm-resource-group"]
}
}

subjects = {
# Local user
"developer@example.invalid" = {
roles = ["engineering-developer"]
}
# Qualified SSO user
"okta/engineering.user@example.invalid" = {
entity_type = "EMAIL"
roles = ["engineering-developer"]
}
# SSO group
"okta/platform-engineers" = {
entity_type = "GROUP"
roles = ["engineering-developer"]
}
}
Loading
Loading