How to apply least‑privilege security to Ballerina services using built‑in constructs?
0 reputation · 12 Oct 2022, 21:26 UTC
0 reputation · 12 Oct 2022, 21:26 UTC
I am looking to harden a Ballerina microservice by limiting its permissions to only what is necessary. Ballerina provides first‑class service constructs and extensible metadata for cloud integration. How can I configure authentication, authorization, and runtime privileges (e.g., user, file system, network) using these language features to achieve a least‑privilege deployment?
35775 reputation · 13 Oct 2022, 03:28 UTC
To harden a Ballerina microservice using only the language’s built‑in security constructs, follow these steps. They assume Ballerina ≥ 2201.0.0 (the version that introduced the @Authorization and @Scope annotations).
import ballerina/http;
import ballerina/jwt;
// Example: OAuth2 JWT validator
@http:SecurityScheme {
scheme: "oauth2",
grantType: "client_credentials",
tokenEndpoint: "https://auth.example.com/token",
// Public key or JWKS URI for signature verification
jwtValidatorConfig: { jwksUrl: "https://auth.example.com/.well-known/jwks.json" }
}
serviceAuthScheme;
@http:ListenerConfig {
host: "0.0.0.0", // bind only to needed interface
port: 9090,
secureSocket: { // enable mTLS if mutual auth is required
enabled: true,
cert: {{ certFile: "cert.pem", keyFile: "key.pem" }},
ca: {{ caFile: "ca.pem" }},
clientAuth: "require"
}
}
service /api on new http:Listener(9090, config = listenerConf) {
attach serviceAuthScheme;
// ...
}
@http:Service { basePath: "/orders" }
service OrdersService on new http:Listener(9090) {
@http:Resource {
methods: ["GET"],
path: "/{id}",
// Requires the read:orders scope
@Authorization { scopes: ["read:orders"] }
@Scope { value: "read:orders" }
}
function getOrder(string id) returns Order|error {
// Business logic – only reachable if token contains read:orders
...
}
@http:Resource {
methods: ["POST"],
path: "/",
@Authorization { scopes: ["write:orders"] }
@Scope { value: "write:orders" }
}
function createOrder(Order payload) returns Order|error {
...
}
}
import ballerina/jwt;
@http:Resource { methods: ["GET"], path: "/admin" }
@Authorization { scopes: ["admin:orders"] }
function adminGet() returns string|error {
// The JWT payload is available via the built‑in $jwt variable
// Example: ensure the token was issued for a specific audience
if jwt:$jwt.aud != "orders-service" {
return error("Invalid audience");
}
return "Admin data";
}
ballerina:ballerina user in a Dockerfile).ballerina/file calls. If file access is required, restrict paths to a dedicated directory and set OS‑level permissions (read‑only where possible).Note: If you are deploying to Kubernetes, verify whether you can set a securityContext to drop capabilities and run as a non‑root user; this step changes the recommendation because the language‑level controls do not replace pod‑level privileges.
Use comments to ask for clarification. Post a solution as an answer.
35,775 reputation · 13 Oct 2022, 01:51 UTC
One clarification worth adding to the answer above: Ballerina's built-in constructs only enforce least privilege — they can't create it. Attaching per-resource scopes means the runtime rejects tokens missing the required scope, but if your identity provider issues every client a token with read:orders and write:orders by default, the annotations change nothing in practice. The real least-privilege work happens at the IdP: register narrow scopes there and grant each client only what it needs.
Two related points worth checking in your deployment:
audience per service so a token minted for one microservice is rejected by another. Without this, a broadly valid token silently bypasses your per-service scope design.configurable variables helps, but the file on disk still needs protection — mount it from your platform's secret store (Kubernetes Secrets, etc.) rather than baking it into the image.Also note the exact authConfig field names differ across Swan Lake releases, so verify against the http module docs for your installed version. A quick end-to-end check: issue a token with only the read scope and confirm the write resource returns 403.