Skip to content

Commit

Permalink
fix(CogRPC): Self-issue TLS certificate (#190)
Browse files Browse the repository at this point in the history
* fix(CogRPC): Self-issue TLS certificate

As one of the few workarounds for kubernetes/ingress-gce#18

* Update URL to CogRPC server in functional test suite

* Fix COGRPC_ADDRESS in functional tests

* Document need to issue cert
  • Loading branch information
gnarea committed Sep 21, 2020
1 parent 35a266d commit ef47a1c
Show file tree
Hide file tree
Showing 8 changed files with 82 additions and 8 deletions.
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ services:
command: build/main/bin/cogrpc-server.js
environment:
GATEWAY_KEY_ID: ${GATEWAY_KEY_ID}
SERVER_IP_ADDRESS: 127.0.0.1
COGRPC_ADDRESS: https://cogrpc.example.com/
MONGO_URI: ${MONGO_URI}
NATS_SERVER_URL: ${NATS_SERVER_URL}
Expand Down
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"mongoose": "^5.10.5",
"node-nats-streaming": "^0.3.2",
"pino": "^6.6.1",
"selfsigned": "^1.10.8",
"stream-to-it": "^0.2.2",
"uuid-random": "^1.3.2"
}
Expand Down
2 changes: 1 addition & 1 deletion src/functional_tests/docker-compose.override.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,4 @@ services:

cogrpc:
environment:
COGRPC_ADDRESS: http://127.0.0.1:8081/
COGRPC_ADDRESS: https://127.0.0.1:8081/
2 changes: 1 addition & 1 deletion src/functional_tests/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { get as getEnvVar } from 'env-var';
import { IS_GITHUB, OBJECT_STORAGE_BUCKET, OBJECT_STORAGE_CLIENT, sleep } from './utils';

export const GW_POHTTP_URL = 'http://127.0.0.1:8080';
export const GW_GOGRPC_URL = 'http://127.0.0.1:8081/';
export const GW_GOGRPC_URL = 'https://127.0.0.1:8081/';
export const PONG_ENDPOINT_ADDRESS = 'http://pong:8080/';

const VAULT_URL = getEnvVar('VAULT_URL').required().asString();
Expand Down
33 changes: 29 additions & 4 deletions src/services/cogrpc/server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { CargoRelayService } from '@relaycorp/cogrpc';
import * as grpc from 'grpc';
import * as grpcHealthCheck from 'grpc-health-check';
import selfsigned from 'selfsigned';

import { mockSpy } from '../../_test_utils';
import { configureMockEnvVars } from '../_test_utils';
Expand All @@ -26,12 +27,19 @@ jest.mock('grpc', () => {
};
});

const mockSelfSignedOutput = {
cert: 'the certificate, PEM-encoded',
private: 'the private key, PEM-encoded',
};
const mockSelfSigned = mockSpy(jest.spyOn(selfsigned, 'generate'), () => mockSelfSignedOutput);

const BASE_ENV_VARS = {
COGRPC_ADDRESS: 'https://cogrpc.example.com/',
GATEWAY_KEY_ID: 'base64-encoded key id',
NATS_CLUSTER_ID: 'nats-cluster-id',
NATS_SERVER_URL: 'nats://example.com',
OBJECT_STORE_BUCKET: 'bucket-name',
SERVER_IP_ADDRESS: '127.0.0.1',
};
const mockEnvVars = configureMockEnvVars(BASE_ENV_VARS);

Expand All @@ -42,6 +50,7 @@ describe('runServer', () => {
'NATS_CLUSTER_ID',
'COGRPC_ADDRESS',
'OBJECT_STORE_BUCKET',
'SERVER_IP_ADDRESS',
])('Environment variable %s should be present', async (envVar) => {
mockEnvVars({ ...BASE_ENV_VARS, [envVar]: undefined });

Expand Down Expand Up @@ -148,13 +157,29 @@ describe('runServer', () => {
});
});

test('Server should not use TLS', async () => {
test('Server should use TLS with a self-issued certificate', async () => {
const spiedCreateSsl = jest.spyOn(grpc.ServerCredentials, 'createSsl');

await runServer();

expect(mockServer.bind).toBeCalledTimes(1);
expect(mockServer.bind).toBeCalledWith(
expect.anything(),
grpc.ServerCredentials.createInsecure(),
expect(spiedCreateSsl).toBeCalledWith(null, [
{
cert_chain: Buffer.from(mockSelfSignedOutput.cert),
private_key: Buffer.from(mockSelfSignedOutput.private),
},
]);
expect(mockSelfSigned).toBeCalledWith(
[{ name: 'commonName', value: BASE_ENV_VARS.SERVER_IP_ADDRESS }],
{
days: 365,
extensions: [
{
altNames: [{ ip: BASE_ENV_VARS.SERVER_IP_ADDRESS, type: 7 }],
name: 'subjectAltName',
},
],
},
);
});

Expand Down
32 changes: 30 additions & 2 deletions src/services/cogrpc/server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { CargoRelayService } from '@relaycorp/cogrpc';
import { get as getEnvVar } from 'env-var';
import { Server, ServerCredentials } from 'grpc';
import { KeyCertPair, Server, ServerCredentials } from 'grpc';
import grpcHealthCheck from 'grpc-health-check';
import * as selfsigned from 'selfsigned';

import { MAX_RAMF_MESSAGE_SIZE } from '../constants';
import { makeServiceImplementation } from './service';
Expand Down Expand Up @@ -47,9 +48,36 @@ export async function runServer(): Promise<void> {
});
server.addService(grpcHealthCheck.service, healthCheckService);

const bindResult = server.bind(NETLOC, ServerCredentials.createInsecure());
const bindResult = server.bind(
NETLOC,
ServerCredentials.createSsl(null, [await selfIssueCertificate()]),
);
if (bindResult < 0) {
throw new Error(`Failed to listen on ${NETLOC}`);
}
server.start();
}

/**
* Self issue certificate.
*
* As a workaround for: https://github.com/kubernetes/ingress-gce/issues/18#issuecomment-694815076
*/
async function selfIssueCertificate(): Promise<KeyCertPair> {
const ipAddress = getEnvVar('SERVER_IP_ADDRESS').required().asString();
const keys = selfsigned.generate([{ name: 'commonName', value: ipAddress }], {
days: 365,
extensions: [
{
altNames: [
{
ip: ipAddress,
type: 7, // IP Address
},
],
name: 'subjectAltName',
},
],
});
return { cert_chain: Buffer.from(keys.cert), private_key: Buffer.from(keys.private) };
}
6 changes: 6 additions & 0 deletions src/types/selfsigned.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
declare module 'selfsigned' {
export function generate(
attrs: readonly object[],
options: { readonly days: number; readonly extensions: readonly any[] },
): { readonly cert: string; readonly private: string };
}

0 comments on commit ef47a1c

Please sign in to comment.