Downscoping with Credential Access Boundaries

This page explains how to use Credential Access Boundaries to downscope, or restrict, the Identity and Access Management (IAM) permissions that a short-lived credential can use.

How Credential Access Boundaries work

To downscope permissions, you define a Credential Access Boundary that specifies which resources the short-lived credential can access, as well as an upper bound on the permissions that are available on each resource. You can then create a short-lived credential, then exchange it for a new credential that respects the Credential Access Boundary.

If you need to give members a distinct set of permissions for each session, using Credential Access Boundaries can be more efficient than creating many different service accounts and granting each service account a different set of roles. For example, if one of your customers needs to access Cloud Storage data that you control, you can create a service account that can access every Cloud Storage bucket that you own, then apply a Credential Access Boundary that only allows access to the bucket with your customer's data.

Examples of Credential Access Boundaries

The following sections show examples of Credential Access Boundaries for common use cases.

Limit permissions for a bucket

The following example shows a simple Credential Access Boundary. It applies to the Cloud Storage bucket example-bucket, and it sets the upper bound to the permissions included in the Storage Object Viewer role (roles/storage.objectViewer):

{
  "accessBoundary": {
    "accessBoundaryRules": [
      {
        "availablePermissions": [
          "inRole:roles/storage.objectViewer"
        ],
        "availableResource": "//storage.googleapis.com/projects/_/buckets/example-bucket"
      }
    ]
  }
}

Limit permissions for multiple buckets

The following example shows a Credential Access Boundary that includes rules for multiple buckets:

  • The Cloud Storage bucket example-bucket-1: For this bucket, only the permissions in the Storage Object Viewer role (roles/storage.objectViewer) are available.
  • The Cloud Storage bucket example-bucket-2: For this bucket, only the permissions in the Storage Object Creator role (roles/storage.objectCreator) are available.
{
  "accessBoundary": {
    "accessBoundaryRules": [
      {
        "availablePermissions": [
          "inRole:roles/storage.objectViewer"
        ],
        "availableResource": "//storage.googleapis.com/projects/_/buckets/example-bucket-1"
      },
      {
        "availablePermissions": [
          "inRole:roles/storage.objectCreator"
        ],
        "availableResource": "//storage.googleapis.com/projects/_/buckets/example-bucket-2"
      }
    ]
  }
}

Limit permissions for specific objects

You can also use IAM Conditions to specify which Cloud Storage objects a member can access. For example, you can add a condition that makes permissions available for objects whose name starts with customer-a:

{
  "accessBoundary": {
    "accessBoundaryRules": [
      {
        "availablePermissions": [
          "inRole:roles/storage.objectViewer"
        ],
        "availableResource": "//storage.googleapis.com/projects/_/buckets/example-bucket",
        "availabilityCondition": {
          "expression" : "resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-a')"
        }
      }
    ]
  }
}

Limit permissions when listing objects

When you list the objects in a Cloud Storage bucket, you are calling a method on a bucket resource, not an object resource. As a result, if a condition is evaluated for a list request, and the condition refers to the resource name, then the resource name identifies the bucket, not an object within the bucket. For example, when you list objects in example-bucket, the resource name is projects/_/buckets/example-bucket.

This naming convention can lead to unexpected behavior when you list objects. For example, suppose you want a Credential Access Boundary that allows view access to objects in example-bucket with the prefix customer-a/invoices/. You might try to use the following condition in the Credential Access Boundary:

Incomplete: Condition that checks only the resource name

resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-a/invoices/')

This condition works for reading objects, but not for listing objects:

  • When a member tries to read an object in example-bucket with the prefix customer-a/invoices/, the condition evaluates to true.
  • When a member tries to list objects with that prefix, the condition evaluates to false. The value of resource.name is projects/_/buckets/example-bucket, which does not start with projects/_/buckets/example-bucket/objects/customer-a/invoices/.

