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:

  1. Set adminAuth in settings.js. Without it, any person on the network can run commands on the host.
  2. Give each person a personal account. Give the read permission to persons who only monitor.
  3. Set httpNodeAuth, or put an access control in front of the http in endpoints and the dashboard.
  4. Set uiHost to 127.0.0.1. Put a reverse proxy with TLS in front of Node-RED.
  5. Let only the engineering zone or a VPN connect to the editor.
  6. Set your own credentialSecret. Keep it out of Git and out of the backup archive.
  7. Set externalModules.palette.allowInstall to false in production.
  8. Remove the exec node with nodesExcludes if your flows do not use it.
  9. Do not run Node-RED as root. Use a dedicated user, a hardened systemd unit or a hardened container.
  10. Block all outbound traffic that your flows do not use.
  11. Use SignAndEncrypt for OPC UA and TLS for MQTT. Do not accept all certificates automatically.
  12. Keep backups. Examine flows.json for exec, function and http in nodes 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:

CVEProductCVSSPublishedCorrection
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-RED10.02026-08-11, revised 2026-11-04Update 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/flows9.5 (v4)2026-02-09Update to FUXA 1.2.11
CVE-2025-41656 (CERT@VDE VDE-2025-045)Pilz IndustrialPI 4, firmware Bullseye up to 2024-0810.02025-07-01New firmware
CVE-2025-24522 (CISA ICSA-25-121-01)KUNBUS Revolution Pi OS Bookworm 01/2025 and earlier (Node-RED on TCP 41880)10.02025-05-01New 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 pointDefault pathWhat it givesMain control
Editor and Admin API/ (httpAdminRoot)Full control: deploy flows, install nodes, read settingsadminAuth, disableEditor, network filter
http in endpoints and dashboard/ (httpNodeRoot)The HTTP API that your flows makehttpNodeAuth, httpNodeMiddleware, proxy
Static fileshttpStaticFiles from a folderhttpStaticAuth
Palette manager (npm)EditorInstallation of code from npmexternalModules
function node modulesEditorrequire of npm modules in flowsfunctionExternalModules
exec and daemon nodesFlowsOperating system commandsnodesExcludes, systemd or container limits
Credential fileflows_cred.jsonPasswords of all configured connectionscredentialSecret, file permissions
Diagnostics endpoint/diagnosticsVersions, settings, host datadiagnostics.enabled
Outbound connectionsFlowsData exfiltration, payload downloadEgress 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.

  1. Open a terminal on the Node-RED host.
  2. Type node-red admin hash-pw.
  3. Type the password when the command asks for it.
  4. 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.

  1. Open settings.js in the user directory.
  2. Find the adminAuth block. Remove the comment marks.
  3. Add one entry for each person. Use the hash from Recipe 1.
  4. Give "*" (full access) only to persons who deploy flows.
  5. Give "read" to persons who only monitor.
  6. 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.

  1. Make a hash with Recipe 1.
  2. Add httpNodeAuth to settings.js.
  3. If you use httpStatic, add httpStaticAuth.
  4. 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 httpNodeRoot path.
  • Set httpNodeMiddleware to 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.

  1. In settings.js, set uiHost, httpAdminRoot and httpNodeRoot.
  2. Restart Node-RED.
  3. Install nginx on the same host.
  4. Add the server block that follows. Change the names, the paths and the networks.
  5. Examine the configuration with sudo nginx -t.
  6. 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 adminAuth active 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.

  1. Put the key and the certificate in a folder that only the Node-RED user can read.
  2. In settings.js, set https, httpsRefreshInterval and requireHttps.
  3. 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.

  1. Make a random secret with openssl rand -hex 32.
  2. Keep the secret in a vault or a secret manager.
  3. Give the secret to the process in the environment variable NODE_RED_CREDENTIAL_SECRET.
  4. In settings.js, set credentialSecret.
  5. Start Node-RED. Node-RED moves the current credentials from the generated key to your key.
  6. 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 credentialSecret after 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.

  1. In settings.js, set externalModules and functionExternalModules as in the example.
  2. Write the approved nodes in package.json in the user directory.
  3. Install them with npm ci --omit=dev.
  4. Examine them with npm audit.
  5. Keep package.json and package-lock.json in 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.

  1. Find the file name of the node. The exec node is in 90-exec.js.
  2. In settings.js, set nodesExcludes.
  3. Restart Node-RED.
  4. 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.

  1. Make the data folder and give it to UID 1000.
  2. Write the compose.yaml file that follows.
  3. Put the credential secret in the .env file. Do not put .env in Git.
  4. Start the container with docker compose up -d.
  5. 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 -minimal image 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 on 127.0.0.1 only, or do not publish it. Put the proxy on the same Docker network.
  • Do not use -u root or --privileged.
  • For a serial port, add only that device with devices:, and the group with group_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.

  1. Make a dedicated system user nodered.
  2. Move the user directory to /var/lib/nodered. Give it to nodered.
  3. Type sudo systemctl edit nodered.service.
  4. Add the lines that follow.
  5. Type sudo systemctl daemon-reload.
  6. Type sudo systemctl restart nodered.
  7. 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. Use DeviceAllow= for each device.
  • Put NODE_RED_CREDENTIAL_SECRET=... in /etc/nodered/secrets.env. Set the permission to 600 and the owner to root.
  • Test all flows after this change. Some exec commands 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:

