From 482e449f320ce3677eb5e7816122e7b91f5e7f55 Mon Sep 17 00:00:00 2001 From: Arya Date: Tue, 12 May 2026 19:30:45 +0530 Subject: [PATCH 1/3] chore: respected config precedence for agent-provided secrets --- .../src/announceCycle/unannounced.js | 4 +- .../test/integration/misc/secrets/app.js | 44 +++++ .../misc/secrets/package.json.template | 12 ++ .../integration/misc/secrets/test_base.js | 184 ++++++++++++++++++ .../src/announceCycle/unannounced.test.js | 58 +++++- packages/core/src/secrets.js | 25 +-- packages/core/src/tracing/index.js | 2 + packages/core/test/secrets_test.js | 70 +++++++ 8 files changed, 379 insertions(+), 20 deletions(-) create mode 100644 packages/collector/test/integration/misc/secrets/app.js create mode 100644 packages/collector/test/integration/misc/secrets/package.json.template create mode 100644 packages/collector/test/integration/misc/secrets/test_base.js diff --git a/packages/collector/src/announceCycle/unannounced.js b/packages/collector/src/announceCycle/unannounced.js index 201cf7161d..230c83d389 100644 --- a/packages/collector/src/announceCycle/unannounced.js +++ b/packages/collector/src/announceCycle/unannounced.js @@ -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; } } } diff --git a/packages/collector/test/integration/misc/secrets/app.js b/packages/collector/test/integration/misc/secrets/app.js new file mode 100644 index 0000000000..85a5cadcdf --- /dev/null +++ b/packages/collector/test/integration/misc/secrets/app.js @@ -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); +} diff --git a/packages/collector/test/integration/misc/secrets/package.json.template b/packages/collector/test/integration/misc/secrets/package.json.template new file mode 100644 index 0000000000..2d4db51dc4 --- /dev/null +++ b/packages/collector/test/integration/misc/secrets/package.json.template @@ -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" + } +} \ No newline at end of file diff --git a/packages/collector/test/integration/misc/secrets/test_base.js b/packages/collector/test/integration/misc/secrets/test_base.js new file mode 100644 index 0000000000..a180a3871d --- /dev/null +++ b/packages/collector/test/integration/misc/secrets/test_base.js @@ -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=&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=&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=&normalParam=value3' + ); + }); + }); + }); + }); +}; diff --git a/packages/collector/test/unit/src/announceCycle/unannounced.test.js b/packages/collector/test/unit/src/announceCycle/unannounced.test.js index de236d2964..1c0bce962b 100644 --- a/packages/collector/test/unit/src/announceCycle/unannounced.test.js +++ b/packages/collector/test/unit/src/announceCycle/unannounced.test.js @@ -49,7 +49,6 @@ describe('unannounced state', () => { afterEach(() => { agentConnectionStub.announceNodeCollector.reset(); tracingStub.activate.reset(); - secretsStub.setMatcher.reset(); agentOptsStub.agentUuid = undefined; agentOptsStub.config = {}; }); @@ -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', @@ -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(); } }); diff --git a/packages/core/src/secrets.js b/packages/core/src/secrets.js index 9765aae006..582b29c0ef 100644 --- a/packages/core/src/secrets.js +++ b/packages/core/src/secrets.js @@ -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.} 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); - } -}; diff --git a/packages/core/src/tracing/index.js b/packages/core/src/tracing/index.js index 80d4db8f91..65e56848f8 100644 --- a/packages/core/src/tracing/index.js +++ b/packages/core/src/tracing/index.js @@ -18,6 +18,7 @@ const supportedVersion = require('./supportedVersion'); const otelInstrumentations = require('./opentelemetry-instrumentations'); const cls = require('./cls'); const coreUtil = require('../util'); +const secrets = require('../secrets'); let tracingEnabled = false; let tracingActivated = false; @@ -268,6 +269,7 @@ exports.activate = function activate(_config = config) { spanBuffer.activate(_config); opentracing.activate(); sdk.activate(); + secrets.activate(config); if (automaticTracingEnabled) { instrumentations.forEach(instrumentationKey => { diff --git a/packages/core/test/secrets_test.js b/packages/core/test/secrets_test.js index 7a458d4219..161b7a7e93 100644 --- a/packages/core/test/secrets_test.js +++ b/packages/core/test/secrets_test.js @@ -304,4 +304,74 @@ describe('secrets with matcher mode', () => { expect(matcher('pass')).to.be.false; }); }); + + describe('activate', () => { + it('should update matcher and keywords when config changes', () => { + secrets.init({ + logger: testUtil.createFakeLogger(), + secrets: { + matcherMode: 'equals', + keywords: ['initialSecret'] + } + }); + + expect(secrets.isSecret('initialSecret')).to.be.true; + expect(secrets.isSecret('newSecret')).to.be.false; + + secrets.activate({ + secrets: { + matcherMode: 'equals', + keywords: ['newSecret', 'anotherSecret'] + } + }); + + expect(secrets.isSecret('initialSecret')).to.be.false; + expect(secrets.isSecret('newSecret')).to.be.true; + expect(secrets.isSecret('anotherSecret')).to.be.true; + }); + + it('should update matcher mode when config changes', () => { + secrets.init({ + logger: testUtil.createFakeLogger(), + secrets: { + matcherMode: 'equals', + keywords: ['secret'] + } + }); + + expect(secrets.isSecret('secret')).to.be.true; + expect(secrets.isSecret('SECRET')).to.be.false; + + secrets.activate({ + secrets: { + matcherMode: 'equals-ignore-case', + keywords: ['secret'] + } + }); + + expect(secrets.isSecret('secret')).to.be.true; + expect(secrets.isSecret('SECRET')).to.be.true; + }); + + it('should not update when only partial config provided', () => { + secrets.init({ + logger: testUtil.createFakeLogger(), + secrets: { + matcherMode: 'contains-ignore-case', + keywords: ['password'] + } + }); + + expect(secrets.isSecret('mypassword')).to.be.true; + + secrets.activate({ + secrets: { + keywords: ['token'] + } + }); + + expect(secrets.isSecret('mypassword')).to.be.true; + expect(secrets.isSecret('mytoken')).to.be.false; + }); + }); }); From a735db62ebd1d96b145cfb58439ada1c3ef2987c Mon Sep 17 00:00:00 2001 From: Arya Date: Tue, 12 May 2026 22:05:45 +0530 Subject: [PATCH 2/3] chore: updated --- packages/core/src/tracing/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/tracing/index.js b/packages/core/src/tracing/index.js index 65e56848f8..2ec258e7c5 100644 --- a/packages/core/src/tracing/index.js +++ b/packages/core/src/tracing/index.js @@ -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'); @@ -18,7 +19,6 @@ const supportedVersion = require('./supportedVersion'); const otelInstrumentations = require('./opentelemetry-instrumentations'); const cls = require('./cls'); const coreUtil = require('../util'); -const secrets = require('../secrets'); let tracingEnabled = false; let tracingActivated = false; @@ -267,9 +267,9 @@ exports.activate = function activate(_config = config) { coreUtil.activate(_config); tracingUtil.activate(_config); spanBuffer.activate(_config); + secrets.activate(_config); opentracing.activate(); sdk.activate(); - secrets.activate(config); if (automaticTracingEnabled) { instrumentations.forEach(instrumentationKey => { From f094c4ffa51585e2f6f960706f27cef430bd0b86 Mon Sep 17 00:00:00 2001 From: Arya Date: Wed, 13 May 2026 17:37:17 +0530 Subject: [PATCH 3/3] chore: update Co-authored-by: kirrg001 --- packages/core/src/tracing/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/tracing/index.js b/packages/core/src/tracing/index.js index 2ec258e7c5..0fa9cb1019 100644 --- a/packages/core/src/tracing/index.js +++ b/packages/core/src/tracing/index.js @@ -267,7 +267,7 @@ exports.activate = function activate(_config = config) { coreUtil.activate(_config); tracingUtil.activate(_config); spanBuffer.activate(_config); - secrets.activate(_config); + secrets.activate(_config); opentracing.activate(); sdk.activate();