diff --git a/README.md b/README.md index e6d38cb..f1aedb6 100644 --- a/README.md +++ b/README.md @@ -59,13 +59,13 @@ Both modes use the same TLS configuration variables: **Option 1: Certificate Files** -- `TLS_KEY_PATH` - Path to private key file -- `TLS_CERT_PATH` - Path to certificate file +- `TLS_KEY_PATH` - Path to private key file (used for both inbound mTLS server and outbound mTLS client to KMS) +- `TLS_CERT_PATH` - Path to certificate file (used for both inbound mTLS server and outbound mTLS client to KMS) **Option 2: Environment Variables** -- `TLS_KEY` - Private key content (PEM format) -- `TLS_CERT` - Certificate content (PEM format) +- `TLS_KEY` - Private key content (PEM format, used for both inbound and outbound) +- `TLS_CERT` - Certificate content (PEM format, used for both inbound and outbound) #### mTLS Settings (when TLS_MODE=mtls) @@ -73,6 +73,15 @@ Both modes use the same TLS configuration variables: - `ALLOW_SELF_SIGNED` - Allow self-signed certificates (default: false) - `MTLS_ALLOWED_CLIENT_FINGERPRINTS` - Comma-separated list of allowed client certificate fingerprints (optional) +#### Outbound mTLS to KMS + +- When `TLS_MODE=mtls`, outbound mTLS to KMS is enabled by default. +- The same `TLS_CERT` and `TLS_KEY` are used as the client certificate and key for outbound mTLS requests to KMS. +- `KMS_TLS_CERT_PATH` - Path to the CA certificate to verify the KMS server (required when outbound mTLS is enabled). +- If `TLS_MODE=disabled`, outbound mTLS to KMS is also disabled by default. + +> **Note:** If you want to use a different client certificate for KMS, you will need to extend the configuration. By default, the same cert/key is used for both inbound and outbound mTLS. + ### Logging and Debug - `HTTP_LOGFILE` - Path to HTTP request log file (optional, used by Morgan for HTTP access logs) diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index 4bcfd2a..69f2e2a 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -62,6 +62,7 @@ describe('Configuration', () => { process.env.KMS_URL = 'http://localhost:3000'; process.env.TLS_KEY = mockTlsKey; process.env.TLS_CERT = mockTlsCert; + process.env.KMS_TLS_CERT_PATH = path.resolve(__dirname, 'mocks/certs/test-ssl-cert.pem'); const cfg = initConfig(); isEnclavedConfig(cfg).should.be.true(); if (isEnclavedConfig(cfg)) { @@ -80,6 +81,7 @@ describe('Configuration', () => { process.env.KMS_URL = 'http://localhost:3000'; process.env.TLS_KEY = mockTlsKey; process.env.TLS_CERT = mockTlsCert; + process.env.KMS_TLS_CERT_PATH = path.resolve(__dirname, 'mocks/certs/test-ssl-cert.pem'); const cfg = initConfig(); isEnclavedConfig(cfg).should.be.true(); if (isEnclavedConfig(cfg)) { @@ -95,6 +97,7 @@ describe('Configuration', () => { process.env.TLS_KEY = mockTlsKey; process.env.TLS_CERT = mockTlsCert; process.env.RECOVERY_MODE = 'true'; + process.env.KMS_TLS_CERT_PATH = path.resolve(__dirname, 'mocks/certs/test-ssl-cert.pem'); const cfg = initConfig(); cfg.recoveryMode!.should.be.true(); }); @@ -103,6 +106,7 @@ describe('Configuration', () => { process.env.KMS_URL = 'http://localhost:3000'; process.env.TLS_KEY = mockTlsKey; process.env.TLS_CERT = mockTlsCert; + process.env.KMS_TLS_CERT_PATH = path.resolve(__dirname, 'mocks/certs/test-ssl-cert.pem'); // Test with TLS disabled process.env.TLS_MODE = 'disabled'; @@ -147,6 +151,7 @@ describe('Configuration', () => { process.env.TLS_KEY = mockTlsKey; process.env.TLS_CERT = mockTlsCert; process.env.MTLS_ALLOWED_CLIENT_FINGERPRINTS = 'ABC123,DEF456'; + process.env.KMS_TLS_CERT_PATH = path.resolve(__dirname, 'mocks/certs/test-ssl-cert.pem'); const cfg = initConfig(); isEnclavedConfig(cfg).should.be.true(); @@ -155,6 +160,7 @@ describe('Configuration', () => { cfg.kmsUrl.should.equal('http://localhost:3000'); cfg.tlsKey!.should.equal(mockTlsKey); cfg.tlsCert!.should.equal(mockTlsCert); + cfg.kmsTlsCertPath!.should.equal(path.resolve(__dirname, 'mocks/certs/test-ssl-cert.pem')); } }); @@ -177,6 +183,7 @@ describe('Configuration', () => { process.env.TLS_MODE = 'disabled'; delete process.env.TLS_KEY; delete process.env.TLS_CERT; + delete process.env.KMS_TLS_CERT_PATH; const cfg = initConfig(); isEnclavedConfig(cfg).should.be.true(); if (isEnclavedConfig(cfg)) { @@ -198,12 +205,20 @@ describe('Configuration', () => { process.env.TLS_KEY = mockTlsKey; process.env.TLS_CERT = mockTlsCert; process.env.HTTP_LOGFILE = '/tmp/test-http-access.log'; + process.env.KMS_TLS_CERT_PATH = path.resolve(__dirname, 'mocks/certs/test-ssl-cert.pem'); const cfg = initConfig(); isEnclavedConfig(cfg).should.be.true(); if (isEnclavedConfig(cfg)) { cfg.httpLoggerFile.should.equal('/tmp/test-http-access.log'); } }); + + it('should throw error when KMS_TLS_CERT_PATH is not set for MTLS mode', () => { + process.env.KMS_URL = 'http://localhost:3000'; + process.env.TLS_MODE = 'mtls'; + delete process.env.KMS_TLS_CERT_PATH; + (() => initConfig()).should.throw('KMS_TLS_CERT is required when TLS mode is MTLS'); + }); }); describe('Master Express Mode', () => { diff --git a/src/initConfig.ts b/src/initConfig.ts index 4329911..3ac13b2 100644 --- a/src/initConfig.ts +++ b/src/initConfig.ts @@ -87,12 +87,13 @@ function enclavedEnvConfig(): Partial { port: Number(readEnvVar('ENCLAVED_EXPRESS_PORT')), bind: readEnvVar('BIND'), ipc: readEnvVar('IPC'), - httpLoggerFile: readEnvVar('HTTP_LOGFILE'), + httpLoggerFile: readEnvVar('HTTP_LOGFILE') || 'logs/http-access.log', timeout: Number(readEnvVar('TIMEOUT')), keepAliveTimeout: Number(readEnvVar('KEEP_ALIVE_TIMEOUT')), headersTimeout: Number(readEnvVar('HEADERS_TIMEOUT')), // KMS settings kmsUrl, + kmsTlsCertPath: readEnvVar('KMS_TLS_CERT_PATH'), // mTLS settings keyPath: readEnvVar('TLS_KEY_PATH'), crtPath: readEnvVar('TLS_CERT_PATH'), @@ -124,6 +125,7 @@ function mergeEnclavedConfigs(...configs: Partial[]): EnclavedCo keepAliveTimeout: get('keepAliveTimeout'), headersTimeout: get('headersTimeout'), kmsUrl: get('kmsUrl'), + kmsTlsCertPath: get('kmsTlsCertPath'), keyPath: get('keyPath'), crtPath: get('crtPath'), tlsKey: get('tlsKey'), @@ -166,6 +168,19 @@ function configureEnclavedMode(): EnclavedConfig { logger.info('Using TLS certificate from environment variable'); } + if (!config.kmsTlsCertPath) { + throw new Error('KMS_TLS_CERT is required when TLS mode is MTLS'); + } + if (config.kmsTlsCertPath) { + try { + config.kmsTlsCert = fs.readFileSync(config.kmsTlsCertPath, 'utf-8'); + logger.info(`Successfully loaded KMS TLS certificate from file: ${config.kmsTlsCertPath}`); + } catch (e) { + const err = e instanceof Error ? e : new Error(String(e)); + throw new Error(`Failed to read KMS TLS certificate from kmsTlsCert: ${err.message}`); + } + } + // Validate that certificates are properly loaded when TLS is enabled validateTlsCertificates(config); } diff --git a/src/kms/kmsClient.ts b/src/kms/kmsClient.ts index 01f0a86..6634b01 100644 --- a/src/kms/kmsClient.ts +++ b/src/kms/kmsClient.ts @@ -1,6 +1,6 @@ import debug from 'debug'; import * as superagent from 'superagent'; -import { EnclavedConfig, isMasterExpressConfig } from '../shared/types'; +import { EnclavedConfig, isMasterExpressConfig, TlsMode } from '../shared/types'; import { PostKeyKmsSchema, PostKeyParams, PostKeyResponse } from './types/postKey'; import { GetKeyKmsSchema, GetKeyParams, GetKeyResponse } from './types/getKey'; import { @@ -13,22 +13,30 @@ import { GenerateDataKeyParams, GenerateDataKeyResponse, } from './types/generateDataKey'; +import https from 'https'; const debugLogger = debug('bitgo:express:kmsClient'); export class KmsClient { private readonly url: string; + private readonly agent?: https.Agent; constructor(cfg: EnclavedConfig) { if (isMasterExpressConfig(cfg)) { throw new Error('Configuration is not in enclaved express mode'); } - if (!cfg.kmsUrl) { throw new Error('KMS URL not configured. Please set KMS_URL in your environment.'); } this.url = cfg.kmsUrl; + if (cfg.tlsMode === TlsMode.MTLS && cfg.kmsTlsCert) { + this.agent = new https.Agent({ + ca: cfg.kmsTlsCert, + cert: cfg.tlsCert, + key: cfg.tlsKey, + }); + } debugLogger('kmsClient initialized with URL: %s', this.url); } @@ -38,7 +46,9 @@ export class KmsClient { // Call KMS to post the key let kmsResponse: any; try { - kmsResponse = await superagent.post(`${this.url}/key`).set('x-api-key', 'abc').send(params); + let req = superagent.post(`${this.url}/key`).set('x-api-key', 'abc').send(params); + if (this.agent) req = req.agent(this.agent); + kmsResponse = await req; } catch (error: any) { console.log('Error posting key to KMS', error); throw error; @@ -63,10 +73,12 @@ export class KmsClient { // Call KMS to get the key let kmsResponse: any; try { - kmsResponse = await superagent.get(`${this.url}/key/${params.pub}`).query({ + let req = superagent.get(`${this.url}/key/${params.pub}`).query({ source: params.source, useLocalEncipherment: params.options?.useLocalEncipherment ?? false, }); + if (this.agent) req = req.agent(this.agent); + kmsResponse = await req; } catch (error: any) { console.log('Error getting key from KMS', error); throw error; @@ -90,7 +102,9 @@ export class KmsClient { // Call KMS to generate the data key let kmsResponse: any; try { - kmsResponse = await superagent.post(`${this.url}/generateDataKey`).send(params); + let req = superagent.post(`${this.url}/generateDataKey`).send(params); + if (this.agent) req = req.agent(this.agent); + kmsResponse = await req; } catch (error: any) { debugLogger('Error generating data key from KMS', error); throw error; @@ -117,7 +131,9 @@ export class KmsClient { // Call KMS to decrypt the data key let kmsResponse: any; try { - kmsResponse = await superagent.post(`${this.url}/decryptDataKey`).send(params); + let req = superagent.post(`${this.url}/decryptDataKey`).send(params); + if (this.agent) req = req.agent(this.agent); + kmsResponse = await req; } catch (error: any) { debugLogger('Error decrypting data key from KMS', error); throw error; diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index 9947170..4877b6e 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -28,6 +28,8 @@ export interface EnclavedConfig extends BaseConfig { appMode: AppMode.ENCLAVED; // KMS settings kmsUrl: string; + kmsTlsCertPath?: string; + kmsTlsCert?: string; // mTLS settings keyPath?: string; crtPath?: string;