seekrit
← all posts

Securing self-hosted agent builders: Dify, Langflow, and Flowise

How-to

Dify, Langflow, and Flowise are the same product from a secrets standpoint. A web UI where you paste provider keys, a database where those keys are stored encrypted, a compose file that holds the encryption key and the database password, and a runtime that hands real values to whatever node a flow calls. A flow with an agent node and an HTTP tool is an agent with every credential the instance knows.

In April 2025 Langflow shipped a fix for CVE-2025-3248: an endpoint that ran submitted Python through exec() with no authentication, CVSS 9.8, exploited in the wild and used to build a botnet. A second remote code execution followed in 2026. That is one project, but the shape is common to the category: a Python or Node process on the internet, with a code-execution feature by design, holding every key in its environment. When one of these is popped, the attacker's first move is env.

This post covers the two layers that decide what env returns: the compose file and the flows.

Layer 1: the compose file

All three ship a docker-compose.yml that reads a .env. The names differ, the roles do not:

Encrypts stored credentialsDatabaseAdmin login
DifySECRET_KEYDB_PASSWORD, REDIS_PASSWORDfirst-run setup
LangflowLANGFLOW_SECRET_KEYLANGFLOW_DATABASE_URLLANGFLOW_SUPERUSER, LANGFLOW_SUPERUSER_PASSWORD
FlowiseFLOWISE_SECRETKEY_OVERWRITEDATABASE_PASSWORDFLOWISE_USERNAME, FLOWISE_PASSWORD

The first column is the one to care about. It is what stands between a database dump and every provider key anyone ever pasted into the UI. Langflow and Flowise generate one on first start if you do not set it, and write it to the data volume, where it survives every backup of that volume.

Import the .env into seekrit, mint a token, and delete the file. Because these compose files already use ${VAR} interpolation, the simplest shape needs no image changes:

seekrit secrets import docker/.env --app dify --env production
seekrit token create --app dify --env production --name dify-host
rm docker/.env

SEEKRIT_TOKEN=skt_… seekrit run -- docker compose up -d

seekrit run resolves the environment, decrypts on the host, and runs docker compose with the values present, so every ${SECRET_KEY} fills in. The values are in the container's environment afterwards and in docker compose config, which is the same exposure as before minus the file on disk. The Compose post has the entrypoint shape for putting the resolver inside each image instead, so only a token appears in docker inspect. With a stack of five services, the interpolation shape is usually the right trade.

If you set the encryption key for the first time here, do it before the first start. Changing it after credentials exist makes them undecryptable in all three products.

Layer 2: the flows

Provider keys pasted into the UI are stored in the database, encrypted with the key above, and decrypted for every flow that uses them. Whichever node runs, the value is in the process. The seekrit change is to store a placeholder in the UI instead of a key and run seekrit-proxy beside the stack.

All three products let a model node use an OpenAI-compatible provider with a custom base URL. Set the base URL to the proxy and the key to a placeholder:

WhereBase URLAPI key
DifySettings, Model Provider, OpenAI-API-compatiblehttp://seekrit-proxy:8080/openai/v1{{seekrit:OPENAI_API_KEY}}
LangflowOpenAI component, OpenAI API Basehttp://seekrit-proxy:8080/openai/v1{{seekrit:OPENAI_API_KEY}}
FlowiseChatOpenAI node, Base Pathhttp://seekrit-proxy:8080/openai/v1{{seekrit:OPENAI_API_KEY}}

The sidecar:

seekrit proxy compose --preset openai --host api.stripe.com=STRIPE_KEY > compose.proxy.yaml
services:
  seekrit-proxy:
    image: seekritdev/proxy
    environment:
      SEEKRIT_TOKEN: ${SEEKRIT_PROXY_TOKEN}
    volumes: ["./seekrit-proxy.toml:/seekrit-proxy.toml"]
    command: ["--listen", "0.0.0.0:8080"]

The proxy publishes no ports. It is reachable by service name from the other containers and from nowhere else. Its token is bound to an environment that holds the provider and tool keys, and that environment is a different one from the compose-file environment in layer 1, so the builder's own token cannot resolve a provider key.

Now the database holds {{seekrit:OPENAI_API_KEY}}. A dump, an exported flow with embedded credentials, or an agent node asked to print its key yields that string. It works only through the proxy, only toward api.openai.com, and only for the paths and methods the route allows.

Tool credentials follow the same shape. An HTTP request node calling Stripe gets an Authorization: Bearer {{seekrit:STRIPE_KEY}} header and a URL under http://seekrit-proxy:8080/stripe/, and the route bounds it:

[[route]]
prefix = "/stripe"
upstream = "https://api.stripe.com"
allow = ["STRIPE_KEY"]
methods = ["GET"]
paths = ["/v1/charges", "/v1/charges/*"]

An agent that was talked into refunding a charge sends a POST and is refused before it leaves the compose network, with the rule named in the response.

Two product-specific notes:

  • Langflow imports environment variables as global variables by default (LANGFLOW_STORE_ENVIRONMENT_VARIABLES, with LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT naming which). With layer 1 in place, that would turn every value in the seekrit environment into a global variable available to every flow. Set the list explicitly to the non-secret names you want flows to see, or set the first variable to false.
  • Flowise's credential UI shows a shortened fake value for stored keys. That is a display protection, not a storage one. The stored value is still the full key, encrypted with the key from layer 1, and a placeholder is still the thing to store.

The part no proxy fixes

An instance on the public internet with a code-execution feature is an instance that will eventually run someone else's code. Keep Langflow above 1.3.0 and current. Put the editor behind authentication that is not the product's own login, or off the internet entirely. Set LANGFLOW_AUTO_LOGIN to false. Disable Dify's and Flowise's code nodes for users who do not need them. Restrict who can edit flows, because whoever can edit a flow can point a node at any URL, and the proxy will refuse the key but cannot refuse the request going somewhere else without it.

Then plant a honey token where the old .env was, and one as a provider credential in the UI with a name that looks real. Nothing legitimate uses either. A trip means the file or the database was read.

Checklist

  1. Import .env into seekrit, delete it, run compose through seekrit run.
  2. Set the encryption key deliberately, before first start.
  3. Run the proxy as a sidecar with its own token and its own environment.
  4. Replace every provider credential in the UI with a placeholder and the proxy's base URL.
  5. Route tool credentials the same way, with methods and paths.
  6. Langflow: narrow LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT.
  7. Patch, put the editor behind your own authentication, and plant a honey token.

The agent proxy guide has the full route syntax, including the forward mode for products that do not expose a base URL field.