Skip to content

Commit 728dad2

Browse files
committed
feat(gcstoken): add CAB volume isolation for downscoped tokens
- Replace objectViewer + objectCreator with single objectAdmin rule - Add CEL condition to restrict access to volumeID/ and volumeID-meta/ prefixes - Use resource.name.startsWith() for GET/PUT operations - Use api.getAttribute(storage.googleapis.com/objectListPrefix) for LIST operations - Add Title field to AvailabilityCondition struct
1 parent f7de719 commit 728dad2

File tree

1 file changed

+23
-13
lines changed
  • packages/orchestrator/internal/gcstoken

1 file changed

+23
-13
lines changed

packages/orchestrator/internal/gcstoken/minter.go

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@ func NewMinter(bucket string, impersonateSA string) *Minter {
4242
}
4343

4444
// MintDownscopedToken creates a downscoped token for volume operations.
45-
// The token is scoped to the volumes bucket with minimal permissions:
46-
// - objectViewer: list + get (for litestream restore)
47-
// - objectCreator: create (for litestream replicate and juicefs write)
45+
// The token is scoped to the specific volume prefix with minimal permissions:
46+
// - objectAdmin: list + get + create (restricted to volumeID/ and volumeID-meta/ prefixes)
4847
//
49-
// Note: No delete permission. No volume isolation via CAB (litestream list
50-
// operations fail with prefix conditions). Time-limited to 1 hour.
48+
// Uses CAB availabilityCondition with:
49+
// - resource.name.startsWith() for GET/PUT operations
50+
// - api.getAttribute('storage.googleapis.com/objectListPrefix') for LIST operations
5151
func (m *Minter) MintDownscopedToken(ctx context.Context, volumeID string) (*Token, error) {
5252
// Step 1: Get base token (either via impersonation or directly from metadata)
5353
baseToken, err := m.getBaseToken(ctx)
@@ -56,21 +56,30 @@ func (m *Minter) MintDownscopedToken(ctx context.Context, volumeID string) (*Tok
5656
}
5757

5858
// Step 2: Create credential access boundary with minimal permissions
59-
// Using viewer + creator instead of objectAdmin to exclude delete
59+
// Using objectAdmin with CEL condition to restrict to volume prefix
6060
bucketResource := fmt.Sprintf("//storage.googleapis.com/projects/_/buckets/%s", m.bucket)
6161

62+
// Build CEL condition for volume isolation
63+
// - resource.name.startsWith() for GET/PUT operations
64+
// - api.getAttribute('storage.googleapis.com/objectListPrefix') for LIST operations
65+
prefixCondition := fmt.Sprintf(
66+
"resource.name.startsWith('projects/_/buckets/%s/objects/%s/') || "+
67+
"resource.name.startsWith('projects/_/buckets/%s/objects/%s-meta/') || "+
68+
"api.getAttribute('storage.googleapis.com/objectListPrefix', '').startsWith('%s/') || "+
69+
"api.getAttribute('storage.googleapis.com/objectListPrefix', '').startsWith('%s-meta/')",
70+
m.bucket, volumeID, m.bucket, volumeID, volumeID, volumeID,
71+
)
72+
6273
cab := CredentialAccessBoundary{
6374
AccessBoundary: AccessBoundary{
6475
AccessBoundaryRules: []AccessBoundaryRule{
6576
{
66-
// objectViewer: storage.objects.list + storage.objects.get
67-
AvailablePermissions: []string{"inRole:roles/storage.objectViewer"},
68-
AvailableResource: bucketResource,
69-
},
70-
{
71-
// objectCreator: storage.objects.create
72-
AvailablePermissions: []string{"inRole:roles/storage.objectCreator"},
77+
AvailablePermissions: []string{"inRole:roles/storage.objectAdmin"},
7378
AvailableResource: bucketResource,
79+
AvailabilityCondition: &AvailabilityCondition{
80+
Title: "Volume isolation",
81+
Expression: prefixCondition,
82+
},
7483
},
7584
},
7685
},
@@ -250,5 +259,6 @@ type AccessBoundaryRule struct {
250259

251260
// AvailabilityCondition is a CEL expression that further restricts access.
252261
type AvailabilityCondition struct {
262+
Title string `json:"title,omitempty"`
253263
Expression string `json:"expression"`
254264
}

0 commit comments

Comments
 (0)