better/compute
IAM integration

Policies

Write Better IAM policies for Better Compute, from the gpu-user and agent-guardrails examples to pool relations, resource attributes and conditions.

Who may do what in Better Compute is written in Better IAM policies. A policy names compute:* actions and compute-* resources (see Actions and resources), and conditions on the attributes compute attaches to each resource. Roles group policies, and bindings give roles to people, groups, service accounts and agents, exactly as for any other Better IAM application.

Compute never writes an organization's policies. It ships two example policies that organization administrators review and apply, and every policy that names a compute:* action must be created on a Better IAM host that has compute's plugin installed. A host without it rejects the policy with INVALID_ACTION.

better-compute iam setup prints the examples as JSON, { "policies": [...] }, parameterized for your deployment:

better-compute iam setup --audience https://compute.example.com \
  --gpu-model H100 --regions eu-west-1,eu-central-1 --max-gpus 8 --max-hours 72
FlagDefaultWhat it sets
--audiencefrom BETTER_COMPUTE_BASE_URLThe compute API URL (reduced to its origin), the audience of session JWTs for compute. Required.
--gpu-modelH100The GPU model family the bounded-reservation statement allows
--regionseu-west-1,eu-central-1The regions it allows
--max-gpus8GPUs per reservation
--max-hours72Hours per reservation, extensions included
--featurecompute.<model>The tenant flag the statement requires in tenant.features

The same function is exported for code: examplePolicies({ audience, gpuModel, regions, maxGpus, maxHours, feature, typePrefix }) returns both documents in the { name, document: { version: 1, statements } } shape that Better IAM's policies.create takes:

apply-policies.ts
import { examplePolicies } from 'better-compute/iam';

const ids: string[] = [];
for (const { name, document } of examplePolicies({ audience: 'https://compute.example.com' }))
  ids.push((await iam.api.policies.create(orgOwner, { tenantId: orgId, name, document })).id);

const role = await iam.api.roles.create(orgOwner, { tenantId: orgId, name: 'gpu-users', policyIds: ids });
await iam.api.bindings.create(orgOwner, {
  tenantId: orgId,
  roleId: role.id,
  subjectType: 'group',
  subjectId: mlTeamGroupId,
});

tests/iam-contract.test.ts applies both policies verbatim to a real Better IAM and checks every condition below.

The gpu-user policy

gpu-user is for members who reserve GPUs from pools shared with them, within bounds:

gpu-user
{
  "name": "gpu-user",
  "document": {
    "version": 1,
    "statements": [
      {
        "sid": "ListInTenant",
        "effect": "allow",
        "actions": ["compute:pools:read", "compute:capacity:read", "compute:reservations:read"],
        "resources": ["compute-tenant/*"]
      },
      {
        "sid": "SeeSharedPools",
        "effect": "allow",
        "actions": ["compute:pools:read", "compute:capacity:read"],
        "resources": ["compute-pool/*"],
        "conditions": { "ArrayContains": { "resource.relations": "user" } }
      },
      {
        "sid": "ReserveBoundedH100",
        "effect": "allow",
        "actions": ["compute:reservations:create"],
        "resources": ["compute-reservation/*"],
        "conditions": {
          "StringEquals": { "resource.gpuModel": "H100", "resource.region": ["eu-west-1", "eu-central-1"] },
          "NumericLessThanEquals": { "resource.gpuCount": 8, "resource.durationHours": 72 },
          "ArrayContains": { "resource.parentRelations": "user", "tenant.features": "compute.h100" }
        }
      },
      {
        "sid": "OwnReservations",
        "effect": "allow",
        "actions": ["compute:reservations:read", "compute:reservations:release"],
        "resources": ["compute-reservation/*"],
        "conditions": { "StringEquals": { "resource.ownerId": "${principal.id}" } }
      },
      {
        "sid": "BoundedExtend",
        "effect": "allow",
        "actions": ["compute:reservations:extend"],
        "resources": ["compute-reservation/*"],
        "conditions": {
          "StringEquals": { "resource.ownerId": "${principal.id}" },
          "NumericLessThanEquals": { "resource.durationHours": 72 }
        }
      },
      {
        "sid": "StopWhenOverBudget",
        "effect": "deny",
        "actions": [
          "compute:reservations:create",
          "compute:reservations:extend",
          "compute:instances:create",
          "compute:sandboxes:create",
          "compute:sandboxes:update"
        ],
        "resources": ["*"],
        "conditions": { "Bool": { "principal.spendExceeded": true } }
      },
      {
        "sid": "SessionJwtForCompute",
        "effect": "allow",
        "actions": ["iam:session-tokens:create"],
        "resources": ["iam/${principal.id}"]
      },
      {
        "sid": "AssertToCompute",
        "effect": "allow",
        "actions": ["iam:assertions:create"],
        "resources": ["iam/https://compute.example.com"]
      }
    ]
  }
}
StatementWhat it allows or denies
ListInTenantThe coarse gate of pools.list, reservations.list and capacity.summary, decided on the tenant scope
SeeSharedPoolsReading a pool, and its capacity, when the member holds the user relation on it, directly or through a group
ReserveBoundedH100Creating a reservation when the draft asks for H100s in the listed regions, at most 8 GPUs for at most 72 hours, in a pool where the member holds user, and the tenant has the compute.h100 flag
OwnReservationsReading and releasing the member's own reservations. ${principal.id} is a Better IAM policy variable.
BoundedExtendExtending the member's own reservations while the total stays within 72 hours
StopWhenOverBudgetDenies new reservations, extensions and instances once an enforced Better IAM budget covering the member is spent, and new sandboxes and sandbox extensions (compute:sandboxes:create, and compute:sandboxes:update, which also covers sandboxes.snapshot)
SessionJwtForCompute, AssertToComputeWhat Better IAM checks before it mints a session JWT whose audience includes compute

