Skip to content
Open
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
8 changes: 8 additions & 0 deletions cmd/tokenizer/.envrc
Original file line number Diff line number Diff line change
@@ -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
38 changes: 32 additions & 6 deletions cmd/tokenizer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package main

import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"flag"
"fmt"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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:])
}

Expand Down
37 changes: 33 additions & 4 deletions tokenizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -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, ""
Expand All @@ -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
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"
}
Expand Down