How Scopes Work#
Access to resources in Taskcluster are controlled by scopes. While they are a simple and effective way of providing access control, they can be difficult to understand.
This article aims to de-mystify scopes by explaining how they work.
Scope Components#
There are four pieces that work together to provide security guarantees from scopes:
Task definition
Deployment configuration
Auth and Queue services
Scope consumer
Let’s look at each piece in more detail and go over how they interact with one another.
Task Definition#
A task declares which scopes it requires in its definition. For example:
scopes:
- my:awesome:scope
- another-scope
# .. rest of task definition
It is a list of strings, where each string denotes a scope that the task will require to execute its payload.
Note
Scopes are typically delimited by :
to break it up into logical
segments, but this is only a convention.
Deployment Configuration#
Each Taskcluster deployment has associated configuration that defines the worker pools, secrets, hooks and yes, scopes.
In its simplest form, this configuration can be input in the Taskcluster web interface. But for more complicated instances, configuration is managed via repository.
Note
Here are some configuration repositories for known Taskcluster instances:
Firefox-CI configuration (managed by the Release Engineering team in #firefox-ci)
Community configuration (managed by the Taskcluster team in #taskcluster)
Each deployment will create a number of roles and determine which scopes those
roles have access to. In this example from the Firefox-CI instance, the scope
hooks:trigger-hook:project-releng/cron-task-mozilla-mobile-firefox-android/nightly
is being granted to a role that is assumed by tasks that run on the main
branch
of the Firefox Android repo.
The deployment configuration is full of rules like that. It acts as the access control list for a specific Taskcluster deployment.
Queue and Auth Services#
Given the requested scopes in a task definition and the access control list in the deployment configuration, Taskcluster can now make a decision about whether a task is authorized to use the scopes it requests or not. This is where the queue and auth services come in.
The queue service:
Inspects the requested scopes in the task’s definition.
Queries the auth service to get the current set of expanded scopes.
Determines whether the current scopes satisfy the task’s requested scopes. If the scopes are satisfied the task is created, otherwise an error occurs.
Where do Current Scopes Come From?#
When the createTask API is used, the caller must have a superset of the scopes required by the task. But where do the caller’s scopes come from? In Taskgraph, the Decision Task is responsible for creating tasks, so it must contain a superset of the scopes all the tasks it creates need. But in that case, where do the Decision task’s scopes come from?
Typically the Decision task gets its scopes from Taskcluster’s Github service. Specifically the Github service assumes a role based on the webhook event generated by Github and the deployment configuration defines which scopes are attached to that role.
Note
The Firefox-CI instance has additional tasks not triggered by Github. Namely
tasks running in response to events from hg.mozilla.org
, cron
and
action
tasks. The role assumed by these tasks is defined by the
build-decision package.
Scope Consumer#
So far we have a set of requested scopes defined in the task definition, an access control list defined in the deployment configuration, and the queue / auth services have validated the task is allowed to contain the scopes.
So far so good! But you may have noticed there is still something missing! How do the scopes actually influence task execution? Take the following task definition:
scopes:
- secrets:get:my-secret
How does adding this scope actually grant access to my-secret
? More
importantly, how does omitting it deny access to my-secret
?
This is where things get a bit ad-hoc. For every scope, there is some code somewhere that operates on a task definition. For the purposes of this article, let’s call this the scope consumer. The scope consumer needs to validate that the task contains the appropriate scope necessary to perform the sensitive action.
For the example above, the scope consumer is the secrets service. The secrets
service has code that will return a 403
response if you attempt to download a secret
without the proper scope. The secrets service doesn’t need to worry about whether the
task is authorized to use the scope, because the queue
and auth
services have
already done so.
It’s worth noting that anything can be a scope consumer. Internal Taskcluster services, external services, workers, packages published to package managers and even simple scripts.
Implicit Scopes#
So far, we’ve only discussed explicit scopes. That is scopes that are defined directly in the task definition. But there are also implicit scopes.
If you’ve worked with Taskcluster long enough, you’ve likely encountered an error that looked something like this:
ERROR - Client ID task-client/Vf_Z9695QM6CUFQ-3DvBlg/0/on/us-west1/1068496344932690688/until/1697225520.15 does not have sufficient scopes and is missing the following scopes:
{
"AnyOf": [
"queue:create-task:highest:gecko-t/t-linux-large-gcp",
"queue:create-task:very-high:gecko-t/t-linux-large-gcp",
"queue:create-task:high:gecko-t/t-linux-large-gcp",
"queue:create-task:medium:gecko-t/t-linux-large-gcp",
"queue:create-task:low:gecko-t/t-linux-large-gcp"
]
}
If you happened to look at the generated definition for the failing task, you
might notice that the task doesn’t declare any queue:create-task:
scopes!
So what gives, why are you getting errors about it?
The queue:create-task:
scopes are an example of implicit scopes. That is,
the queue service derives the scopes from other parts of the task definition.
In this case, from the task’s taskQueueId key. Since the taskQueueId
is
already known and we know that using it requires a scope, forcing task authors
to define both taskQueueId
and a scope with the same name would be adding
unnecessary busy work.
Implicit Scopes with External Scope Consumers#
Implicit scopes are typically always ones where the scope consumer is an internal Taskcluster service. The reason is that if scope satisfaction fails, the task can be prevented from running.
It’s theoretically possible for external scope consumers to use implicit scopes as well. They could:
Derive scopes from other parts of a task’s definition.
Query the
auth
service in a similar manner as thequeue
service does.Abort the operation if scopes are not satisfied.
The major downside to this approach is that the satisfaction check happens during task execution instead of before the task is created. This means, not only does this task waste resources, but also its dependencies and potentially every task in the entire task group is wasted (depending how important the task in question is).
It’s ok for internal Taskcluster services to do this because they run before the task is created, not after.