diff --git a/cmd/tokenizer/.envrc b/cmd/tokenizer/.envrc index 3415a9d..dbb546c 100644 --- a/cmd/tokenizer/.envrc +++ b/cmd/tokenizer/.envrc @@ -1,2 +1,10 @@ export LISTEN_ADDRESS="127.0.0.1:8823" export OPEN_KEY=0d88a36d5c41d5c3d97b929fdebf0bea57e5fb4616da88121ba972431a161cab + +# Allow requests without any sealed secrets +# export OPEN_PROXY=1 + +# HTTPS MITM - enables interception of HTTPS requests to inject credentials +# Clients must trust this CA certificate +# export MITM_CA_CERT_PATH=cert.pem +# export MITM_CA_KEY_PATH=key.pem diff --git a/cmd/tokenizer/main.go b/cmd/tokenizer/main.go index 352561f..9a4b42a 100644 --- a/cmd/tokenizer/main.go +++ b/cmd/tokenizer/main.go @@ -2,6 +2,8 @@ package main import ( "context" + "crypto/tls" + "crypto/x509" "errors" "flag" "fmt" @@ -89,6 +91,25 @@ func runServe() { opts = append(opts, tokenizer.OpenProxy()) } + // MITM CA certificate for HTTPS interception + mitmCertPath := os.Getenv("MITM_CA_CERT_PATH") + mitmKeyPath := os.Getenv("MITM_CA_KEY_PATH") + if mitmCertPath != "" && mitmKeyPath != "" { + cert, err := tls.LoadX509KeyPair(mitmCertPath, mitmKeyPath) + if err != nil { + logrus.WithError(err).Fatal("failed to load MITM CA certificate") + } + // Parse the x509 certificate - goproxy needs this for TLSConfigFromCA + if cert.Leaf == nil && len(cert.Certificate) > 0 { + cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) + if err != nil { + logrus.WithError(err).Fatal("failed to parse MITM CA certificate") + } + } + opts = append(opts, tokenizer.MitmCACert(cert)) + logrus.Info("HTTPS MITM enabled - clients must trust the CA certificate") + } + tkz := tokenizer.NewTokenizer(key, opts...) if len(os.Getenv("DEBUG")) != 0 { @@ -188,12 +209,17 @@ Flags: Configuration — tokenizer is configured using the following environment variables: - OPEN_KEY - Hex encoded curve25519 private key. You can provide 32 - random, hex encoded bytes. The log output will contain - the associated public key. - LISTEN_ADDRESS - The host:port address to listen at. Default: ":8080" - FILTERED_HEADERS - Comma separated list of headers to filter from client - requests. + OPEN_KEY - Hex encoded curve25519 private key. You can provide 32 + random, hex encoded bytes. The log output will contain + the associated public key. + LISTEN_ADDRESS - The host:port address to listen at. Default: ":8080" + FILTERED_HEADERS - Comma separated list of headers to filter from client + requests. + MITM_CA_CERT_PATH - Path to CA certificate for HTTPS MITM proxying. + When set along with MITM_CA_KEY_PATH, enables interception + of HTTPS CONNECT requests to inject credentials. + Clients must trust this CA certificate. + MITM_CA_KEY_PATH - Path to CA private key for HTTPS MITM proxying. `[1:]) } diff --git a/tokenizer.go b/tokenizer.go index 1cfa27f..c8ffc8b 100644 --- a/tokenizer.go +++ b/tokenizer.go @@ -46,6 +46,10 @@ type tokenizer struct { // OpenProxy dictates whether requests without any sealed secrets are allowed. OpenProxy bool + // MitmEnabled enables HTTPS MITM proxying with CA certificate. + // When enabled, the proxy can intercept HTTPS connections to inject credentials. + MitmEnabled bool + // tokenizerHostnames is a list of hostnames where tokenizer can be reached. // If provided, this allows tokenizer to transparently proxy requests (ie. // accept normal HTTP requests with arbitrary hostnames) while blocking @@ -66,6 +70,20 @@ func OpenProxy() Option { } } +// MitmCACert configures the CA certificate for HTTPS MITM proxying. +// When configured, the proxy can intercept HTTPS CONNECT requests and +// inject credentials into them. Clients must trust this CA certificate. +func MitmCACert(cert tls.Certificate) Option { + return func(t *tokenizer) { + t.MitmEnabled = true + goproxy.GoproxyCa = cert + goproxy.OkConnect = &goproxy.ConnectAction{Action: goproxy.ConnectMitm, TLSConfig: goproxy.TLSConfigFromCA(&cert)} + goproxy.MitmConnect = &goproxy.ConnectAction{Action: goproxy.ConnectMitm, TLSConfig: goproxy.TLSConfigFromCA(&cert)} + goproxy.HTTPMitmConnect = &goproxy.ConnectAction{Action: goproxy.ConnectHTTPMitm, TLSConfig: goproxy.TLSConfigFromCA(&cert)} + goproxy.RejectConnect = &goproxy.ConnectAction{Action: goproxy.ConnectReject, TLSConfig: goproxy.TLSConfigFromCA(&cert)} + } +} + // TokenizerHostnames is a list of hostnames where tokenizer can be reached. If // provided, this allows tokenizer to transparently proxy requests (ie. accept // normal HTTP requests with arbitrary hostnames) while blocking circular @@ -134,7 +152,7 @@ func NewTokenizer(openKey string, opts ...Option) *tokenizer { }) proxy.Tr = &http.Transport{ - Dial: dialFunc(tkz.tokenizerHostnames), + Dial: tkz.dialFunc(), // probably not necessary, but I don't want to worry about desync/smuggling DisableKeepAlives: true, } @@ -181,7 +199,7 @@ func (t *tokenizer) HandleConnect(host string, ctx *goproxy.ProxyCtx) (*goproxy. } _, port, _ := strings.Cut(host, ":") - if port == "443" { + if port == "443" && !t.MitmEnabled { pud.connLog.Warn("attempt to proxy to https downstream") ctx.Resp = errorResponse(ErrBadRequest) return goproxy.RejectConnect, "" @@ -196,6 +214,11 @@ func (t *tokenizer) HandleConnect(host string, ctx *goproxy.ProxyCtx) (*goproxy. ctx.UserData = pud + // For HTTPS (port 443) with MITM enabled, use MitmConnect to do TLS interception + // For HTTP tunnels, use HTTPMitmConnect to read plaintext HTTP + if port == "443" && t.MitmEnabled { + return goproxy.MitmConnect, host + } return goproxy.HTTPMitmConnect, host } @@ -398,7 +421,8 @@ func errorResponse(err error) *http.Response { // our proxy can't do passthrough TLS. // - It forces the upstream connection to be TLS. We want the actual upstream // connection to be over TLS because security. -func dialFunc(badAddrs []string) func(string, string) (net.Conn, error) { +func (t *tokenizer) dialFunc() func(string, string) (net.Conn, error) { + badAddrs := t.tokenizerHostnames _, fdaaNet, err := net.ParseCIDR("fdaa::/8") if err != nil { panic(err) @@ -450,7 +474,12 @@ func dialFunc(badAddrs []string) func(string, string) (net.Conn, error) { } switch port { case "443": - return nil, fmt.Errorf("%w: proxied request must be HTTP", ErrBadRequest) + if !t.MitmEnabled { + return nil, fmt.Errorf("%w: proxied request must be HTTP", ErrBadRequest) + } + addr = fmt.Sprintf("%s:%s", hostname, port) + + return netDialer.Dial(network, addr) case "80", "": port = "443" }