To prevent this issue, in addition to using resource.name.startsWith(), your condition can check an API attribute named storage.googleapis.com/objectListPrefix. This attribute contains the value of the prefix parameter that was used to filter the list of objects. As a result, you can write a condition that refers to the value of the prefix parameter.

The following example shows how to use the API attribute in a condition. It allows reading and listing objects in example-bucket with the prefix customer-a/invoices/:

Complete: Condition that checks the resource name and the prefix

resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-a/invoices/')  ||
    api.getAttribute('storage.googleapis.com/objectListPrefix', '')
                     .startsWith('customer-a/invoices/')

You can now use this condition in a Credential Access Boundary:

{
  "accessBoundary": {
    "accessBoundaryRules": [
      {
        "availablePermissions": [
          "inRole:roles/storage.objectViewer"
        ],
        "availableResource": "//storage.googleapis.com/projects/_/buckets/example-bucket",
        "availabilityCondition": {
          "expression":
            "resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-a/invoices/') || api.getAttribute('storage.googleapis.com/objectListPrefix', '').startsWith('customer-a/invoices/')"
        }
      }
    ]
  }
}

Before you begin

Before you use Credential Access Boundaries, make sure you meet the following requirements:

  • You need to downscope permissions only for Cloud Storage, not for other Google Cloud services.

    If you need to downscope permissions for additional Google Cloud services, you can create multiple service accounts and grant a different set of roles to each service account.

  • You can use OAuth 2.0 access tokens for authentication. Other types of short-lived credentials do not support Credential Access Boundaries.

Creating a downscoped short-lived credential

To create an OAuth 2.0 access token with downscoped permissions, follow these steps:

  1. Grant the appropriate IAM roles to a user or service account.
  2. Define a Credential Access Boundary that sets an upper bound on the permissions that are available to the user or service account.
  3. Create an OAuth 2.0 access token for the user or service account.
  4. Exchange the OAuth 2.0 access token for a new token that respects the Credential Access Boundary.

You can then use the new, downscoped OAuth 2.0 access token to authenticate requests to Cloud Storage.

Granting IAM roles

A Credential Access Boundary sets an upper bound on the available permissions for a resource. It can subtract permissions from a member, but it cannot add permissions that the member does not already have.

As a result, you must also grant roles to the member that provide the permissions they need, either on a Cloud Storage bucket or on a higher-level resource, such as the project.

For example, suppose you need to create a downscoped short-lived credential that allows a service account to create objects in a bucket:

  • At a minimum, you must grant a role to the service account that includes the storage.objects.create permission, such as the Storage Object Creator role (roles/storage.objectCreator). The Credential Access Boundary must also include this permission.
  • You can also grant a role that includes more permissions, such as the Storage Object Admin role (roles/storage.objectAdmin). The service account can use only the permissions that appear in both the role grant and the Credential Access Boundary.

To learn about predefined roles for Cloud Storage, see Cloud Storage roles.

Defining a Credential Access Boundary

A Credential Access Boundary is a JSON object that contains a list of access boundary rules. Each rule contains the following information:

  • The resource that the rule applies to.
  • The upper bound of the permissions that are available on that resource.
  • Optional: A condition that further restricts permissions. A condition includes the following:
    • A condition expression that evaluates to true or false. If it evaluates to true, access is allowed; otherwise, access is denied.
    • Optional: A title that identifies the condition.
    • Optional: A description with more information about the condition.

If you apply a Credential Access Boundary to a short-lived credential, then the credential can access only the resources in the Credential Access Boundary. No permissions are available on other resources.

A Credential Access Boundary can contain up to 10 access boundary rules. You can apply only one Credential Access Boundary to each short-lived credential.

A Credential Access Boundary contains the following fields:

Fields
accessBoundary

object

A container for the Credential Access Boundary.

accessBoundary.accessBoundaryRules[]

object

A list of access boundary rules to apply to a short-lived credential.

accessBoundary.accessBoundaryRules[].availablePermissions[]

