guest@websec ~ % man 15-environmental-security
15 · Environmental Security
Server hardening is its own job. But every developer owns one part: the secrets the app runs on. They must never end up where an attacker can read them.
What counts as a secret
- Database URLs and passwords
- The app's signing key (
secret_key_base). Whoever has it can forge any session. - Third-party API keys: payment providers, email services, cloud credentials
- OAuth client secrets, webhook signing secrets, private TLS/SSH keys
Where they leak: Git commits, Docker images, logs, chat, front-end bundles.
Three ways to manage secrets
Secrets come from env vars set by the platform. Locally, a git-ignored .env file.
Simple, but no audit trail.
The Rails way: secrets in an encrypted file you can commit (credentials.yml.enc). The key (master.key) stays out of Git.
master.key is git-ignored; servers get RAILS_MASTER_KEY.A service like Vault or AWS Secrets Manager hands secrets to the app, logs every read, and rotates them.
Encrypt some secrets, see what's safe to commit, then try the wrong key.
credentials.yml.enc (committed to git)
server boot
Reading secrets in code
Load secrets at startup and crash if one's missing. A silent fallback like "changeme" is how apps end up signing sessions with a known key.
# bin/rails credentials:edit --environment production # → config/credentials/production.yml.enc + config/credentials/production.key Rails.application.credentials.stripe_secret_key! # raises if blank: fail fast Rails.application.credentials.fetch(:database) # KeyError if missing Rails.application.credentials.dig(:aws, :secret_access_key) ENV.fetch("DATABASE_URL") # fetch, don't default ENV["SECRET_KEY_BASE"] || "changeme"
// config.js: validate env at boot with zod (or envalid) import { z } from "zod"; const env = z.object({ DATABASE_URL: z.string().url(), SESSION_SECRET: z.string().min(32), STRIPE_SECRET_KEY: z.string().startsWith("sk_"), }).parse(process.env); // throws at startup if anything is missing or malformed export default env; // local dev only: load .env (git-ignored) if (process.env.NODE_ENV !== "production") await import("dotenv/config"); // never: process.env.SESSION_SECRET || "changeme"
# settings.py: pydantic-settings validates and fails fast from pydantic import Field, PostgresDsn from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env") # .env is git-ignored database_url: PostgresDsn secret_key: str = Field(min_length=50) stripe_secret_key: str = Field(pattern=r"^sk_") settings = Settings() # raises ValidationError at startup if missing # never: SECRET_KEY = os.environ.get("SECRET_KEY", "changeme")
credentials.some_key quietly returns nil. Use credentials.some_key! so a missing secret stops the boot.Is it safe to commit?
If a secret leaks
Deleting the commit doesn't help; bots grab keys within minutes. Rotate the key first. Then add a scanner to pre-commit and CI.