Skip to content

Access policy»

Warning

Access policies are deprecated and will be entirely disabled on May 30, 2026. Use space access control instead.

By default, non-admin Spacelift users have no access to any stacks or modules and must be granted explicit permissions. There are two levels of non-admin access, reader and writer. For now all we need to care about is that access policies are what we use to give appropriate level of access to individual stacks to non-admin users in your account.

This type of access control is typically done either by building a separate user management system on your end or piggy-backing on one created by your identity provider. Both solutions have their limitations:

  • Separate user management system: Makes it more difficult for organizations to onboard and offboard users, and can be difficult to get right, especially if you require granular and sophisticated access controls.
  • Identity provider: A safer bet used by many DevTools vendors. With this approach, having some access level to a GitHub repo would give you the same access level to all Spacelift stacks and/or modules associated with it, which can be inflexible if you want separate access controls for two stacks linked to one repo (such as staging and production).

Access policies give you a tool to build your own access management system using data obtained either from our identity provider (GitHub), or from your identity provider if using the Single Sign-On integration.

Rules»

Your access policy can define the following boolean rules:

  • write: Grants the current user write access to the stack or module.
  • read: Grants the current user read access to the stack or module.
  • deny: Denies the current user all access to the stack or module, no matter the outcome of other rules.
  • deny_write: Denies the current user write access to the stack or module, no matter the outcome of other rules.

Write access automatically assumes read permissions, so there's no need to define separate read policies for writers.

Access policies are executed quickly. Internally, we expect that running all access policies on all the stacks in one request (stacks in the GraphQL API) will take less than 500 milliseconds, otherwise the request fails. While that is usually enough time for modern computers, fancy regex rules in your access policies could cause issues in some cases.

Access levels»

Note

The levels below describe the deprecated access policy model. For the current access levels, see the built-in system roles (Read, Write, Admin) in the RBAC system reference.

There are two levels of non-admin access to a Spacelift stack or module: reader and writer. These are pretty intuitive for most developers, but this section will cover them in more detail to avoid any possible confusion.

In every non-trivial organization there will be different roles: people who build and manage shared infrastructure, people who build and manage their team or project-level infrastructure, and people who use this infrastructure to build great things.

Admins»

The people who manage your Spacelift accounts. They need to be able to set up:

Use login policies to manage admin access.

Writers»

The people who manage their team or project-level infrastructure. This group should be able to:

Writers should still operate within the boundaries defined by admins, who do that mainly by attaching contexts and policies to the stacks.

Readers»

The people who build on top of existing infrastructure. They don't need to define the infra, but they need to understand what's available and when things are changing. You'll probably want to allow them to contribute to infra definitions, too, and allow them to see feedback from proposed runs.

Readers can't do anything, but they can see everything. Most modern organizations tend to provide this level of access to as many stakeholders as possible to maintain transparency and facilitate collaboration.

Data input schema»

Each policy request will receive this data input.

Official Schema Reference

