Skip to main content

Service-to-Service Authorization

Gate internal API calls between your backend services with the same policy engine that authorizes your users.

Vengtoo treats every caller as a subject: users, services, AI agents. A service is just a subject with type: "service". No special infrastructure required.

How it works

  1. Create a subject for each service (e.g., service:billing, service:webhook)
  2. Write policies that grant those subjects access to specific resources and actions
  3. Each service authenticates to Vengtoo via client credentials and calls /access/v1/evaluation before accessing another service's resources

Step 1: Create service subjects

Via Terraform

resource "vengtoo_subject" "billing_service" {
application_id = vengtoo_application.platform.id
name = "service:billing"
type = "service"
attributes = {
environment = "production"
team = "payments"
}
}

resource "vengtoo_subject" "webhook_service" {
application_id = vengtoo_application.platform.id
name = "service:webhook"
type = "service"
}

Via the Console

Go to SubjectsCreate Subject. Set the Type to Service.

Step 2: Write policies

Grant the billing service read access to invoices:

resource "vengtoo_policy" "billing_reads_invoices" {
application_id = vengtoo_application.platform.id
name = "billing_reads_invoices"
effect = "ALLOW"

subjects = [{ subject_id = vengtoo_subject.billing_service.id }]

resources = [{
resource_id = vengtoo_resource.invoices.id
actions = ["read", "list"]
}]
}

Grant the webhook service publish access to events:

resource "vengtoo_policy" "webhook_publishes_events" {
application_id = vengtoo_application.platform.id
name = "webhook_publishes_events"
effect = "ALLOW"

subjects = [{ subject_id = vengtoo_subject.webhook_service.id }]

resources = [{
resource_id = vengtoo_resource.events.id
actions = ["publish"]
}]
}

You can also use roles: create a service_reader role and assign it to multiple service subjects.

Step 3: Check before every internal call

Each service authenticates to Vengtoo using OAuth2 client credentials and checks authorization before accessing another service's resources.

curl -X POST http://localhost:8181/access/v1/evaluation \
-H "Authorization: Bearer $VENGTOO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"subject": { "id": "service:billing", "type": "service" },
"resource": { "type": "invoice", "name": "inv-2024-001" },
"action": { "name": "read" }
}'

When the service acts on behalf of a user

The calls above authorize the service on its own authority: service:billing is allowed to read invoices, full stop. But most internal calls are not the service acting for itself: they are the service carrying out a user's request. A report exporter reads a document because Alice asked it to, not because the exporter should be able to read every document on its own.

Model that explicitly so the service borrows the user's access instead of holding standing rights of its own:

  1. Create a delegation from the user (the delegator) to the service subject (the delegate), scoped to just the actions this workflow needs:

    curl -X POST https://api.vengtoo.com/v1/delegations \
    -H "Authorization: Bearer $VENGTOO_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
    "delegator_id": "ALICE_SUBJECT_ID",
    "delegate_id": "service:report-exporter",
    "scope": ["read"],
    "description": "Export run requested by Alice"
    }'
  2. The service then checks access with its own subject ID, exactly like Step 3. Because the delegation exists, it borrows Alice's access, narrowed to the delegated scope, and no wider than Alice's own permissions. Grant the service subject no policies of its own and every action it takes is traceable to a real user who authorized it.

This matters for two reasons a plain service policy can't cover:

  • Least privilege that follows the user. If Alice loses access, the export loses it on the next call, no stale service grant to remember to revoke. Revoking the delegation (DELETE /v1/delegations/{id}) cuts it immediately.
  • An audit trail with both parties. The Decision Log records the actor (the service that made the call) and the principal (the user whose authority it carried), so "who did this?" has an honest answer even when a service did the mechanical work.
Never treat an inbound header as the subject identity

Do not fake this by reading a user ID from an inbound header and passing it through as the subject: a header your service didn't mint is caller-controlled, and treating it as identity lets any caller name any user. Establish the delegation from a trusted context, then let the service call as itself.

See Delegated authority for scope intersection, chains, and multi-hop attenuation.

Using ABAC conditions

You can add attribute-based conditions to service policies. For example, only allow the billing service to read invoices in production:

resource "vengtoo_policy" "billing_prod_only" {
application_id = vengtoo_application.platform.id
name = "billing_prod_only"
effect = "ALLOW"

subjects = [{ subject_id = vengtoo_subject.billing_service.id }]

resources = [{
resource_id = vengtoo_resource.invoices.id
actions = ["read"]
}]

conditions = [{
field = "subject.attributes.environment"
operator = "equals"
value = "production"
}]
}

Audit trail

Every service-to-service authorization decision is logged in the Decision Log with the full context: subject, resource, action, matched policy, and latency. Filter by type: service to see only service calls.