From 86d876b09e0021a071eb75eb964cfb832668f8f2 Mon Sep 17 00:00:00 2001 From: Suheb <39208279+saubyk@users.noreply.github.com> Date: Mon, 3 Aug 2026 22:33:29 -0700 Subject: [PATCH] Stop logging the eclair auth header at DEBUG level (#1664) * Stop logging the eclair auth header at DEBUG level getChannels in the eclair channels controller logged its whole request options object. Eclair authenticates with HTTP basic auth, so those options carry the configured lnApiPassword in an authorization header - raising an eclair node's logLevel to DEBUG wrote "authorization":"Basic " into the node log file, which is a recoverable form of the credential and is routinely shared when debugging. The log now carries only the request url and form, matching every other DEBUG log in the controllers. This was the only site in server/ passing a whole options object to the logger; the rest log options.form, .url, .body or .qs, none of which hold credentials. Present since 0.12.0 and only reachable by opting in to DEBUG (the default log level is ERROR), but it contradicted the logging guarantee stated for #1659. Found by scanning node logs at DEBUG while verifying the 0.15.10 branch against the regtest fixture. Regression test added in test/backend/eclair-channels.test.mjs; it fails on the previous code with "auth header key must not reach the node log". * Fill in PR number in release note (#1664) --- backend/controllers/eclair/channels.js | 5 ++- release-notes/Release-notes-0.15.10.md | 12 +++++ server/controllers/eclair/channels.ts | 5 ++- test/backend/eclair-channels.test.mjs | 62 ++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 test/backend/eclair-channels.test.mjs diff --git a/backend/controllers/eclair/channels.js b/backend/controllers/eclair/channels.js index ea433bc8..b2a0a26b 100644 --- a/backend/controllers/eclair/channels.js +++ b/backend/controllers/eclair/channels.js @@ -53,7 +53,10 @@ export const getChannels = (req, res, next) => { options.form = req.query; logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Channels Node Id', data: options.form }); } - logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Options', data: options }); + // Log the call's shape only. Eclair authenticates with HTTP basic auth, so the options + // object carries the node's lnApiPassword in its authorization header, and node logs are + // routinely shared when debugging. + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Options', data: { url: options.url, form: options.form } }); if (common.read_dummy_data) { common.getDummyData('Channels', req.session.selectedNode.lnImplementation).then((data) => { res.status(200).json(data); }); } diff --git a/release-notes/Release-notes-0.15.10.md b/release-notes/Release-notes-0.15.10.md index 201ba580..876c3360 100644 --- a/release-notes/Release-notes-0.15.10.md +++ b/release-notes/Release-notes-0.15.10.md @@ -19,6 +19,18 @@ this release should add its entry under the appropriate section below. path. Adds regression coverage (`test/backend/common.test.mjs`). Users are encouraged to update promptly. +- **Eclair: stop logging the node's auth header at DEBUG level** + ([#1664](https://github.com/Ride-The-Lightning/RTL/pull/1664)). + `getChannels` in the Eclair channels controller logged its entire request options object, + which for Eclair carries HTTP basic auth — so raising an Eclair node's `logLevel` to + `DEBUG` wrote `authorization: Basic ` into the node log, a recoverable form of the + configured `lnApiPassword`. The log now carries only the request url and form, matching + every other DEBUG log in the controllers. Present since 0.12.0 and only reachable by + opting in to `DEBUG` (the default level is `ERROR`), but it contradicted the logging + guarantee stated for #1659. Regression coverage added + (`test/backend/eclair-channels.test.mjs`). Found by auditing node logs at `DEBUG` while + verifying this release against the regtest fixture. + ## Code Health - **Bound remaining unbounded LND alias-resolution fan-outs** diff --git a/server/controllers/eclair/channels.ts b/server/controllers/eclair/channels.ts index b6e9d8c5..24dcf4ce 100644 --- a/server/controllers/eclair/channels.ts +++ b/server/controllers/eclair/channels.ts @@ -55,7 +55,10 @@ export const getChannels = (req, res, next) => { options.form = req.query; logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Channels Node Id', data: options.form }); } - logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Options', data: options }); + // Log the call's shape only. Eclair authenticates with HTTP basic auth, so the options + // object carries the node's lnApiPassword in its authorization header, and node logs are + // routinely shared when debugging. + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Options', data: { url: options.url, form: options.form } }); if (common.read_dummy_data) { common.getDummyData('Channels', req.session.selectedNode.lnImplementation).then((data) => { res.status(200).json(data); }); } else { diff --git a/test/backend/eclair-channels.test.mjs b/test/backend/eclair-channels.test.mjs new file mode 100644 index 00000000..a1e811e2 --- /dev/null +++ b/test/backend/eclair-channels.test.mjs @@ -0,0 +1,62 @@ +import assert from 'node:assert/strict'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; + +import { getChannels } from '../../backend/controllers/eclair/channels.js'; + +// Eclair authenticates with HTTP basic auth, so the request options carry the node's +// lnApiPassword in the authorization header. A DEBUG log of the whole options object +// therefore writes a recoverable credential into the node log file. +const buildRequest = (logFile) => ({ + session: { + selectedNode: { + index: 1, + lnNode: 'eclair-node', + lnImplementation: 'ECL', + authentication: { + options: { + url: '', + rejectUnauthorized: false, + json: true, + headers: { authorization: 'Basic ' + Buffer.from(':super-secret-password').toString('base64') } + } + }, + settings: { lnServerUrl: 'http://127.0.0.1:1/', logLevel: 'DEBUG', logFile: logFile } + } + }, + query: {} +}); + +const waitForLog = async (logFile) => { + // logger.log appends asynchronously; give it a few turns to flush. + for (let i = 0; i < 40; i++) { + const contents = readFileSync(logFile, 'utf-8'); + if (contents.includes('Channels =>')) { return contents; } + await new Promise((resolve) => setTimeout(resolve, 25)); + } + return readFileSync(logFile, 'utf-8'); +}; + +test('getChannels does not write the eclair auth header to the node log at DEBUG level', async () => { + const tempDir = mkdtempSync(join(tmpdir(), 'ecl-channels-')); + const logFile = join(tempDir, 'RTL-Node-1.log'); + writeFileSync(logFile, ''); + const req = buildRequest(logFile); + const res = { status: () => ({ json: () => { } }) }; + + try { + getChannels(req, res, () => { }); + const contents = await waitForLog(logFile); + + assert.ok(contents.includes('Channels =>'), 'expected the controller to have logged at DEBUG level'); + assert.ok(!contents.includes('authorization'), 'auth header key must not reach the node log'); + assert.ok(!contents.includes('super-secret-password'), 'lnApiPassword must not reach the node log'); + assert.ok(!contents.includes(Buffer.from(':super-secret-password').toString('base64')), 'encoded credential must not reach the node log'); + // The diagnostic value of the log — where the call went — is still there. + assert.ok(contents.includes('/channels'), 'request url should still be logged for diagnostics'); + } finally { + rmSync(tempDir, { force: true, recursive: true }); + } +});