For the most up-to-date and complete schema definition, please refer to the official Spacelift policy contract schema under the ACCESS policy type.

  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
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
{
  "request": {
    // A UI or API check carries remote_ip. A Slack check carries timestamp_ns instead.
    "remote_ip": "string - IP of the user making a request",
    "timestamp_ns": "number - current Unix timestamp in nanoseconds"
  },
  "session": { // absent when the check comes from a Slack interaction
    "admin": "boolean - is the current user a Spacelift admin",
    "creator_ip": "string - IP address of the user who created the session",
    "login": "string - GitHub username of the logged in user",
    "name":  "string - full name of the logged in GitHub user - may be empty",
    "teams": ["string - names of org teams the user is a member of"],
    "machine": "boolean - whether the creator is a machine or a user"
  },
  "stack": { // when access to a stack is being evaluated
    "id": "string - unique ID of the stack",
    "additional_project_globs": ["string - additional project glob patterns"],
    "administrative": "boolean - is the stack administrative",
    "autodeploy": "boolean - is the stack currently set to autodeploy",
    "branch": "string - tracked branch of the stack",
    "contexts": ["string - slugs of the contexts attached to the stack"],
    "labels": ["string - list of arbitrary, user-defined selectors"],
    "locked_by": "optional string - if the stack is locked, this is the name of the user who did it",
    "name": "string - name of the stack",
    "namespace": "string - repository namespace, only relevant to GitLab repositories",
    "project_root": "optional string - project root as set on the Stack, if any",
    "repository": "string - name of the source repository",
    "roles": [{
      "id": "string - the role slug, kept under this name for backwards compatibility",
      "name": "string - the role name",
      "slug": "string - the role slug, eg. space-admin",
      "ulid": "string - the role ULID"
    }],
    "space": {
      "id": "string - the space slug",
      "labels": ["string - list of arbitrary, user-defined selectors"],
      "name": "string - name of the space"
    },
    "state": "string - current state of the stack",
    "terraform_version": "string or null - last Terraform version used to apply changes (null for non-Terraform vendors)",
    "terraform_workflow_tool": "string or null - Terraform workflow tool, for Terraform stacks only",
    "tracked_commit": {
      "author": "string - GitHub login if available, name otherwise",
      "branch": "string - branch to which the commit was pushed",
      "created_at": "number - creation Unix timestamp in nanoseconds",
      "hash": "string - the commit hash",
      "message": "string - commit message",
      "tag": "string - Git tag, if the commit carries one",
      "url": "string - URL to the commit"
    },
    "worker_pool": {
      "id": "string - the worker pool ID. Absent for the public pool",
      "labels": ["string - list of arbitrary, user-defined selectors. Absent for the public pool"],
      "name": "string - name of the worker pool. Absent for the public pool",
      "public": "boolean - is the worker pool public"
    }
  },
  "module": { // when access to a module is being evaluated
    "administrative": "boolean - is the module administrative",
    "branch": "string - tracked branch of the module",
    "contexts": ["string - slugs of the contexts attached to the module"],
    "id": "string - unique ID of the module",
    "labels": ["string - list of arbitrary, user-defined selectors"],
    "name": "string - name of the module",
    "namespace": "string - repository namespace, only relevant to GitLab repositories",
    "project_root": "string - project root as set on the module, if any",
    "repository": "string - name of the source repository",
    "space": {
      "id": "string - the space slug",
      "labels": ["string - list of arbitrary, user-defined selectors"],
      "name": "string - name of the space"
    },
    "terraform_provider": "string - name of the main Terraform provider used by the module",
    "worker_pool": {
      "id": "string - the worker pool ID. Absent for the public pool",
      "labels": ["string - list of arbitrary, user-defined selectors. Absent for the public pool"],
      "name": "string - name of the worker pool. Absent for the public pool",
      "public": "boolean - is the worker pool public"
    }
  },
  "slack": { // only when the access check comes from a Slack interaction
    "channel": {
      "id": "string - Slack channel identifier",
      "name": "string - Slack channel name"
    },
    "command": "string - Slack command used",
    "team": {
      "id": "string - Slack team identifier",
      "name": "string - Slack team name"
    },
    "user": {
      "deleted": "boolean - whether the Slack account is deleted",
      "display_name": "string - display name from the user's Slack profile",
      "enterprise": {
        "enterprise_id": "string - Enterprise Grid organisation identifier",
        "enterprise_name": "string - Enterprise Grid organisation name",
        "id": "string - identifier of the user within the Enterprise Grid organisation",
        "is_admin": "boolean - whether the user administers the Enterprise Grid organisation",
        "is_owner": "boolean - whether the user owns the Enterprise Grid organisation",
        "teams": ["string - workspaces within the organisation that the user belongs to"]
      },
      "has_2fa": "boolean - whether the user has two factor authentication enabled",
      "id": "string - Slack user identifier",
      "is_admin": "boolean - whether the user administers the workspace",
      "is_owner": "boolean - whether the user owns the workspace",
      "is_primary_owner": "boolean - whether the user is the workspace's primary owner",
      "is_restricted": "boolean - whether the user is a multi-channel guest",
      "is_stranger": "boolean - whether the user belongs to no shared workspace",
      "is_ultra_restricted": "boolean - whether the user is a single-channel guest",
      "real_name": "string - the user's real name as Slack holds it",
      "tz": "string - the user's Slack timezone"
    }
  }
}

Examples»

Tip

We maintain a library of example policies that are ready to use or alter to meet your specific needs.

If you cannot find what you are looking for below or in the library, please reach out to our support and we will craft a policy to do exactly what you need.

This section will cover some common examples that can be copied more or less directly, and some contrived ones to serve as inspiration.

Access policies must be attached to a stack or a module to take effect.

Read access to engineers»

In this policy, every member of the engineering team gets read access:

1
2
3
4
5
6
package spacelift

read if {
  some team in input.session.teams
  team == "Engineering"
}
1
2
3
package spacelift

read { input.session.teams[_] == "Engineering" }

Write only in-office during business hours»

In this policy, write access is only provided to users that are in the office (using the office's IP address range) during business hours (9 to 5, weekdays). Write access is restricted in other cases. This policy is best combined with one that gives read access.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package spacelift

now := input.request.timestamp_ns
clock := time.clock([now, "America/Los_Angeles"])
weekend := {"Saturday", "Sunday"}
weekday := time.weekday(now)
ip := input.request.remote_ip

write if {
  some team in input.session.teams
  team == "Product team"
}

deny_write if weekend[weekday]
deny_write if clock[0] < 9
deny_write if clock[0] > 17
deny_write if not net.cidr_contains("12.34.56.0/24", ip)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package spacelift

now     := input.request.timestamp_ns
clock   := time.clock([now, "America/Los_Angeles"])
weekend := { "Saturday", "Sunday" }
weekday := time.weekday(now)
ip      := input.request.remote_ip

write      { input.session.teams[_] == "Product team" }
deny_write { weekend[weekday] }
deny_write { clock[0] < 9 }
deny_write { clock[0] > 17 }
deny_write { not net.cidr_contains("12.34.56.0/24", ip) }

Protect administrative stacks»

Warning

The administrative flag was deprecated and, on June 1st, 2026, automatically replaced by a Space Admin role attachment. The flag now always returns false, so prefer reacting to the stack's roles instead.

Administrative stacks are powerful. Having write access to one is almost as good as being an admin, as you can define and attach contexts and policies. In this policy, we deny write access to administrative stacks entirely. This works since access policies are not evaluated for admin users.

1
2
3
package spacelift

deny_write if input.stack.administrative
1
2
3
package spacelift

deny_write { input.stack.administrative }