Skip to content

Commit 28d8534

Browse files
authored
Make ENV vars sensitive by default (#660)
* Make ENV vars sensitive by default * Explicitly insensitive flag config vars in template
1 parent 6061c12 commit 28d8534

File tree

7 files changed

+86
-3
lines changed

7 files changed

+86
-3
lines changed

docs/components/adapter.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const config = new AdapterConfig({
2828
fn: () => {},
2929
}, // If applicable, a Validator object to validate the env var value. Return an error message for a failed validation, or undefined if it passes.
3030
required: true, // If the env var should be required. Default = false
31-
sensitive: true, // Set to true to censor this env var from logs. Default = false
31+
sensitive: false, // Set to false if the env var is safe to show uncensored in logs or telemetry. Default = true
3232
},
3333
})
3434
```

docs/guides/porting-a-v2-ea-to-v3.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ export const customSettings = {
238238
default: 'foo', // If applicable, a default value
239239
validate: (value?: string) => {}, // If applicable, a function to validate the env var value. Return an error message for a failed validation, or undefined if it passes.
240240
required: true, // If the env var should be required. Default = false
241-
sensitive: true, // Set to true to censor this env var from logs. Default = false
241+
sensitive: false, // Set to false if the env var is safe to show uncensored in logs or telemetry. Default = true
242242
},
243243
} as const
244244
```

scripts/generator-adapter/generators/app/templates/src/config/index.ts.ejs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,22 @@ export const config = new AdapterConfig(
1414
'An API endpoint for Data Provider',
1515
type: 'string',
1616
default: 'https://dataproviderapi.com',
17+
sensitive: false,
1718
},
1819
WS_API_ENDPOINT: {
1920
description:
2021
'WS endpoint for Data Provider',
2122
type: 'string',
2223
default: 'ws://localhost:9090',
24+
sensitive: false,
2325
},
2426
<% if (setBgExecuteMsEnv) { %>
2527
BACKGROUND_EXECUTE_MS: {
2628
description:
2729
'The amount of time the background execute should sleep before performing the next request',
2830
type: 'number',
2931
default: 10_000,
32+
sensitive: false,
3033
},<% } %>
3134
},
3235
)

