AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Env Secrets Manager

skill-kissrosecicd-hub-agents-evolution-patricio0312rev-env-secrets-manager · by kissrosecicd-hub

Manages environment variables and secrets securely with encryption, rotation, and provider integration. Use when users request "secrets management", "environment variables", "API keys", "credentials storage", or "secret rotation".

No reviews yet
0 installs
7 views
0.0% view→install

Install

$ agentstack add skill-kissrosecicd-hub-agents-evolution-patricio0312rev-env-secrets-manager

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access Used
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-kissrosecicd-hub-agents-evolution-patricio0312rev-env-secrets-manager)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
4mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Env Secrets Manager? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Environment Secrets Manager

Securely manage secrets and environment variables across environments.

Core Workflow

  1. Identify secrets: Classify sensitive data
  2. Choose provider: Select secrets manager
  3. Configure storage: Encrypted storage
  4. Implement access: Secure retrieval
  5. Setup rotation: Automatic key rotation
  6. Audit access: Monitor usage

Local Development

Environment Files

# .env.example (commit this)
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
REDIS_URL=redis://localhost:6379
API_KEY=your-api-key-here
JWT_SECRET=your-jwt-secret-here

# AWS
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=us-east-1

# Third-party services
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
# .env.local (never commit)
DATABASE_URL=postgresql://user:actualpassword@localhost:5432/mydb
JWT_SECRET=super-secret-jwt-key-that-is-long-enough
STRIPE_SECRET_KEY=sk_test_actual_key
# .gitignore
.env
.env.local
.env.*.local
.env.production
*.pem
*.key
secrets/

Environment Validation

// config/env.ts
import { z } from 'zod';

const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
  PORT: z.coerce.number().default(3000),

  // Database
  DATABASE_URL: z.string().url(),

  // Redis
  REDIS_URL: z.string().url().optional(),

  // Authentication
  JWT_SECRET: z.string().min(32),
  JWT_EXPIRES_IN: z.string().default('7d'),

  // AWS (optional in development)
  AWS_ACCESS_KEY_ID: z.string().optional(),
  AWS_SECRET_ACCESS_KEY: z.string().optional(),
  AWS_REGION: z.string().default('us-east-1'),

  // External Services
  STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
  STRIPE_WEBHOOK_SECRET: z.string().startsWith('whsec_'),
});

export type Env = z.infer;

function validateEnv(): Env {
  const result = envSchema.safeParse(process.env);

  if (!result.success) {
    console.error('❌ Invalid environment variables:');
    console.error(result.error.format());
    throw new Error('Invalid environment configuration');
  }

  return result.data;
}

export const env = validateEnv();

T3 Env Pattern

// env.mjs (for Next.js)
import { createEnv } from '@t3-oss/env-nextjs';
import { z } from 'zod';

export const env = createEnv({
  server: {
    DATABASE_URL: z.string().url(),
    JWT_SECRET: z.string().min(32),
    STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
  },
  client: {
    NEXT_PUBLIC_API_URL: z.string().url(),
    NEXT_PUBLIC_STRIPE_KEY: z.string().startsWith('pk_'),
  },
  runtimeEnv: {
    DATABASE_URL: process.env.DATABASE_URL,
    JWT_SECRET: process.env.JWT_SECRET,
    STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
    NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
    NEXT_PUBLIC_STRIPE_KEY: process.env.NEXT_PUBLIC_STRIPE_KEY,
  },
});

AWS Secrets Manager

// lib/secrets/aws.ts
import {
  SecretsManagerClient,
  GetSecretValueCommand,
  CreateSecretCommand,
  UpdateSecretCommand,
  RotateSecretCommand,
} from '@aws-sdk/client-secrets-manager';

const client = new SecretsManagerClient({
  region: process.env.AWS_REGION,
});

// Cache for secrets
const secretsCache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes

export async function getSecret>(
  secretName: string
): Promise {
  // Check cache
  const cached = secretsCache.get(secretName);
  if (cached && cached.expiresAt > Date.now()) {
    return cached.value as T;
  }

  try {
    const command = new GetSecretValueCommand({
      SecretId: secretName,
    });

    const response = await client.send(command);

    let secretValue: T;

    if (response.SecretString) {
      secretValue = JSON.parse(response.SecretString);
    } else if (response.SecretBinary) {
      const buff = Buffer.from(response.SecretBinary);
      secretValue = JSON.parse(buff.toString('utf-8'));
    } else {
      throw new Error('Secret has no value');
    }

    // Update cache
    secretsCache.set(secretName, {
      value: secretValue,
      expiresAt: Date.now() + CACHE_TTL,
    });

    return secretValue;
  } catch (error) {
    console.error(`Failed to retrieve secret ${secretName}:`, error);
    throw error;
  }
}

