guest@websec: ~/websec/15-environmental-security — zsh — 100×40 by SaKKo ENTH code:

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.

Credentials files are encrypted and safe to commit. 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.

sim: encrypted credentials filereal AES-GCM in your browser

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?

quiz: sort the filesclick commit or ignore for each

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.

sim: secret scannerpaste a diff or config file