keyless_tls is designed so that the tunnel application handles the TLS handshake and traffic encryption/decryption, while only the CertificateVerify signature is delegated to a remote signer.
- TLS engine, session keys, traffic crypto:
tunneling app - TLS signing (
CertificateVerify): remoterelay signer - Signer transport:
HTTPS + JSONwith mandatorymTLS
This repository supports two usage modes:
- Use as an SDK library (
keylesspackage) - Run the provided binaries under
cmd/*
- I want to attach directly to my app (
http.Server): SDK mode - I want to run it immediately and validate behavior: Binary mode
The tunnel app keeps only the public certificate chain (cert PEM) and does not hold the private key.
The keyless SDK attaches a remote signer as if it were a crypto.Signer, so handshake signing is performed remotely.
keyless.AttachToHTTPServer: simplest entry point (attach directly tohttp.Server)keyless.NewRemoteSigner: create a remote signer client explicitlykeyless.NewServerTLSConfig: buildtls.Configmanually
package main
import (
"log"
"net/http"
"os"
"github.com/gosuda/keyless_tls/keyless"
)
func main() {
certPEM := mustRead("certs/public-chain.crt")
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("ok\n"))
})
srv := &http.Server{
Addr: ":8443",
Handler: mux,
}
remoteSigner, err := keyless.AttachToHTTPServer(srv, keyless.HTTPServerAttachConfig{
CertPEM: certPEM,
RemoteSigner: keyless.RemoteSignerConfig{
Endpoint: "127.0.0.1:9443",
ServerName: "relay.internal",
KeyID: "relay-cert",
RootCAPEM: mustRead("certs/relay-ca.crt"),
ClientCertPEM: mustRead("certs/tunnel-client.crt"),
ClientKeyPEM: mustRead("certs/tunnel-client.key"),
},
})
if err != nil {
log.Fatal(err)
}
defer remoteSigner.Close()
log.Fatal(srv.ListenAndServeTLS("", ""))
}
func mustRead(path string) []byte {
b, err := os.ReadFile(path)
if err != nil {
panic(err)
}
return b
}Use this when you already have your own tls.Config construction flow, or when integrating with components other than http.Server.
rSigner, err := keyless.NewRemoteSigner(remoteSignerCfg, certPEM)
if err != nil {
// handle error
}
defer rSigner.Close()
tlsConf, err := keyless.NewServerTLSConfig(keyless.ServerTLSConfig{
CertPEM: certPEM,
Signer: rSigner,
NextProtos: []string{"h2", "http/1.1"},
// MinVersion: tls.VersionTLS13,
})
if err != nil {
// handle error
}If you are implementing your own relay/proxy with this library, use the relay/l4
APIs to inspect ClientHello and route by SNI/ALPN while keeping all policy in caller code.
l4.InspectClientHello(conn, timeout): parseServerName/ALPNProtocolsand return a wrappednet.Connl4.Proxy.DialByClientHello(ctx, info, parseErr): caller decides route/fallback/reject policy
How this works in practice:
- incoming TCP connection arrives
- library reads only ClientHello metadata (no TLS termination)
- your callback receives
info.ServerName,info.ALPNProtocols, andparseErr - your code selects upstream target (or rejects)
- relay continues raw TCP forwarding with no payload loss
Typical SDK routing policies:
- Multi-tenant host routing:
app1.example.com -> tenant A,app2.example.com -> tenant B - Protocol-aware routing:
h2preferred upstream vshttp/1.1upstream - Strict security mode: reject when ClientHello parse fails
- Compatibility mode: fallback to default upstream when parse fails
Concrete policy example (easy to adapt):
routes := map[string]string{
"app1.demo.local": "127.0.0.1:9001",
"app2.demo.local": "127.0.0.1:9002",
}
proxy := &l4.Proxy{
ListenAddr: ":443",
ClientHelloTimeout: 2 * time.Second,
DialByClientHello: func(ctx context.Context, info l4.ClientHelloInfo, parseErr error) (net.Conn, error) {
d := net.Dialer{Timeout: 3 * time.Second}
// 1) Decide what to do with non-TLS / invalid ClientHello
if parseErr != nil {
// strict mode: return nil, parseErr
// compatibility mode: send to default route
return d.DialContext(ctx, "tcp", "127.0.0.1:9011")
}
// 2) SNI host-based route
if target, ok := routes[strings.ToLower(strings.TrimSuffix(info.ServerName, "."))]; ok {
return d.DialContext(ctx, "tcp", target)
}
// 3) Optional ALPN-aware split
for _, proto := range info.ALPNProtocols {
if proto == "h2" {
return d.DialContext(ctx, "tcp", "127.0.0.1:9443")
}
}
// 4) Default route
return d.DialContext(ctx, "tcp", "127.0.0.1:9011")
},
}For a complete runnable SDK-style routing sample with 10 hosts, see examples/relay-10-targets.
- Deploy only the public certificate chain (
cert PEM) in the tunnel app - Configure signer endpoint/server name/
KeyID/root CA - Provide mTLS client materials (
client cert/key) - Call
remoteSigner.Close()on shutdown
cmd/ contains production-oriented main packages (runnable binaries).
Example applications are separated under examples/.
cmd/relay-signer: remote signer HTTPS servercmd/relay-l4: L4 TCP relay with optional SNI-based route mappingexamples/tunnel-http: example tunnel HTTP server integrated with the SDKexamples/relay-10-targets: one relay server routing to 10 target hosts via SNI
If you are building your own relay/proxy, use relay/l4.InspectClientHello to read
ClientHello metadata (ServerName, ALPNProtocols) without terminating TLS.
The helper returns a wrapped net.Conn that replays already-read bytes, so your
relay can continue normal TCP forwarding after routing decisions.
relay/l4.Proxy also supports callback-based dialing through
DialByClientHello(ctx, info, parseErr), so all policy decisions (fallback, reject,
default route) remain in caller code.
- Run signer server
go run ./cmd/relay-signer \
-listen :9443 \
-key-id relay-cert \
-tls-cert certs/relay-server.crt \
-tls-key certs/relay-server.key \
-sign-key certs/relay-signing.key- Run tunnel app
go run ./examples/tunnel-http \
-listen :8443 \
-cert certs/public-chain.crt \
-signer-addr 127.0.0.1:9443 \
-signer-name relay.internal \
-key-id relay-cert \
-client-cert certs/tunnel-client.crt \
-client-key certs/tunnel-client.key \
-root-ca certs/relay-ca.crt- Run L4 relay
go run ./cmd/relay-l4 \
-listen :443 \
-route app1.example.com=127.0.0.1:8443 \
-default-upstream 127.0.0.1:8443SNI route mode (-route can be repeated):
go run ./cmd/relay-l4 \
-listen :443 \
-route app1.example.com=127.0.0.1:8441 \
-route app2.example.com=127.0.0.1:8442 \
-default-upstream 127.0.0.1:8440cmd/relay-l4 does not enforce routing policy. Caller-side policy is controlled by flags,
including whether ClientHello parse failures may use the default upstream.
Useful cmd/relay-l4 route-mode flags:
-route host=upstream(repeatable): explicit SNI mapping-default-upstream: fallback target for unknown SNI-allow-parse-error: allow non-TLS/invalid ClientHello to use fallback-clienthello-timeout: maximum ClientHello inspection time
examples/relay-10-targets demonstrates a practical ingress layout:
- one public relay listener
- ten target tunnel apps
- SNI-based target selection implemented by caller code
Run the example relay:
go run ./examples/relay-10-targets \
-listen :443 \
-upstream-host 127.0.0.1 \
-base-port 9001 \
-domain demo.local \
-default-upstream 127.0.0.1:9011Generated static routes:
app1.demo.local -> 127.0.0.1:9001app2.demo.local -> 127.0.0.1:9002app3.demo.local -> 127.0.0.1:9003app4.demo.local -> 127.0.0.1:9004app5.demo.local -> 127.0.0.1:9005app6.demo.local -> 127.0.0.1:9006app7.demo.local -> 127.0.0.1:9007app8.demo.local -> 127.0.0.1:9008app9.demo.local -> 127.0.0.1:9009app10.demo.local -> 127.0.0.1:9010
Policy remains caller-owned:
- known SNI: route to mapped upstream
- unknown SNI: route to
-default-upstreamwhen configured - non-TLS or invalid ClientHello: route to
-default-upstreamwhen configured, otherwise reject
Important flags for examples/relay-10-targets:
-listen: public relay address-upstream-host: host used for generated targets-base-port: first target port (app1)-domain: host suffix used for SNI matching-default-upstream: optional fallback upstream-dial-timeout: upstream dial timeout-clienthello-timeout: ClientHello inspection timeout
Signer and tunnel clients must always be configured for mutual TLS.
go run ./cmd/relay-signer \
-listen :9443 \
-key-id relay-cert \
-tls-cert certs/relay-server.crt \
-tls-key certs/relay-server.key \
-client-ca certs/client-ca.crt \
-sign-key certs/relay-signing.key
go run ./examples/tunnel-http \
-listen :8443 \
-cert certs/public-chain.crt \
-signer-addr 127.0.0.1:9443 \
-signer-name relay.internal \
-key-id relay-cert \
-client-cert certs/tunnel-client.crt \
-client-key certs/tunnel-client.key \
-root-ca certs/relay-ca.crt- Store private keys only in
relay-signer; never distribute them to tunnel apps - Keep only the public certificate chain in tunnel apps
- Enforce signer mTLS and pair it with
KeyID-scoped ACLs
Request:
{
"key_id": "relay-cert",
"algorithm": "RSA_PSS_SHA256",
"digest": "<base64>",
"timestamp_unix": 1735628400,
"nonce": "c4d76ad40f5d8f95a1fe4b2f1c922f4a"
}Response:
{
"key_id": "relay-cert",
"algorithm": "RSA_PSS_SHA256",
"signature": "<base64>"
}keyless: SDK for application developers (tunnel app integration point)keyless/signerclient: remote signer client implementationrelay/signrpc: signer JSON request/response typesrelay/signer: signing service/key storerelay/server: signer HTTPS (mandatory mTLS) server launcherkeyless/lifecycle: per-lease mTLS identity management (issue, renew, validate, disk-backed encrypted store)relay/l4: TCP passthrough relay + optional ClientHello (SNI/ALPN) inspection hook
This implementation is at an early stage. Before production use, consider adding:
- replay cache
- rate limiting
- key rotation policy
- observability (OTel/metrics/log correlation)