Skip to content
Draft
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
165 changes: 165 additions & 0 deletions .agents/skills/stackit-backplane.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
---
description: STACKIT backplane identity conventions for meshstack-hub modules under modules/stackit/. Covers service account + key pattern, required variables/outputs, provider configuration, meshstack_integration.tf wiring, and the STACKIT backplane checklist.
---

# STACKIT Backplane Identity Conventions

STACKIT backplanes **must** use a **service account with a long-lived key** as the automation
principal for building block execution. The key JSON is provisioned in the backplane and injected
as a sensitive static input into the building block definition.

## Rationale

- **Self-contained credentials**: The service account and its key are provisioned once in the
backplane Terraform module. The key JSON is a single credential that bundles the service account
email, key ID, and private key — no extra wiring needed.
- **Least-privilege**: Each building block gets its own service account with exactly the roles it
needs (project-scoped or organization-scoped).
- **No provider configuration in backplane**: The backplane module does not include a `provider.tf`.
Authentication for the backplane itself is configured by the caller (e.g. the platform team running
`tofu apply` or the integration runtime).
- **Sensitive by default**: The `service_account_key_json` output is marked `sensitive = true`.
meshStack's STATIC input wiring uses the `sensitive.argument.secret_value` field to ensure the
key is stored and transmitted as a secret.

<!-- scorecard-checks: stackit_uses_service_account_key -->
## Implementation Pattern

```hcl
# backplane/main.tf — service account + key + role assignments

resource "stackit_service_account" "backplane" {
project_id = var.project_id
name = "mesh-<service-name>"
}

resource "stackit_service_account_key" "backplane" {
project_id = var.project_id
service_account_email = stackit_service_account.backplane.email
}

# Project-scoped role assignment (use this for project-level resources):
resource "stackit_authorization_project_role_assignment" "this" {
resource_id = var.project_id
role = "<required-role>"
subject = stackit_service_account.backplane.email
}

# Organization-scoped role assignment (use this for org-level resources):
resource "stackit_authorization_organization_role_assignment" "this" {
resource_id = var.organization_id
role = "<required-role>"
subject = stackit_service_account.backplane.email
}
```

<!-- scorecard-checks: stackit_service_account_key_output -->
## Backplane Outputs (STACKIT)

Every STACKIT backplane must output the service account key JSON:

```hcl
output "service_account_key_json" {
value = stackit_service_account_key.backplane.json
description = "Service account key JSON for authenticating the STACKIT provider in the buildingblock."
sensitive = true
}
```

Additional outputs (e.g. `project_id`, resource IDs) can be added as needed.

## Backplane Variables (STACKIT)

All STACKIT backplanes require at minimum:

```hcl
variable "project_id" {
type = string
nullable = false
description = "STACKIT project ID where the service account will be created."
}
```

Backplanes that manage organization-level resources also require:

```hcl
variable "organization_id" {
type = string
nullable = false
description = "STACKIT organization ID where the service account will be granted permissions."
}
```

<!-- scorecard-checks: stackit_provider_uses_key -->
## Buildingblock Provider Configuration

The buildingblock `provider.tf` must use `service_account_key` for authentication.
Do **not** use `service_account_email` alone — it does not authenticate.

```hcl
# buildingblock/provider.tf
provider "stackit" {
service_account_key = var.service_account_key_json
# Add any extra provider flags required by the resources (e.g. enable_beta_resources, experiments):
# enable_beta_resources = true
# experiments = ["some-feature"]
}
```

## Buildingblock Variable

```hcl
variable "service_account_key_json" {
type = string
nullable = false
sensitive = true
description = "Service account key JSON for authenticating the STACKIT provider."
}
```

The key JSON bundles the service account email — do **not** add a separate `service_account_email`
variable when `service_account_key_json` is present.

## `meshstack_integration.tf` Wiring (STACKIT)

Pass the key from the backplane as a **STATIC sensitive** input:

```hcl
module "backplane" {
source = "github.com/meshcloud/meshstack-hub//modules/stackit/<service>/backplane?ref=${var.hub.git_ref}"

project_id = var.stackit_project_id
# organization_id = var.stackit_organization_id # if org-scoped roles are needed
}

# Inside meshstack_backplane_definition version_spec.inputs:
service_account_key_json = {
display_name = "Service Account Key JSON"
description = "Service account key JSON for authenticating the STACKIT provider."
type = "STRING"
assignment_type = "STATIC"
sensitive = {
argument = {
secret_value = module.backplane.service_account_key_json
}
}
}
```

