Files
2026-01-30 03:04:10 +00:00

5.5 KiB

Patterns

Secret Rotation

Zero-downtime rotation with versioned naming (api_key_v1, api_key_v2):

interface Env {
  PRIMARY_KEY: { get(): Promise<string> };
  FALLBACK_KEY?: { get(): Promise<string> };
}

async function fetchWithAuth(url: string, key: string) {
  return fetch(url, { headers: { "Authorization": `Bearer ${key}` } });
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    let resp = await fetchWithAuth("https://api.example.com", await env.PRIMARY_KEY.get());
    
    // Fallback during rotation
    if (!resp.ok && env.FALLBACK_KEY) {
      resp = await fetchWithAuth("https://api.example.com", await env.FALLBACK_KEY.get());
    }
    
    return resp;
  }
}

Workflow: Create api_key_v2 → add fallback binding → deploy → swap primary → deploy → remove v1

Encryption with KV

interface Env {
  CACHE: KVNamespace;
  ENCRYPTION_KEY: { get(): Promise<string> };
}

async function encryptValue(value: string, key: string): Promise<string> {
  const enc = new TextEncoder();
  const keyMaterial = await crypto.subtle.importKey(
    "raw", enc.encode(key), { name: "AES-GCM" }, false, ["encrypt"]
  );
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const encrypted = await crypto.subtle.encrypt(
    { name: "AES-GCM", iv }, keyMaterial, enc.encode(value)
  );
  
  const combined = new Uint8Array(iv.length + encrypted.byteLength);
  combined.set(iv);
  combined.set(new Uint8Array(encrypted), iv.length);
  return btoa(String.fromCharCode(...combined));
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const key = await env.ENCRYPTION_KEY.get();
    const encrypted = await encryptValue("sensitive-data", key);
    await env.CACHE.put("user:123:data", encrypted);
    return Response.json({ ok: true });
  }
}

HMAC Signing

interface Env {
  HMAC_SECRET: { get(): Promise<string> };
}

async function signRequest(data: string, secret: string): Promise<string> {
  const enc = new TextEncoder();
  const key = await crypto.subtle.importKey(
    "raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]
  );
  const sig = await crypto.subtle.sign("HMAC", key, enc.encode(data));
  return btoa(String.fromCharCode(...new Uint8Array(sig)));
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const secret = await env.HMAC_SECRET.get();
    const payload = await request.text();
    const signature = await signRequest(payload, secret);
    return Response.json({ signature });
  }
}

Audit & Monitoring

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    const startTime = Date.now();
    try {
      const apiKey = await env.API_KEY.get();
      const resp = await fetch("https://api.example.com", {
        headers: { "Authorization": `Bearer ${apiKey}` }
      });
      
      ctx.waitUntil(
        fetch("https://log.example.com/log", {
          method: "POST",
          body: JSON.stringify({
            event: "secret_used",
            secret_name: "API_KEY",
            timestamp: new Date().toISOString(),
            duration_ms: Date.now() - startTime,
            success: resp.ok
          })
        })
      );
      return resp;
    } catch (error) {
      ctx.waitUntil(
        fetch("https://log.example.com/log", {
          method: "POST",
          body: JSON.stringify({
            event: "secret_access_failed",
            secret_name: "API_KEY",
            error: error instanceof Error ? error.message : "Unknown"
          })
        })
      );
      return new Response("Error", { status: 500 });
    }
  }
}

Migration from Worker Secrets

Change env.SECRET (direct) to await env.SECRET.get() (async).

Steps:

  1. Create in Secrets Store: wrangler secrets-store secret create <store-id> --name API_KEY --scopes workers --remote
  2. Add binding to wrangler.jsonc: {"binding": "API_KEY", "store_id": "abc123", "secret_name": "api_key"}
  3. Update code: const key = await env.API_KEY.get();
  4. Test staging, deploy
  5. Remove old: wrangler secret delete API_KEY

Sharing Across Workers

Same secret, different binding names:

// worker-1: binding="SHARED_DB", secret_name="postgres_url"
// worker-2: binding="DB_CONN", secret_name="postgres_url"

JSON Secret Parsing

Store structured config as JSON secrets:

interface Env {
  DB_CONFIG: { get(): Promise<string> };
}

interface DbConfig {
  host: string;
  port: number;
  username: string;
  password: string;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    try {
      const configStr = await env.DB_CONFIG.get();
      const config: DbConfig = JSON.parse(configStr);
      
      // Use parsed config
      const dbUrl = `postgres://${config.username}:${config.password}@${config.host}:${config.port}`;
      
      return Response.json({ connected: true });
    } catch (error) {
      if (error instanceof SyntaxError) {
        return new Response("Invalid config JSON", { status: 500 });
      }
      throw error;
    }
  }
}

Store JSON secret:

echo '{"host":"db.example.com","port":5432,"username":"app","password":"secret"}' | \
  wrangler secrets-store secret create <store-id> \
    --name DB_CONFIG --scopes workers --remote

Integration

Service Bindings

Auth Worker signs JWT with Secrets Store; API Worker verifies via service binding.

See: workers for service binding patterns.

See: api.md, gotchas.md