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
- Create a subject for each service (e.g.,
service:billing,service:webhook) - Write policies that grant those subjects access to specific resources and actions
- Each service authenticates to Vengtoo via client credentials and calls
/access/v1/evaluationbefore 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 Subjects → Create 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
- Go
- Node.js
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" }
}'
client := vengtoo.NewClient("",
vengtoo.WithOAuth(os.Getenv("VENGTOO_CLIENT_ID"), os.Getenv("VENGTOO_CLIENT_SECRET")),
vengtoo.WithBaseURL("http://localhost:8181"),
)
result, err := client.Evaluate(ctx, &vengtoo.EvaluationRequest{
Subject: vengtoo.Subject{ID: "service:billing", Type: "service"},
Resource: vengtoo.Resource{Type: "invoice", Name: invoiceID},
Action: vengtoo.Action{Name: "read"},
})
if !result.Decision {
return fmt.Errorf("service:billing not authorized to read invoice %s", invoiceID)
}
import { Vengtoo } from "@vengtoo/sdk";
const vengtoo = new Vengtoo({
clientId: process.env.VENGTOO_CLIENT_ID,
clientSecret: process.env.VENGTOO_CLIENT_SECRET,
baseUrl: "http://localhost:8181",
});
async function fetchInvoice(invoiceId: string) {
const { decision } = await vengtoo.evaluate({
subject: { id: "service:billing", type: "service" },
resource: { type: "invoice", name: invoiceId },
action: { name: "read" },
});
if (!decision) throw new Error("service:billing is not authorized to read this invoice");
return await invoiceService.get(invoiceId);
}
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:
-
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"}' -
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.
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.
Related
- Subjects: Creating and managing service subjects
- OAuth2 Client Credentials: Authenticating services
- ABAC Conditions: Attribute-based policies for services
- Terraform Provider: Managing service subjects and policies as code