--allow-env isn't security

Deno asks permission to read environment variables. It never asks what's in them. Permissions control access. Encryption controls content.

the paradox
deno run --allow-env app.ts
// Deno asks: "Can this script read env vars?"
// You said: "Yes"
// Deno's job: done
cat .env
STRIPE_KEY=sk_live_xxxxxxxxxx
// Still plaintext on disk. Deno can't help you here.

The Deno Paradox

Security-first runtime. Plaintext config files.

What Deno protects

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

What Deno doesn't protect

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

Permissions + Encryption = Actual Security

Deno controls who can read. envv controls what they read.

The Deno Way

Validate permissions. Validate config. Fail fast.

src/config.ts

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.

src/server.ts

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 });
});
run
envv run -- deno run --allow-env --allow-net src/server.ts
Listening on http://localhost:8000/

deno.json Tasks

Encode your permission model once. Run it everywhere.

deno.json — before

{
  "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

deno.json — after

{
  "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

Permission Traps

--allow-all

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

Explicit permissions

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

Logging everything

console.log('Debug:', Deno.env.toObject());

// Dumps EVERY environment variable
// Including all your API keys
// Into whatever log aggregator
// Your company uses

Mask before logging

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

Fresh Framework

Full-stack Deno. Same security model.

routes/api/health.ts

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(),
    });
  },
};
fresh dev
envv run -- deno task start
Listening on http://localhost:8000

Two Models, One Goal

Deno's model

# "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.

envv's model

# "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.

Setup

one time
# Install
curl -fsSL https://getenvv.com/envv | sh
# Encrypt your config
envv push .env --env dev
rm .env
# Update .gitignore
echo ".env" >> .gitignore
# Update deno.json tasks
envv run -- deno task start

Secure by default. Encrypted by design.

Deno gave you the permission model. Now encrypt the content.

curl -fsSL https://getenvv.com/envv | sh