BoundedExtend cannot be bypassed by extending in small steps. Before deciding, reservations.extend writes the reservation's new total durationHours into the projection row's pending overlay, and the resolver reports the pending value. A denied extension clears the overlay.

The flag in ReserveBoundedH100 must be on for the organization in Better IAM (its feature flags), or the statement never applies. --feature names a different flag.

The agent-guardrails policy

agent-guardrails keeps AI agents away from the most dangerous actions and caps what they may reserve:

agent-guardrails
{
  "name": "agent-guardrails",
  "document": {
    "version": 1,
    "statements": [
      {
        "sid": "AgentKeys",
        "effect": "deny",
        "actions": ["compute:nodes:ssh-root", "compute:nodes:provision", "compute:images:publish"],
        "resources": ["*"],
        "conditions": { "StringEquals": { "principal.kind": "agent" } }
      },
      {
        "sid": "DelegatedSessions",
        "effect": "deny",
        "actions": ["compute:nodes:ssh-root", "compute:nodes:provision", "compute:images:publish"],
        "resources": ["*"],
        "conditions": { "Bool": { "principal.delegated": true } }
      },
      {
        "sid": "DelegatedSmall",
        "effect": "deny",
        "actions": ["compute:reservations:create", "compute:reservations:extend"],
        "resources": ["compute-reservation/*"],
        "conditions": {
          "Bool": { "principal.delegated": true },
          "NumericGreaterThan": { "resource.gpuCount": 4 }
        }
      }
    ]
  }
}

Two statements are needed because Better IAM reports the identity's kind in principal.kind. An agent using its own key is agent, but a delegated session acts as the person, so its kind is user and only principal.delegated tells it apart. Both keys are always present, so these denies cannot be skipped by a missing key. To let agents ask a person instead of being refused outright, see Delegated confirmation.

Resource attributes

Conditions on resource.* read what compute writes into each resource's projection row. These are the attributes the rows carry today:

