Skip to main content
Picture a Friday evening deploy at a payments team in Pune. Someone moves the server to a new host and copies the environment across by hand. One line is missed: the database connection string. The server comes up, the health check says it is alive, and everyone goes home. On Saturday morning the first refund of the day, say ₹18,000 to a customer whose order was cancelled, fails with an error nobody has seen before. Or worse, it does not fail. The server quietly fell back to keeping its records in memory, and by Monday, after a restart, the record of that refund is gone. That is the failure I worry about most in operations. Not a crash. A server that looks healthy and is running with a weaker version of the rules than the one you think you deployed.

The problem: lazy failure

Most software reads its configuration as it needs it. A missing key file is discovered when the first request tries to sign something. A missing database is discovered when the first write happens. Until then, everything looks fine. For an authorization layer that is backwards. The whole point of Parmana is that an agent’s action runs only after checks that leave evidence behind. If the thing that stores the evidence is missing, the right moment to find out is before any request is accepted, not halfway through one. There is a second, quieter version of the same problem: the silent fallback. A setting is wrong, so the code picks something that works. It feels helpful. In a security system it is the worst choice available, because the operator believes they have one protection and actually have a weaker one.

The idea: fail closed, at startup, by name

The rule I follow is simple. If a setting the server cannot work safely without is missing or wrong, the server refuses to start, and the error names the variable. No degraded mode, no default that looks like the real thing. The environment variable reference ends with a table of these startup errors and their fixes, because they are meant to be read by the person deploying, at the moment they deploy.

How Parmana does it

These are the checks the claims register backs (sections 2.17 and 2.18):
  • No caller keys, no server. If PARMANA_API_KEYS is empty or unset, the server will not start. The only way around it is setting PARMANA_AUTH_DISABLED=true explicitly, which is for local development, logs a warning on every boot, and shows up as authDisabled on the readiness check so monitoring can alert on it.
  • No policy directory, no server. An unset or blank PARMANA_POLICY_DIR stops startup with a named error, instead of an obscure crash on the first request.
  • No database, no server. Outside tests, the nonce store and the caller audit trail always live in Postgres. If DATABASE_URL is missing, startup fails. There is a test that the server never silently falls back to the in-memory versions.
  • No signing keys, no server. If the default key pair is not present, as files or through PARMANA_KEY_MATERIAL_JSON, startup stops.
  • No pretend key custody. KEY_PROVIDER accepts some values that have no implementation: azure-key-vault, gcp-kms and hsm. Until 2026-09-10, setting one of them quietly gave you private keys on disk. Now the server refuses to start and says the value is not implemented. That fix is the clearest example of why this matters: an operator expecting a key vault got files, with no error at any point.
  • No odd key ids. The file key provider rejects any key id with characters outside letters, digits, dot, underscore and hyphen, so a crafted id such as one containing ../ cannot point outside the key directory.
A few other settings follow the same pattern: a half-configured refund connector, a retired variable that is still set, a sandbox approver set outside a sandbox. Each stops the server with its own message. On a container host, these checks run before the server opens its port. On Vercel, where production runs, the checks that need no network call run when the function loads, and the check that the KMS signing key is reachable runs on the first request instead, because the credentials it needs only exist inside a request. Either way, nothing is authorized before the check passes.

What it does not do

  • Not every setting fails at startup. An unimplemented HASH_PROVIDER value passes validation and fails the first time the server hashes. An unrecognized LOG_LEVEL falls back to info. I would rather say so than claim a guarantee I have not built.
  • Some checks are fail open on purpose. The check that live policies still match their signed approvals runs after the server is up and logs a mismatch; it does not stop traffic. The execution path enforces policy approval separately.
  • It checks presence, not wisdom. A valid but wrong database URL, or a caller key given too many capabilities, passes. Startup validation catches missing and malformed settings, not bad decisions.
  • Development modes are not covered. NODE_ENV=test turns off the durable stores, and development relaxes three safety gates. A deployment that sets them has removed protections on purpose, and the threat model says so.

The takeaway

A server that refuses to start is annoying for ten minutes. A server that runs with weaker rules than you think can be wrong for months. When the job is deciding whether an agent may move ₹18,000, I want the annoying version every time. Read more: Environment variable reference, Production deployment runbook.