Agent skill
secret_management
Install this agent skill to your Project
npx add-skill https://github.com/CleanExpo/ATO/tree/main/.agent/skills/secret_management
SKILL.md
Secret Management
Environment variable patterns, secret validation, rotation, and leak prevention for NodeJS-Starter-V1.
Metadata
| Field | Value |
|---|---|
| Skill ID | secret-management |
| Category | Authentication & Security |
| Complexity | Low |
| Complements | ci-cd-patterns, docker-patterns, health-check |
| Version | 1.0.0 |
| Locale | en-AU |
Description
Codifies secret management patterns for NodeJS-Starter-V1: typed environment variable loading with validation, secret strength checks at startup, rotation procedures, leak prevention in logs and error messages, CI/CD secret injection, Docker secret patterns, and .env file hygiene.
When to Apply
Positive Triggers
- Adding new API keys or secrets to the project
- Validating environment variables at application startup
- Implementing secret rotation procedures
- Preventing secret leakage in logs, errors, or API responses
- Configuring CI/CD secrets for GitHub Actions
- Reviewing
.envfile security and.gitignorecoverage
Negative Triggers
- JWT token creation and validation (use
auth/jwt.pydirectly) - OAuth flow implementation (future
oauth-flowskill) - RBAC and permission management (future
rbac-patternsskill) - Encrypting data at rest (out of scope — use database-level encryption)
Core Principles
The Three Laws of Secrets
- Validate at Startup, Fail Fast: Check all required secrets exist and meet minimum strength before the application starts. A missing secret at runtime is a production incident.
- Never Log, Never Return: Secrets must never appear in logs, error messages, API responses, or stack traces. Redact automatically — don't rely on developers remembering.
- Rotate Without Downtime: Design secret usage so rotation requires only an environment variable change and restart — no code changes, no migrations.
Pattern 1: Typed Environment Variables (Python)
Pydantic Settings with Validation
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings
class AppSettings(BaseSettings):
"""Validated application settings from environment variables."""
# Required secrets
jwt_secret_key: str = Field(min_length=32)
database_url: str = Field(pattern=r"^postgresql://")
backend_api_key: str = Field(min_length=16)
# Optional secrets (with safe defaults)
anthropic_api_key: str | None = None
stripe_secret_key: str | None = None
stripe_webhook_secret: str | None = None
jina_api_key: str | None = None
exa_api_key: str | None = None
linear_api_key: str | None = None
# Non-secret config
ai_provider: str = "ollama"
log_level: str = "info"
node_env: str = "development"
@field_validator("jwt_secret_key")
@classmethod
def jwt_secret_not_default(cls, v: str) -> str:
if "change-in-production" in v and cls._is_production():
raise ValueError("JWT secret must be changed in production")
return v
@staticmethod
def _is_production() -> bool:
import os
return os.getenv("NODE_ENV") == "production"
model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
Project Reference: apps/backend/src/config/ — the existing settings module loads env vars but lacks minimum-length validation and production-specific checks. The default JWT secret in .env.example:20 contains "change-in-production" which should be caught.
Pattern 2: Typed Environment Variables (TypeScript)
Zod Schema for Next.js
import { z } from "zod";
const envSchema = z.object({
// Required
NEXT_PUBLIC_BACKEND_URL: z.string().url().default("http://localhost:8000"),
NEXT_PUBLIC_FRONTEND_URL: z.string().url().default("http://localhost:3000"),
// Optional secrets (server-side only)
STRIPE_SECRET_KEY: z.string().startsWith("sk_").optional(),
STRIPE_WEBHOOK_SECRET: z.string().startsWith("whsec_").optional(),
CRON_SECRET: z.string().min(16).optional(),
// Runtime config
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
});
export type Env = z.infer<typeof envSchema>;
function validateEnv(): Env {
const result = envSchema.safeParse(process.env);
if (!result.success) {
console.error("Environment validation failed:");
for (const issue of result.error.issues) {
console.error(` ${issue.path.join(".")}: ${issue.message}`);
}
if (process.env.NODE_ENV === "production") {
throw new Error("Invalid environment configuration");
}
}
return result.success ? result.data : (process.env as unknown as Env);
}
export const env = validateEnv();
Rule: In production, throw on invalid env. In development, warn but continue — this prevents blocking local development when optional secrets are missing.
Pattern 3: Secret Redaction
Log Sanitiser
import re
SECRET_PATTERNS = [
re.compile(r"(sk[-_](?:test|live|ant)[-_])\w+", re.IGNORECASE),
re.compile(r"(whsec_)\w+"),
re.compile(r"(Bearer\s+)\S+"),
re.compile(r"(password[\"':\s=]+)\S+", re.IGNORECASE),
re.compile(r"(api[_-]?key[\"':\s=]+)\S+", re.IGNORECASE),
]
def redact_secrets(message: str) -> str:
"""Replace secret values with redacted placeholders."""
for pattern in SECRET_PATTERNS:
message = pattern.sub(r"\1[REDACTED]", message)
return message
Integrate with the structured-logging skill by adding redact_secrets as a structlog processor:
structlog.configure(
processors=[
# ... existing processors
lambda _, __, event_dict: {
k: redact_secrets(str(v)) if isinstance(v, str) else v
for k, v in event_dict.items()
},
structlog.dev.ConsoleRenderer(),
]
)
Project Reference: apps/backend/src/utils/logging.py:1-39 — the existing structlog config has no secret redaction. Add the redaction processor before the renderer.
Pattern 4: Startup Health Check
Secret Presence and Strength Validation
def check_secrets_health() -> dict:
"""Validate secret presence and strength at startup."""
settings = get_settings()
issues: list[str] = []
# Required secrets
required = {
"JWT_SECRET_KEY": settings.jwt_secret_key,
"DATABASE_URL": settings.database_url,
"BACKEND_API_KEY": settings.backend_api_key,
}
for name, value in required.items():
if not value or len(value) < 16:
issues.append(f"{name}: missing or too short (min 16 chars)")
# Production-only checks
if settings.node_env == "production":
if "localhost" in settings.database_url:
issues.append("DATABASE_URL: points to localhost in production")
if settings.jwt_secret_key and len(settings.jwt_secret_key) < 64:
issues.append("JWT_SECRET_KEY: should be ≥64 chars in production")
return {
"status": "healthy" if not issues else "unhealthy",
"issues": issues,
"secrets_configured": len(required) - len(issues),
"secrets_total": len(required),
}
Complements: health-check skill — add check_secrets_health() as a dependency check in the deep health endpoint. Return "degraded" if optional secrets are missing, "unhealthy" if required ones fail.
Pattern 5: CI/CD Secret Injection
GitHub Actions Secrets
# .github/workflows/ci.yml
env:
# Map GitHub Secrets to environment variables
DATABASE_URL: postgresql://test_user:test_pass@localhost:5432/test_db
JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY || 'test-secret-for-ci-only-not-production' }}
BACKEND_API_KEY: ${{ secrets.BACKEND_API_KEY || 'ci-test-key-minimum-16' }}
Rules for CI secrets:
| Rule | Rationale |
|---|---|
| Always provide fallback for optional secrets | CI must pass without external service keys |
| Never use production secrets in CI | Test isolation |
Use secrets.GITHUB_TOKEN for GitHub API calls |
Auto-provided, no setup needed |
| Store deployment secrets in environment-specific secrets | Prevents cross-environment leakage |
Project Reference: .github/workflows/ci.yml — already follows the no-secrets-required pattern. Add JWT_SECRET_KEY fallback for test runs.
Pattern 6: Docker Secret Patterns
Environment Variable Injection
# docker-compose.yml
services:
backend:
env_file:
- .env
environment:
# Override specific vars (higher precedence than .env)
- DATABASE_URL=postgresql://starter_user:local_dev_password@postgres:5432/starter_db
Rules:
| Rule | Implementation |
|---|---|
| Never bake secrets into Docker images | Use env_file or environment, never ENV in Dockerfile |
| Use Docker secrets for Swarm/K8s | docker secret create + /run/secrets/ mount |
.env files never in Docker image |
Add .env to .dockerignore |
Project Reference: apps/backend/Dockerfile — verify .dockerignore excludes .env* files. docker-compose.yml — already uses env_file: .env pattern correctly.
Pattern 7: .env File Hygiene
Audit Checklist
| Check | Status | File |
|---|---|---|
.env in .gitignore |
Required | .gitignore:26 |
.env.example has no real values |
Required | .env.example |
.env.local in .gitignore |
Required | .gitignore:28 |
All secrets have xxx or your-* placeholders |
Required | .env.example |
| Production env uses unique secrets | Required | Deployment config |
Project Reference: .gitignore:25-29 — correctly ignores .env, .env.local, .env.*.local and preserves .env.example. The .env.example uses your-* and xxx placeholders correctly.
Secret Inventory
| Secret | Source | Required? | Rotation Frequency |
|---|---|---|---|
JWT_SECRET_KEY |
Self-generated | Yes | Annually |
DATABASE_URL |
PostgreSQL | Yes | On compromise |
BACKEND_API_KEY |
Self-generated | Yes | Quarterly |
ANTHROPIC_API_KEY |
Anthropic dashboard | No | On compromise |
STRIPE_SECRET_KEY |
Stripe dashboard | No | On compromise |
STRIPE_WEBHOOK_SECRET |
Stripe dashboard | No | On re-register |
JINA_API_KEY |
Jina dashboard | No | On compromise |
LINEAR_API_KEY |
Linear settings | No | On compromise |
EXA_API_KEY |
Exa dashboard | No | On compromise |
Anti-Patterns
| Pattern | Problem | Correct Approach |
|---|---|---|
| Hardcoded secrets in source code | Leaked on push | Environment variables only |
| Default secrets in production | Predictable, exploitable | Validate at startup, reject defaults |
| Secrets in log output | Exposed in log aggregators | Automatic redaction processor |
| Same secret across environments | One compromise exposes all | Per-environment unique secrets |
No .env in .gitignore |
Secrets committed to git | Always gitignore .env* (except .example) |
| Secrets in URL query parameters | Visible in logs, referrers | Use headers or request body |
Checklist
Before merging secret-management changes:
- Pydantic
AppSettingsvalidates all required secrets at startup - Zod
envSchemavalidates frontend environment variables - Secret redaction processor added to structlog pipeline
- Startup health check reports secret presence and strength
- CI/CD provides fallback values for optional secrets
-
.dockerignoreexcludes.env*files -
.env.exampleuses placeholder values only - Production deployment uses unique, strong secrets
Response Format
When applying this skill, structure implementation as:
### Secret Management Implementation
**Validation**: [Pydantic / Zod / both]
**Redaction**: [structlog processor / manual]
**Health Check**: [startup / deep endpoint / both]
**CI/CD**: [GitHub Actions secrets / env fallbacks]
**Docker**: [env_file / Docker secrets / both]
**Rotation**: [documented / automated]
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
ato-rate-scraping
Automated scraping and extraction of current Australian tax rates from ATO.gov.au and related government sources
rate_limiter
payg-instalment-optimization
PAYG instalment strategy analysis — amount vs rate method, variation penalty risk, GDP adjustment
idea-queue-capture
Instantly capture user ideas to work queue with intelligent grouping
simple-report-export
Simple report generation without Google Cloud - uses Gmail App Password or local file export (PDF, Excel, Word-compatible formats)
cashflow-forecasting
Project future tax obligations and cash flow requirements based on historical transaction patterns
Didn't find tool you were looking for?