TypeAttributesOwner and parent
compute-reservationgpuModel (other when the pool has none), gpuCount (whole GPUs, times the gang size), gpuSlices, gangSize, durationHours, region, binding (hold or node), spot, renewable, and projectId when givenownerId is the requesting identity; the parent is the pool
compute-instancevcpu, memoryGiB, gpus, imageId, imageVerified, and projectId when givenownerId is the requesting identity; the parent is the pool
compute-poolgpuModel, class, sharing, region (its site's), and owned (true in the owner tenant's view)The parent is the site
compute-noderegion, gpuCount, lifecycle (the provision state), trust, and gpuModel when the node is in a pool with oneThe parent is the pool
compute-siteregion, kind, and iamRegion when set
compute-projectnoneownerId is the creator
compute-sandboxtemplateId, vcpu, memoryGiB and gpus (the template's flavor), ttlSeconds (the lifetime asked for; during sandboxes.extend, the pending total from creation to the new expiry), warm (true when the template keeps a warm pool, warm.min > 0, and the create is not a restore), and fromSnapshot (the snapshot id) on a restoreownerId is the requesting identity; the parent is the template's pool
compute-sandbox-templatenoneownerId is the template's author

A reservation's gpuCount counts whole GPUs only; fractional requests show up in gpuSlices. The catalog also declares gpuMemoryGb on pools and dedicatedTo on nodes, which no row carries yet, so a condition on them never matches. Rows written by metal verbs carry provisionState, poolId and siteId instead of the node controller's attributes.

A missing attribute fails every condition operator except Exists. For a deny statement that means the deny does not apply, so guard denies on optional attributes such as projectId with an Exists statement.

Pool relations

Relationships let an organization share a pool with a team without listing ids in policies. The catalog declares four relations on compute-pool (viewer, user, operator, admin), operator on compute-node, and member and admin on compute-project. A relation grants nothing by itself: policies test it, as resource.relations on the resource itself and as resource.parentRelations on its children. Reservations, instances and nodes have their pool as parent.

share-pool.ts
await iam.api.relationships.create(orgOwner, {
  tenantId: orgId,
  type: 'compute-pool',
  id: poolId, // the pool's compute id (pool_...)
  relation: 'user',
  subjectType: 'group',
  subjectId: mlTeamGroupId,
});

Relationships live in the organization's own tenant, so an organization can share a platform pool that is offered to it. One common split of the four pool relations:

RelationTypical statements
viewercompute:pools:read and compute:capacity:read on compute-pool/* with resource.relations
userThe above, plus compute:reservations:create on compute-reservation/* with resource.parentRelations
operatorcompute:nodes:read, compute:nodes:cordon and compute:nodes:drain on compute-node/* with resource.parentRelations
admincompute:pools:manage, compute:pools:share and the compute:join-tokens:* actions on compute-pool/* with resource.relations
Operate the nodes of pools you operate
{
  "sid": "OperatePoolNodes",
  "effect": "allow",
  "actions": ["compute:nodes:read", "compute:nodes:cordon", "compute:nodes:drain"],
  "resources": ["compute-node/*"],
  "conditions": { "ArrayContains": { "resource.parentRelations": "operator" } }
}

Share pools, not reservations

Never create one relationship per reservation or instance. Better IAM loads all of a tenant's relationship tuples for every decision, so per-item tuples slow down every call in the tenant. Share pools and projects, and test ownership with resource.ownerId.

Conditions compute relies on

These Better IAM context keys carry most compute policies:

KeyUse
resource.<attribute>The attributes in the table above, including pending extend values
resource.ownerId"Own" statements, with the ${principal.id} variable
resource.relations, resource.parentRelationsRelations on the resource and on its pool
principal.kinduser, service or agent; a delegated session is user
principal.delegatedTrue while an AI agent acts for the person
principal.mfaTrue for MFA-verified sessions; always false for API keys and delegated sessions
principal.spendExceededTrue once an enforced budget covering the principal is spent
tenant.featuresThe flags that are on for the decision's tenant, such as compute.h100

Flags gate features inside decisions at no extra cost. PLAN.md names compute.baremetal, compute.h100 and compute.byo-nodes as examples; compute's code reads flags through IamPort.featureEnabled, backed by the flag cache, and no built-in method requires a flag today.

Fleet administrators

Fleet-wide methods decide on compute-fleet/global in the root tenant, so platform operators hold root-tenant policies. This example is a starting point, not a shipped policy:

fleet-operator
{
  "name": "fleet-operator",
  "document": {
    "version": 1,
    "statements": [
      {
        "sid": "Fleet",
        "effect": "allow",
        "actions": ["compute:fleet:admin", "compute:sites:manage", "compute:pools:create", "compute:quotas:manage"],
        "resources": ["compute-fleet/global"]
      },
      {
        "sid": "ListInRoot",
        "effect": "allow",
        "actions": ["compute:sites:read", "compute:pools:read", "compute:nodes:read"],
        "resources": ["compute-tenant/*"]
      },
      {
        "sid": "PlatformPools",
        "effect": "allow",
        "actions": [
          "compute:pools:read", "compute:pools:manage", "compute:pools:share", "compute:capacity:read",
          "compute:join-tokens:create", "compute:join-tokens:read", "compute:join-tokens:revoke"
        ],
        "resources": ["compute-pool/*"]
      },
      {
        "sid": "PlatformNodes",
        "effect": "allow",
        "actions": ["compute:nodes:read", "compute:nodes:cordon", "compute:nodes:drain", "compute:nodes:update", "compute:nodes:exec"],
        "resources": ["compute-node/*"]
      }
    ]
  }
}

A root-tenant operator acts on an organization's resources only through Better IAM's root override, which needs root authority and an MFA-verified user session, and which Better IAM audits. See Tenancy.

Break-glass access

PLAN.md describes break-glass root access to platform nodes for v0.3, when nodes.issueSshCert and tunnels.open arrive (both are planned in this release). The policy allows the console and root SSH only on platform-trust nodes from an MFA session:

break-glass
{
  "sid": "BreakGlass",
  "effect": "allow",
  "actions": ["compute:nodes:console", "compute:nodes:ssh-root"],
  "resources": ["compute-node/*"],
  "conditions": {
    "Bool": { "principal.mfa": true },
    "StringEquals": { "resource.trust": "platform" }
  }
}

It is bound as an eligible binding, which a person must activate before use: iam.api.bindings.create(credential, { tenantId, roleId, subjectType, subjectId, eligible: true, requireMfa: true, requireJustification: true, requireApproval: true }). The planned certificate and tunnel lifetimes never exceed the activation's expiry, and deactivating the binding closes the sessions. MFA on join tokens is a per-pool policy you may add, not a default, because CI cannot carry MFA.

Validation and testing

  • Create compute policies on a host with compute's plugin. A wildcard such as compute:* validates on any host, but it decides only where the plugin is installed; elsewhere every compute decision denies.
  • A denial never explains itself: callers see ACCESS_DENIED. Use Better IAM's policy simulation as an administrator to see why.
  • tests/iam-bridge-matrix.test.ts grants each action and type pair of the method table in a real Better IAM and checks that a grant on one type never allows the action on another. Use the same approach, with createEmbeddedTestIam from better-compute/iam/testing, to test your own policies. See Testing.

Was this page helpful?

Better Compute is created by Sean Filimon

Last updated

On this page