Deno asks permission to read environment variables. It never asks what's in them. Permissions control access. Encryption controls content.
Security-first runtime. Plaintext config files.
// Network access requires a flag
deno run --allow-net app.ts
// File reads require a flag
deno run --allow-read app.ts
// Environment access requires a flag
deno run --allow-env app.ts
// Deno's permission model is brilliant.
// It stops malicious code from silently
// accessing things it shouldn't.
// Your .env file sitting in plaintext
STRIPE_KEY=sk_live_xxxxxxxx
DATABASE_URL=postgres://prod:pass@host/db
// Anyone with file access can read it
// Anyone with your laptop can read it
// Git history might contain it
// Deno's permissions gate access.
// They don't encrypt content.
Deno controls who can read. envv controls what they read.
Validate permissions. Validate config. Fail fast.
function requireEnv(key: string): string {
const value = Deno.env.get(key);
if (!value) {
console.error(`Missing: ${key}`);
console.error('Run: envv run -- deno run --allow-env ...');
Deno.exit(1);
}
return value;
}
// Validated, typed config
export const config = {
databaseUrl: requireEnv('DATABASE_URL'),
stripeKey: requireEnv('STRIPE_KEY'),
port: parseInt(Deno.env.get('PORT') ?? '8000'),
} as const;
// If this module loads, config is valid.
// If config is missing, app never starts.
import { config } from './config.ts';
// Config already validated at import time
Deno.serve({ port: config.port }, (req) => {
const url = new URL(req.url);
if (url.pathname === '/health') {
return Response.json({
status: 'ok',
port: config.port,
// Never log actual secrets
stripe: config.stripeKey.slice(0, 7) + '...',
});
}
return new Response('Not Found', { status: 404 });
});
Encode your permission model once. Run it everywhere.
{
"tasks": {
"start": "deno run --allow-env --allow-net src/server.ts",
"dev": "deno run --watch --allow-env --allow-net src/server.ts"
}
}
// Assumes .env exists
// Assumes it's populated
// Assumes nobody stole it
{
"tasks": {
"start": "envv run -- deno run --allow-env --allow-net src/server.ts",
"dev": "envv run -- deno run --watch --allow-env --allow-net src/server.ts"
}
}
// Decrypts at runtime
// Fails if missing
// Safe in git
deno run --allow-all app.ts
# "I give up on security"
# Any malicious import can:
# - Read all files
# - Access all networks
# - Dump all env vars
# - Run arbitrary commands
deno run \
--allow-env=DATABASE_URL,PORT \
--allow-net=api.stripe.com \
app.ts
# Specific env vars only
# Specific hosts only
# Malicious code can't escape
console.log('Debug:', Deno.env.toObject());
// Dumps EVERY environment variable
// Including all your API keys
// Into whatever log aggregator
// Your company uses
function mask(s: string): string {
if (s.length <= 8) return '***';
return `${s.slice(0, 4)}...${s.slice(-4)}`;
}
console.log('Key:', mask(config.stripeKey));
// Output: Key: sk_l...xxxx
Full-stack Deno. Same security model.
import { Handlers } from "$fresh/server.ts";
export const handler: Handlers = {
GET(_req, _ctx) {
const dbHost = Deno.env.get('DATABASE_URL')?.split('@').pop();
return Response.json({
status: 'ok',
database: dbHost ?? 'not configured',
timestamp: new Date().toISOString(),
});
},
};
# "What can this code access?"
--allow-env # Read env vars
--allow-net # Make network requests
--allow-read # Read files
--allow-write # Write files
--allow-run # Spawn processes
# Permissions are code-level.
# They prevent untrusted code from
# accessing things it shouldn't.
# "What can this person access?"
.env.encrypted # Ciphertext at rest
age keys # Decryption credentials
role-based ACL # Who gets what secrets
# Encryption is content-level.
# It prevents unauthorized people from
# reading things they shouldn't.
Deno controls what code can do. envv controls what people can see.
You need both.
Deno gave you the permission model. Now encrypt the content.
curl -fsSL https://getenvv.com/envv | sh