Skip to content

Stack role bindingsยป

Stacks can receive role bindings to perform operations with elevated permissions, similar to how users, API keys, and IdP groups receive permissions through Spacelift's RBAC system.

Stack role attachments replace the deprecated Administrative flag, providing a more flexible, auditable, and powerful approach to granting stacks elevated permissions.

Why use stack role attachmentsยป

Stack role attachments offer significant advantages over the deprecated administrative flag:

Cross-space accessยป

Administrative flag limitation: Can only create resources in the stack's own space and subspaces.

Role attachments advantage: Can attach roles to sibling spaces, enabling horizontal access across the space tree. In the example below, a stack in ChildSpace1 can be granted access to ChildSpace2:

graph TD
    A[root] --> B[ChildSpace1]
    A --> C[ChildSpace2]

Fine-grained access controlยป

Administrative flag limitation: All-or-nothing approach - grants full Space Admin permission with every available permission.

Role attachments advantage: Use custom roles with specific permissions (for example, only context:create and workerpool:create).

This means a stack can create contexts and worker pools, but cannot manage any other resources, such as policies or webhooks.

Enhanced audit trailยป

Administrative flag: Basic audit trail with stack actor information.

Role attachments advantage: Audit trail webhooks include role information in the actor_roles field (array of role slugs).

This provides better visibility into what permissions the stack was using when performing actions. See the audit trail documentation for details.

Modern RBAC consistencyยป

Role attachments align stacks with the broader role-based access model already used by users, IdP groups, and API keys, providing a consistent permission management experience across all actors.

Assign roles to stacksยป

Prerequisitesยป

To attach a role to a stack, you need:

  • StackManage permission (or Space admin permission as fallback) to the stack's space
  • Space admin permission to the binding space (the space where the role will be effective)

Why both permissions are required

Creating a role binding that grants permissions to a space effectively allows the stack to act in that space. To prevent privilege escalation, you must have admin access to both spaces: the space where the stack resides and the space where the role will be effective.

Using the Web UIยป

  1. Navigate to the stack's Settings page, then choose Roles on the left
  2. Click Manage Roles on the top right
  3. In the sidebar, select the desired role and the target space
  4. Click Add

Using the Terraform providerยป

Use the spacelift_role_attachment resource:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
resource "spacelift_space" "devops" {
  name            = "devops"
  description     = "A space for devops engineers"
  parent_space_id = "root"
}

resource "spacelift_stack" "devops_admin" {
  name        = "Admin stacks for Devs"
  repository  = "stacks-for-devs"
  space_id    = spacelift_space.devops.id
  description = "Only has permissions to create another stacks in the dev space"
  branch      = "main"
}

resource "spacelift_role" "stack_creator" {
  name        = "Stack creator"
  description = "A role solely for managing stacks"
  actions     = ["STACK_MANAGE"]
}

resource "spacelift_space" "dev" {
  name            = "dev"
  description     = "A space for development stacks"
  parent_space_id = "root"
}

resource "spacelift_role_attachment" "stack_creator_to_devops_admin_stack" {
  stack_id = spacelift_stack.devops_admin.id # (1)
  role_id  = spacelift_role.stack_creator.id # (2)
  space_id = spacelift_space.dev.id # (3)
}
  1. The stack receiving the role attachment.
  2. The role to attach to the stack.
  3. The target space: this is where the role will be effective.

In the above scenario, the devops_admin stack will have the Stack creator role effective in the dev space, allowing it to create and manage stacks within that space.

For more information, see the Spacelift Terraform provider documentation.

Permission cascadingยป

Role attachments cascade down to child spaces, similar to how the administrative flag worked:

graph TD
    role{{ Role }}
    parentSpace[ParentSpace]
    childSpace1[ChildSpace1]
    childSpace2[ChildSpace2]
    grandchildSpace[GrandchildSpace]

    role ~~~ parentSpace
    role e1@-. Attached to .-> parentSpace
    e1@{animate: true}
    parentSpace --> childSpace1
    parentSpace --> childSpace2
    childSpace2 --> grandchildSpace

