I was on a call with a customer's IT lead four months after their system went live. He was calm, which was the strange part. Then he said the sentence I have not been able to unhear since: "We found our ERP key in a data export."
It had been sitting in a table row the whole time. Someone in procurement had filled in a field labelled "Access Token" during go-live testing. That field was in the export template. The export template was emailed to three people. One of them had a habit of dropping exports on a shared drive.
Nothing was hacked. Nothing needed to be. The platform had done exactly what we built it to do: it treated a credential as data.
Every secret we own has a home now
Let me describe my own product honestly, because the pattern is not unique to us.
At the team level we store a company secret — the key that guards the tenant's data. At the application level we store an app key per application. At the user level we issue API keys so external agents and MCP clients can call the platform on a user's behalf, with an optional expiry, and you can create as many as you like for different agents and different lifespans.
Three scopes, one idea, and each of them was invented separately because each solved a problem at a different layer. Nobody sat down and designed "credential management." We designed an integration builder, then a customization capability, then an AI access path, and each one needed a key.
That is how a low-code platform quietly becomes a credential store. Not by decision. By accumulation.
The uncomfortable part is not that we store secrets. Every platform does. The uncomfortable part is that we store them as fields, and fields inherit every property of the data model around them — the read permissions, the export path, the audit trail, the search index, the dashboard aggregation, the notification template, the AI agent's query scope. A password stored in a text field is not a password. It is a record.
The four questions nobody writes down
When a customer connects their ERP, nobody asks these questions out loud. Six months later, everybody does, usually during an incident.
Who owns it? Not who created it — who is accountable when it expires at 2 AM on a Sunday. A shared human account is not an owner. Every connector should have a service identity and a named person behind it. In practice, the key belongs to whoever pasted it in, and that person may have left the company.
Where does it live? If the answer is "in the configuration," then it also lives in every clone, every export, every backup, every screenshot attached to a support ticket, and every environment it was copied into. Our backup runs keep thirty days of platform configuration offsite, which is good disaster recovery and terrible secret hygiene. The backup is not the problem. The secret in the backup is.
How long does it live? We let people set an expiry on API keys, which is more than many platforms do. But an expiry you cannot enforce is a suggestion. If rotation means editing a field in five places — the connector config, the scheduled job that reads it, the script that calls the API, the test environment clone, and the documentation page — then rotation is a project, and nobody schedules projects for things that are currently working.
Who can read it? This is the question that breaks the mental model, because in a low-code platform the answer is not "security team." The answer is "everyone who can see the table." Anyone with read permission on that configuration record, anyone who can export, anyone whose dashboard is built on it, anyone who receives a workflow notification that includes it, and — increasingly — any AI agent that was granted data access and decides the table looks interesting.
Cloning is our best feature and our worst credential habit
Here is the thing that no integration tutorial tells you: our clone button is the most dangerous feature on the platform for credentials.
Clone an application and you clone its connector configurations, including whatever key was embedded in them. Test environment now points at production. A developer runs a test and creates a real invoice, or worse, a test that sends a real notification to a real customer. When this happens, teams blame the developer. The developer did nothing wrong. The platform gave them a one-click path from a safe environment to a production credential.
Then it compounds. The clone gets exported for a partner. The export gets version-controlled. The repository is private, until it is not — or until someone pastes the configuration into a chat message to ask a question. Every configuration artifact that leaves the platform carries whatever was inlined into it, and a credential inlined into configuration is a credential in the wild.
Where the leak actually comes from
Most credential incidents I have seen were not attacks. They were features.
The "test connection" button that helpfully prints the outgoing request when it fails. The error page that includes the endpoint URL with the token in the query string. The audit history that records that a field changed — and stores both old and new values, forever, in a table anyone with audit access can search. The search index that crawls a configuration table because indexing everything is what indexes do. The attachment URL that carries its own token, so the link works for anyone who receives it. The screenshot in the ticket that the customer posted to a public support forum.
Our own integration documentation is explicit about the intent: workflows should reference managed secrets rather than exposing values in configuration, logs, screenshots, or exports. That is the correct rule. It is also a rule that a drag-and-drop builder makes very easy to break, because the easiest way to make an integration work in ten minutes is to paste the key into the box.
What "done" would look like, and why it never is
If I were to design this properly — and I am now arguing we have to — a credential would not be a field. It would be an entity with a name, an owner, a scope, an environment, a rotation date, and a read log. Connectors would reference it by name. The value itself would never be returned to the interface after creation, because there is no legitimate reason for a builder to read a key it already configured.
Rotation would be a scheduled job on the platform, since the platform already has a scheduler. Revocation would actually revoke: deleting a key in the interface must invalidate the cached copy inside a running process, not just the row in the database. And rotation would need a dual-key window, because the old key has to keep working until every consumer has the new one. That is not a feature you can bolt on later. It is a design decision about how the platform talks to the outside world.
The hard truth is that none of this is new. It is standard service-identity hygiene that any backend team already knows. The only thing low-code adds is scale and pace. We made credential creation point-and-click, and then measured our success by how fast a business analyst could connect a system without talking to IT.
The uncomfortable conclusion
I used to be proud of that ten-minute integration. A procurement analyst built a live connection to the ERP without a single ticket, and I put it in a slide.
What she also built, in those ten minutes, was an enterprise-wide credential with no owner, no expiry, no scope, no inventory, and no way for the security team to enumerate it. The platform made secret management so frictionless that it stopped looking like security work at all.
That is the uncomfortable conclusion. Our best-loved feature was a credential factory, and we shipped it without a registry.
The integration was never the hard part.
Keeping the key is.
Top comments (1)
tr.ee/dev-to