Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tunnel server TLS endpoint #427

Merged
merged 2 commits into from
Feb 11, 2024
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
31 changes: 7 additions & 24 deletions tunnel-server/docker-compose.tls.yml
Original file line number Diff line number Diff line change
@@ -1,40 +1,23 @@
version: '3.7'
## NEED TO ADD TLS CONFIGURATION for traefik and stunnel

secrets:
tls-key:
file: ./tls/key.pem
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'
33 changes: 32 additions & 1 deletion tunnel-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -120,14 +132,33 @@ 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)
});

['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)
Expand Down
30 changes: 16 additions & 14 deletions tunnel-server/src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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 ({
Expand Down
9 changes: 8 additions & 1 deletion tunnel-server/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
22 changes: 22 additions & 0 deletions tunnel-server/src/tls-server.ts
Original file line number Diff line number Diff line change
@@ -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<http.Server, 'emit'>
sshServer: Pick<ssh.Server, 'injectSocket'>
tlsConfig: tls.TlsOptions
sshHostnames: Set<string>
}) => 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)
}
})
16 changes: 0 additions & 16 deletions tunnel-server/tls/sslh.conf

This file was deleted.

Loading