Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,20 +59,29 @@ 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)

- `MTLS_REQUEST_CERT` - Request client certificates (default: true)
- `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)
Expand Down
15 changes: 15 additions & 0 deletions src/__tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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)) {
Expand All @@ -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();
});
Expand All @@ -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';
Expand Down Expand Up @@ -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();
Expand All @@ -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'));
}
});

Expand All @@ -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)) {
Expand All @@ -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', () => {
Expand Down
17 changes: 16 additions & 1 deletion src/initConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,13 @@ function enclavedEnvConfig(): Partial<EnclavedConfig> {
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'),
Expand Down Expand Up @@ -124,6 +125,7 @@ function mergeEnclavedConfigs(...configs: Partial<EnclavedConfig>[]): EnclavedCo
keepAliveTimeout: get('keepAliveTimeout'),
headersTimeout: get('headersTimeout'),
kmsUrl: get('kmsUrl'),
kmsTlsCertPath: get('kmsTlsCertPath'),
keyPath: get('keyPath'),
crtPath: get('crtPath'),
tlsKey: get('tlsKey'),
Expand Down Expand Up @@ -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);
}
Expand Down
28 changes: 22 additions & 6 deletions src/kms/kmsClient.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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);
}

Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/shared/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down