Skip to content

Commit 54fb773

Browse files
authored
Merge pull request #135 from kaleido-io/mtls-support
Mtls support
2 parents 5d9269a + 74b9e7b commit 54fb773

6 files changed

Lines changed: 81 additions & 12 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,13 @@ The configurable retry settings are:
206206
Setting `RETRY_CONDITION` to `""` disables retries. Setting `RETRY_MAX_ATTEMPTS` to `-1` causes it to retry indefinitely.
207207

208208
Note, the token connector will make a total of `RETRY_MAX_ATTEMPTS` + 1 calls for a given retryable call (1 original attempt and `RETRY_MAX_ATTEMPTS` retries)
209+
210+
## TLS
211+
212+
Mutual TLS can be enabled by providing three environment variables:
213+
214+
- `TLS_CA`
215+
- `TLS_CERT`
216+
- `TLS_KEY`
217+
218+
Each should be a path to a file on disk. Providing all three environment variables will result in a token connector running with TLS enabled, and requiring all clients to provide client certificates signed by the certificate authority.

src/event-stream/event-stream.service.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import WebSocket from 'ws';
2222
import { FFRequestIDHeader } from '../request-context/constants';
2323
import { Context } from '../request-context/request-context.decorator';
2424
import { IAbiMethod } from '../tokens/tokens.interfaces';
25-
import { basicAuth } from '../utils';
25+
import { getHttpRequestOptions, getWebsocketOptions } from '../utils';
2626
import {
2727
Event,
2828
EventBatch,
@@ -58,9 +58,7 @@ export class EventStreamSocket {
5858
this.disconnectDetected = false;
5959
this.closeRequested = false;
6060

61-
const auth =
62-
this.username && this.password ? { auth: `${this.username}:${this.password}` } : undefined;
63-
this.ws = new WebSocket(this.url, auth);
61+
this.ws = new WebSocket(this.url, getWebsocketOptions(this.username, this.password));
6462
this.ws
6563
.on('open', () => {
6664
if (this.disconnectDetected) {
@@ -173,7 +171,7 @@ export class EventStreamService {
173171
}
174172
}
175173
headers[FFRequestIDHeader] = ctx.requestId;
176-
const config = basicAuth(this.username, this.password);
174+
const config = getHttpRequestOptions(this.username, this.password);
177175
config.headers = headers;
178176
return config;
179177
}

src/health/health.controller.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Controller, Get } from '@nestjs/common';
22
import { HealthCheckService, HealthCheck, HttpHealthIndicator } from '@nestjs/terminus';
33
import { BlockchainConnectorService } from '../tokens/blockchain.service';
4-
import { basicAuth } from '../utils';
4+
import { getHttpRequestOptions } from '../utils';
55

66
@Controller('health')
77
export class HealthController {
@@ -25,7 +25,7 @@ export class HealthController {
2525
this.http.pingCheck(
2626
'ethconnect',
2727
`${this.blockchain.baseUrl}/status`,
28-
basicAuth(this.blockchain.username, this.blockchain.password),
28+
getHttpRequestOptions(this.blockchain.username, this.blockchain.password),
2929
),
3030
]);
3131
}

src/main.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
// See the License for the specific language governing permissions and
1515
// limitations under the License.
1616

17-
import { ShutdownSignal, ValidationPipe } from '@nestjs/common';
17+
import { NestApplicationOptions, ShutdownSignal, ValidationPipe } from '@nestjs/common';
1818
import { ConfigService } from '@nestjs/config';
1919
import { NestFactory } from '@nestjs/core';
2020
import { WsAdapter } from '@nestjs/platform-ws';
@@ -36,6 +36,7 @@ import {
3636
import { TokensService } from './tokens/tokens.service';
3737
import { newContext } from './request-context/request-context.decorator';
3838
import { AbiMapperService } from './tokens/abimapper.service';
39+
import { getNestOptions } from './utils';
3940

4041
const API_DESCRIPTION = `
4142
<p>All POST APIs are asynchronous. Listen for websocket notifications on <code>/api/ws</code>.
@@ -50,7 +51,8 @@ export function getApiConfig() {
5051
}
5152

5253
async function bootstrap() {
53-
const app = await NestFactory.create(AppModule);
54+
const app = await NestFactory.create(AppModule, getNestOptions());
55+
5456
app.setGlobalPrefix('api/v1');
5557
app.useWebSocketAdapter(new WsAdapter(app));
5658
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));

src/tokens/blockchain.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
2626
import { lastValueFrom } from 'rxjs';
2727
import { EventStreamReply } from '../event-stream/event-stream.interfaces';
28-
import { basicAuth } from '../utils';
28+
import { getHttpRequestOptions } from '../utils';
2929
import { Context } from '../request-context/request-context.decorator';
3030
import { FFRequestIDHeader } from '../request-context/constants';
3131
import { EthConnectAsyncResponse, EthConnectReturn, IAbiMethod } from './tokens.interfaces';
@@ -80,7 +80,7 @@ export class BlockchainConnectorService {
8080
}
8181
}
8282
headers[FFRequestIDHeader] = ctx.requestId;
83-
const config = basicAuth(this.username, this.password);
83+
const config = getHttpRequestOptions(this.username, this.password);
8484
config.headers = headers;
8585
return config;
8686
}

src/utils.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,71 @@
1+
import * as fs from 'fs';
2+
import * as https from 'https';
3+
import { NestApplicationOptions } from '@nestjs/common';
14
import { AxiosRequestConfig } from 'axios';
5+
import { ClientOptions } from 'ws';
26

3-
export const basicAuth = (username: string, password: string) => {
7+
interface Certificates {
8+
key: string;
9+
cert: string;
10+
ca: string;
11+
}
12+
13+
const getCertificates = (): Certificates | undefined => {
14+
let key, cert, ca;
15+
if (
16+
process.env['TLS_KEY'] === undefined ||
17+
process.env['TLS_CERT'] === undefined ||
18+
process.env['TLS_CA'] === undefined
19+
) {
20+
return undefined;
21+
}
22+
try {
23+
key = fs.readFileSync(process.env['TLS_KEY']).toString();
24+
cert = fs.readFileSync(process.env['TLS_CERT']).toString();
25+
ca = fs.readFileSync(process.env['TLS_CA']).toString();
26+
} catch (error) {
27+
console.error(`Error reading certificates: ${error}`);
28+
process.exit(-1);
29+
}
30+
return { key, cert, ca };
31+
};
32+
33+
export const getWebsocketOptions = (username: string, password: string): ClientOptions => {
34+
const requestOptions: ClientOptions = {};
35+
if (username && username !== '' && password && password !== '') {
36+
requestOptions.headers = {
37+
Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`,
38+
};
39+
}
40+
const certs = getCertificates();
41+
if (certs) {
42+
requestOptions.ca = certs.ca;
43+
requestOptions.cert = certs.cert;
44+
requestOptions.key = certs.key;
45+
}
46+
return requestOptions;
47+
};
48+
49+
export const getHttpRequestOptions = (username: string, password: string) => {
450
const requestOptions: AxiosRequestConfig = {};
551
if (username !== '' && password !== '') {
652
requestOptions.auth = {
753
username: username,
854
password: password,
955
};
1056
}
57+
const certs = getCertificates();
58+
if (certs) {
59+
requestOptions.httpsAgent = new https.Agent({ ...certs, requestCert: true });
60+
}
1161
return requestOptions;
1262
};
63+
64+
export const getNestOptions = (): NestApplicationOptions => {
65+
const options: NestApplicationOptions = {};
66+
const certs = getCertificates();
67+
if (certs) {
68+
options.httpsOptions = certs;
69+
}
70+
return options;
71+
};

0 commit comments

Comments
 (0)