Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/collector/src/announceCycle/unannounced.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,9 @@ function applySecretsConfiguration(agentResponse) {
${agentResponse.secrets.list}`
);
} else {
secrets.setMatcher(agentResponse.secrets.matcher, agentResponse.secrets.list);
ensureNestedObjectExists(agentOpts.config, ['secrets']);
agentOpts.config.secrets.matcherMode = agentResponse.secrets.matcher;
agentOpts.config.secrets.keywords = agentResponse.secrets.list;
}
}
}
Expand Down
44 changes: 44 additions & 0 deletions packages/collector/test/integration/misc/secrets/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* (c) Copyright IBM Corp. 2026
*/

'use strict';

// NOTE: c8 bug https://github.com/bcoe/c8/issues/166
process.on('SIGTERM', () => {
process.disconnect();
process.exit(0);
});

const instanaConfig =
process.env.USE_INCODE_SECRETS_CONFIG === 'true'
? {
secrets: {
matcherMode: 'equals',
keywords: ['incodeToken']
}
}
: {};

require('@instana/collector')(instanaConfig);

const express = require('express');
const port = require('@_local/collector/test/test_util/app-port')();
const app = express();

const logPrefix = `Secrets Config Precedence Test (${process.pid}):\t`;

app.get('/', (req, res) => {
res.send('OK');
});

app.listen(port, () => {
log(`Listening on port: ${port}`);
});

function log() {
const args = Array.prototype.slice.call(arguments);
args[0] = `${logPrefix}${args[0]}`;
// eslint-disable-next-line no-console
console.log.apply(console, args);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "test-secrets-config-precedence",
"version": "1.0.0",
"private": true,
"main": "app.js",
"dependencies": {
"@instana/collector": "{{collectorVersion}}",
"@instana/core": "{{coreVersion}}",
"@instana/shared-metrics": "{{sharedMetricsVersion}}",
"express": "^4.17.1"
}
}
184 changes: 184 additions & 0 deletions packages/collector/test/integration/misc/secrets/test_base.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*
* (c) Copyright IBM Corp. 2026
*/

'use strict';

const path = require('path');
const { expect } = require('chai');

const config = require('@_local/core/test/config');
const { retry } = require('@_local/core/test/test_util');
const ProcessControls = require('@_local/collector/test/test_util/ProcessControls');
const { AgentStubControls } = require('@_local/collector/test/apps/agentStubControls');

module.exports = function (name, version) {
const inVersionDir =
path.basename(__dirname).startsWith('_v') || path.basename(path.dirname(__dirname)).startsWith('_v');
const versionDir = inVersionDir ? __dirname : path.join(__dirname, `_v${version}`);

this.timeout(config.getTestTimeout() * 2);

describe('Secrets Configuration Precedence', () => {
describe('when both agent config and env var are set, env var takes precedence', () => {
const customAgentControls = new AgentStubControls();
let controls;

before(async () => {
await customAgentControls.startAgent({
secrets: {
matcher: 'equals',
list: ['agentToken']
}
});

controls = new ProcessControls({
agentControls: customAgentControls,
dirname: __dirname,
cwd: versionDir,
env: {
INSTANA_SECRETS: 'equals:envToken'
}
});
await controls.startAndWaitForAgentConnection();
});

beforeEach(async () => {
await customAgentControls.clearReceivedTraceData();
});

after(async () => {
await customAgentControls.stopAgent();
await controls.stop();
});

afterEach(async () => {
await controls.clearIpcMessages();
});

it('should use env var config (equals:envToken) and ignore agent config', async () => {
await controls.sendRequest({
method: 'GET',
path: '/?agentToken=value1&envToken=value2&normalParam=value3'
});

await retry(async () => {
const spans = await customAgentControls.getSpans();
expect(spans.length).to.be.at.least(1);

const httpEntry = spans.find(span => span.n === 'node.http.server');
expect(httpEntry).to.exist;

expect(httpEntry.data.http.params).to.equal('agentToken=value1&envToken=<redacted>&normalParam=value3');
});
});
});

describe('when only agent config is provided', () => {
const customAgentControls = new AgentStubControls();
let controls;

before(async () => {
await customAgentControls.startAgent({
secrets: {
matcher: 'equals',
list: ['agentOnlyToken']
}
});

controls = new ProcessControls({
agentControls: customAgentControls,
dirname: __dirname,
cwd: versionDir
});
await controls.startAndWaitForAgentConnection();
});

beforeEach(async () => {
await customAgentControls.clearReceivedTraceData();
});

after(async () => {
await customAgentControls.stopAgent();
await controls.stop();
});

afterEach(async () => {
await controls.clearIpcMessages();
});

it('should use agent config', async () => {
await controls.sendRequest({
method: 'GET',
path: '/?agentOnlyToken=value1&otherParam=value2'
});

await retry(async () => {
const spans = await customAgentControls.getSpans();
expect(spans.length).to.be.at.least(1);

const httpEntry = spans.find(span => span.n === 'node.http.server');
expect(httpEntry).to.exist;

expect(httpEntry.data.http.params).to.equal('agentOnlyToken=<redacted>&otherParam=value2');
});
});
});

describe('when in-code config is provided along with agent config', () => {
const customAgentControls = new AgentStubControls();
let controls;

before(async () => {
await customAgentControls.startAgent({
secrets: {
matcher: 'equals',
list: ['agentCodeToken']
}
});

controls = new ProcessControls({
agentControls: customAgentControls,
dirname: __dirname,
cwd: versionDir,
env: {
USE_INCODE_SECRETS_CONFIG: 'true'
}
});
await controls.startAndWaitForAgentConnection();
});

beforeEach(async () => {
await customAgentControls.clearReceivedTraceData();
});

after(async () => {
await customAgentControls.stopAgent();
await controls.stop();
});

afterEach(async () => {
await controls.clearIpcMessages();
});

it('should use in-code config and ignore agent config', async () => {
await controls.sendRequest({
method: 'GET',
path: '/?agentCodeToken=value1&incodeToken=value2&normalParam=value3'
});

await retry(async () => {
const spans = await customAgentControls.getSpans();
expect(spans.length).to.be.at.least(1);

const httpEntry = spans.find(span => span.n === 'node.http.server');
expect(httpEntry).to.exist;

expect(httpEntry.data.http.params).to.equal(
'agentCodeToken=value1&incodeToken=<redacted>&normalParam=value3'
);
});
});
});
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ describe('unannounced state', () => {
afterEach(() => {
agentConnectionStub.announceNodeCollector.reset();
tracingStub.activate.reset();
secretsStub.setMatcher.reset();
agentOptsStub.agentUuid = undefined;
agentOptsStub.config = {};
});
Expand Down Expand Up @@ -90,7 +89,7 @@ describe('unannounced state', () => {
});
});

it('should use secrets config response', done => {
it('should store secrets config in agentOpts.config', done => {
prepareAnnounceResponse({
secrets: {
matcher: 'equals',
Expand All @@ -99,7 +98,60 @@ describe('unannounced state', () => {
});
unannouncedState.enter({
transitionTo: () => {
expect(secretsStub.setMatcher).to.have.been.calledWith('equals', ['hidden', 'opaque']);
expect(agentOptsStub.config.secrets).to.deep.equal({
matcherMode: 'equals',
keywords: ['hidden', 'opaque']
});
done();
}
});
});

it('should store secrets config with different matcher modes', done => {
prepareAnnounceResponse({
secrets: {
matcher: 'contains-ignore-case',
list: ['password', 'token']
}
});
unannouncedState.enter({
transitionTo: () => {
expect(agentOptsStub.config.secrets).to.deep.equal({
matcherMode: 'contains-ignore-case',
keywords: ['password', 'token']
});
done();
}
});
});

it('should handle empty secrets list', done => {
prepareAnnounceResponse({
secrets: {
matcher: 'none',
list: []
}
});
unannouncedState.enter({
transitionTo: () => {
expect(agentOptsStub.config.secrets).to.deep.equal({
matcherMode: 'none',
keywords: []
});
done();
}
});
});

it('should not set secrets config when not provided by agent', done => {
prepareAnnounceResponse({
tracing: {
enabled: true
}
});
unannouncedState.enter({
transitionTo: () => {
expect(agentOptsStub.config.secrets).to.be.undefined;
done();
}
});
Expand Down
25 changes: 9 additions & 16 deletions packages/core/src/secrets.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,25 +181,18 @@ exports.init = function init(config) {
isSecretInternal = matchers[config.secrets.matcherMode](config.secrets.keywords);
};

/**
* @param {import('./config').InstanaConfig} config
*/
exports.activate = function activate(config) {
if (config.secrets.matcherMode && config.secrets.keywords) {
isSecretInternal = matchers[config.secrets.matcherMode](config.secrets.keywords);
}
};

exports.matchers = matchers;

/** @type {(key: string) => boolean} */
exports.isSecret = function isSecret(key) {
return isSecretInternal(key);
};

/**
* @param {import('@instana/core/src/config').MatchingOption} matcherId
* @param {Array.<string>} secretsList
*/
exports.setMatcher = function setMatcher(matcherId, secretsList) {
if (!(typeof matcherId === 'string')) {
logger.warn(`Received invalid secrets configuration, attribute matcher is not a string: ${matcherId}`);
} else if (Object.keys(exports.matchers).indexOf(matcherId) < 0) {
logger.warn(`Received invalid secrets configuration, matcher is not supported: ${matcherId}`);
} else if (!Array.isArray(secretsList)) {
logger.warn(`Received invalid secrets configuration, attribute list is not an array: ${secretsList}`);
} else {
isSecretInternal = exports.matchers[matcherId](secretsList);
}
};
2 changes: 2 additions & 0 deletions packages/core/src/tracing/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
'use strict';

const sdk = require('./sdk');
const secrets = require('../secrets');
const constants = require('./constants');
const tracingMetrics = require('./metrics');
const opentracing = require('./opentracing');
Expand Down Expand Up @@ -266,6 +267,7 @@ exports.activate = function activate(_config = config) {
coreUtil.activate(_config);
tracingUtil.activate(_config);
spanBuffer.activate(_config);
secrets.activate(_config);
opentracing.activate();
sdk.activate();

Expand Down
Loading