From 2a89874c18ce9671b69776cba89f99d514cb8ad2 Mon Sep 17 00:00:00 2001 From: Roy Razon Date: Thu, 8 Feb 2024 11:46:14 +0200 Subject: [PATCH 1/2] wip - use either http or https on specified port according to configuration --- tunnel-server/docker-compose.tls.yml | 27 +++++++------------- tunnel-server/index.ts | 12 +++++++++ tunnel-server/src/app/index.ts | 38 +++++++++++++++++----------- tunnel-server/tls/sslh.conf | 2 +- 4 files changed, 45 insertions(+), 34 deletions(-) diff --git a/tunnel-server/docker-compose.tls.yml b/tunnel-server/docker-compose.tls.yml index 90824b92..8ca09e12 100644 --- a/tunnel-server/docker-compose.tls.yml +++ b/tunnel-server/docker-compose.tls.yml @@ -1,5 +1,4 @@ version: '3.7' -## NEED TO ADD TLS CONFIGURATION for traefik and stunnel secrets: tls-key: @@ -14,27 +13,19 @@ services: proxy: environment: BASE_URL: ${BASE_URL:-https://local.livecycle.run:8044} + secrets: + - source: tls-key + target: /app/tls/key.pem + configs: + - source: tls-cert + target: /app/tls/cert.pem + healthcheck: + test: wget --no-verbose --tries=1 --spider https://localhost:3000/healthz || exit 1 sslh: image: oorabona/sslh:v2.0-rc1 command: [sslh-ev, --config=/etc/sslh/config] configs: - source: sslh-config target: /etc/sslh/config - - stunnel: - image: dweomer/stunnel - environment: - - STUNNEL_SERVICE=proxy - - STUNNEL_ACCEPT=0.0.0.0:443 - - STUNNEL_CONNECT=sslh:2443 - - STUNNEL_KEY=/etc/certs/preview-proxy/key.pem - - STUNNEL_CRT=/etc/certs/preview-proxy/cert.pem - - STUNNEL_DEBUG=err ports: - - '8044:443' - secrets: - - source: tls-key - target: /etc/certs/preview-proxy/key.pem - configs: - - source: tls-cert - target: /etc/certs/preview-proxy/cert.pem + - '8044:2443' diff --git a/tunnel-server/index.ts b/tunnel-server/index.ts index 7893df07..8d94f2b7 100644 --- a/tunnel-server/index.ts +++ b/tunnel-server/index.ts @@ -47,6 +47,17 @@ const readFileSyncOrUndefined = (filename: string) => { } } +const tlsConfig = (() => { + const cert = readFileSyncOrUndefined('./tls/cert.pem') + const key = readFileSyncOrUndefined('./tls/key.pem') + if (!cert || !key) { + log.info('No TLS cert or key found, TLS will be disabled') + return undefined + } + log.info('TLS will be enabled') + return { cert, key } +})() + const saasIdp = (() => { const saasPublicKeyStr = process.env.SAAS_PUBLIC_KEY || readFileSyncOrUndefined('/etc/certs/preview-proxy/saas.key.pub') if (!saasPublicKeyStr) { @@ -75,6 +86,7 @@ const authFactory = ( const activeTunnelStore = inMemoryActiveTunnelStore({ log }) const sessionStore = cookieSessionStore({ domain: BASE_URL.hostname, schema: claimsSchema, keys: process.env.COOKIE_SECRETS?.split(' ') }) const app = await createApp({ + tlsConfig, sessionStore, activeTunnelStore, baseUrl: BASE_URL, diff --git a/tunnel-server/src/app/index.ts b/tunnel-server/src/app/index.ts index c46a5e1b..0df66320 100644 --- a/tunnel-server/src/app/index.ts +++ b/tunnel-server/src/app/index.ts @@ -1,9 +1,11 @@ import fastify, { FastifyServerFactory, RawServerDefault } from 'fastify' import { fastifyRequestContext } from '@fastify/request-context' import http from 'http' +import https from 'https' import { Logger } from 'pino' import { KeyObject } from 'crypto' import { validatorCompiler, serializerCompiler, ZodTypeProvider } from 'fastify-type-provider-zod' +import { Duplex } from 'stream' import { SessionStore } from '../session.js' import { Authenticator, Claims } from '../auth.js' import { ActiveTunnelStore } from '../tunnel-store/index.js' @@ -15,10 +17,12 @@ const HEALTZ_URL = '/healthz' const serverFactory = ({ log, + tlsConfig, baseUrl, proxy, }: { log: Logger + tlsConfig?: https.ServerOptions baseUrl: URL proxy: Proxy }): FastifyServerFactory => handler => { @@ -30,33 +34,36 @@ const serverFactory = ({ log.debug('authHostname %j', authHostname) const isNonProxyRequest = ({ headers }: http.IncomingMessage) => { - log.debug('isNonProxyRequest %j', headers) const host = headers.host?.split(':')?.[0] return (host === authHostname) || (host === apiHostname) } - const server = http.createServer((req, res) => { + const serverHandler = (req: http.IncomingMessage, res: http.ServerResponse) => { if (req.url !== HEALTZ_URL) { log.debug('request %j', { method: req.method, url: req.url, headers: req.headers }) } const proxyHandler = !isNonProxyRequest(req) && proxy.routeRequest(req) return proxyHandler ? proxyHandler(req, res) : handler(req, res) - }) - .on('upgrade', (req, socket, head) => { - log.debug('upgrade %j', { method: req.method, url: req.url, headers: req.headers }) - const proxyHandler = !isNonProxyRequest(req) && proxy.routeUpgrade(req) - if (proxyHandler) { - return proxyHandler(req, socket, head) - } + } + + const serverUpgradeHandler = (req: http.IncomingMessage, socket: Duplex, head: Buffer) => { + log.debug('upgrade %j', { method: req.method, url: req.url, headers: req.headers }) + const proxyHandler = !isNonProxyRequest(req) && proxy.routeUpgrade(req) + if (proxyHandler) { + return proxyHandler(req, socket, head) + } + + log.warn('upgrade request %j not found', { method: req.method, url: req.url, host: req.headers.host }) + socket.end('Not found') + return undefined + } - log.warn('upgrade request %j not found', { method: req.method, url: req.url, host: req.headers.host }) - socket.end('Not found') - return undefined - }) - return server + return (tlsConfig ? https.createServer(tlsConfig, serverHandler) : http.createServer(serverHandler)) + .on('upgrade', serverUpgradeHandler) } export const createApp = async ({ + tlsConfig, proxy, sessionStore, baseUrl, @@ -66,6 +73,7 @@ export const createApp = async ({ authFactory, }: { log: Logger + tlsConfig?: https.ServerOptions baseUrl: URL saasBaseUrl?: URL sessionStore: SessionStore @@ -73,7 +81,7 @@ export const createApp = async ({ authFactory: (client: { publicKey: KeyObject; publicKeyThumbprint: string }) => Authenticator proxy: Proxy }) => { - const app = await fastify({ logger: log, serverFactory: serverFactory({ log, baseUrl, proxy }) }) + const app = await fastify({ logger: log, serverFactory: serverFactory({ log, baseUrl, tlsConfig, proxy }) }) app.setValidatorCompiler(validatorCompiler) app.setSerializerCompiler(serializerCompiler) app.withTypeProvider() diff --git a/tunnel-server/tls/sslh.conf b/tunnel-server/tls/sslh.conf index 201886f1..0b8f4f12 100644 --- a/tunnel-server/tls/sslh.conf +++ b/tunnel-server/tls/sslh.conf @@ -12,5 +12,5 @@ listen: protocols: ( { name: "ssh"; service: "ssh"; host: "proxy"; port: "2222"; }, - { name: "http"; host: "proxy"; port: "3000"; }, + { name: "tls"; host: "proxy"; port: "3000"; }, ); \ No newline at end of file From b77139ce087f9780548b13bb209fff75801b7514 Mon Sep 17 00:00:00 2001 From: Roy Razon Date: Thu, 8 Feb 2024 16:28:29 +0200 Subject: [PATCH 2/2] add tls port --- tunnel-server/docker-compose.tls.yml | 16 ++++------------ tunnel-server/index.ts | 23 +++++++++++++++++++++-- tunnel-server/src/app/index.ts | 10 ++-------- tunnel-server/src/env.ts | 9 ++++++++- tunnel-server/src/tls-server.ts | 22 ++++++++++++++++++++++ tunnel-server/tls/sslh.conf | 16 ---------------- 6 files changed, 57 insertions(+), 39 deletions(-) create mode 100644 tunnel-server/src/tls-server.ts delete mode 100644 tunnel-server/tls/sslh.conf diff --git a/tunnel-server/docker-compose.tls.yml b/tunnel-server/docker-compose.tls.yml index 8ca09e12..5f013db5 100644 --- a/tunnel-server/docker-compose.tls.yml +++ b/tunnel-server/docker-compose.tls.yml @@ -6,26 +6,18 @@ secrets: configs: tls-cert: file: ./tls/cert.pem - sslh-config: - file: ./tls/sslh.conf services: proxy: environment: - BASE_URL: ${BASE_URL:-https://local.livecycle.run:8044} + BASE_URL: ${BASE_URL:-https://local.livecycle.run:8443} secrets: - source: tls-key target: /app/tls/key.pem configs: - source: tls-cert target: /app/tls/cert.pem - healthcheck: - test: wget --no-verbose --tries=1 --spider https://localhost:3000/healthz || exit 1 - sslh: - image: oorabona/sslh:v2.0-rc1 - command: [sslh-ev, --config=/etc/sslh/config] - configs: - - source: sslh-config - target: /etc/sslh/config ports: - - '8044:2443' + - '8030:3000' + - '8443:8443' + - '2223:2222' diff --git a/tunnel-server/index.ts b/tunnel-server/index.ts index 8d94f2b7..51277d33 100644 --- a/tunnel-server/index.ts +++ b/tunnel-server/index.ts @@ -14,6 +14,7 @@ import { cookieSessionStore } from './src/session.js' import { IdentityProvider, claimsSchema, cliIdentityProvider, jwtAuthenticator, saasIdentityProvider } from './src/auth.js' import { createSshServer } from './src/ssh/index.js' import { calcLoginUrl } from './src/app/urls.js' +import { createTlsServer } from './src/tls-server.js' const log = pino.default(appLoggerFromEnv()) @@ -86,7 +87,6 @@ const authFactory = ( const activeTunnelStore = inMemoryActiveTunnelStore({ log }) const sessionStore = cookieSessionStore({ domain: BASE_URL.hostname, schema: claimsSchema, keys: process.env.COOKIE_SECRETS?.split(' ') }) const app = await createApp({ - tlsConfig, sessionStore, activeTunnelStore, baseUrl: BASE_URL, @@ -132,6 +132,21 @@ app.listen({ host: LISTEN_HOST, port: PORT }).catch(err => { process.exit(1) }) +const TLS_PORT = numberFromEnv('TLS_PORT') ?? 8443 +const tlsLog = log.child({ name: 'tls_server' }) +const tlsServer = tlsConfig + ? createTlsServer({ + log: tlsLog, + tlsConfig, + sshServer, + httpServer: + app.server, + sshHostnames: new Set([BASE_URL.hostname]), + }) + : undefined + +tlsServer?.listen({ host: LISTEN_HOST, port: TLS_PORT }, () => { tlsLog.info('TLS server listening on port %j', TLS_PORT) }) + runMetricsServer(8888).catch(err => { app.log.error(err) }); @@ -139,7 +154,11 @@ runMetricsServer(8888).catch(err => { ['SIGTERM', 'SIGINT'].forEach(signal => { process.once(signal, () => { app.log.info(`shutting down on ${signal}`) - Promise.all([promisify(sshServer.close).call(sshServer), app.close()]) + Promise.all([ + promisify(sshServer.close).call(sshServer), + app.close(), + tlsServer ? promisify(tlsServer.close).call(tlsServer) : undefined, + ]) .catch(err => { app.log.error(err) process.exit(1) diff --git a/tunnel-server/src/app/index.ts b/tunnel-server/src/app/index.ts index 0df66320..3fed14d5 100644 --- a/tunnel-server/src/app/index.ts +++ b/tunnel-server/src/app/index.ts @@ -1,7 +1,6 @@ import fastify, { FastifyServerFactory, RawServerDefault } from 'fastify' import { fastifyRequestContext } from '@fastify/request-context' import http from 'http' -import https from 'https' import { Logger } from 'pino' import { KeyObject } from 'crypto' import { validatorCompiler, serializerCompiler, ZodTypeProvider } from 'fastify-type-provider-zod' @@ -17,12 +16,10 @@ const HEALTZ_URL = '/healthz' const serverFactory = ({ log, - tlsConfig, baseUrl, proxy, }: { log: Logger - tlsConfig?: https.ServerOptions baseUrl: URL proxy: Proxy }): FastifyServerFactory => handler => { @@ -58,12 +55,10 @@ const serverFactory = ({ return undefined } - return (tlsConfig ? https.createServer(tlsConfig, serverHandler) : http.createServer(serverHandler)) - .on('upgrade', serverUpgradeHandler) + return http.createServer(serverHandler).on('upgrade', serverUpgradeHandler) } export const createApp = async ({ - tlsConfig, proxy, sessionStore, baseUrl, @@ -73,7 +68,6 @@ export const createApp = async ({ authFactory, }: { log: Logger - tlsConfig?: https.ServerOptions baseUrl: URL saasBaseUrl?: URL sessionStore: SessionStore @@ -81,7 +75,7 @@ export const createApp = async ({ authFactory: (client: { publicKey: KeyObject; publicKeyThumbprint: string }) => Authenticator proxy: Proxy }) => { - const app = await fastify({ logger: log, serverFactory: serverFactory({ log, baseUrl, tlsConfig, proxy }) }) + const app = await fastify({ logger: log, serverFactory: serverFactory({ log, baseUrl, proxy }) }) app.setValidatorCompiler(validatorCompiler) app.setSerializerCompiler(serializerCompiler) app.withTypeProvider() diff --git a/tunnel-server/src/env.ts b/tunnel-server/src/env.ts index ee13f2c3..0c6d4908 100644 --- a/tunnel-server/src/env.ts +++ b/tunnel-server/src/env.ts @@ -8,5 +8,12 @@ export const requiredEnv = (key: string): string => { export const numberFromEnv = (key: string) => { const s = process.env[key] - return s === undefined ? undefined : Number(s) + if (!s) { + return undefined + } + const result = Number(s) + if (Number.isNaN(result)) { + throw new Error(`env var ${key} is not a number: "${s}"`) + } + return result } diff --git a/tunnel-server/src/tls-server.ts b/tunnel-server/src/tls-server.ts new file mode 100644 index 00000000..ba4cce33 --- /dev/null +++ b/tunnel-server/src/tls-server.ts @@ -0,0 +1,22 @@ +import { Logger } from 'pino' +import http from 'http' +import ssh from 'ssh2' +import tls from 'tls' + +export const createTlsServer = ({ log, httpServer, sshServer, tlsConfig, sshHostnames }: { + log: Logger + httpServer: Pick + sshServer: Pick + tlsConfig: tls.TlsOptions + sshHostnames: Set +}) => tls.createServer(tlsConfig) + .on('error', err => { log.error(err) }) + .on('secureConnection', socket => { + const { servername } = (socket as { servername?: string }) + log.debug('TLS connection: %j', servername) + if (servername && sshHostnames.has(servername)) { + sshServer.injectSocket(socket) + } else { + httpServer.emit('connection', socket) + } + }) diff --git a/tunnel-server/tls/sslh.conf b/tunnel-server/tls/sslh.conf deleted file mode 100644 index 0b8f4f12..00000000 --- a/tunnel-server/tls/sslh.conf +++ /dev/null @@ -1,16 +0,0 @@ -foreground: true; -verbose-config: 1; # print configuration at startup -verbose-config-error: 1; # print configuration errors -verbose-connections-error: 1; # connection errors -verbose-probe-error: 1; # failures and problems during probing -verbose-system-error: 1; # system call problem, i.e. malloc, fork, failing -verbose-int-error: 1; # internal errors, the kind that should never happen -listen: -( - { host: "0.0.0.0"; port: "2443"; } -); -protocols: -( - { name: "ssh"; service: "ssh"; host: "proxy"; port: "2222"; }, - { name: "tls"; host: "proxy"; port: "3000"; }, -); \ No newline at end of file