## What to Avoid

- ❌ `service_account_email` alone in the provider — missing authentication credential
- ❌ Long-lived `STACKIT_SERVICE_ACCOUNT_TOKEN` injected via env var — not reproducible across runs
- ❌ Hardcoded key values in integration files
- ❌ Non-sensitive output for `service_account_key_json` — always mark it `sensitive = true`

## Checklist for STACKIT Backplanes

- [ ] `stackit_service_account` resource present
- [ ] `stackit_service_account_key` resource present (same project as the service account)
- [ ] Required role assignments present (`stackit_authorization_project_role_assignment` or `stackit_authorization_organization_role_assignment`)
- [ ] `service_account_key_json` output marked `sensitive = true`
- [ ] Buildingblock `provider.tf` uses `service_account_key = var.service_account_key_json`
- [ ] Buildingblock `variables.tf` has `service_account_key_json` (sensitive, nullable = false)
- [ ] No separate `service_account_email` variable in buildingblock when key is present
- [ ] `meshstack_integration.tf` wires key via `sensitive.argument.secret_value`
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,10 @@ See [.agents/skills/aws-backplane.md](.agents/skills/aws-backplane.md) for the f

See [.agents/skills/azure-backplane.md](.agents/skills/azure-backplane.md) for the full Azure backplane identity conventions, including UAMI patterns, WIF wiring, required variables/outputs, and the Azure backplane checklist.

## STACKIT Backplane Identity Conventions

See [.agents/skills/stackit-backplane.md](.agents/skills/stackit-backplane.md) for the full STACKIT backplane identity conventions, including the service account + key pattern, required variables/outputs, provider configuration, and the STACKIT backplane checklist.

---

