Authentication
The Aster Policy Engine enforces a three-layer security model on every request. Each layer is independent; all three must be satisfied for a request to succeed.
Layer 1 — Tenant Isolation (X-Tenant-Id)
Every request must carry an X-Tenant-Id header. This header establishes the tenant context for the request: policies, audit records, and all data are scoped to the specified tenant and are never visible to other tenants.
Format: ^[a-zA-Z0-9_-]{1,64}$
A missing or malformed X-Tenant-Id returns 400 Bad Request immediately, before any other validation is performed.
curl https://policy.aster-lang.dev/api/v1/policies \
-H "X-Tenant-Id: acme-corp"Tenant ID conventions
Use a stable, human-readable identifier such as your organisation slug or environment name (e.g. acme-corp, acme-corp-staging). Tenant IDs are case-sensitive.
Layer 2 — HMAC Request Signing
Every request must include HMAC signature headers. The server validates the signature before processing the request, preventing tampering in transit and protecting against replay attacks.
Required Headers
| Header | Description |
|---|---|
X-Aster-Signature | Hex-encoded HMAC-SHA256 of the canonical message (see below) |
X-Aster-Nonce | A randomly-generated string (recommend 16+ bytes, hex or UUID) |
X-Aster-Timestamp | Unix timestamp in milliseconds at the moment of signing |
A request missing any of these three headers returns 401 Unauthorized.
Canonical Message Format
The HMAC is computed over the following pipe-delimited string:
{HTTP-method}|{path}|{query}|{X-Aster-Timestamp}|{X-Aster-Nonce}|{body-sha256}| Component | Description | Example |
|---|---|---|
HTTP-method | Uppercase HTTP method | POST |
path | Request URI path | /api/v1/policies/evaluate-source |
query | Raw query string (empty string if none) | trace=true |
X-Aster-Timestamp | Timestamp value from header | 1708776000000 |
X-Aster-Nonce | Nonce value from header | c3ab8ff13720e8ad9047dd39466b3c89 |
body-sha256 | Lowercase hex SHA-256 hash of the raw request body | a1b2c3d4... |
The HMAC key is the API secret issued to your tenant. The secret is resolved in priority order:
- Environment variable
ASTER_HMAC_SECRET_{TENANT_ID}(tenant ID uppercased, dashes replaced with underscores) - Configuration property
aster.security.hmac.secret-key
Example: Computing the Signature (Bash)
TENANT_ID="acme-corp"
TIMESTAMP=$(($(date +%s) * 1000))
NONCE=$(openssl rand -hex 16)
BODY='{"source":"Module demo.\n\nRule ping produce Text:\n Return \"pong\".","functionName":"ping","context":{},"locale":"en-US"}'
API_SECRET="your-api-secret-here"
METHOD="POST"
PATH_URI="/api/v1/policies/evaluate-source"
QUERY=""
BODY_HASH=$(printf '%s' "${BODY}" | openssl dgst -sha256 | awk '{print $2}')
CANONICAL="${METHOD}|${PATH_URI}|${QUERY}|${TIMESTAMP}|${NONCE}|${BODY_HASH}"
SIGNATURE=$(printf '%s' "${CANONICAL}" | openssl dgst -sha256 -hmac "${API_SECRET}" | awk '{print $2}')
curl -X POST "https://policy.aster-lang.dev${PATH_URI}" \
-H "Content-Type: application/json" \
-H "X-Tenant-Id: ${TENANT_ID}" \
-H "X-User-Role: MEMBER" \
-H "X-Aster-Signature: ${SIGNATURE}" \
-H "X-Aster-Nonce: ${NONCE}" \
-H "X-Aster-Timestamp: ${TIMESTAMP}" \
-d "${BODY}"Replay Prevention
The server rejects requests where X-Aster-Timestamp is more than 5 minutes (300,000 milliseconds) in the past or future, returning 401 Unauthorized. Additionally, each nonce is stored for the duration of the replay window; a second request using the same nonce within the window is rejected with 409 Conflict.
Layer 3 — Role-Based Access Control (X-User-Role)
The X-User-Role header carries the caller's role claim. The server enforces a strict role hierarchy:
OWNER > ADMIN > MEMBER > VIEWEREach role is additive: a higher role inherits all permissions of roles below it.
Role Permissions
| Role | Permitted actions |
|---|---|
VIEWER | Read stored policies (GET) |
MEMBER | All VIEWER permissions + evaluate policies (POST to evaluation endpoints) |
ADMIN | All MEMBER permissions + read and verify audit logs |
OWNER | All ADMIN permissions + manage tenant settings and RBAC assignments |
Endpoint Requirements
| Endpoint category | Minimum required role |
|---|---|
| Policy evaluation | MEMBER |
| Policy management (CRUD) | MEMBER |
| Audit log read | ADMIN |
| Audit log verification | ADMIN |
| Tenant administration | OWNER |
A request with an insufficient role returns 403 Forbidden.
Optional Headers
| Header | Description | Default |
|---|---|---|
X-User-Id | Identifies the caller in audit logs | anonymous |
Example: Full Request with All Layers
TENANT_ID="acme-corp"
TIMESTAMP=$(($(date +%s) * 1000))
NONCE=$(openssl rand -hex 16)
BODY='{"source":"Module demo.\n\nRule ping produce Text:\n Return \"pong\".","functionName":"ping","context":{},"locale":"en-US"}'
API_SECRET="your-api-secret-here"
METHOD="POST"
PATH_URI="/api/v1/policies/evaluate-source"
BODY_HASH=$(printf '%s' "${BODY}" | openssl dgst -sha256 | awk '{print $2}')
SIGNATURE=$(printf '%s' "${METHOD}|${PATH_URI}||${TIMESTAMP}|${NONCE}|${BODY_HASH}" | openssl dgst -sha256 -hmac "${API_SECRET}" | awk '{print $2}')
curl -X POST "https://policy.aster-lang.dev${PATH_URI}" \
-H "Content-Type: application/json" \
-H "X-Tenant-Id: ${TENANT_ID}" \
-H "X-User-Id: user@acme.com" \
-H "X-User-Role: MEMBER" \
-H "X-Aster-Signature: ${SIGNATURE}" \
-H "X-Aster-Nonce: ${NONCE}" \
-H "X-Aster-Timestamp: ${TIMESTAMP}" \
-d "${BODY}"Summary
| Layer | Header(s) | Required | Failure response |
|---|---|---|---|
| 1 | X-Tenant-Id | Always | 400 Bad Request |
| 2 | X-Aster-Signature, X-Aster-Nonce, X-Aster-Timestamp | Always | 401 Unauthorized |
| 3 | X-User-Role | Always | 403 Forbidden |
| — | X-User-Id | Optional | Defaults to anonymous |