Secrets¶
API keys, bearer tokens and other credentials should never be typed directly
into a node's configuration. A workflow is a Drupal config entity, so
anything you type into it is written out by drush config:export and committed
to git — and it is copied again into every job record the workflow produces,
where anyone who can view pipeline runs can read it.
Instead, store the credential in the Key module and put a reference to it in the node config:
${{ secrets.NAME }}
The reference is what gets saved. The real value is substituted in memory while the node runs, and never reaches storage.
Setup¶
1. Install the Key module. It is an optional dependency:
composer require drupal/key
drush en key
Without it, references are left as written and nothing is resolved.
2. Add the secret at FlowDrop → Configuration → Secrets
(/admin/flowdrop/config/secrets). Set the value as an environment variable
in your hosting environment first, then register it here by name — FlowDrop
stores only the variable name, never the value, so nothing sensitive reaches
a configuration or database export. Adding it here also allows it in one step.
Nothing resolves until a secret is allowed
The allowlist starts empty, and a reference to a secret that is not on it fails exactly as a reference to one that does not exist. This is deliberate — see Why the allowlist — but it does mean a reference to a key created elsewhere will fail until an administrator ticks it on this screen.
Using a key created elsewhere¶
Keys defined directly in the Key module (Configuration → System → Keys) also appear on the FlowDrop screen; tick one to allow it. Key can read a value from an environment variable, a file outside the docroot, configuration, or state.
Prefer environment variables or files
The config and state providers store the value inside Drupal — the
config provider writes it into a config entity, so drush config:export
carries it into git, which is exactly the exposure secret references exist
to remove. FlowDrop flags these on the settings screen. Use them only if you
understand where the value ends up.
Using a reference¶
A reference works anywhere a string appears in node config, including inside a JSON field. The most common case is an HTTP Request node's headers:
{
"Authorization": "Bearer ${{ secrets.stripe_api_key }}",
"Content-Type": "application/json"
}
It does not have to be the whole value — it can sit inside a longer string, and a single field can contain more than one reference.
Whitespace inside the braces is optional: ${{secrets.name}} and
${{ secrets.name }} are the same.
What happens when something is wrong¶
| Situation | Result |
|---|---|
| Key module not installed | Reference left as written; the node runs |
| Secret not allowed | Node fails |
| Secret does not exist | Node fails (identical message) |
| Secret exists but holds no value | Node fails |
The settings screen may say a variable is not set
Adding an environment secret warns if the variable is not visible to the web process — but it saves anyway. Cron and queue workers run under a different environment, so a variable set only for the CLI is a perfectly normal setup that resolves at execution time. Treat the warning as a spelling check, not a failure.
A missing secret fails the node rather than quietly becoming an empty
string. Substituting empty would send Authorization: Bearer to the remote
service, and you would be debugging somebody else's 401 instead of reading a
local error naming the key.
The not-allowlisted and does-not-exist cases produce the same message on purpose, so the error cannot be used to work out which keys a site holds.
Where a secret can and cannot go¶
A reference is only resolved when it comes from configuration you control as the workflow author — the value you typed into the node, or a default set on the node type.
A reference arriving in a runtime value is never resolved. It stays literal
text. That covers anything flowing in from an upstream node's output, a webhook
payload, or a tool parameter an AI model chose. Without this, a prompt-injected
model could simply ask for ${{ secrets.stripe_api_key }} and be handed it.
This has one practical consequence worth planning around:
Put secrets on inputs that are not wired
If a parameter's input port is connectable and exposed, a value arriving on a wire replaces your configured value. Your reference is not leaked — the incoming value is passed through unsubstituted — but it is not used either. Keep secrets on parameters whose ports are hidden, or that are not connectable at all.
Why the allowlist¶
A reference lets a workflow author use a credential without being able to read it. That is the point — but it also means that without a restriction, "can edit a workflow" would quietly amount to "can spend any credential this site holds". The allowlist keeps that an explicit administrator decision, one key at a time.
Leak protection¶
Two further precautions run automatically:
- Resolved values are excluded from FlowDrop's per-parameter debug logging.
- Resolved values are scrubbed out of job input, output and error messages before they are saved, so a node that echoes its own configuration — or an HTTP client that quotes the failing request in an exception — does not put the credential back into the database.
Common mistake: the missing $¶
{{ secrets.my_key }} ← wrong: no $
${{ secrets.my_key }} ← correct
Without the $, this is not a secret reference at all — it is ordinary
Prompt Template variable syntax, which resolves to nothing and
raises no error anywhere. FlowDrop warns about this when you save a workflow;
that warning is the only signal you will get, so do not ignore it.