https://storyboard.openstack.org/#!/story/2001771
The aim of this spec is to extend the existing web API of Zuul to privileged actions, and to scope these actions to tenants, projects and privileged users.
Zuul 3 introduced tenant isolation, and most privileged actions, being scoped to a specific tenant, reflect that change. However the only way to trigger these actions is through the Zuul CLI, which assumes either access to the environment of a Zuul component or to Zuul's configuration itself. This is a problem as being allowed to perform privileged actions on a tenant or for a specific project should not entail full access to Zuul's admin capabilities.
Zuul will expose privileged actions through its web API. In order to do so, Zuul needs to support user authentication. A JWT (JSON Web Token) will be used to carry user information; from now on it will be called the Authentication Token for the rest of this specification.
Zuul needs also to support authorization and access control. Zuul's configuration will be modified to include access control rules.
A Zuul operator will also be able to generate an Authentication Token manually for a user, and communicate the Authentication Token to said user. This Authentication Token can optionally include authorization claims that override Zuul's authorization configuration, so that an operator can provide privileges temporarily to a user.
By querying Zuul's web API with the Authentication Token set in an "Authorization" header, the user can perform administration tasks.
Zuul will need to provide the following minimal new features:
The manual generation of Authentication Tokens can also be used for testing purposes or non-production environments.
Note that JWTs can be arbitrarily extended with custom claims, giving flexibility in its contents. It also allows to extend the format as needed for future features.
In its minimal form, the Authentication Token's contents will have the following format:
{
'iss': 'jwt_provider',
'aud': 'my_zuul_deployment',
'exp': 1234567890,
'iat': 1234556780,
'sub': 'alice'
}
These are standard JWT claims and ensure that Zuul can consume JWTs issued by external authentication systems as Authentication Tokens, assuming the claims are set correctly.
Authentication Tokens lacking any of these claims will be rejected.
Authentication Tokens with an iss
claim not matching the white list of accepted issuers in Zuul's configuration will be rejected.
Authentication Tokens addressing a different audience than the expected one for the specific issuer will be rejected.
Unsigned or incorrectly signed Authentication Tokens will be rejected.
Authentication Tokens with an expired timestamp will be rejected.
Some JWT Providers can issue extra claims about a user, like preferred_username or email. Zuul will allow an operator to set such an extra claim as the default, unique user identifier in place of sub if it is more convenient.
If the chosen claim is missing from the Authentication Token, it will be rejected.
If the Authentication Token is issued manually by a Zuul Operator, it can include extra claims extending Zuul's authorization rules for the Authentication Token's bearer:
{
'iss': 'zuul_operator',
'aud': 'zuul.openstack.org',
'exp': 1234567890,
'iat': 1234556780,
'sub': 'alice',
'zuul': {
'admin': ['tenantA', 'tenantB']
}
}
In the previous example, user alice can perform privileged actions on every project of tenantA and tenantB. This is on top of alice's default authorizations.
These are intended to be whitelists: if a tenant is unlisted the user is assumed not to be allowed to perform a privileged action (unless the authorization rules in effect for this deployment of Zuul allow it.)
Note that iss is set to zuul_operator
. This can be used to reject Authentication Tokens with a zuul
claim if they come from other issuers.
The Zuul main.yaml configuration file will accept new admin-rule objects describing access rules for privileged actions.
Authorization rules define conditions on the claims in an Authentication Token; if these conditions are met the action is authorized.
In order to allow the parsing of claims with complex structures like dictionaries, an XPath-like format will be supported.
Here is an example of how rules can be defined:
- admin-rule:
name: affiliate_or_admin
conditions:
- resources_access.account.roles: "affiliate"
iss: external_institution
- resources_access.account.roles: "admin"
- admin-rule:
name: alice_or_bob
conditions:
- zuul_uid: alice
- zuul_uid: bob
Zuul's authorization engine will adapt matching tests depending on the nature of the claim in the Authentication Token, eg:
The special zuul_uid
claim refers to the uid_claim
setting in an authenticator's configuration, as will be explained below. By default it refers to the sub
claim of an Authentication Token.
This configuration file is completely optional, if the zuul.admin
claim is set in the Authentication Token to define tenants on which privileged actions are allowed.
Under the above example, the following Authentication Token would match rules affiliate_or_admin
and alice_or_bob
:
{
'iss': 'external_institution',
'aud': 'my_zuul_deployment',
'exp': 1234567890,
'iat': 1234556780,
'sub': 'alice',
'resources_access': {
'account': {
'roles': ['affiliate', 'other_role']
}
},
}
And this Authentication Token would only match rule affiliate_or_admin
:
{
'iss': 'some_hellish_dimension',
'aud': 'my_zuul_deployment',
'exp': 1234567890,
'sub': 'carol',
'iat': 1234556780,
'resources_access': {
'account': {
'roles': ['admin', 'other_role']
}
},
}
Privileged actions are tenant-scoped. Therefore the access control will be set in tenants definitions, e.g:
- tenant:
name: tenantA
admin_rules:
- an_authz_rule
- another_authz_rule
source:
gerrit:
untrusted-projects:
- org/project1:
- org/project2
- ...
- tenant:
name: tenantB
admin_rules:
- yet_another_authz_rule
source:
gerrit:
untrusted-projects:
- org/project1
- org/project3
- ...
An action on the tenantA
tenant will be allowed if an_authz_rule
OR another_authz_rule
is matched.
An action on the tenantB
tenant will be authorized if yet_another_authz_rule
is matched.
Unless specified, all the following endpoints require the presence of the Authorization
header in the HTTP query.
Unless specified, all calls to the endpoints return with HTTP status code 201 if successful, 401 if unauthenticated, 403 if the user is not allowed to perform the action, and 400 with a JSON error description otherwise. In case of a 401 code, an additional WWW-Authenticate
header is emitted, for example:
WWW-Authenticate: Bearer realm="zuul.openstack.org"
error="invalid_token"
error_description="Token expired"
Zuul's web API will be extended to provide the following endpoints:
This call allows a user to re-enqueue a buildset, like the enqueue or enqueue-ref subcommands of Zuul's CLI.
To trigger the re-enqueue of a change, the following JSON body must be sent in the query:
To trigger the re-enqueue of a ref, the following JSON body must be sent in the query:
{"trigger": <Zuul trigger>,
"ref": <ref>,
"oldrev": <oldrev>,
"newrev": <newrev>,
"pipeline": <pipeline>}
This call allows a user to dequeue a buildset, like the dequeue subcommand of Zuul's CLI.
To dequeue a change, the following JSON body must be sent in the query:
To dequeue a ref, the following JSON body must be sent in the query:
This call allows a user to automatically put a node set on hold in case of a build failure on the chosen job, like the autohold subcommand of Zuul's CLI.
Any of the following JSON bodies must be sent in the query:
{"change": <changeID>,
"reason": <reason>,
"count": <count>,
"node_hold_expiration": <expiry>,
"job": <job>}
or
{"ref": <ref>,
"reason": <reason>,
"count": <count>,
"node_hold_expiration": <expiry>,
"job": <job>}
This call returns the list of tenant the authenticated user can perform privileged actions on.
This endpoint can be consumed by web clients in order to know which actions to display according to the user's authorizations, either from Zuul's configuration or from the valid Authentication Token's zuul.admin
claim if present.
The return value is similar in form to the zuul.admin claim:
The call needs authentication and returns with HTTP code 200, or 401 if no valid Authentication Token is passed in the request's headers. If no rule applies to the user, the return value is
Zuul will log an event when a user presents an Authentication Token with a zuul.admin
claim, and if the authorization override is granted or denied:
At DEBUG level the log entry will also contain the zuul.admin
claim.
Zuul will log an event when a user presents a valid Authentication Token to perform a privileged action:
At DEBUG level the log entry will also contain the JSON body passed to the query.
The events will be logged at zuul.web's level but a new handler focused on auditing could also be created.
The CLI will be modified to call the REST API instead of using a Gearman server if the CLI's configuration file is lacking a [gearman]
section but has a [web]
section.
In that case the CLI will take the --auth-token argument on the autohold
, enqueue
, enqueue-ref
and dequeue
commands. The Authentication Token will be used to query the web API to execute these commands; allowing non-privileged users to use the CLI remotely.
$ zuul --auth-token AaAa.... autohold --tenant openstack --project example_project --job example_job --reason "reason text" --count 1
Connecting to https://zuul.openstack.org...
<usual autohold output>
A new command will be added to the Zuul Client CLI to allow an operator to generate an Authorization Token for a third party. It will return the contents of the Authorization
header as it should be set when querying the admin web API.
$ zuul create-auth-token --auth-config zuul-operator --user alice --tenant tenantA --expires-in 1800
bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwOi8vbWFuYWdlc2Yuc2ZyZG90ZXN0aW5zdGFuY2Uub3JnIiwienV1bC50ZW5hbnRzIjp7ImxvY2FsIjoiKiJ9LCJleHAiOjE1Mzc0MTcxOTguMzc3NTQ0fQ.DLbKx1J84wV4Vm7sv3zw9Bw9-WuIka7WkPQxGDAHz7s
The auth-config
argument refers to the authenticator configuration to use (see configuration changes below). The configuration must mention the secret to use to sign the Token.
This way of generating Authorization Tokens is meant for testing purposes only and should not be used in production, where the use of an external Identity Provider is preferred.
JWT creation and validation require a secret and an algorithm. While several algorithms are supported by the pyJWT library, using RS256
offers asymmetrical encryption, which allows the public key to be used in untrusted contexts like javascript code living browser side. Therefore this should be the preferred algorithm for issuers. Zuul will also support HS256
as the most widely used algorithm.
Some identity providers use key sets (also known as JWKS), therefore the key to use when verifying the Authentication Token's signatures cannot be known in advance. Zuul must support the RS256
algorithm with JWKS as well.
Here is an example defining the three supported types of authenticators:
[web]
listen_address=127.0.0.1
port=9000
static_cache_expiry=0
status_url=https://zuul.example.com/status
# symmetrical encryption
[auth "zuul_operator"]
driver=HS256
# symmetrical encryption only needs a shared secret
secret=exampleSecret
# accept "zuul.actions" claim in Authentication Token
allow_authz_override=true
# what the "aud" claim must be in Authentication Token
client_id=zuul.openstack.org
# what the "iss" claim must be in Authentication Token
issuer_id=zuul_operator
# the claim to use as the unique user identifier, defaults to "sub"
uid_claim=sub
# Auth realm, used in 401 error messages
realm=openstack
# (optional) Ensure a Token cannot be valid for longer than this amount of time, in seconds
max_validity_time = 1800000
# (optional) Account for skew between clocks, in seconds
skew = 3
# asymmetrical encryption
[auth "my_oidc_idp"]
driver=RS256
public_key=/path/to/key.pub
# optional, needed only if Authentication Token must be generated manually as well
private_key=/path/to/key
# if not explicitly set, allow_authz_override defaults to False
# what the "aud" claim must be in Authentication Token
client_id=my_zuul_deployment_id
# what the "iss" claim must be in Authentication Token
issuer_id=my_oidc_idp_id
# Auth realm, used in 401 error messages
realm=openstack
# (optional) Ensure a Token cannot be valid for longer than this amount of time, in seconds
max_validity_time = 1800000
# (optional) Account for skew between clocks, in seconds
skew = 3
# asymmetrical encryption using JWKS for validation
# The signing secret being known to the Identity Provider only, this
# authenticator cannot be used to manually issue Tokens with the CLI
[auth google_oauth_playground]
driver=RS256withJWKS
# URL of the JWKS; usually found in the .well-known config of the Identity Provider
keys_url=https://www.googleapis.com/oauth2/v3/certs
# what the "aud" claim must be in Authentication Token
client_id=XXX.apps.googleusercontent.com
# what the "iss" claim must be in Authentication Token
issuer_id=https://accounts.google.com
uid_claim=name
# Auth realm, used in 401 error messages
realm=openstack
# (optional) Account for skew between clocks, in seconds
skew = 3
mhu
Use Gerrit topic "zuul_admin_web" for all patches related to this spec.
Due to its complexity the spec should be implemented in smaller "chunks":
Anybody with a valid Authentication Token can perform administration tasks exposed through the Web API. Revoking JWT is not trivial, and not in the scope of this spec.
As a mitigation, Authentication Tokens should be generated with a short time to live, like 30 minutes or less. This is especially important if the Authentication Token overrides predefined authorizations with a zuul.admin
claim. This could be the default value for generating Tokens with the CLI; this will depend on the configuration of other external issuers otherwise. If using the zuul.admin
claims, the Authentication Token should also be generated with as little a scope as possible (one tenant only) to reduce the surface of attack should the Authentication Token be compromised.
Exposing administration tasks can impact build results (dequeue-ing buildsets), and pose potential resources problems with Nodepool if the autohold
feature is abused, leading to a significant number of nodes remaining in "hold" state for extended periods of time. Such power should be handed over responsibly.
These security considerations concern operators and the way they handle this feature, and do not impact development. They however need to be clearly documented, as operators need to be aware of the potential side effects of delegating privileges to other users.
The following items fall outside of the scope of this spec but are logical features to implement once the tenant-scoped admin REST API gets finalized: