Skip to main content

Command Palette

Search for a command to run...

Introducing Application-Level File System Permissions in Platformatic Watt

Updated
6 min read
Introducing Application-Level File System Permissions in Platformatic Watt

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.

Why File System Permissions Matter

In a multi-service architecture, security through isolation is critical. You might have:

  • A public-facing API service that should only read from specific directories
  • A backend service that processes uploads and needs write access to a temporary directory
  • A reporting service that should read from logs but never modify them
  • Sensitive configuration files that only specific services should access

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.

How It Works

The permissions model is configured per-application in your Watt configuration. When enabled, each application runs with Node.js's permission model, which restricts:

  • File system access (read and write)
  • Native module loading
  • Child process execution
  • Worker thread creation
  • Inspector protocol access
  • WASI (WebAssembly System Interface)

What This Feature Is (and Isn't)

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:

  • Prevent accidental file access by well-intentioned code
  • Catch bugs where code tries to read/write files it shouldn't
  • Enforce the principle of least privilege during development and operations
  • Reduce the blast radius of coding mistakes

❌ This feature does NOT:

  • Provide security guarantees against malicious code
  • Replace security best practices or code review
  • Protect against all attack vectors (e.g., symbolic links can bypass restrictions)

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.

Configuration

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"
          ]
        }
      }
    }
  ]
}

Path Resolution

  • Relative paths are resolved relative to the application's directory
  • Absolute paths are used as-is
  • Environment variables are fully supported: "{PLT_DATA_DIR}/templates"

Automatic node_modules Access

Don't worry about dependencies! When permissions are enabled, Platformatic Watt automatically grants read access to:

  • Watt's node_modules directory
  • The application's own node_modules directory
  • Any node_modules directories in parent directories

This ensures your application can load dependencies while still maintaining security.

Real-World Example

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')
  })
}

What Happens When Access Is Denied?

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.

Use Cases

1. Multi-Tenant Applications

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"]
        }
      }
    }
  ]
}

2. Read-Only Services

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
        }
      }
    }
  ]
}

3. Secure Configuration Access

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
        }
      }
    }
  ]
}

Testing Your Permissions

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)
})

Important Considerations

Before using the permission model, be aware of these limitations:

  1. 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.

  2. File Descriptors: Using existing file descriptors via the fs module can bypass the Permission Model.

  3. Not for Malicious Code: This feature is designed to prevent accidental access in trusted code, not to defend against malicious actors.

  4. Worker Threads: The permission model does not inherit to worker threads.

  5. Early Initialization: Flags like --env-file or --openssl-config read files before permission initialization and are not subject to permission rules.

Getting Started

Application permissions are available now in Platformatic Watt. To get started:

  1. Update your Watt configuration to add permissions to your applications
  2. Specify the fs.read and fs.write arrays with the paths each service needs
  3. Test your configuration in development to ensure proper file access boundaries
  4. Deploy knowing your services follow the principle of least privilege

Conclusion

Application-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.