export async function createSecret(
  secretName: string,
  secretValue: Record
): Promise {
  const command = new CreateSecretCommand({
    Name: secretName,
    SecretString: JSON.stringify(secretValue),
    Tags: [
      { Key: 'Environment', Value: process.env.NODE_ENV || 'development' },
      { Key: 'Application', Value: 'my-app' },
    ],
  });

  await client.send(command);
}

export async function updateSecret(
  secretName: string,
  secretValue: Record
): Promise {
  const command = new UpdateSecretCommand({
    SecretId: secretName,
    SecretString: JSON.stringify(secretValue),
  });

  await client.send(command);

  // Invalidate cache
  secretsCache.delete(secretName);
}

export async function rotateSecret(secretName: string): Promise {
  const command = new RotateSecretCommand({
    SecretId: secretName,
    RotateImmediately: true,
  });

  await client.send(command);

  // Invalidate cache
  secretsCache.delete(secretName);
}

Initialize Secrets on Startup

// lib/secrets/init.ts
import { getSecret } from './aws';

interface AppSecrets {
  database: {
    url: string;
    readUrl?: string;
  };
  jwt: {
    secret: string;
    refreshSecret: string;
  };
  stripe: {
    secretKey: string;
    webhookSecret: string;
  };
}

let appSecrets: AppSecrets | null = null;

export async function initializeSecrets(): Promise {
  if (appSecrets) return appSecrets;

  const [dbSecrets, jwtSecrets, stripeSecrets] = await Promise.all([
    getSecret('myapp/production/database'),
    getSecret('myapp/production/jwt'),
    getSecret('myapp/production/stripe'),
  ]);

  appSecrets = {
    database: dbSecrets,
    jwt: jwtSecrets,
    stripe: stripeSecrets,
  };

  return appSecrets;
}

export function getSecrets(): AppSecrets {
  if (!appSecrets) {
    throw new Error('Secrets not initialized. Call initializeSecrets() first.');
  }
  return appSecrets;
}

// Usage in app startup
async function startApp() {
  await initializeSecrets();
  // ... start server
}

HashiCorp Vault

// lib/secrets/vault.ts
import vault from 'node-vault';

const vaultClient = vault({
  apiVersion: 'v1',
  endpoint: process.env.VAULT_ADDR,
  token: process.env.VAULT_TOKEN,
});

export async function getSecretFromVault(path: string): Promise {
  try {
    const result = await vaultClient.read(`secret/data/${path}`);
    return result.data.data as T;
  } catch (error) {
    console.error(`Failed to read secret from path ${path}:`, error);
    throw error;
  }
}

export async function writeSecretToVault(
  path: string,
  data: Record
): Promise {
  await vaultClient.write(`secret/data/${path}`, { data });
}

// Dynamic database credentials
export async function getDatabaseCredentials(): Promise {
  const result = await vaultClient.read('database/creds/my-role');
  return {
    username: result.data.username,
    password: result.data.password,
  };
}

Doppler Integration

// lib/secrets/doppler.ts
import { DopplerSDK } from '@dopplerhq/node-sdk';

const doppler = new DopplerSDK({
  accessToken: process.env.DOPPLER_TOKEN,
});

export async function fetchSecrets(
  project: string,
  config: string
): Promise> {
  const response = await doppler.secrets.download({
    project,
    config,
    format: 'json',
  });

  return JSON.parse(response);
}

// CLI usage
// doppler run -- npm start

Local Encryption

// lib/secrets/local.ts
import { createCipheriv, createDecipheriv, randomBytes, scrypt } from 'crypto';
import { promisify } from 'util';
import fs from 'fs/promises';

const scryptAsync = promisify(scrypt);

const ALGORITHM = 'aes-256-gcm';
const KEY_LENGTH = 32;
const IV_LENGTH = 16;
const AUTH_TAG_LENGTH = 16;
const SALT_LENGTH = 32;

export async function encrypt(plaintext: string, password: string): Promise {
  const salt = randomBytes(SALT_LENGTH);
  const key = (await scryptAsync(password, salt, KEY_LENGTH)) as Buffer;
  const iv = randomBytes(IV_LENGTH);

  const cipher = createCipheriv(ALGORITHM, key, iv);
  const encrypted = Buffer.concat([
    cipher.update(plaintext, 'utf8'),
    cipher.final(),
  ]);
  const authTag = cipher.getAuthTag();

  // Combine: salt + iv + authTag + encrypted
  const combined = Buffer.concat([salt, iv, authTag, encrypted]);
  return combined.toString('base64');
}

