Skip to content
Ironflow is pre-1.0: APIs can change between releases. Pin a version. What is ready →

Secrets Management

Ironflow provides environment-scoped secrets management with AES-256-GCM encryption backed by NATS KV. Secret values are never exposed through the API, CLI, or Dashboard. Only metadata (name, revision, timestamps) is readable. Plaintext values are exclusively injected at runtime into function execution contexts.

Secrets are designed around a “Management vs. Runtime” separation:

┌───────────────────────────────────────────────┐
│ Management Plane │
│ CLI / Dashboard / API │
│ • Set, delete, list secrets │
│ • Only metadata visible (name, revision) │
│ • Plaintext values NEVER returned │
└────────────────────┬──────────────────────────┘
│ AES-256-GCM encrypted
▼
┌───────────────────────────────────────────────┐
│ NATS JetStream KV │
│ • Encrypted values at rest │
│ • Revision tracking for atomic updates │
│ • System events on every mutation │
└────────────────────┬──────────────────────────┘
│ Decrypted at runtime
▼
┌───────────────────────────────────────────────┐
│ Runtime Plane │
│ Function execution (push/pull mode) │
│ • Secrets injected via `secrets.get()` │
│ • Only declared secrets are resolved │
│ • Values live only in function memory │
└───────────────────────────────────────────────┘

Functions declare which secrets they need in their configuration. The engine resolves and injects only those secrets at runtime:

createFunction(
{
id: "payout",
triggers: [{ event: "payout.requested" }],
secrets: ["STRIPE_KEY", "WEBHOOK_SECRET"], // Declare dependencies
},
async ({ secrets, step }) => {
const key = secrets.get("STRIPE_KEY"); // Injected at runtime
}
);

This prevents functions from accessing secrets they haven’t declared, following the principle of least privilege.

If a declared secret does not exist in the environment, the function runs without it and the server logs a warning. If a declared secret exists but cannot be read, the run fails with the error code SECRETS_UNAVAILABLE and the function does not run. This happens when the master key changed after the secret was stored, or when the stored value is damaged. If the secrets store does not answer, the function does not run either: a push run is retried within the function’s retry budget, and a pull run is retried when its lease expires.

All secret values are encrypted with a master key before storage. The master key is provided via the IRONFLOW_MASTER_KEY environment variable as a hex-encoded 32-byte value (64 hex characters — e.g., the output of openssl rand -hex 32). When set, secrets are encrypted with AES-256-GCM. When unset, the system operates in dev mode without encryption (plaintext on disk).

Every secret mutation emits a system event on the internal NATS bus — system.secret.{name}.{created|updated|deleted}. This enables:

  • Audit logging: Track who rotated what and when.
  • Automated workflows: Trigger cache invalidation or service restarts on rotation.
  • Dashboard notifications: Real-time visibility into secret lifecycle.
FeatureUse ForEncryptedReadable
SecretsAPI keys, DB passwords, certificatesYes (AES-256-GCM)Runtime only
ConfigApp settings, feature flags, togglesNoYes
KV StoreSessions, caching, countersNoYes

Like config, secrets are scoped to environments. A secret named STRIPE_KEY in production is entirely separate from STRIPE_KEY in staging, enabling safe per-environment credential management.