From c7d8f6020f87383d4fadccdad1660b959a9f88e3 Mon Sep 17 00:00:00 2001 From: droguljic <1875821+droguljic@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:11:25 +0200 Subject: [PATCH] fix: harden OpenTelemetry config writing Replace the fragile OpenTelemetry collector config writer with base64-encoded generated YAML decoded inside the config writer container. This avoids shell quoting hazards with quotes, shell metacharacters, multiline values, and environment-like strings while preserving the existing sidecar-init pattern. --- src/otel/index.test.ts | 77 ++++++++++++++++++++++++++++++++++++++++++ src/otel/index.ts | 23 ++++++++++--- 2 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 src/otel/index.test.ts diff --git a/src/otel/index.test.ts b/src/otel/index.test.ts new file mode 100644 index 00000000..03dcb71e --- /dev/null +++ b/src/otel/index.test.ts @@ -0,0 +1,77 @@ +import { Buffer } from 'node:buffer'; +import { describe, it } from 'node:test'; +import * as assert from 'node:assert/strict'; +import * as pulumi from '@pulumi/pulumi'; +import * as yaml from 'yaml'; +import { OtelCollector } from './index'; + +const shellSensitiveConfig: OtelCollector.Config = { + receivers: { + otlp: { + protocols: { + http: { + endpoint: "0.0.0.0:4318; echo 'broken' && $HOME `whoami`", + }, + }, + }, + }, + processors: {}, + exporters: { + debug: { + verbosity: "it's detailed\n$(touch /tmp/pwned) `uname`; & | > <", + }, + }, + extensions: {}, + service: { + pipelines: {}, + telemetry: { + logs: { + level: "warn: '$HOME'\nnext line", + }, + }, + }, +}; + +describe('OtelCollector', () => { + it('should write collector config through a base64-decoded payload', async () => { + const collector = new OtelCollector( + 'service-name', + 'test', + shellSensitiveConfig, + ); + const commandInput = collector.configContainer.command; + + assert.ok(commandInput); + + const command = await resolveCommand(commandInput); + const [, , script] = command; + const encodedConfig = Buffer.from( + yaml.stringify(shellSensitiveConfig), + 'utf8', + ).toString('base64'); + + assert.deepEqual(command.slice(0, 2), ['sh', '-c']); + assert.match(script, /base64 -d/); + assert.match(script, /mktemp \/etc\/otelcol-contrib\/config\.yaml\.XXXXXX/); + assert.match( + script, + /mv "\$tmp_config" \/etc\/otelcol-contrib\/config\.yaml/, + ); + assert.match(script, new RegExp(encodedConfig)); + assert.doesNotMatch(script, /echo '/); + assert.doesNotMatch(script, /touch \/tmp\/pwned/); + assert.doesNotMatch(script, /whoami/); + }); +}); + +async function resolveCommand( + input: pulumi.Input[]>, +): Promise { + return new Promise(resolve => { + pulumi.output(input).apply(command => { + resolve(command); + + return command; + }); + }); +} diff --git a/src/otel/index.ts b/src/otel/index.ts index 4fb747f2..be49cb2c 100644 --- a/src/otel/index.ts +++ b/src/otel/index.ts @@ -1,3 +1,4 @@ +import { Buffer } from 'node:buffer'; import * as pulumi from '@pulumi/pulumi'; import * as aws from '@pulumi/aws'; import * as yaml from 'yaml'; @@ -202,6 +203,22 @@ export class OtelCollector { ]; } + private createConfigWriterCommand(config: OtelCollector.Config): string[] { + const configYaml = yaml.stringify(config); + const encodedConfig = Buffer.from(configYaml, 'utf8').toString('base64'); + + return [ + 'sh', + '-c', + [ + 'set -eu', + 'tmp_config=$(mktemp /etc/otelcol-contrib/config.yaml.XXXXXX)', + `printf '%s' '${encodedConfig}' | base64 -d > "$tmp_config"`, + 'mv "$tmp_config" /etc/otelcol-contrib/config.yaml', + ].join('\n'), + ]; + } + private createConfigContainer( config: pulumi.Output, volume: pulumi.Input, @@ -210,11 +227,7 @@ export class OtelCollector { name: 'otel-config-writer', image: 'amazonlinux:latest', essential: false, - command: config.apply(config => [ - 'sh', - '-c', - `echo '${yaml.stringify(config)}' > /etc/otelcol-contrib/config.yaml`, - ]), + command: config.apply(config => this.createConfigWriterCommand(config)), mountPoints: [ { sourceVolume: volume,