src/config/index.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const BaseSettingsDefinition = {
1919
description: 'Starting path for the EA handler endpoint',
2020
type: 'string',
2121
default: '/',
22+
sensitive: false,
2223
},
2324
CACHE_LOCK_DURATION: {
2425
description: 'Time (in ms) used as a baseline for the acquisition and extension of cache locks',
@@ -57,6 +58,7 @@ export const BaseSettingsDefinition = {
5758
description: 'Hostname for the Redis instance to be used',
5859
type: 'string',
5960
default: '127.0.0.1',
61+
sensitive: false,
6062
},
6163
CACHE_REDIS_MAX_RECONNECT_COOLDOWN: {
6264
description: 'Max cooldown (in ms) before attempting redis reconnection',
@@ -72,6 +74,7 @@ export const BaseSettingsDefinition = {
7274
CACHE_REDIS_PATH: {
7375
description: 'The UNIX socket string of the Redis server',
7476
type: 'string',
77+
sensitive: false,
7578
},
7679
CACHE_REDIS_PORT: {
7780
description: 'Port for the Redis instance to be used',
@@ -102,6 +105,7 @@ export const BaseSettingsDefinition = {
102105
description: 'Specifies a prefix to use for cache keys',
103106
type: 'string',
104107
default: '',
108+
sensitive: false,
105109
},
106110
STREAM_HANDLER_RETRY_MAX_MS: {
107111
type: 'number',
@@ -156,6 +160,7 @@ export const BaseSettingsDefinition = {
156160
description: 'Minimum level required for logs to be output',
157161
type: 'string',
158162
default: 'info',
163+
sensitive: false,
159164
},
160165
CENSOR_SENSITIVE_LOGS: {
161166
description: 'Controls whether the logging of sensitive information is enabled or disabled',
@@ -187,6 +192,7 @@ export const BaseSettingsDefinition = {
187192
description:
188193
'Rate limiting tier to use from the available options for the adapter. If not present, the adapter will run using the first tier on the list.',
189194
type: 'string',
195+
sensitive: false,
190196
},
191197
RATE_LIMIT_CAPACITY: {
192198
description: 'Used as rate limit capacity per minute and ignores tier settings if defined',
@@ -274,13 +280,15 @@ export const BaseSettingsDefinition = {
274280
description: 'Default key to be used when one cannot be determined from request parameters',
275281
type: 'string',
276282
default: 'DEFAULT_CACHE_KEY',
283+
sensitive: false,
277284
},
278285
EA_HOST: {
279286
description:
280287
'Host this EA will listen for REST requests on (if mode is set to "reader" or "reader-writer")',
281288
type: 'string',
282289
default: '::',
283290
validate: validator.host(),
291+
sensitive: false,
284292
},
285293
EA_MODE: {
286294
description:
@@ -316,6 +324,7 @@ export const BaseSettingsDefinition = {
316324
description: 'Base64 Public Key of TSL/SSL certificate',
317325
type: 'string',
318326
validate: validator.base64(),
327+
sensitive: false,
319328
},
320329
TLS_PASSPHRASE: {
321330
description: 'Password to be used to generate an encryption key',
@@ -658,6 +667,7 @@ export class AdapterConfig<T extends SettingsDefinitionMap = SettingsDefinitionM
658667
* RPC_URL are potentially sensitive given it may contain API keys in path
659668
*/
660669
buildCensorList() {
670+
const alwaysCensored = ['RPC_URL', 'API_KEY']
661671
const censorList: CensorKeyValue[] = Object.entries(
662672
BaseSettingsDefinition as SettingsDefinitionMap,
663673
)
@@ -666,7 +676,8 @@ export class AdapterConfig<T extends SettingsDefinitionMap = SettingsDefinitionM
666676
([name, setting]) =>
667677
setting &&
668678
setting.type === 'string' &&
669-
(setting.sensitive || name.endsWith('RPC_URL')) &&
679+
(setting.sensitive !== false ||
680+
alwaysCensored.some((pattern) => name.includes(pattern))) &&
670681
(this.settings as Record<string, ValidSettingValue>)[name],
671682
)
672683
.map(([name]) => ({

test/config.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,48 @@ test.serial('sensitive configuration constants are properly flagged', (t) => {
190190
t.deepEqual(actualSensitiveSettings, expectedSensitiveSettings)
191191
})
192192

193+
test.serial('API_KEY prefix/suffix settings are always censored', (t) => {
194+
process.env['API_KEY_PRIMARY'] = 'prefixed-key'
195+
process.env['PRIMARY_API_KEY'] = 'suffixed-key'
196+
process.env['NOT_SECRET'] = 'plain-value'
197+
const customSettings: SettingsDefinitionMap = {
198+
API_KEY_PRIMARY: {
199+
description: 'API key that is mistakenly marked as insensitive',
200+
type: 'string',
201+
sensitive: false,
202+
},
203+
PRIMARY_API_KEY: {
204+
description: 'API key suffix that is mistakenly marked as insensitive',
205+
type: 'string',
206+
sensitive: false,
207+
},
208+
NOT_SECRET: {
209+
description: 'Plain text that should not be censored',
210+
type: 'string',
211+
sensitive: false,
212+
},
213+
}
214+
const config = new AdapterConfig(customSettings)
215+
config.initialize()
216+
config.validate()
217+
config.buildCensorList()
218+
219+
const adapter = new Adapter({
220+
name: 'TEST_ADAPTER',
221+
endpoints: [],
222+
config: config,
223+
})
224+
225+
const settingsList = buildSettingsList(adapter)
226+
const apiKeyPrimary = settingsList.find((entry) => entry.name === 'API_KEY_PRIMARY')
227+
const primaryApiKey = settingsList.find((entry) => entry.name === 'PRIMARY_API_KEY')
228+
const notSecret = settingsList.find((entry) => entry.name === 'NOT_SECRET')
229+
230+
t.is(apiKeyPrimary?.value, '[API_KEY_PRIMARY REDACTED]')
231+
t.is(primaryApiKey?.value, '[PRIMARY_API_KEY REDACTED]')
232+
t.is(notSecret?.value, 'plain-value')
233+
})
234+
193235
test.serial('multiline sensitive configuration constants are properly redacted', async (t) => {
194236
// GIVEN
195237
process.env['PRIVATE_KEY'] =

test/debug-endpoints.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ test.serial('/debug/settings/raw endpoint returns expected values', async (t) =>
141141
'Rate limiting tier to use from the available options for the adapter. If not present, the adapter will run using the first tier on the list.',
142142
name: 'RATE_LIMIT_API_TIER',
143143
required: false,
144+
sensitive: false,
144145
customSetting: false,
145146
},
146147
)

test/logger.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,19 @@ test.before(async () => {
1313
type: 'string',
1414
sensitive: true,
1515
},
16+
AB_LAMBO_MODEL: {
17+
description: 'Test harmless env var that is safe to log',
18+
type: 'string',
19+
sensitive: false,
20+
},
21+
API_PRIVATE_KEY: {
22+
description: 'Test env var that a developer forgot to explicitly flag as sensitive',
23+
type: 'string',
24+
},
1625
} satisfies SettingsDefinitionMap
1726
process.env['API_KEY'] = 'mock-api-key'
27+
process.env['AB_LAMBO_MODEL'] = 'revuelto'
28+
process.env['API_PRIVATE_KEY'] = 'mock-private-key'
1829
const config = new AdapterConfig(customSettings)
1930
const adapter = new Adapter({
2031
name: 'TEST',
@@ -33,6 +44,11 @@ test('properly builds censor list', async (t) => {
3344
const censorList = CensorList.getAll()
3445
// eslint-disable-next-line prefer-regex-literals
3546
t.deepEqual(censorList[0], { key: 'API_KEY', value: RegExp('mock\\-api\\-key', 'gi') })
47+
t.deepEqual(censorList[1], {
48+
key: 'API_PRIVATE_KEY',
49+
// eslint-disable-next-line prefer-regex-literals
50+
value: RegExp('mock\\-private\\-key', 'gi'),
51+
})
3652
})
3753

3854
test('properly redacts API_KEY (string)', async (t) => {
@@ -65,6 +81,16 @@ test('properly handles undefined', async (t) => {
6581
t.deepEqual(redacted, undefined)
6682
})
6783

84+
test('does not censor vars flagged as sensitive = false', async (t) => {
85+
const redacted = censor({ abLamboModel: 'revuelto' }, CensorList.getAll())
86+
t.deepEqual(redacted, { abLamboModel: 'revuelto' })
87+
})
88+
89+
test('censor vars not explicitly flagged as sensitive', async (t) => {
90+
const redacted = censor({ publicApiKey: 'mock-private-key' }, CensorList.getAll())
91+
t.deepEqual(redacted, { publicApiKey: '[API_PRIVATE_KEY REDACTED]' })
92+
})
93+
6894
test('properly redacts API_KEY (multiple nested values)', async (t) => {
6995
const redacted = censor(
7096
{ apiKey: 'mock-api-key', config: { headers: { auth: 'mock-api-key' } } },

0 commit comments

Comments
 (0)