Introducing Application-Level File System Permissions in Platformatic Watt

Search for a command to run...

No comments yet. Be the first to comment.
Nitro is a server toolkit used by many JavaScript applications. You can use it on its own for APIs or full-stack servers, or combine it with Vite to get a familiar frontend workflow with a Nitro serve

All durable execution engines give the same versioning advice: pin your runs. Temporal pins runs to worker builds. Vercel's Workflow SDK pins each run to the deployment that started it. Azure Durable

Eve organizes AI agents in a simple way: use Markdown for instructions and skills, TypeScript for tools, and separate files for channels, schedules, and subagents. Behind this setup, Eve relies on the

Picture an online store launching a new product and sending out a mailing list campaign. Thousands of users click the same link at once. The product page, built with a Node.js app like Next.js, needs

How Platformatic ICC Outperforms AWS ECS Target Tracking and Step Scaling

Today we're excited to announce a powerful new security feature in Platformatic Watt: application-level file system permissions. This feature provides the simplest way to segregate applications in Watt, giving you fine-grained control over what files each individual application can access.
In a multi-service architecture, security through isolation is critical. You might have:
A vulnerability in one service could compromise your entire application without proper isolation. Platformatic's new permissions model leverages Node.js's native permission system to help prevent trusted code from unintentionally accessing files outside their designated scope.
The permissions model is configured per-application in your Watt configuration. When enabled, each application runs with Node.js's permission model, which restricts:
The Node.js Permission Model implements a "seat belt" approach designed to prevent trusted code from unintentionally accessing resources. It's important to understand:
✅ This feature helps:
❌ This feature does NOT:
As stated in the Node.js documentation: "This feature does not protect against malicious code. Malicious code can bypass the permission model and execute arbitrary code without the restrictions imposed by the permission model."
Think of it as a helpful guardrail that keeps your trusted code on the right path, not a security wall against adversaries.
Add a permissions object to any application in your Watt configuration:
{
"applications": [
{
"id": "api-service",
"path": "./services/api",
"config": "platformatic.service.json",
"permissions": {
"fs": {
"read": [
"./data/templates",
"/var/log/app.log"
],
"write": [
"./uploads"
]
}
}
}
]
}
"{PLT_DATA_DIR}/templates"Don't worry about dependencies! When permissions are enabled, Platformatic Watt automatically grants read access to:
node_modules directorynode_modules directorynode_modules directories in parent directoriesThis ensures your application can load dependencies while still maintaining security.
Let's say you're building a document processing service that reads templates and generates PDFs:
{
"applications": [
{
"id": "document-service",
"path": "./services/documents",
"config": "platformatic.service.json",
"permissions": {
"fs": {
"read": [
"./templates",
"{DATA_DIR}/customer-data"
],
"write": [
"{OUTPUT_DIR}/generated-pdfs"
]
}
}
}
]
}
Your service implementation:
import { readFile } from 'node:fs/promises'
import { writeFile } from 'node:fs/promises'
export default async function (app) {
app.post('/generate-invoice', async (request, reply) => {
// ✅ Allowed: Reading from configured template directory
const template = await readFile('./templates/invoice.html', 'utf8')
// ✅ Allowed: Reading from customer data directory
const customerData = await readFile(
`${process.env.DATA_DIR}/customer-data/customer-${request.body.customerId}.json`,
'utf8'
)
// Generate PDF...
const pdf = generatePDF(template, customerData)
// ✅ Allowed: Writing to output directory
await writeFile(
`${process.env.OUTPUT_DIR}/generated-pdfs/invoice-${Date.now()}.pdf`,
pdf
)
return { success: true }
})
app.get('/read-logs', async () => {
// ❌ DENIED: Trying to read from a path not in permissions
// This will throw ERR_ACCESS_DENIED
return readFile('/var/log/system.log', 'utf8')
})
}
When an application tries to access a file outside its permitted paths, Node.js's permission model blocks the operation:
// Error response:
{
"statusCode": 500,
"code": "ERR_ACCESS_DENIED",
"message": "Access to this API has been restricted. Use --allow-fs-read to manage permissions."
}
This immediate failure helps catch accidental file access bugs and makes permission issues easy to debug during development.
Segregate file access between tenants:
{
"applications": [
{
"id": "tenant-a-api",
"path": "./services/api",
"permissions": {
"fs": {
"read": ["./data/tenant-a"],
"write": ["./uploads/tenant-a"]
}
}
},
{
"id": "tenant-b-api",
"path": "./services/api",
"permissions": {
"fs": {
"read": ["./data/tenant-b"],
"write": ["./uploads/tenant-b"]
}
}
}
]
}
Create services that can only read data:
{
"applications": [
{
"id": "analytics-service",
"path": "./services/analytics",
"permissions": {
"fs": {
"read": ["./data/logs", "./data/metrics"]
// No write permissions at all
}
}
}
]
}
Limit which services can read sensitive configuration:
{
"applications": [
{
"id": "auth-service",
"path": "./services/auth",
"permissions": {
"fs": {
"read": [
"./config/auth-secrets.json",
"./config/jwt-keys"
]
}
}
},
{
"id": "public-api",
"path": "./services/public",
"permissions": {
"fs": {
"read": ["./config/public-settings.json"]
// Cannot access auth secrets
}
}
}
]
}
We recommend testing your permissions configuration in development:
// In your test suite
import { test } from 'node:test'
import { strictEqual } from 'node:assert'
test('service respects file permissions', async (t) => {
const response = await fetch('http://localhost:3042/restricted-file')
strictEqual(response.status, 500)
const body = await response.json()
strictEqual(body.code, 'ERR_ACCESS_DENIED')
})
test('service can access allowed files', async (t) => {
const response = await fetch('http://localhost:3042/allowed-file')
strictEqual(response.status, 200)
})
Before using the permission model, be aware of these limitations:
Symbolic Links: Symbolic links will be followed even to locations outside permitted paths. Relative symbolic links may allow access to arbitrary files. Ensure paths with granted access don't contain relative symbolic links.
File Descriptors: Using existing file descriptors via the fs module can bypass the Permission Model.
Not for Malicious Code: This feature is designed to prevent accidental access in trusted code, not to defend against malicious actors.
Worker Threads: The permission model does not inherit to worker threads.
Early Initialization: Flags like --env-file or --openssl-config read files before permission initialization and are not subject to permission rules.
Application permissions are available now in Platformatic Watt. To get started:
permissions to your applicationsfs.read and fs.write arrays with the paths each service needsApplication-level file system permissions provide a simple yet powerful way to add guardrails to your Platformatic Watt applications. By explicitly declaring what each service can access, you reduce the risk of accidental file access and help catch bugs before they reach production.
The permissions model is the simplest way to segregate applications in Platformatic Watt, implementing the principle of least privilege with minimal configuration. While it's not a security boundary against malicious code, it's an effective "seat belt" that helps prevent unintended mistakes in your trusted code.
Ready to add file system guardrails to your applications? Check out the full documentation to learn more.
Have questions or feedback? Join our Discord community or open an issue on GitHub.