Designing Secure Deno Services with Fine‑Grained Permissions
Learn how to apply Deno’s capability‑based permission model to grant a service only the filesystem and network access it truly needs, with concrete flags, runtime checks, and failure‑mode analysis.
28 Mar 2026, 06:35 UTC

Problem
When you expose a Deno script to third‑party code or untrusted inputs, the default sandbox prevents accidental or malicious access to the host filesystem, network, or environment. However, many real‑world services need limited access—for example, reading a configuration directory or calling a specific API. Granting broad flags like --allow-all defeats the security model, while denying all access makes the service unusable. The challenge is to define the smallest set of permissions that satisfies the functional requirement while keeping the blast radius minimal.
Requirements
- The script must read JSON configuration files from
/etc/myapp/config. - It must make outbound HTTPS calls to
api.example.comonly. - No access to other directories, other network endpoints, or environment variables is permitted.
- Permission checks should be visible at runtime for operational monitoring.
Smallest Suitable Design
Start with Deno’s secure‑by‑default baseline and add only the two capabilities needed:
deno run \
--allow-read=/etc/myapp/config \
--allow-net=api.example.com:443 \
service.ts
The --allow-read flag limits file system access to the exact directory (no sub‑directory wildcards are needed because the flag already recurses). The --allow-net flag restricts outgoing TCP connections to the specified host and port; attempting to connect to any other host results in a PermissionDenied error.
Trust/Data Boundaries
The trust boundary lies between the Deno runtime and the host OS. The runtime mediates every system call that could cross this boundary:
- File‑system calls (
open,stat, etc.) are checked against the allowed‑read list. - Network calls (
connect) are checked against the allowed‑net list. - Environment variable reads (
Deno.env.get) require--allow-env, which we omit, so any attempt throws.
Because the checks occur at the system‑call boundary, the V8 engine cannot bypass them; even a compromised script cannot directly invoke privileged syscalls.
Operational Checks
You can verify the effective permissions without modifying the script:
# Query the permission state for network access to api.example.com
deno eval \
"Deno.permissions.query({name: 'net', host: 'api.example.com'}).then(p => console.log(p.state))"
Expected output: "granted". Changing the host to something else (e.g., evil.com) yields "denied".
Similarly, to confirm the read restriction:
deno eval \
"Deno.permissions.query({name: 'read', path: '/etc/myapp/config'}).then(p => console.log(p.state))"
Should print "granted". Querying a sibling directory like /etc returns "denied".
Failure Modes
- Mis‑scoped flags: Using
--allow-read=/etcwould unintentionally expose other configuration files, expanding the blast radius. - Port omission:
--allow-net=api.example.comwithout a port allows any port on that host; if the service only needs 443, specifyapi.example.com:443to limit further. - Dynamic permission escalation: A script could call
Deno.permissions.requestat runtime to obtain additional rights. Review code for such calls or disable runtime requests with the--no‑promptflag (Deno 2.0+). - Performance overhead: Each system call incurs a permission check, typically adding sub‑microsecond latency. For high‑frequency I/O, profile with
deno benchto ensure overhead stays within budget.
When to Reconsider the Design
If the service later needs to:
- Read from multiple, non‑hierarchical directories (e.g.,
/var/dataand/opt/cache), consider using--allow-readwith a comma‑separated list or a dedicated volume mount. - Make calls to a set of domains that change frequently, evaluate a reverse proxy or API gateway that performs allow‑listing outside Deno.
- Require environment variables for secrets, migrate those to a secret‑management service and access them via
--allow-envonly for the specific variable names, or better, inject them as file‑system secrets.
In each case, revisit the permission flags to keep them as narrow as possible while satisfying the new requirement.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.