If a role is attached to ParentSpace, the same role will be effective in ChildSpace1, ChildSpace2, and GrandchildSpace as well.

Root space caution

Since the root space is the parent of all spaces, attaching roles to it affects all spaces in your account. Use this with extreme caution and only when necessary.

Root space restrictionยป

You can only assign a role to the root space if the stack itself is located in the root space. This restriction prevents unintentional access elevation - a stack in a child-of-root space cannot be granted permissions that cascade to all spaces in your account. If you need a stack in a child space to access resources across multiple spaces, attach roles to specific spaces rather than the root space.

Administrative flagยป

The administrative flag was deprecated and, on June 1st, 2026, automatically replaced by a Space Admin role attachment on each stack's own space. The flag is now ineffective: setting administrative = true does nothing, and any attached roles always take effect. If your OpenTofu/Terraform configuration still carries the attribute, see Migration from administrative flag for how to reconcile your state.

Policy integrationยป

Policies can react to stack role attachments through the stack.roles field in policy inputs. This enables policy-based logic based on what roles a stack has attached.

Example: Reject Space Admin role usageยป

1
2
3
4
5
6
package spacelift

reject_with_note contains "Don't use the Space Admin role!" if {
  some role in input.stack.roles
  role.id == "space-admin" # (1)
}
1
2
3
4
5
6
package spacelift

reject_with_note["Don't use the Space Admin role!"] {
  role := input.stack.roles[_]
  role.id == "space-admin" # (1)
}
  1. Role slug. Use either "Copy Slug" button in the UI or the spacelift_role data source to retrieve it.

Multiple rolesยป

Stacks can have multiple role bindings:

  • Different roles in different spaces for varied access levels
  • Multiple roles in the same space (permissions are additive)
  • Combinations of Space Admin in own space and Reader in other spaces

Example: Multiple role attachmentsยป

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Admin access to development space
resource "spacelift_role_attachment" "dev_admin" {
  stack_id = spacelift_stack.platform.id
  role_id  = "space-admin"
  space_id = "development-space-id"
}

# Read access to production space
resource "spacelift_role_attachment" "prod_reader" {
  stack_id = spacelift_stack.platform.id
  role_id  = "space-reader"
  space_id = "production-space-id"
}

External state accessยป

External state access allows you to read the state of a stack from outside authorized runs and tasks. See the documentation here for further details.

In order for your stack to access another stack's OpenTofu/Terraform state, the stack needs the stack:state-read and stack:state-download actions on the target stack's space. This can be achieved by attaching a role with those actions to the stack for the target stack's space.

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
resource "spacelift_stack" "consumer" {
  # Properties are omitted for brevity
}

resource "spacelift_role" "state_reader" {
  name    = "state-reader"
  actions = ["STACK_STATE_READ", "STACK_STATE_DOWNLOAD"]
}

resource "spacelift_role_attachment" "state_reader" {
  stack_id = spacelift_stack.consumer.id
  role_id  = spacelift_role.state_reader.id
  space_id = spacelift_stack.provider.space_id
}

resource "spacelift_stack" "provider" {
  # Properties are omitted for brevity
  terraform_external_state_access = true
}

Note

The Space writer and Space admin roles also include stack:state-read and stack:state-download.

Migration from administrative flagยป

On June 1st, 2026, Spacelift automatically disabled every administrative flag and attached the built-in Space Admin role to each stack's own space. At the permission level this migration is fully backward compatible: every stack kept the exact access it had before.

If you manage your stacks through OpenTofu/Terraform, there's one thing left to reconcile. Your configuration still declares administrative = true, but the flag is now ineffective, and the role binding that replaced it was created on the backend, outside your state. As a result, the next plan shows drift, and you can't fix it by re-running apply: the flag can no longer be set back to true. The way out is to drop the dead attribute and import the role binding the migration already created. The rest of this section walks through that.