string

A list that defines the upper bound on the available permissions for the resource.

Each value is the identifier for an IAM predefined role or custom role, with the prefix inRole:. For example: inRole:roles/storage.objectViewer. Only the permissions in these roles will be available.

accessBoundary.accessBoundaryRules[].availableResource

string

The full resource name of the Cloud Storage bucket that the rule applies to. Use the format //storage.googleapis.com/projects/_/buckets/bucket-name.

accessBoundary.accessBoundaryRules[].availabilityCondition

object

Optional. A condition that restricts the availability of permissions to specific Cloud Storage objects.

Use this field if you want to make permissions available for specific objects, rather than all objects in a Cloud Storage bucket.

accessBoundary.accessBoundaryRules[].availabilityCondition.expression

string

A condition expression that specifies the Cloud Storage objects where permissions are available.

To learn how to refer to specific objects in a condition expression, see resource.name attribute.

accessBoundary.accessBoundaryRules[].availabilityCondition.title

string

Optional. A short string that identifies the purpose of the condition.

accessBoundary.accessBoundaryRules[].availabilityCondition.description

string

Optional. Details about the purpose of the condition.

For examples, see Examples of Credential Access Boundaries on this page.

Create a JSON file that defines a Credential Access Boundary. You will use this file in a later step.

Creating an OAuth 2.0 access token

Before you create a downscoped short-lived credential, you must create a normal OAuth 2.0 access token. You can then exchange the normal credential for a downscoped credential. When you create the access token, use the OAuth 2.0 scope https://www.googleapis.com/auth/cloud-platform.

To create an access token for a service account, you can complete the server-to-server OAuth 2.0 flow, or you can use the Service Account Credentials API to generate an OAuth 2.0 access token.

To create an access token for a user, see Obtaining OAuth 2.0 access tokens. You can also use the OAuth 2.0 Playground to create an access token for your own Google Account.

Exchanging the OAuth 2.0 access token

After you create an OAuth 2.0 access token, you can exchange the access token for a new token that respects the Credential Access Boundary. You exchange the access token through the Security Token Service API, which is part of Identity Platform.

To exchange the access token, use the following HTTP method and URL:

POST https://sts.googleapis.com/v1beta/token

Set the Content-Type header in the request to application/x-www-form-urlencoded. Include the following fields in the request body:

Fields
grant_type

string

Use the value urn:ietf:params:oauth:grant-type:token-exchange.

options

string

The percent-encoded Credential Access Boundary.

requested_token_type

string

Use the value urn:ietf:params:oauth:token-type:access_token.

subject_token

string

The OAuth 2.0 access token that you want to exchange.

subject_token_type

string

Use the value urn:ietf:params:oauth:token-type:access_token.

The response is a JSON object that contains the following fields:

Fields
access_token

string

A new OAuth 2.0 access token that respects the Credential Access Boundary.

expires_in

number

The amount of time until the new access token expires, in seconds.

This field is present only if the original access token represents a service account. When this field is not present, the new access token has the same time to expire as the original access token.

issued_token_type

string

Contains the value urn:ietf:params:oauth:token-type:access_token.

token_type

string

Contains the value Bearer.

For example, if the Credential Access Boundary is stored in the file ./access-boundary.json, you can use the following curl command to exchange the access token. Replace original-token with the original access token:

curl -H "Content-Type:application/x-www-form-urlencoded" \
    -X POST \
    https://sts.googleapis.com/v1beta/token \
    -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange&subject_token_type=urn:ietf:params:oauth:token-type:access_token&requested_token_type=urn:ietf:params:oauth:token-type:access_token&subject_token=original-token" \
    --data-urlencode "options=$(cat ./access-boundary.json)"

The response is similar to the following example:

{
  "access_token": "ya29.dr.AbCDeFg-123456...",
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
  "token_type": "Bearer",
  "expires_in": 3600
}

What's next