diff --git a/tunnel-server/docker-compose.tls.yml b/tunnel-server/docker-compose.tls.yml index 90824b92..5f013db5 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: @@ -7,34 +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} - 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' + BASE_URL: ${BASE_URL:-https://local.livecycle.run:8443} secrets: - source: tls-key - target: /etc/certs/preview-proxy/key.pem + target: /app/tls/key.pem configs: - source: tls-cert - target: /etc/certs/preview-proxy/cert.pem + target: /app/tls/cert.pem + ports: + - '8030:3000' + - '8443:8443' + - '2223:2222' diff --git a/tunnel-server/index.ts b/tunnel-server/index.ts index 7893df07..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()) @@ -47,6 +48,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) { @@ -120,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) }); @@ -127,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 c46a5e1b..3fed14d5 100644 --- a/tunnel-server/src/app/index.ts +++ b/tunnel-server/src/app/index.ts @@ -4,6 +4,7 @@ import http from 'http' 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' @@ -30,30 +31,31 @@ 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 http.createServer(serverHandler).on('upgrade', serverUpgradeHandler) } export const createApp = async ({ 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 201886f1..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: "http"; host: "proxy"; port: "3000"; }, -); \ No newline at end of file