I’ve been looking at the problem with passwords/API tokens in a CODESYS PLC project (WAGO PFC300), and I’m curious how others here handle the same thing.
The problem: a WAGO PFC300 needs a password to authenticate against another system (in our case, a router used for SMS alerting). Writing it in plaintext into the project file is bad practice, but I did not find any support for Bitwarden, lastpass etc — so you can’t just call a modern secrets API (like Bitwarden Secrets Manager) directly; the response comes back encrypted.
My proposed solution:
A small Python bridge (~30 lines) on a Debian VM (Proxmox) does the actual decryption via Bitwarden’s official bws CLI and caches the result in memory. The PLC only ever speaks plain HTTP/JSON to the bridge, never directly with Bitwarden. Tailscale connects the PLC and the bridge privately, with an ACL restricting which devices can even reach the bridge’s port. The PLC only fetches on request (not at boot) and caches the last value in a RETAIN variable — so an outage of the bridge only blocks rotating a secret, never operations.
Security doesn’t rely on one thing — it’s layered:
On the Bitwarden side: the bridge authenticates as a dedicated machine account, not a regular user. It can be granted read-only access, never write/delete, and scoped to one named project in the vault — not the rest of the organization’s secrets. If the bridge VM were ever compromised, the blast radius is limited to exactly that one project.
On the network side: Tailscale connects the PLC and bridge over a private, encrypted network — no public port at all. A Tailscale ACL tags the PLCs and the bridge VM separately and only allows traffic from the right tag to the right tag, on the right port — we have many other customer sites on the same tailnet, and none of them can reach the bridge unless explicitly tagged for it. ufw on the VM itself is an independent second layer restricting the same port to the Tailscale interface only. The bridge has no authentication of its own — deliberately, to keep the code simple; it’s the network layer and the Bitwarden scoping together that make it defensible.
Diagram:
Questions for you all: How do you solve this on WAGO/CODESYS controllers — something built into your SCADA platform, Vault, Home Assistant/Node-RED as a middle layer? Is “fetch on demand, not at boot” something you’ve also landed on for resilience, or do you handle it differently?
