R2 API Token Terraform Module
Now that R2 has bucket scoped tokens, I wanted to create a simple way to generate the S3 tokens that can be used with any S3 compatible service. I was playing around with terraform and created a simple module that will generate the tokens for you. The module is available from the Terraform registry here and you can view the source here. Before I break down the module, I want to go over the basics of how to use it.
Example Usage
The following example will return a token that has read and write access:
1module "r2-api-token" {
2 source = "Cyb3r-Jak3/r2-api-token/cloudflare"
3 version = "1.0.0"
4 account_id = "your_account_id"
5 buckets = ["bucket-1", "bucket-2"]
6}
The following example will return a token that has read only access:
1module "r2-api-token" {
2 source = "Cyb3r-Jak3/r2-api-token/cloudflare"
3 version = "3.0.0"
4 account_id = "your_account_id"
5 buckets = ["bucket-1", "bucket-2"]
6 bucket_write = false
7}
There are outputs from the module of id
and secret
that you can use with S3 compatible services. If you want to be able to get the raw token, you can use the following:
1 output "id" {
2 value = module.r2-api-token.id
3 }
4 output "secret" {
5 value = module.r2-api-token.secret
6 }
Module Breakdown
The main part of the module is:
1terraform {
2 required_version = ">= 1.2.0"
3 required_providers {
4 cloudflare = {
5 source = "cloudflare/cloudflare"
6 version = ">= 4.13.0"
7 }
8 }
9}
10
11data "cloudflare_api_token_permission_groups" "this" {}
12
13locals {
14 resources = length(var.buckets) > 0 ? { for bucket in var.buckets : "com.cloudflare.edge.r2.bucket.${var.account_id}_default_${bucket}" => "*" } : { "com.cloudflare.edge.r2.bucket.*" = "*" }
15 token_bucket_names = length(var.buckets) > 0 ? "All-Buckets" : join(",", var.buckets)
16}
17
18resource "cloudflare_api_token" "token" {
19 name = var.token_name != "" ? var.token_name : "R2-${join(",", var.buckets)}-${var.bucket_read ? "Read" : ""}-${var.bucket_write ? "Write" : ""}"
20 policy {
21 permission_groups = compact([
22 var.bucket_read ? module.api-permissions.r2["Workers R2 Storage Bucket Item Read"] : null,
23 var.bucket_write ? module.api-permissions.r2["Workers R2 Storage Bucket Item Write"] : null,
24 ])
25 resources = { for bucket in var.buckets : "com.cloudflare.edge.r2.bucket.${var.account_id}_default_${bucket}" => "*" }
26 }
27 not_before = var.not_before != "" ? var.not_before : timestamp()
28 expires_on = var.expires_on != "" ? var.expires_on : null
29 condition {
30 request_ip {
31 in = var.condition_ip_in
32 not_in = var.condition_ip_not_in
33 }
34 }
35}
Resource
1resource "cloudflare_api_token" "token" {
2 name = var.token_name != "" ? var.token_name : "R2-${local.token_bucket_names}-${var.bucket_read ? "Read" : ""}-${var.bucket_write ? "Write" : ""}"
3 policy {
4 permission_groups = compact([
5 var.bucket_read ? data.cloudflare_api_token_permission_groups.this.r2["Workers R2 Storage Bucket Item Read"] : null,
6 var.bucket_write ? data.cloudflare_api_token_permission_groups.this.r2["Workers R2 Storage Bucket Item Write"] : null,
7 ])
8 resources = local.resources
9 }
10 not_before = var.not_before != "" ? var.not_before : null
11 expires_on = var.expires_on != "" ? var.expires_on : null
12 condition {
13 request_ip {
14 in = var.condition_ip_in
15 not_in = var.condition_ip_not_in
16 }
17 }
18}
This is the main meat of the module. It will create the token with the following attributes:
name
is either the name you pass in or it will be generated based on the buckets and permissions. So token with Read Write and a bucket ofbucket-1
will beR2-bucket-1-Read-Write
.permissions groups
lookup the ones specified for R2 and will only add the ones that are needed. So if you only want read access, it will only add the read permission.compact
is used to remove any null values.resources
are generated based on the buckets you pass in. This is something that I had use dev tools to figure out the format of it. So if you pass inbucket-1
andbucket-2
, it will generate the following resources:com.cloudflare.edge.r2.bucket.${var.account_id}_default_bucket-1
com.cloudflare.edge.r2.bucket.${var.account_id}_default_bucket-2
not_before
andexpires_on
are set to either the values you pass in or the current timestamp and null respectively.condition
allows you to choose the IPs that can use the token. You can either specify a list of IPs that can use the token or a list of IPs that can't use the token. If you don't specify either, then it will allow all IPs to use the token.buckets
is a list of buckets that you want to grant access to. If you don't specify any, then it will grant access to all buckets.
Inputs
Inputs
Name | Description | Type | Default | Required |
---|---|---|---|---|
account_id | Cloudflare Account ID | string | n/a | yes |
bucket_read | If true, grant read access to the bucket(s) | bool | true | no |
bucket_write | If true, grant write access to the bucket(s) | bool | true | no |
buckets | List of R2 buckets to grant access to. If empty, all buckets will be granted access. | list(string) | [] | no |
condition_ip_in | List of IP addresses or CIDR notation where the token may be used from. If not specified, the token will be valid for all IP addresses. | list(string) | [] | no |
condition_ip_not_in | List of IP addresses or CIDR notation where the token should not be used from. | list(string) | [] | no |
expires_on | The expiration time on or after which the token MUST NOT be accepted for processing. If not specified, the token will not expire. | string | "" | no |
not_before | The time before which the token MUST NOT be accepted for processing. If not specified, the token will be valid immediately. | string | "" | no |
token_name | Name of the API token. If none given then the fomart is: R2-<comma separated names>-<Read if 'bucket-read'>-<Write if 'bucket-write'> | string | "" | no |