The capability needed (if any) depends on which node owns the credentials being read and how they are accessed. There are three distinct patterns.
Pattern 1: Reading a node’s own credentials
A node reading this.credentials in its own constructor or message handler needs node:credentials:read granted to its own package.
// node-red-contrib-my-api/index.js
module.exports = function (RED) {
function MyApiNode(config) {
RED.nodes.createNode(this, config);
var apiKey = this.credentials.apiKey; // guarded: needs node:credentials:read
this.on("input", function (msg) {
// use apiKey ...
this.send(msg);
});
}
RED.nodes.registerType("my-api", MyApiNode);
};
// settings.js
sentinel: {
allow: {
"node-red-contrib-my-api": ["registry:register", "node:credentials:read"],
},
}
Why the node needs an explicit grant to read its own credentials: if any node could always read this.credentials without a grant, a compromised or malicious package that registers a node type would automatically get access to whatever credentials the operator stored for it — no configuration signal, no audit trail, nothing for the operator to review or approve. Requiring the explicit grant means the operator has consciously decided “I trust this package to handle credentials.” It also makes the intent visible: you can scan settings.js and immediately see which packages touch credential data.
If credentials were silently self-readable, node:credentials:read would only be needed for cross-node access — but then the presence or absence of the grant would tell you nothing about whether a package handles its own secrets, removing half its value as an audit signal.
Pattern 2: Config node idiomatic pattern (no credential cap needed)
This pattern avoids the credential proxy entirely. The config node reads this.credentials in its own constructor — Sentinel only proxies nodes returned from getNode(), not a node’s own this. The config node stores the secret as a plain property, and consumers read that plain property without ever needing a credential capability.
Config node reads its own credentials in its constructor
The config node accesses this.credentials on the raw this — not via a getNode() proxy. No credential cap is needed because Sentinel only guards getNode() return values.// node-red-contrib-influxdb/index.js
function InfluxConfigNode(config) {
RED.nodes.createNode(this, config);
// Reads this.credentials on the raw this — not via a getNode() proxy.
// No credential cap needed because Sentinel only guards getNode() return values.
this.token = this.credentials.token;
this.host = config.host;
}
RED.nodes.registerType("influxdb-config", InfluxConfigNode);
Consumer reads the plain property
The consumer calls RED.nodes.getNode() to obtain the config node, then reads the plain property that was stored in step 1. No credential cap is needed because configNode.token is just a regular property, not configNode.credentials.token.function InfluxWriteNode(config) {
RED.nodes.createNode(this, config);
// Consumer reads the plain property — no credential cap needed.
var configNode = RED.nodes.getNode(config.configId);
this.on("input", function (msg) {
writeToInflux(configNode.host, configNode.token, msg.payload);
this.send(msg);
});
}
RED.nodes.registerType("influxdb-write", InfluxWriteNode);
Grant only registry:register
Neither node in this pattern requires a credential capability.// settings.js — no node:credentials:read needed for either package under this pattern
sentinel: {
allow: {
"node-red-contrib-influxdb": ["registry:register"],
},
}
The complete example:
// node-red-contrib-influxdb/index.js
module.exports = function (RED) {
function InfluxConfigNode(config) {
RED.nodes.createNode(this, config);
// Reads this.credentials on the raw this — not via a getNode() proxy.
// No credential cap needed because Sentinel only guards getNode() return values.
this.token = this.credentials.token;
this.host = config.host;
}
RED.nodes.registerType("influxdb-config", InfluxConfigNode);
function InfluxWriteNode(config) {
RED.nodes.createNode(this, config);
// Consumer reads the plain property — no credential cap needed.
var configNode = RED.nodes.getNode(config.configId);
this.on("input", function (msg) {
writeToInflux(configNode.host, configNode.token, msg.payload);
this.send(msg);
});
}
RED.nodes.registerType("influxdb-write", InfluxWriteNode);
};
// settings.js — no node:credentials:read needed for either package under this pattern
sentinel: {
allow: {
"node-red-contrib-influxdb": ["registry:register"],
},
}
Pattern 3: Cross-node credential access via getNode()
If a consumer accesses configNode.credentials.token directly via the proxy returned by getNode(), that access goes through Sentinel’s capability check.
// node-red-contrib-reader/index.js — wants to read credentials from node-red-contrib-target
module.exports = function (RED) {
function ReaderNode(config) {
RED.nodes.createNode(this, config);
this.on("input", function (msg) {
var target = RED.nodes.getNode(config.targetId);
// Sentinel identifies node-red-contrib-reader as the caller and checks
// its grants — not the target node's owning package.
var secret = target.credentials.secret;
this.send(msg);
});
}
RED.nodes.registerType("reader", ReaderNode);
};
// settings.js — the ACCESSOR needs node:credentials:read, not the target's package
sentinel: {
allow: {
"node-red-contrib-reader": ["registry:register", "node:credentials:read"],
"node-red-contrib-target": ["registry:register"],
},
}
The dual-axis check
Cross-node credential access uses a two-axis check. Either axis alone is sufficient to allow the access; if neither passes, the proxy returns undefined for credentials.
The nodeTypes section in .sentinel-grants.json provides target-side control: the target node type’s author can list exactly which caller packages are approved, granting them access without requiring a broad package grant.{
"nodeTypes": {
"influxdb": {
"node:credentials:read": ["node-red-contrib-influxdb"]
}
}
}
Only node-red-contrib-influxdb gets step-1 access via the target allowlist. Any other package without node:credentials:read in its grant list is blocked. node:credentials:read in the accessor’s grant list is a broad capability — it lets that package read .credentials from any node it obtains via getNode().// settings.js
sentinel: {
allow: {
"node-red-contrib-reader": ["registry:register", "node:credentials:read"],
},
}
nodeTypes examples
Grant specific packages access to a config node’s credentials:
{
"nodeTypes": {
"influxdb": {
"node:credentials:read": ["node-red-contrib-influxdb"]
}
}
}
Document that no caller is listed for a config node type:
An empty array [] records that no package is an approved caller via the target-side check. It has the same effect as not having an entry. Use it to make the intent explicit:
{
"nodeTypes": {
"my-vault-config": {
"node:credentials:read": []
}
}
}
An empty [] only closes the target-based path, not the caller-based path. A package that has node:credentials:read in its packages entry (either in settings.js or in the packages section of .sentinel-grants.json) can still access credentials via the caller-side axis.
Allow multiple consumers, block all others:
{
"nodeTypes": {
"mqtt-broker": {
"node:credentials:read": [
"node-red-contrib-mqtt-in",
"node-red-contrib-mqtt-out",
"node-red-contrib-mqtt-dynamic"
]
}
}
}
Complete example combining both sections:
{
"packages": {
"node-red-contrib-influxdb": ["registry:register", "node:credentials:read"],
"node-red-contrib-flow-audit": ["registry:register", "node:list"]
},
"nodeTypes": {
"influxdb": {
"node:credentials:read": ["node-red-contrib-influxdb"]
},
"mqtt-broker": {
"node:credentials:read": ["node-red-contrib-mqtt-in", "node-red-contrib-mqtt-out"]
},
"my-internal-config": {
"node:credentials:read": []
}
}
}
The nodeTypes section is purely additive: if the caller is in the list, access is granted immediately. If it is not, Sentinel falls through to the packages section and settings.js grants as normal. An entry here cannot block a package that already holds the capability in its grants.