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..44318a2e 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** + ([#TBD](https://github.com/Ride-The-Lightning/RTL/pull/TBD)). + `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 }); + } +});