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
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,13 @@ jobs:
- name: Terraform Format Check
run: terraform fmt -check -recursive terraform/

- name: Test Azure federation identity guards
if: matrix.cloud == 'azure'
run: |
terraform -chdir=iac/federation/azure-target/terraform init -backend=false -lockfile=readonly -input=false
terraform -chdir=iac/federation/azure-target/terraform fmt -check -recursive
terraform -chdir=iac/federation/azure-target/terraform test

- name: Terraform Init
run: |
cd terraform/environments/${{ matrix.cloud }}
Expand Down
7 changes: 5 additions & 2 deletions iac/federation/azure-target/terraform/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

233 changes: 233 additions & 0 deletions iac/federation/azure-target/terraform/tests/identity_guards.tftest.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
mock_provider "azuread" {}
mock_provider "http" {}
mock_provider "azurerm" {
mock_data "azurerm_subscription" {
defaults = {
id = "/subscriptions/11111111-1111-1111-1111-111111111111"
subscription_id = "11111111-1111-1111-1111-111111111111"
tenant_id = "22222222-2222-2222-2222-222222222222"
}
}
}

variables {
subscription_id = "11111111-1111-1111-1111-111111111111"
tenant_id = "22222222-2222-2222-2222-222222222222"
cudly_issuer_url = "https://cudly.example.com/oidc"
cudly_api_url = ""
}

run "defaults" {
command = plan
assert {
condition = (
azuread_application_federated_identity_credential.cudly.issuer == var.cudly_issuer_url &&
azuread_application_federated_identity_credential.cudly.subject == "cudly-controller" &&
azuread_application_federated_identity_credential.cudly.audiences == tolist(["api://AzureADTokenExchange"])
)
error_message = "Default identity must be preserved exactly."
}
}

run "literal_claims" {
command = plan
variables {
cudly_issuer_url = "https://CUDly.example.com:8443/a_b/~v1%20/oidc"
cudly_federated_subject = "quote\"backslash\\backtick`apostrophe':é"
cudly_federated_audience = "quote\"backslash\\backtick`apostrophe':é"
}
assert {
condition = (
azuread_application_federated_identity_credential.cudly.issuer == var.cudly_issuer_url &&
azuread_application_federated_identity_credential.cudly.subject == var.cudly_federated_subject &&
azuread_application_federated_identity_credential.cudly.audiences == tolist([var.cudly_federated_audience])
)
error_message = "Literal identity bytes and singleton audience must be preserved."
}
}

run "invalid_claim_0" {
command = plan
variables {
cudly_federated_subject = null
cudly_federated_audience = null
}
expect_failures = [var.cudly_federated_subject, var.cudly_federated_audience]
}

run "invalid_claim_1" {
command = plan
variables {
cudly_federated_subject = ""
cudly_federated_audience = ""
}
expect_failures = [var.cudly_federated_subject, var.cudly_federated_audience]
}

run "invalid_claim_2" {
command = plan
variables {
cudly_federated_subject = "a b"
cudly_federated_audience = "a b"
}
expect_failures = [var.cudly_federated_subject, var.cudly_federated_audience]
}

run "invalid_claim_3" {
command = plan
variables {
cudly_federated_subject = "a\tb"
cudly_federated_audience = "a\tb"
}
expect_failures = [var.cudly_federated_subject, var.cudly_federated_audience]
}

run "invalid_claim_4" {
command = plan
variables {
cudly_federated_subject = "a\nb"
cudly_federated_audience = "a\nb"
}
expect_failures = [var.cudly_federated_subject, var.cudly_federated_audience]
}

run "invalid_claim_5" {
command = plan
variables {
cudly_federated_subject = "a\rb"
cudly_federated_audience = "a\rb"
}
expect_failures = [var.cudly_federated_subject, var.cudly_federated_audience]
}

run "invalid_claim_6" {
command = plan
variables {
cudly_federated_subject = "a\u000bb"
cudly_federated_audience = "a\u000bb"
}
expect_failures = [var.cudly_federated_subject, var.cudly_federated_audience]
}

run "invalid_claim_7" {
command = plan
variables {
cudly_federated_subject = "a\u000cb"
cudly_federated_audience = "a\u000cb"
}
expect_failures = [var.cudly_federated_subject, var.cudly_federated_audience]
}

run "invalid_claim_8" {
command = plan
variables {
cudly_federated_subject = "a$b"
cudly_federated_audience = "a$b"
}
expect_failures = [var.cudly_federated_subject, var.cudly_federated_audience]
}

run "invalid_claim_9" {
command = plan
variables {
cudly_federated_subject = "a*b"
cudly_federated_audience = "a*b"
}
expect_failures = [var.cudly_federated_subject, var.cudly_federated_audience]
}