<!-- scorecard-checks: readme_frontmatter, logo, app_team_readme, bbd_readme, bbd_readme_no_leading_heading, bbd_readme_shared_responsibility, no_documentation_md_output -->
Expand Down
2 changes: 1 addition & 1 deletion modules/stackit/meshstack_integration.tf
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ terraform {
}
stackit = {
source = "stackitcloud/stackit"
version = "~> 0.89.0"
version = ">= 0.88.0"
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion modules/stackit/project/backplane/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ module "project_backplane" {
| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.11.0 |
| <a name="requirement_stackit"></a> [stackit](#requirement\_stackit) | ~> 0.89.0 |
| <a name="requirement_stackit"></a> [stackit](#requirement\_stackit) | >= 0.88.0 |

## Modules

Expand Down
2 changes: 1 addition & 1 deletion modules/stackit/project/backplane/versions.tf
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ terraform {
required_providers {
stackit = {
source = "stackitcloud/stackit"
version = "~> 0.89.0"
version = ">= 0.88.0"
}
}
}
35 changes: 35 additions & 0 deletions modules/stackit/spoke-network/backplane/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
resource "stackit_service_account" "backplane" {
project_id = var.project_id
name = "mesh-spoke-network"
}

resource "stackit_service_account_federated_identity_provider" "backplane" {
for_each = { for i, s in var.workload_identity_federation.subjects : tostring(i) => s }

project_id = var.project_id
service_account_email = stackit_service_account.backplane.email
name = "meshstack-${each.key}"
issuer = var.workload_identity_federation.issuer

assertions = [
{
item = "aud"
operator = "equals"
value = "api://AzureADTokenExchange"
},
{
item = "sub"
operator = "equals"
value = each.value
}
]
}

# network.admin at org scope allows managing routing tables in the network area
# and routed networks in tenant projects. Least-privilege alternative: if STACKIT
# introduces a narrower "network.editor" role, prefer that.
resource "stackit_authorization_organization_role_assignment" "network_admin" {
resource_id = var.organization_id
role = "iaas.network.admin"
subject = stackit_service_account.backplane.email
}
4 changes: 4 additions & 0 deletions modules/stackit/spoke-network/backplane/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
output "service_account_email" {
value = stackit_service_account.backplane.email
description = "Email of the STACKIT service account used by the buildingblock provider via WIF."
}
20 changes: 20 additions & 0 deletions modules/stackit/spoke-network/backplane/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
variable "project_id" {
type = string
nullable = false
description = "STACKIT project ID where the service account will be created."
}

variable "organization_id" {
type = string
nullable = false
description = "STACKIT organization ID where the service account will be granted network management permissions."
}

variable "workload_identity_federation" {
type = object({
issuer = string
subjects = list(string)
})
nullable = false
description = "WIF issuer URL and subject list for the meshStack building block identity provider."
}
10 changes: 10 additions & 0 deletions modules/stackit/spoke-network/backplane/versions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
terraform {
required_version = ">= 1.12.0"

required_providers {
stackit = {
source = "stackitcloud/stackit"
version = "~> 0.98.0"
}
}
}
31 changes: 31 additions & 0 deletions modules/stackit/spoke-network/buildingblock/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
name: STACKIT Spoke Network
supportedPlatforms:
- stackit
description: Provisions a routed network in a STACKIT project and attaches it to the platform hub network area.
---

# STACKIT Spoke Network — Building Block

Provisions a routed network in a STACKIT project and attaches it to the platform hub network area. Optionally creates a custom routing table with a default route via a firewall next-hop.

## Inputs

| Name | Type | Description |
|------|------|-------------|
| `project_id` | string | Tenant STACKIT project ID (from PLATFORM_TENANT_ID) |
| `organization_id` | string | STACKIT organization ID |
| `network_area_id` | string | Hub network area ID |
| `service_account_key_json` | string (sensitive) | Backplane SA credentials |
| `network_prefix_length` | number | Subnet prefix length (24–28, default 25) |
| `firewall_next_hop_ip` | string | Next-hop IP for default route; null = no routing table |
| `ipv4_nameservers` | string | JSON-encoded nameserver list; null = STACKIT defaults |

## Outputs

| Name | Description |
|------|-------------|
| `network_id` | Spoke network ID |
| `network_cidr` | Allocated CIDR block |
| `routing_table_id` | Custom routing table ID (null if no firewall) |
| `summary` | Markdown summary rendered in meshStack |
10 changes: 10 additions & 0 deletions modules/stackit/spoke-network/buildingblock/SUMMARY.md.tftpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Spoke Network

| Property | Value |
|----------|-------|
| **Network ID** | `${network_id}` |
| **Network CIDR** | `${network_cidr}` |
| **Hub Network Area** | `${network_area_id}` |
%{~ if has_routing_table}
| **Routing Table** | `${routing_table_id}` |
%{~ endif}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 29 additions & 0 deletions modules/stackit/spoke-network/buildingblock/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
locals {
nameservers = var.ipv4_nameservers != null && var.ipv4_nameservers != "" ? split(",", var.ipv4_nameservers) : null
}

resource "stackit_routing_table" "this" {
count = var.firewall_next_hop_ip != null ? 1 : 0
organization_id = var.organization_id
network_area_id = var.network_area_id
name = "spoke-${var.project_id}"
system_routes = false
}

resource "stackit_routing_table_route" "this" {
count = var.firewall_next_hop_ip != null ? 1 : 0
organization_id = var.organization_id
network_area_id = var.network_area_id
routing_table_id = stackit_routing_table.this[0].routing_table_id
destination = { type = "cidrv4", value = "0.0.0.0/0" }
next_hop = { type = "ipv4", value = var.firewall_next_hop_ip }
}

resource "stackit_network" "this" {
project_id = var.project_id
name = "spoke-routed"
ipv4_prefix_length = var.network_prefix_length
ipv4_nameservers = local.nameservers
routed = true
routing_table_id = var.firewall_next_hop_ip != null ? stackit_routing_table.this[0].routing_table_id : null
}
25 changes: 25 additions & 0 deletions modules/stackit/spoke-network/buildingblock/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
output "network_id" {
value = stackit_network.this.network_id
description = "ID of the spoke network."
}

output "network_cidr" {
value = stackit_network.this.ipv4_prefix
description = "Allocated IPv4 CIDR block of the spoke network."
}

output "routing_table_id" {
value = var.firewall_next_hop_ip != null ? stackit_routing_table.this[0].routing_table_id : null
description = "ID of the custom routing table, or null if no firewall next-hop is configured."
}

output "summary" {
description = "Summary with spoke network details."
value = templatefile("${path.module}/SUMMARY.md.tftpl", {
network_id = stackit_network.this.network_id
network_cidr = stackit_network.this.ipv4_prefix
network_area_id = var.network_area_id
has_routing_table = var.firewall_next_hop_ip != null
routing_table_id = var.firewall_next_hop_ip != null ? stackit_routing_table.this[0].routing_table_id : ""
})
}
6 changes: 6 additions & 0 deletions modules/stackit/spoke-network/buildingblock/provider.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
provider "stackit" {
service_account_email = var.service_account_email
use_oidc = true
enable_beta_resources = true
experiments = ["routing-tables", "network"]
}
Loading
Loading