ConduitDirectionPortsPeers
Editor accessEngineering zone to Node-RED443/tcp (proxy)Engineering workstations or VPN only
Field dataNode-RED to PLCs4840/tcp (OPC UA), 502/tcp (Modbus), 102/tcp (S7)Specified PLC addresses only
Upstream dataNode-RED to IT or cloud8883/tcp (MQTT over TLS), 443/tcpSpecified brokers and APIs only
All other trafficAnyAnyBlocked
  1. Do not make the editor available from the internet. Use a VPN for remote access.
  2. Make a default-deny policy for outbound traffic.
  3. 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:

  1. Set the security mode to SignAndEncrypt.
  2. Use a current security policy: Basic256Sha256, Aes128_Sha256_RsaOaep or Aes256_Sha256_RsaPss.
  3. Do not use the security mode None in production.
  4. Trust the certificate of each server explicitly. Do not accept all certificates automatically.
  5. Use a user account on the server if the server supports it. Give this account only the necessary rights.
  6. 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:

  1. Use mqtts:// on port 8883.
  2. Examine the certificate of the broker against your CA.
  3. Give a different client certificate or account to each Node-RED instance.
  4. 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.

  1. In settings.js, set logging.console.audit to true.
  2. Send the logs to a central log server (syslog, journald or SIEM).
  3. Make an alert for these events:
    • A POST /flows request from an unknown address.
    • A series of login failures (auth.login.fail).
    • A deploy outside the change window.
    • New exec, function or http in nodes in flows.json.
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.

  1. Set diagnostics.enabled to false. Node-RED then closes the /diagnostics endpoint.
  2. Keep runtimeState.enabled at false (default). Node-RED then closes /flows/state.
  3. Set telemetry.enabled to the value of your site policy. On an isolated network, set it to false.
  4. Set editorTheme.tours to false.
  5. Keep exportGlobalContextKeys at false (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 in flowFile)
  • flows_cred.json
  • settings.js
  • package.json and package-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:

  1. Extract the archive into the user directory.
  2. Set the owner of the files (nodered, or 1000:1000 for Docker).
  3. Type npm ci --omit=dev in the user directory.
  4. Start Node-RED in safe mode (Recipe 17, step 3).
  5. Examine the flows.
  6. 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.

  1. Disconnect the host from the network, or block all its traffic except your access.
  2. Make a copy of the user directory and of the logs for the investigation.
  3. Start Node-RED in safe mode. In safe mode, Node-RED loads the flows but does not start them.
  4. Examine flows.json with jq (see the commands below).
  5. Examine package.json for nodes that you did not install.
  6. Examine settings.js for changes to adminAuth, httpAdminRoot and functionGlobalContext.
  7. Examine /tmp, /dev/shm and the process list for unknown files and processes.
  8. Build the host again from a known good image. Do not only clean it.
  9. From a different computer, change all credentials that are in flows_cred.json.
  10. Set a new credentialSecret, and type the credentials again.
  11. 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.

  1. Examine the login type of the editor. The correct result is {"type":"credentials",...} or {"type":"strategy",...}. The result {} shows that adminAuth is not set.
  2. Try to read the flows without a token. The correct result is 401.
  3. Try to connect directly to port 1880 and to the vendor port (for example 41880). The connection must fail from all zones.
  4. Try to open an http in endpoint without credentials. The correct result is 401.
  5. 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.
  6. 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

#ControlRecipeDone
1adminAuth is set. Each person has a personal account.1, 2☐
2Monitor-only persons have the read permission.2☐
3The http in endpoints and the dashboard ask for credentials.3☐
4Node-RED listens on 127.0.0.1. A TLS reverse proxy is in front.4, 5☐
5Only the engineering zone or a VPN can open the editor.4, 12☐
6credentialSecret comes from a secret store. It is not in Git.6☐
7Palette installation is not possible in production.7☐
8package-lock.json is in Git. npm audit shows no critical problem.7☐
9Unused dangerous nodes (exec) are excluded.8☐
10The editor is off in production, or its access is limited.9☐
11Node-RED does not run as root. Container or systemd limits are active.10, 11☐
12The outbound traffic policy is default-deny.12☐
13OPC UA uses SignAndEncrypt. MQTT uses TLS. No automatic trust.13☐
14Audit logs go to a central log server. Alerts are set.14☐
15The diagnostics endpoint is closed. Telemetry follows the site policy.15☐
16Backups exist. A restore test is complete.16☐
17The incident procedure is written. The team knows it.17☐
18The external test is correct from each zone.18☐
19Node-RED, Node.js and the nodes are on supported versions. A patch procedure exists.All☐
20Devices 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.

  1. Do not deliver Node-RED with an open editor.
  2. Make a unique credential for each device, or force the user to set one at the first start.
  3. Do not start Node-RED by default if the customer does not use it.
  4. In your documentation, give the port, the user directory and the security steps.
  5. 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

Node-RED documentation

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.