run "invalid_issuer_0" {
command = plan
variables {
cudly_issuer_url = null
}
expect_failures = [var.cudly_issuer_url]
}

run "invalid_issuer_1" {
command = plan
variables {
cudly_issuer_url = ""
}
expect_failures = [var.cudly_issuer_url]
}

run "invalid_issuer_2" {
command = plan
variables {
cudly_issuer_url = "http://example.com/oidc"
}
expect_failures = [var.cudly_issuer_url]
}

run "invalid_issuer_3" {
command = plan
variables {
cudly_issuer_url = "https://"
}
expect_failures = [var.cudly_issuer_url]
}

run "invalid_issuer_4" {
command = plan
variables {
cudly_issuer_url = "https://example.com/oidc/"
}
expect_failures = [var.cudly_issuer_url]
}

run "invalid_issuer_5" {
command = plan
variables {
cudly_issuer_url = "https://user@example.com/oidc"
}
expect_failures = [var.cudly_issuer_url]
}

run "invalid_issuer_6" {
command = plan
variables {
cudly_issuer_url = "https://example.com/?q=1"
}
expect_failures = [var.cudly_issuer_url]
}

run "invalid_issuer_7" {
command = plan
variables {
cudly_issuer_url = "https://example.com/#f"
}
expect_failures = [var.cudly_issuer_url]
}

run "invalid_issuer_8" {
command = plan
variables {
cudly_issuer_url = "https://example.com/a b"
}
expect_failures = [var.cudly_issuer_url]
}

run "invalid_issuer_9" {
command = plan
variables {
cudly_issuer_url = "https://example.com/a\nb"
}
expect_failures = [var.cudly_issuer_url]
}

run "invalid_issuer_10" {
command = plan
variables {
cudly_issuer_url = "https://example.com/a\"b"
}
expect_failures = [var.cudly_issuer_url]
}

run "invalid_issuer_11" {
command = plan
variables {
cudly_issuer_url = "https://example.com/a\\b"
}
expect_failures = [var.cudly_issuer_url]
}
19 changes: 19 additions & 0 deletions iac/federation/azure-target/terraform/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,37 @@ variable "app_display_name" {
variable "cudly_issuer_url" {
description = "CUDly OIDC issuer URL (e.g. https://cudly.example.com/oidc). Azure AD fetches JWKS from this issuer to verify client assertion JWTs."
type = string

# CUDly contract, mirrored by the two azure-wif shell templates.
validation {
condition = var.cudly_issuer_url == null ? false : (
can(regex("^https://[A-Za-z0-9.-]+(:[0-9]+)?(/[A-Za-z0-9._~%/-]*)?$", var.cudly_issuer_url)) &&
!endswith(var.cudly_issuer_url, "/")
)
error_message = "Use a CUDly HTTPS issuer URL without a trailing slash, query, fragment or userinfo."
}
}

variable "cudly_federated_subject" {
description = "Subject claim in the client assertion JWT. Must match what CUDly signs."
type = string
default = "cudly-controller"

validation {
condition = var.cudly_federated_subject == null ? false : can(regex("^[^ \\t\\n\\r\\x0b\\f$*]+$", var.cudly_federated_subject))
error_message = "Subject must be nonempty without ASCII whitespace, dollar signs or asterisks."
}
}

variable "cudly_federated_audience" {
description = "Audience for the federated identity credential."
type = string
default = "api://AzureADTokenExchange"

validation {
condition = var.cudly_federated_audience == null ? false : can(regex("^[^ \\t\\n\\r\\x0b\\f$*]+$", var.cudly_federated_audience))
error_message = "Audience must be nonempty without ASCII whitespace, dollar signs or asterisks."
}
}

variable "cudly_api_url" {
Expand Down
8 changes: 7 additions & 1 deletion internal/iacfiles/templates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ from this directory via the `//go:embed` directive in `internal/iacfiles/embed.g
Use `scripts/generate-federation-iac.go`, a self-contained Go script with no
external dependencies.

Azure targets require `--cudly-api-url` with the CUDly HTTPS base URL, without a
trailing slash. The generator appends `/oidc` for the issuer and preserves custom
paths. Contact email remains optional; AWS and GCP targets may omit the API URL
to skip automatic registration.

Every AWS target with a non-AWS source requires `--oidc-subject-claim`: it is
the workload subject the generated AWS trust policy pins to, and there is no
working default (see #1640). Pass the calling workload's subject claim, which is
Expand Down Expand Up @@ -101,7 +106,8 @@ go run scripts/generate-federation-iac.go \
go run scripts/generate-federation-iac.go \
--target azure --source aws \
--account-name "prod-azure" --account-id "sub-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
--tenant-id "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
--tenant-id "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" \
--cudly-api-url "https://cudly.example.com"

# GCP target, AWS source
go run scripts/generate-federation-iac.go \
Expand Down
Loading
Loading