Secure Node-RED in production: a 101 guide with recipes
By Etienne Rossignon, CEO of Sterfive and maintainer of node-opcua. Version 2.2, updated 5 November 2026. Applies to Node-RED 4.x and 5.x (reference: Node-RED 5.0.9, Node.js 22.9 or later).
Short answer: Node-RED has no password by default. To make it safe for production, set adminAuth, put Node-RED behind a TLS reverse proxy, and set your own credentialSecret. Then lock the palette and run Node-RED as a non-root user. Block the network traffic that your flows do not use. This guide gives the 12 rules first, then 18 recipes with examples. For OPC UA servers that you write with node-opcua, read Secure your node-opcua server.
About this guide
This guide tells you how to make a Node-RED installation safe for production. It is for integrators, OT engineers, IT administrators and machine builders.
The text uses ASD-STE100 Simplified Technical English (STE), Issue 9. The sentences are short. Each instruction has one action. The names of settings, files and commands are technical names. We show them in code.
Some settings have a different name in old versions. Upgrade Node-RED to version 4 or 5 before you use this guide.
TL;DR: the 12 rules
Do these 12 tasks before you connect Node-RED to a production network:
- Set
adminAuthinsettings.js. Without it, any person on the network can run commands on the host. - Give each person a personal account. Give the
readpermission to persons who only monitor. - Set
httpNodeAuth, or put an access control in front of thehttp inendpoints and the dashboard. - Set
uiHostto127.0.0.1. Put a reverse proxy with TLS in front of Node-RED. - Let only the engineering zone or a VPN connect to the editor.
- Set your own
credentialSecret. Keep it out of Git and out of the backup archive. - Set
externalModules.palette.allowInstalltofalsein production. - Remove the
execnode withnodesExcludesif your flows do not use it. - Do not run Node-RED as
root. Use a dedicated user, a hardened systemd unit or a hardened container. - Block all outbound traffic that your flows do not use.
- Use
SignAndEncryptfor OPC UA and TLS for MQTT. Do not accept all certificates automatically. - Keep backups. Examine
flows.jsonforexec,functionandhttp innodes that you do not know.
Then test the result from a different computer with Recipe 18.
1. Why this guide is necessary
1.1 The problem is the configuration, not Node-RED
By default, Node-RED does not ask for a password. A person who can connect to the editor can deploy a flow. A flow can contain an exec node or a function node. These nodes can run operating system commands. Thus, an open editor gives remote code execution on the host.
This is a function of the product, not a bug. The Node-RED core project has no published CVE from 2023 to November 2026. The critical CVEs in this period are on products that contain Node-RED without authentication:
| CVE | Product | CVSS | Published | Correction |
|---|---|---|---|---|
| CVE-2026-58115 (Siemens SSA-834709, CISA ICSA-26-237-03) | SIMATIC IoT2050 Advanced, Basic, M.2 and SM with Industrial OS below V4.3.4-1 and Node-RED | 10.0 | 2026-08-11, revised 2026-11-04 | Update to V4.3.4-1, or harden or remove Node-RED |
| CVE-2026-25938 (GHSA-v4p5-w6r3-2x4f) | FUXA SCADA 1.2.8 to 1.2.10, Node-RED plugin: authentication bypass on /nodered/flows | 9.5 (v4) | 2026-02-09 | Update to FUXA 1.2.11 |
| CVE-2025-41656 (CERT@VDE VDE-2025-045) | Pilz IndustrialPI 4, firmware Bullseye up to 2024-08 | 10.0 | 2025-07-01 | New firmware |
| CVE-2025-24522 (CISA ICSA-25-121-01) | KUNBUS Revolution Pi OS Bookworm 01/2025 and earlier (Node-RED on TCP 41880) | 10.0 | 2025-05-01 | New OS image, or set Node-RED authentication |
All four have the same weakness: CWE-306, "Missing Authentication for Critical Function".
1.2 Attackers use this now
On 30 October 2026, VulnCheck saw attacks on open Node-RED instances. The attack is one normal Admin API request: POST /flows. The flow contains an inject node and an exec node. The exec node downloads a Linux bot to /dev/shm or /tmp and starts it.
Other requests add an http in endpoint that sends its payload to an exec node. This is a web shell. The web shell is in flows.json. Thus, it stays after a reboot.
The attacker does not use exploit code. The scanners do not look for Siemens devices only. They attack all Node-RED instances that have no adminAuth.
No public source gives a dated count of Node-RED instances on the internet. Thus, scan your own address ranges (Recipe 18).
1.3 What an attacker can do after access
- Run commands on the host with the privileges of the Node-RED process.
- Read the credentials of your MQTT brokers, databases and OPC UA servers.
- Use the connections that your flows have to PLCs and to the IT network.
- Change the values that your flows write to the process.
WARNING: Do not connect a Node-RED editor without authentication to a network. An open editor on an OT network can change process values and cause damage to equipment.
2. Know the attack surface
Node-RED has these entry points:
| Entry point | Default path | What it gives | Main control |
|---|---|---|---|
| Editor and Admin API | / (httpAdminRoot) | Full control: deploy flows, install nodes, read settings | adminAuth, disableEditor, network filter |
http in endpoints and dashboard | / (httpNodeRoot) | The HTTP API that your flows make | httpNodeAuth, httpNodeMiddleware, proxy |
| Static files | httpStatic | Files from a folder | httpStaticAuth |
| Palette manager (npm) | Editor | Installation of code from npm | externalModules |
function node modules | Editor | require of npm modules in flows | functionExternalModules |
exec and daemon nodes | Flows | Operating system commands | nodesExcludes, systemd or container limits |
| Credential file | flows_cred.json | Passwords of all configured connections | credentialSecret, file permissions |
| Diagnostics endpoint | /diagnostics | Versions, settings, host data | diagnostics.enabled |
| Outbound connections | Flows | Data exfiltration, payload download | Egress firewall |
The function node runs in a Node.js vm context. This context is not a security sandbox. A person who can edit a function node can run code with the privileges of Node-RED.
3. Recipes
Each recipe has a goal, the steps and an example. The default user directory is ~/.node-red. In the official Docker image, it is /data.
Recipe 1: Make a password hash
Goal: Make a bcrypt hash for settings.js. Do not write passwords in clear text.
- Open a terminal on the Node-RED host.
- Type
node-red admin hash-pw. - Type the password when the command asks for it.
- Copy the hash. It starts with
$2.
node-red admin hash-pw
In Docker, type this command:
docker exec -it mynodered npx node-red admin hash-pw
CAUTION: Use a long password. Do not use the same password for two accounts.
Recipe 2: Set authentication on the editor and the Admin API
Goal: Make sure that only known users can open the editor and deploy flows.
- Open
settings.jsin the user directory. - Find the
adminAuthblock. Remove the comment marks. - Add one entry for each person. Use the hash from Recipe 1.
- Give
"*"(full access) only to persons who deploy flows. - Give
"read"to persons who only monitor. - Restart Node-RED.
adminAuth: {
type: "credentials",
sessionExpiryTime: 28800, // 8 hours, in seconds (default: 7 days)
users: [
{
username: "alice",
password: "$2b$08$REPLACE_WITH_HASH",
permissions: "*"
},
{
username: "operator",
password: "$2b$08$REPLACE_WITH_HASH",
permissions: "read"
}
]
},
Single sign-on. For many users, connect Node-RED to your identity provider. Set type: "strategy" and use a Passport.js strategy (OpenID Connect, SAML or LDAP). The users list gives the permissions to each identity.
WARNING: Do not give permissions to anonymous users in production. If you set
default: { permissions: "*" }, all persons get full access without a password.
Recipe 3: Protect the http in endpoints, the dashboard and the static files
Goal: Make sure that the HTTP API of your flows and the dashboard ask for credentials.
adminAuth does not protect httpNodeRoot. You must protect it separately.
- Make a hash with Recipe 1.
- Add
httpNodeAuthtosettings.js. - If you use
httpStatic, addhttpStaticAuth. - Restart Node-RED.
httpNodeAuth: { user: "api", pass: "$2b$08$REPLACE_WITH_HASH" },
httpStaticAuth: { user: "static", pass: "$2b$08$REPLACE_WITH_HASH" },
httpNodeAuth gives one shared account (HTTP Basic authentication). For personal access, use one of these options:
- Put an authentication proxy (for example oauth2-proxy) in front of the
httpNodeRootpath. - Set
httpNodeMiddlewareto a function that examines a token.
httpNodeMiddleware: function (req, res, next) {
if (req.get("Authorization") === "Bearer " + process.env.NR_API_TOKEN) {
return next();
}
res.sendStatus(401);
},
The dashboard (@flowfuse/node-red-dashboard, Dashboard 2.0) is served below httpNodeRoot. Thus, these settings also apply to it. The old node-red-dashboard package is not maintained. Do not use it for new projects.
Recipe 4: Listen on loopback only and use a reverse proxy
Goal: Make sure that Node-RED is not directly on the network. The reverse proxy adds TLS and access control.
- In
settings.js, setuiHost,httpAdminRootandhttpNodeRoot. - Restart Node-RED.
- Install nginx on the same host.
- Add the server block that follows. Change the names, the paths and the networks.
- Examine the configuration with
sudo nginx -t. - Reload nginx with
sudo systemctl reload nginx.
uiHost: "127.0.0.1",
uiPort: 1880,
httpAdminRoot: "/admin", // editor and Admin API
httpNodeRoot: "/api", // http in endpoints and dashboard
# /etc/nginx/conf.d/node-red.conf
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl;
http2 on;
server_name nodered.example.com;
ssl_certificate /etc/ssl/nodered/fullchain.pem;
ssl_certificate_key /etc/ssl/nodered/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
add_header Strict-Transport-Security "max-age=31536000" always;
# Editor and Admin API: engineering zone only
location /admin/ {
allow 10.10.0.0/24;
deny all;
proxy_pass http://127.0.0.1:1880;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # WebSocket for /admin/comms
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
}
# Flow endpoints and dashboard
location /api/ {
proxy_pass http://127.0.0.1:1880;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # the dashboard uses WebSocket
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
}
location / {
return 404;
}
}
If the editor shows "Lost connection to server", the proxy does not send the WebSocket headers. Examine the Upgrade and Connection lines.
For more protection, put the editor on a separate host name. Then you can add client certificates (mTLS) with ssl_verify_client on on that server only.
CAUTION: Keep
adminAuthactive behind the proxy. The proxy is a second barrier. It does not replace the authentication of Node-RED.
Recipe 5: Use TLS directly in Node-RED
Goal: Encrypt the traffic when you cannot use a reverse proxy.
- Put the key and the certificate in a folder that only the Node-RED user can read.
- In
settings.js, sethttps,httpsRefreshIntervalandrequireHttps. - Restart Node-RED.
https: function () {
return {
key: require("fs").readFileSync("/etc/nodered/tls/privkey.pem"),
cert: require("fs").readFileSync("/etc/nodered/tls/cert.pem")
};
},
httpsRefreshInterval: 12, // hours: Node-RED calls the function again
requireHttps: true, // sends HTTP requests to HTTPS
With the function form, Node-RED reads a renewed certificate without a restart.
Recipe 6: Protect the credential file
Goal: Make sure that a key that you control encrypts flows_cred.json.
Node-RED encrypts flows_cred.json. If you do not set credentialSecret, Node-RED makes a key and writes it in .config.runtime.json, in the same folder. Thus, a copy of the folder gives the passwords.
- Make a random secret with
openssl rand -hex 32. - Keep the secret in a vault or a secret manager.
- Give the secret to the process in the environment variable
NODE_RED_CREDENTIAL_SECRET. - In
settings.js, setcredentialSecret. - Start Node-RED. Node-RED moves the current credentials from the generated key to your key.
- Set the file permissions to
600.
credentialSecret: process.env.NODE_RED_CREDENTIAL_SECRET,
openssl rand -hex 32
chmod 600 ~/.node-red/flows_cred.json ~/.node-red/settings.js
WARNING: Do not set
credentialSecret: false. This value keeps all credentials in clear text.
CAUTION: Do not change
credentialSecretafter you set it. If you change or lose it, Node-RED cannot decrypt the credentials. Then you must type all passwords again.
Use a different secret for each environment (development, test, production).
Recipe 7: Lock the palette and the external modules
Goal: Prevent the installation of npm code from the editor in production.
All persons can publish an npm package with the name prefix node-red-contrib-. This prefix does not show that a package is safe. The OpenSSF malicious packages database lists npm malware with this prefix, for example MAL-2024-1030 and MAL-2025-27643.
- In
settings.js, setexternalModulesandfunctionExternalModulesas in the example. - Write the approved nodes in
package.jsonin the user directory. - Install them with
npm ci --omit=dev. - Examine them with
npm audit. - Keep
package.jsonandpackage-lock.jsonin Git.
externalModules: {
autoInstall: false,
palette: {
allowInstall: false,
allowUpdate: false,
allowUpload: false
},
modules: {
allowInstall: false
}
},
functionExternalModules: false,
cd ~/.node-red
npm ci --omit=dev
npm audit
If users must install some nodes, use allowList and denyList:
palette: {
allowInstall: true,
allowList: ["@flowfuse/node-red-dashboard", "@opcua/for-node-red"],
denyList: ["*"]
},
Before you approve a node, examine its npm page, its source repository, its maintainers and its install scripts.
Recipe 8: Remove the nodes that you do not use
Goal: Remove the exec node and other dangerous core nodes from the runtime.
- Find the file name of the node. The
execnode is in90-exec.js. - In
settings.js, setnodesExcludes. - Restart Node-RED.
- Open the palette. Make sure that the node is not there.
nodesExcludes: ["90-exec.js"],
Flows that use an excluded node stop. Remove a node only if your flows do not use it.
This step makes the attack more difficult. It does not stop an attacker who can edit a function node. Thus, Recipe 2 stays necessary.
Recipe 9: Run production without the editor
Goal: Remove the editor from the production instance. Make changes on a development instance. Deploy them with a controlled procedure.
Option A: Keep the Admin API for CI. Remove the editor user interface with disableEditor: true. The Admin API stays active, and adminAuth protects it.
Option B: Remove the editor and the Admin API with httpAdminRoot: false. Then you deploy with a new flows.json file and a restart.
disableEditor: true, // option A
// httpAdminRoot: false, // option B
This example shows a CI deploy with option A:
TOKEN=$(curl -s https://nodered.example.com/admin/auth/token \
--data "client_id=node-red-admin&grant_type=password&scope=*&username=ci&password=$NR_CI_PASSWORD" \
| jq -r .access_token)
curl -s -X POST https://nodered.example.com/admin/flows \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Node-RED-Deployment-Type: full" \
--data @flows.json
curl -s --data "token=$TOKEN" -H "Authorization: Bearer $TOKEN" \
https://nodered.example.com/admin/auth/revoke
Keep the flows in Git. On the development instance, you can use the Projects function (editorTheme.projects.enabled: true).
Recipe 10: Run Node-RED in a hardened container
Goal: Limit what a compromised flow can do on the host.
The official image nodered/node-red runs as the user node-red (UID 1000, GID 1000). It keeps all data in /data.
- Make the data folder and give it to UID 1000.
- Write the
compose.yamlfile that follows. - Put the credential secret in the
.envfile. Do not put.envin Git. - Start the container with
docker compose up -d. - Examine the logs with
docker compose logs -f node-red.
sudo mkdir -p /srv/node-red/data
sudo chown -R 1000:1000 /srv/node-red/data
services:
node-red:
image: nodered/node-red:5.0.9-24-minimal # pin the exact tag or the digest
user: "1000:1000"
read_only: true
tmpfs:
- /tmp
volumes:
- /srv/node-red/data:/data
environment:
- TZ=Europe/Paris
- NODE_OPTIONS=--max-old-space-size=512
- NODE_RED_CREDENTIAL_SECRET=${NODE_RED_CREDENTIAL_SECRET}
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
ports:
- "127.0.0.1:1880:1880" # loopback only: the reverse proxy is in front
mem_limit: 768m
pids_limit: 256
restart: unless-stopped
Notes:
- The
-minimalimage has no compiler and no Python. Use it if your nodes do not use native modules. Then an attacker has fewer tools. - With
read_only: true, the palette cannot install nodes. This is correct for production (Recipe 7). Test your flows with this value. - Docker can open published ports through a host firewall such as
ufw. Thus, publish the port on127.0.0.1only, or do not publish it. Put the proxy on the same Docker network. - Do not use
-u rootor--privileged. - For a serial port, add only that device with
devices:, and the group withgroup_add: [dialout].
Recipe 11: Run Node-RED with a hardened systemd unit
Goal: Limit the Node-RED process on a Linux host or a gateway.
The official installer makes nodered.service. This unit has no sandbox directives. Add them in a drop-in file. The installer does not change drop-in files.
- Make a dedicated system user
nodered. - Move the user directory to
/var/lib/nodered. Give it tonodered. - Type
sudo systemctl edit nodered.service. - Add the lines that follow.
- Type
sudo systemctl daemon-reload. - Type
sudo systemctl restart nodered. - Examine the result with
systemd-analyze security nodered.service.
sudo useradd --system --home /var/lib/nodered --create-home --shell /usr/sbin/nologin nodered
[Service]
User=nodered
Group=nodered
WorkingDirectory=/var/lib/nodered
ExecStart=
ExecStart=/usr/bin/env node-red-pi $NODE_OPTIONS $NODE_RED_OPTIONS --userDir /var/lib/nodered
Environment="NODE_OPTIONS=--max-old-space-size=512"
EnvironmentFile=-/etc/nodered/secrets.env
NoNewPrivileges=yes
ProtectSystem=strict
ReadWritePaths=/var/lib/nodered
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
SystemCallArchitectures=native
SystemCallFilter=@system-service
CapabilityBoundingSet=
AmbientCapabilities=
UMask=0077
# Optional egress filter (see Recipe 12)
# IPAddressDeny=any
# IPAddressAllow=localhost 10.20.0.0/24
Notes:
- Do not set
MemoryDenyWriteExecute=yes. The V8 JIT compiler of Node.js writes and runs code in memory. Node-RED does not operate with this directive. - If you use serial ports or GPIO, remove
PrivateDevices=yes. UseDeviceAllow=for each device. - Put
NODE_RED_CREDENTIAL_SECRET=...in/etc/nodered/secrets.env. Set the permission to600and the owner toroot. - Test all flows after this change. Some
execcommands can stop because of these limits. That is the purpose.
Recipe 12: Put Node-RED in the correct network zone
Goal: Make sure that a compromised Node-RED cannot connect to all systems.
Use the zone and conduit model of IEC 62443:
| Conduit | Direction | Ports | Peers |
|---|---|---|---|
| Editor access | Engineering zone to Node-RED | 443/tcp (proxy) | Engineering workstations or VPN only |
| Field data | Node-RED to PLCs | 4840/tcp (OPC UA), 502/tcp (Modbus), 102/tcp (S7) | Specified PLC addresses only |
| Upstream data | Node-RED to IT or cloud | 8883/tcp (MQTT over TLS), 443/tcp | Specified brokers and APIs only |
| All other traffic | Any | Any | Blocked |
- Do not make the editor available from the internet. Use a VPN for remote access.
- Make a default-deny policy for outbound traffic.
- Let only the peers in the table connect.
This example uses nftables. It filters the outbound traffic of the nodered user:
sudo nft add table inet nodered
sudo nft add chain inet nodered out '{ type filter hook output priority 0; policy accept; }'
sudo nft add rule inet nodered out meta skuid nodered oif lo accept
sudo nft add rule inet nodered out meta skuid nodered ip daddr 10.20.0.0/24 tcp dport '{ 4840, 502 }' accept
sudo nft add rule inet nodered out meta skuid nodered ip daddr 10.30.0.5 tcp dport 8883 accept
sudo nft add rule inet nodered out meta skuid nodered udp dport 53 accept
sudo nft add rule inet nodered out meta skuid nodered drop
The attacks of October 2026 download a bot from the internet. An egress filter stops this step.
Do not filter port 1880 only. Some products use other ports, for example 41880 on Revolution Pi.
If the data must go from OT to IT in one direction only, use a data diode. See OPC UA data diode.
Recipe 13: Secure the connections to OPC UA servers and MQTT brokers
Goal: Make sure that the data between Node-RED and the field is authentic and encrypted.
OPC UA:
- Set the security mode to
SignAndEncrypt. - Use a current security policy:
Basic256Sha256,Aes128_Sha256_RsaOaeporAes256_Sha256_RsaPss. - Do not use the security mode
Nonein production. - Trust the certificate of each server explicitly. Do not accept all certificates automatically.
- Use a user account on the server if the server supports it. Give this account only the necessary rights.
- For many devices, manage the certificates centrally with a Global Discovery Server (GDS). See What is the OPC UA GDS?.
OPC UA for Node-RED (@opcua/for-node-red) has a certificate manager and GDS pull onboarding (OPC UA Part 12).
If your flows or gateways contain an OPC UA server, apply the 20 rules of Secure your node-opcua server.
MQTT:
- Use
mqtts://on port 8883. - Examine the certificate of the broker against your CA.
- Give a different client certificate or account to each Node-RED instance.
- Set ACLs on the broker. Each client can read and write only its topics.
Recipe 14: Record the actions of the users
Goal: Know who deployed what, and when.
- In
settings.js, setlogging.console.audittotrue. - Send the logs to a central log server (syslog, journald or SIEM).
- Make an alert for these events:
- A
POST /flowsrequest from an unknown address. - A series of login failures (
auth.login.fail). - A deploy outside the change window.
- New
exec,functionorhttp innodes inflows.json.
- A
logging: {
console: {
level: "info",
metrics: false,
audit: true
}
},
Recipe 15: Reduce the information that Node-RED shows
Goal: Give less information to an attacker. Prevent unwanted outbound calls.
- Set
diagnostics.enabledtofalse. Node-RED then closes the/diagnosticsendpoint. - Keep
runtimeState.enabledatfalse(default). Node-RED then closes/flows/state. - Set
telemetry.enabledto the value of your site policy. On an isolated network, set it tofalse. - Set
editorTheme.tourstofalse. - Keep
exportGlobalContextKeysatfalse(default).
diagnostics: { enabled: false, ui: false },
runtimeState: { enabled: false, ui: false },
telemetry: { enabled: false, updateNotification: false },
exportGlobalContextKeys: false,
editorTheme: {
tours: false,
projects: { enabled: false } // set to true on development instances only
},
If you set telemetry.enabled to false, the editor does not tell you about new versions. Then you must monitor the Node-RED releases and advisories yourself.
Recipe 16: Back up and restore
Goal: Restore a known good instance quickly.
Back up these files from the user directory:
flows.json(or the file name inflowFile)flows_cred.jsonsettings.jspackage.jsonandpackage-lock.json.config.*.json- The
lib/folder and the files that your flows use
Do not back up node_modules. You can make it again with npm ci. Keep the credentialSecret in the vault, not in the archive.
sudo systemctl stop nodered
tar --exclude=node_modules -czf "nodered-$(date +%F).tgz" -C /var/lib nodered
sudo systemctl start nodered
To restore an instance:
- Extract the archive into the user directory.
- Set the owner of the files (
nodered, or1000:1000for Docker). - Type
npm ci --omit=devin the user directory. - Start Node-RED in safe mode (Recipe 17, step 3).
- Examine the flows.
- Start Node-RED in normal mode.
Recipe 17: Examine an instance for compromise
Goal: Find malicious nodes and stop them.
WARNING: If you think that an instance is compromised, isolate it from the network first. Do not deploy from the compromised editor.
- Disconnect the host from the network, or block all its traffic except your access.
- Make a copy of the user directory and of the logs for the investigation.
- Start Node-RED in safe mode. In safe mode, Node-RED loads the flows but does not start them.
- Examine
flows.jsonwithjq(see the commands below). - Examine
package.jsonfor nodes that you did not install. - Examine
settings.jsfor changes toadminAuth,httpAdminRootandfunctionGlobalContext. - Examine
/tmp,/dev/shmand the process list for unknown files and processes. - Build the host again from a known good image. Do not only clean it.
- From a different computer, change all credentials that are in
flows_cred.json. - Set a new
credentialSecret, and type the credentials again. - Restore the flows from Git. Apply Recipes 1 to 15 before you connect the host again.
# Start in safe mode
node-red --safe
# Docker: docker run -e NODE_RED_ENABLE_SAFE_MODE=true ...
# Count the node types
jq -r '.[].type' flows.json | sort | uniq -c | sort -rn
# Show the exec and daemon nodes and their commands
jq '.[] | select(.type=="exec" or .type=="daemon") | {id, z, name, command, addpay, append}' flows.json
# Show the http in endpoints
jq '.[] | select(.type=="http in") | {id, z, method, url}' flows.json
# Show the function nodes that use child_process, eval or base64
jq -r '.[] | select(.type=="function")
| select(.func | test("child_process|require\\(|eval\\(|Function\\(|base64"; "i"))
| "\(.id) \(.name)\n\(.func)\n---"' flows.json
# Compare with the last known good version in Git
diff <(jq -S . known-good/flows.json) <(jq -S . flows.json)
Recipe 18: Test your instance from the outside
Goal: Make sure that the controls operate correctly. Do this test from a different computer in each network zone.
- Examine the login type of the editor. The correct result is
{"type":"credentials",...}or{"type":"strategy",...}. The result{}shows thatadminAuthis not set. - Try to read the flows without a token. The correct result is
401. - Try to connect directly to port 1880 and to the vendor port (for example 41880). The connection must fail from all zones.
- Try to open an
http inendpoint without credentials. The correct result is401. - On the Node-RED host, as the Node-RED user, try to connect to the internet. The connection must fail if you applied Recipe 12.
- Scan your public address ranges for Node-RED. Use your own scanner or an internet search service.
# 1. Login type
curl -s https://nodered.example.com/admin/auth/login
# 2. Flows without a token (expected: 401)
curl -s -o /dev/null -w "%{http_code}\n" https://nodered.example.com/admin/flows
# 3. Direct ports (expected: failure)
nc -vz -w 3 nodered-host 1880
nc -vz -w 3 nodered-host 41880
# 5. Egress from the host (expected: failure)
sudo -u nodered curl -m 5 -sI https://example.com
4. Go-live checklist
| # | Control | Recipe | Done |
|---|---|---|---|
| 1 | adminAuth is set. Each person has a personal account. | 1, 2 | β |
| 2 | Monitor-only persons have the read permission. | 2 | β |
| 3 | The http in endpoints and the dashboard ask for credentials. | 3 | β |
| 4 | Node-RED listens on 127.0.0.1. A TLS reverse proxy is in front. | 4, 5 | β |
| 5 | Only the engineering zone or a VPN can open the editor. | 4, 12 | β |
| 6 | credentialSecret comes from a secret store. It is not in Git. | 6 | β |
| 7 | Palette installation is not possible in production. | 7 | β |
| 8 | package-lock.json is in Git. npm audit shows no critical problem. | 7 | β |
| 9 | Unused dangerous nodes (exec) are excluded. | 8 | β |
| 10 | The editor is off in production, or its access is limited. | 9 | β |
| 11 | Node-RED does not run as root. Container or systemd limits are active. | 10, 11 | β |
| 12 | The outbound traffic policy is default-deny. | 12 | β |
| 13 | OPC UA uses SignAndEncrypt. MQTT uses TLS. No automatic trust. | 13 | β |
| 14 | Audit logs go to a central log server. Alerts are set. | 14 | β |
| 15 | The diagnostics endpoint is closed. Telemetry follows the site policy. | 15 | β |
| 16 | Backups exist. A restore test is complete. | 16 | β |
| 17 | The incident procedure is written. The team knows it. | 17 | β |
| 18 | The external test is correct from each zone. | 18 | β |
| 19 | Node-RED, Node.js and the nodes are on supported versions. A patch procedure exists. | All | β |
| 20 | Devices that contain Node-RED have the latest firmware (for example IoT2050 V4.3.4-1 or later). | 1.1 | β |
5. For machine builders and OEMs
If you sell a product that contains Node-RED, you are responsible for its default configuration. IEC 62443-4-2 requirement CR 1.1 asks for the identification and authentication of all human users.
- Do not deliver Node-RED with an open editor.
- Make a unique credential for each device, or force the user to set one at the first start.
- Do not start Node-RED by default if the customer does not use it.
- In your documentation, give the port, the user directory and the security steps.
- Publish security updates and advisories for the Node-RED part of your product.
6. Frequently asked questions
Is Node-RED secure by default?
No. By default, the Node-RED editor and Admin API do not ask for a password. Any person who can connect to the port can deploy a flow and run commands on the host. You must set adminAuth before you use Node-RED in production.
What is CVE-2026-58115?
CVE-2026-58115 is a critical vulnerability (CVSS 10.0) in Siemens SIMATIC IoT2050 devices with Industrial OS below V4.3.4-1 and Node-RED. The Node-RED HTTP interface has no authentication. A remote attacker can deploy a flow and run code with maximum privileges. Update to V4.3.4-1 or later, or harden or remove Node-RED.
How do I add a password to the Node-RED editor?
Make a bcrypt hash with node-red admin hash-pw. Add an adminAuth block with type: "credentials" and a users list to settings.js. Then restart Node-RED.
Does adminAuth protect the Node-RED dashboard?
No. adminAuth protects only the editor and the Admin API. The dashboard and the http in endpoints are below httpNodeRoot. Protect them with httpNodeAuth, httpNodeMiddleware or an authentication proxy.
Can I put Node-RED on the internet?
Do not put the Node-RED editor directly on the internet. Use a VPN, or a TLS reverse proxy with an IP allowlist, client certificates or single sign-on. Keep adminAuth active behind the proxy.
How do I know if my Node-RED instance is compromised?
Examine flows.json for exec nodes, http in endpoints and function nodes that you do not know. Examine /tmp and /dev/shm for unknown files. Compare the flows with the last known good version in Git. If you find a malicious node, isolate the host and build it again.
7. References
Vulnerabilities and attacks
- Siemens SSA-834709, CVE-2026-58115: CIRCL mirror, Siemens CSAF
- VulnCheck, "Exploitation Targeting Node-RED, a Tool Embedded in ICS Products", 2 November 2026: vulncheck.com
- FUXA, CVE-2026-25938 / GHSA-v4p5-w6r3-2x4f: GCVE database
- CISA ICSA-25-121-01, KUNBUS Revolution Pi, CVE-2025-24522: cisa.gov
- Pen Test Partners, Revolution Pi research: pentestpartners.com
- CVE-2025-41656, Pilz IndustrialPI 4, VDE-2025-045: certvde.com
- OpenSSF malicious packages: MAL-2024-1030, MAL-2025-27643
Node-RED documentation
- Securing Node-RED: nodered.org
- Configuration (
settings.js): nodered.org, default settings.js on GitHub - Admin API security: nodered.org
- Docker: nodered.org, node-red-docker on GitHub
- Raspberry Pi and Linux installer: nodered.org
Platform and standards
-
OWASP Docker Security Cheat Sheet: owasp.org
-
OWASP Node.js Security Cheat Sheet: owasp.org
-
NIST SP 800-82 Rev. 3, Guide to Operational Technology (OT) Security: csrc.nist.gov
-
Node.js permission model: nodejs.org
-
systemd sandbox directives:
man systemd.exec -
ASD-STE100 Simplified Technical English: asd-ste100.org
Get help
Sterfive gives OPC UA security training, OPC UA with Node-RED training and support for industrial Node-RED and OPC UA installations. Contact us.