"Feature Spotlight: How secrets management keeps your credentials out of agent state"
Modulo Team
Feature Spotlight: How secrets management keeps your credentials out of agent state
Your agents need API keys, database passwords, and OAuth tokens to do their work. Modulo encrypts those credentials at rest with Fernet symmetric encryption, decrypts them once at run-start into a transient context object, and never writes them to LangGraph state, checkpoint blobs, logs, or OpenTelemetry spans. A connector receives the decrypted value in-process, uses it for the call, and discards it. The credential is never serialised, never logged, and never returned in an API response.
In short
- Credentials are encrypted at rest with Fernet, keyed from
FERNET_KEY, which is a separate key from the JWT-signingSECRET_KEY. - Decrypted credentials live only in a transient, run-scoped context object. They never enter LangGraph state, checkpoint blobs, OpenTelemetry span attributes, or log output.
- The DOM rule: sensitive values render as dots by default; a reveal action calls the server, which verifies authentication and returns the value for a 30-second display window.
- A pluggable
SecretsBackendinterface is planned for v2, enabling Vault or AWS Secrets Manager as the backing store.
The problem it solves
Take Priya, a platform engineer building an agent pipeline that pulls data from a SaaS API and posts summaries to Slack. The pipeline needs the SaaS API key and the Slack webhook secret. Her first instinct is to hard-code them in the agent prompt or pass them through the pipeline state so every node can read them. That works until someone inspects the checkpoint blob and sees the key in plain text, or the OpenTelemetry trace exports the webhook secret as a span attribute, or a log line captures the decrypted value during a retry. Any of those is a credential leak, and each one has a different blast radius: the checkpoint blob persists across restarts, the trace goes to an observability vendor, the log goes to a log aggregator. The damage depends on where the secret ends up, but the root cause is always the same: the credential was allowed to exist somewhere it should not.
How it works
Modulo's secrets management has five layers. Each one exists because a different failure mode demanded it.
Fernet encryption keyed from FERNET_KEY
Every connector credential and model backend API key is encrypted at rest with Fernet symmetric encryption. The encryption key comes from the FERNET_KEY environment variable. Modulo refuses to start if FERNET_KEY is absent or shorter than 32 bytes, so a misconfigured deployment never silently stores plaintext credentials. Fernet is an authenticated encryption scheme: it provides confidentiality and integrity, so a tampered ciphertext fails to decrypt rather than producing a corrupted credential.
Separate FERNET_KEY and SECRET_KEY
FERNET_KEY handles credential encryption. SECRET_KEY handles JWT signing. Two distinct keys, two distinct cryptographic purposes. This separation means you can rotate connector credentials without invalidating every active session, and you can rotate the JWT signing key without re-encrypting every stored credential. Mixing the two into a single key creates a coupling where a rotation on one side forces a rotation on the other, and that coupling has caused outages in systems that tried it.
The credential-in-state rule
Decrypted connector credentials and model backend API keys must never enter LangGraph state, checkpoint blobs, OpenTelemetry span attributes, or log output. When a run starts, Modulo decrypts the credentials once into a transient, run-scoped context object. Connectors receive the decrypted value through that context, use it for the API call, and do not serialise it. The context object is not persisted. If a checkpoint is taken after the call, the credential is not in the checkpoint. If a span is exported, the credential is not in the span. If a log line is written, the credential is not in the log. This is a hard rule, not a convention that developers are asked to follow.
Never logged or returned in API responses
Secrets are never logged and never returned in API responses. When you list connectors through the API, you see the connector name, type, and configuration metadata, but the encrypted credential itself is not exposed. When you list model backends, the API key is not in the response. This is the same rule applied at the API boundary: if a value is a secret, it does not leave the server in a response body.
DOM sensitive data rule with 30-second reveal window
Sensitive field values — API key secrets, connector credentials, webhook secrets, model backend API keys — must never exist in the DOM in plaintext unless the user has explicitly completed a server-authenticated reveal action. By default, the frontend renders these values as ●●●●●●●. When a user clicks reveal, the frontend calls the server, the server verifies authentication, returns the value in the API response body, and the frontend injects it into the DOM for a time-limited display window. The default window is 30 seconds. After that, the value is removed from the DOM and the element reverts to dots. The value is never stored in a JavaScript variable beyond the display window.
Pluggable SecretsBackend
A pluggable SecretsBackend interface is planned for v2. The interface will allow Modulo to use external secret stores — HashiCorp Vault, AWS Secrets Manager, or any backend that implements the interface — instead of the built-in Fernet-at-rest store. The credential-in-state rule, the DOM rule, and the logging rule apply regardless of which backend is in use. The backend controls storage and retrieval; the security model controls what happens after retrieval.
Journey example: before and after
Before
Priya, the platform engineer, builds her pipeline with the SaaS API key passed through the pipeline state. Every node that needs the key reads it from state. The checkpoint blob stores the full state, including the key. When she enables OpenTelemetry tracing, the key appears as a span attribute on every API call. A colleague spots the key in a Jaeger trace during a debugging session and opens a security ticket. The key is rotated, the trace data is purged, and the pipeline is rewritten to avoid passing the key through state. That rewrite takes a day, and the team loses confidence in the pipeline's security model.
After
Priya configures the SaaS connector in Modulo with the API key. The key is encrypted at rest with Fernet. When the pipeline runs, the key is decrypted once into a transient context. The connector reads the key from the context, makes the API call, and discards it. The checkpoint blob does not contain the key. The OpenTelemetry trace does not contain the key. The log output does not contain the key. Priya checks the traces after the first run and sees the API call span with no credential data. The security ticket is never filed. The pipeline runs without incident.
When to use it vs. when not to
Use Modulo's secrets management whenever an agent or connector needs a credential: API keys, database passwords, OAuth tokens, webhook secrets, or model backend keys. If a value would cause damage if leaked, it belongs in the secrets system. This includes credentials that seem harmless — a read-only API key can still expose data, and a webhook secret can still be used to forge events.
Do not store non-secret configuration (endpoint URLs, timeouts, retry counts) as secrets. Those values are not sensitive, and storing them as secrets adds overhead without benefit. The encryption and decryption cycle is cheap, but unnecessary encryption of non-sensitive config creates noise in the secrets list and makes it harder to audit which values are actually sensitive.
Where it fits in a pipeline
Secrets management is a cross-cutting concern that touches every node that uses a connector or model backend. It works alongside HITL gates (which control human review at high-stakes steps) and the principles of Modulo (which treat autonomy as earned, not default). Secrets are one part of the security model that makes autonomous pipelines safe to run.
Go deeper
Read the secrets docs for the full encryption flow, or the related docs on connectors and model backends. Try the hosted demo at demo.modulo.run, or use the contact page to talk to us about securing credentials in your own pipeline.