export async function decrypt(ciphertext: string, password: string): Promise {
  const combined = Buffer.from(ciphertext, 'base64');

  const salt = combined.subarray(0, SALT_LENGTH);
  const iv = combined.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
  const authTag = combined.subarray(
    SALT_LENGTH + IV_LENGTH,
    SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH
  );
  const encrypted = combined.subarray(SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH);

  const key = (await scryptAsync(password, salt, KEY_LENGTH)) as Buffer;

  const decipher = createDecipheriv(ALGORITHM, key, iv);
  decipher.setAuthTag(authTag);

  const decrypted = Buffer.concat([
    decipher.update(encrypted),
    decipher.final(),
  ]);

  return decrypted.toString('utf8');
}

// Encrypted env file
export async function loadEncryptedEnv(filepath: string, password: string): Promise {
  const encrypted = await fs.readFile(filepath, 'utf8');
  const decrypted = await decrypt(encrypted, password);

  const lines = decrypted.split('\n');
  for (const line of lines) {
    const [key, ...valueParts] = line.split('=');
    if (key && valueParts.length > 0) {
      process.env[key.trim()] = valueParts.join('=').trim();
    }
  }
}

Kubernetes Secrets

# k8s/secrets.yaml
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
  namespace: production
type: Opaque
stringData:
  DATABASE_URL: postgresql://user:password@host:5432/db
  JWT_SECRET: your-jwt-secret
  STRIPE_SECRET_KEY: sk_live_...
---
# Using External Secrets Operator
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: app-secrets
  namespace: production
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: ClusterSecretStore
  target:
    name: app-secrets
    creationPolicy: Owner
  data:
    - secretKey: DATABASE_URL
      remoteRef:
        key: production/myapp/database
        property: url
    - secretKey: JWT_SECRET
      remoteRef:
        key: production/myapp/auth
        property: jwtSecret
// Reading K8s secrets in Node.js
import fs from 'fs';

function getSecretFromVolume(secretName: string): string {
  const secretPath = `/var/run/secrets/${secretName}`;
  return fs.readFileSync(secretPath, 'utf8').trim();
}

Secret Rotation

// lib/secrets/rotation.ts
interface RotationConfig {
  secretName: string;
  rotationInterval: number; // milliseconds
  onRotation: (newSecret: string) => Promise;
}

class SecretRotator {
  private intervals: Map = new Map();

  async startRotation(config: RotationConfig): Promise {
    // Immediate rotation
    await this.rotate(config);

    // Schedule recurring rotation
    const interval = setInterval(
      () => this.rotate(config),
      config.rotationInterval
    );

    this.intervals.set(config.secretName, interval);
  }

  private async rotate(config: RotationConfig): Promise {
    try {
      // Generate new secret
      const newSecret = this.generateSecret();

      // Update in secrets manager
      await updateSecret(config.secretName, { value: newSecret });

      // Notify application
      await config.onRotation(newSecret);

      console.log(`Rotated secret: ${config.secretName}`);
    } catch (error) {
      console.error(`Failed to rotate ${config.secretName}:`, error);
    }
  }

  private generateSecret(): string {
    return randomBytes(32).toString('base64');
  }

  stopRotation(secretName: string): void {
    const interval = this.intervals.get(secretName);
    if (interval) {
      clearInterval(interval);
      this.intervals.delete(secretName);
    }
  }
}

Best Practices

  1. Never commit secrets: Use .gitignore
  2. Validate on startup: Fail fast on missing secrets
  3. Use secret managers: AWS, Vault, Doppler
  4. Rotate regularly: Automate key rotation
  5. Encrypt at rest: Even in env files
  6. Audit access: Log secret retrieval
  7. Limit scope: Minimal permissions
  8. Different per environment: Never share production secrets

Output Checklist

Every secrets implementation should include:

  • [ ] .env.example with placeholders
  • [ ] .gitignore for secret files
  • [ ] Environment validation schema
  • [ ] Secret manager integration
  • [ ] Caching with TTL
  • [ ] Rotation mechanism
  • [ ] Audit logging
  • [ ] Kubernetes/Docker support
  • [ ] Development vs production separation
  • [ ] Documentation for team

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.