Note

The Space Admin role is a built-in system role, so you don't need to create it manually, it already exists in your Spacelift account.

What the automatic migration didยป

On June 1st, 2026, Spacelift:

  • Disabled all administrative flags
  • Attached the Space Admin role to each stack's own space (fully backward compatible)
    • Note: if you move the stack to a different space later, the role attachment remains unchanged and will not follow the stack's new space
  • Removed the administrative flag from the UI
  • Made the flag ineffective in the GraphQL API (even if set to true, it behaves as false)

Reconciling your Terraform/OpenTofu stateยป

This only applies to stacks managed through OpenTofu/Terraform that still carry an administrative attribute. Stacks managed only through the UI need no action.

Because the migration already created the Space Admin role binding for you, don't add a new spacelift_role_attachment and apply it: that would either fail or leave you with a duplicate binding. Import the existing binding instead.

1. Remove the administrative attributeยป

The flag no longer does anything, and leaving administrative = true in your configuration keeps producing drift. Remove it:

1
2
3
4
resource "spacelift_stack" "management" {
   name           = "Management Stack"
-  administrative = true
}

2. Get the role binding IDยป

You need the ID of the Space Admin binding the migration created. In the UI, open the stack's Settings page, choose Roles, find the Space admin binding on the stack's own space, open the row's action menu (the ellipsis on the right) and click Copy ID. The same ID is also available through the GraphQL API.

3. Add the resource and import the bindingยป

Declare the attachment in your configuration so Terraform has somewhere to import into:

1
2
3
4
5
6
7
8
9
data "spacelift_role" "admin_role" {
  slug = "space-admin"
}

resource "spacelift_role_attachment" "stack_admin" {
  stack_id = spacelift_stack.management.id
  space_id = spacelift_stack.management.space_id
  role_id  = data.spacelift_role.admin_role.id
}

Then import the existing binding. The import ID is the STACK/ prefix followed by the role binding ID from the previous step:

1
terraform import spacelift_role_attachment.stack_admin STACK/<role-binding-id>

If you prefer a declarative import that runs as part of terraform apply, add an import block:

1
2
3
4
import {
  to = spacelift_role_attachment.stack_admin
  id = "STACK/<role-binding-id>"
}

4. Verify the plan is cleanยป

Run terraform plan. After removing the flag and importing the binding, the plan should report no changes for either the stack or the attachment. Trigger a tracked run to confirm the stack still performs the same operations as before.

If a stack has more access than it needsยป

The migration grants every stack the Space Admin role, which mirrors the old administrative flag. Re-enabling the flag is no longer an option, so you can't roll back to it. If Space Admin turns out to be broader than a stack actually needs, replace the binding with a narrower custom role: create a role with only the actions the stack uses, attach it to the stack's space, and unassign the Space Admin binding.

Adjust policies if necessaryยป

If any of your policies reference the stack.administrative field, update them to use the stack.roles field instead. For example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Old policy:
deny contains "Administrative stacks are not allowed" if {
  stack := input.spacelift.stack
  stack.administrative == true
}

# Would become:
deny contains "Administrative stacks are not allowed" if {
  some role in input.stack.roles
  role.id == "space-admin" # (1)
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Old policy:
deny["Administrative stacks are not allowed"] {
  stack := input.spacelift.stack
  stack.administrative == true
}

# Would become:
deny["Administrative stacks are not allowed"] {
  role := input.stack.roles[_]
  role.id == "space-admin" # (1)
}
  1. Role slug. Use either "Copy Slug" button in the UI or the spacelift_role data source to retrieve it.

Edge casesยป

Stack moving between spacesยป

When a stack moves to a different space, existing role bindings remain unchanged. This is intentional and important for Terraform provider stability.

If you want to update role bindings after moving a stack, you need to explicitly modify the role attachments.