diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go
index ea03dc867..38a904e21 100644
--- a/modules/caddyhttp/reverseproxy/httptransport.go
+++ b/modules/caddyhttp/reverseproxy/httptransport.go
@@ -35,15 +35,15 @@ func init() {
 
 // HTTPTransport is essentially a configuration wrapper for http.Transport.
 // It defines a JSON structure useful when configuring the HTTP transport
-// for Caddy's reverse proxy.
+// for Caddy's reverse proxy. It builds its http.Transport at Provision.
 type HTTPTransport struct {
 	// TODO: It's possible that other transports (like fastcgi) might be
 	// able to borrow/use at least some of these config fields; if so,
-	// move them into a type called CommonTransport and embed it
+	// maybe move them into a type called CommonTransport and embed it?
 	TLS                   *TLSConfig     `json:"tls,omitempty"`
 	KeepAlive             *KeepAlive     `json:"keep_alive,omitempty"`
 	Compression           *bool          `json:"compression,omitempty"`
-	MaxConnsPerHost       int            `json:"max_conns_per_host,omitempty"` // TODO: NOTE: we use our health check stuff to enforce max REQUESTS per host, but this is connections
+	MaxConnsPerHost       int            `json:"max_conns_per_host,omitempty"`
 	DialTimeout           caddy.Duration `json:"dial_timeout,omitempty"`
 	FallbackDelay         caddy.Duration `json:"dial_fallback_delay,omitempty"`
 	ResponseHeaderTimeout caddy.Duration `json:"response_header_timeout,omitempty"`
@@ -53,7 +53,7 @@ type HTTPTransport struct {
 	ReadBufferSize        int            `json:"read_buffer_size,omitempty"`
 	Versions              []string       `json:"versions,omitempty"`
 
-	RoundTripper http.RoundTripper `json:"-"`
+	Transport *http.Transport `json:"-"`
 }
 
 // CaddyModule returns the Caddy module information.
@@ -64,12 +64,23 @@ func (HTTPTransport) CaddyModule() caddy.ModuleInfo {
 	}
 }
 
-// Provision sets up h.RoundTripper with a http.Transport
+// Provision sets up h.Transport with a *http.Transport
 // that is ready to use.
 func (h *HTTPTransport) Provision(_ caddy.Context) error {
 	if len(h.Versions) == 0 {
 		h.Versions = []string{"1.1", "2"}
 	}
+
+	rt, err := h.newTransport()
+	if err != nil {
+		return err
+	}
+	h.Transport = rt
+
+	return nil
+}
+
+func (h *HTTPTransport) newTransport() (*http.Transport, error) {
 	dialer := &net.Dialer{
 		Timeout:       time.Duration(h.DialTimeout),
 		FallbackDelay: time.Duration(h.FallbackDelay),
@@ -107,14 +118,14 @@ func (h *HTTPTransport) Provision(_ caddy.Context) error {
 		var err error
 		rt.TLSClientConfig, err = h.TLS.MakeTLSClientConfig()
 		if err != nil {
-			return fmt.Errorf("making TLS client config: %v", err)
+			return nil, fmt.Errorf("making TLS client config: %v", err)
 		}
 	}
 
 	if h.KeepAlive != nil {
 		dialer.KeepAlive = time.Duration(h.KeepAlive.ProbeInterval)
-		if enabled := h.KeepAlive.Enabled; enabled != nil {
-			rt.DisableKeepAlives = !*enabled
+		if h.KeepAlive.Enabled != nil {
+			rt.DisableKeepAlives = !*h.KeepAlive.Enabled
 		}
 		rt.MaxIdleConns = h.KeepAlive.MaxIdleConns
 		rt.MaxIdleConnsPerHost = h.KeepAlive.MaxIdleConnsPerHost
@@ -131,21 +142,30 @@ func (h *HTTPTransport) Provision(_ caddy.Context) error {
 		}
 	}
 
-	h.RoundTripper = rt
-
-	return nil
+	return rt, nil
 }
 
-// RoundTrip implements http.RoundTripper with h.RoundTripper.
-func (h HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
-	return h.RoundTripper.RoundTrip(req)
+// RoundTrip implements http.RoundTripper.
+func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+	h.setScheme(req)
+	return h.Transport.RoundTrip(req)
+}
+
+// setScheme ensures that the outbound request req
+// has the scheme set in its URL; the underlying
+// http.Transport requires a scheme to be set.
+func (h *HTTPTransport) setScheme(req *http.Request) {
+	if req.URL.Scheme == "" {
+		req.URL.Scheme = "http"
+		if h.TLS != nil {
+			req.URL.Scheme = "https"
+		}
+	}
 }
 
 // Cleanup implements caddy.CleanerUpper and closes any idle connections.
 func (h HTTPTransport) Cleanup() error {
-	if ht, ok := h.RoundTripper.(*http.Transport); ok {
-		ht.CloseIdleConnections()
-	}
+	h.Transport.CloseIdleConnections()
 	return nil
 }
 
diff --git a/modules/caddyhttp/reverseproxy/ntlm.go b/modules/caddyhttp/reverseproxy/ntlm.go
new file mode 100644
index 000000000..06ee4f8fd
--- /dev/null
+++ b/modules/caddyhttp/reverseproxy/ntlm.go
@@ -0,0 +1,234 @@
+// Copyright 2015 Matthew Holt and The Caddy Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package reverseproxy
+
+import (
+	"context"
+	"fmt"
+	"net"
+	"net/http"
+	"sync"
+
+	"github.com/caddyserver/caddy/v2"
+	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
+)
+
+func init() {
+	caddy.RegisterModule(NTLMTransport{})
+}
+
+// NTLMTransport proxies HTTP+NTLM authentication is being used.
+// It basically wraps HTTPTransport so that it is compatible with
+// NTLM's HTTP-hostile requirements. Specifically, it will use
+// HTTPTransport's single, default *http.Transport for all requests
+// (unless the client's connection is already mapped to a different
+// transport) until a request comes in with Authorization header
+// that has "NTLM" or "Negotiate"; when that happens, NTLMTransport
+// maps the client's connection (by its address, req.RemoteAddr)
+// to a new transport that is used only by that downstream conn.
+// When the upstream connection is closed, the mapping is deleted.
+// This preserves NTLM authentication contexts by ensuring that
+// client connections use the same upstream connection. It does
+// hurt performance a bit, but that's NTLM for you.
+//
+// This transport also forces HTTP/1.1 and Keep-Alives in order
+// for NTLM to succeed.
+type NTLMTransport struct {
+	*HTTPTransport
+
+	transports   map[string]*http.Transport
+	transportsMu *sync.RWMutex
+}
+
+// CaddyModule returns the Caddy module information.
+func (NTLMTransport) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		Name: "http.handlers.reverse_proxy.transport.http_ntlm",
+		New:  func() caddy.Module { return new(NTLMTransport) },
+	}
+}
+
+// Provision sets up the transport module.
+func (n *NTLMTransport) Provision(ctx caddy.Context) error {
+	n.transports = make(map[string]*http.Transport)
+	n.transportsMu = new(sync.RWMutex)
+
+	if n.HTTPTransport == nil {
+		n.HTTPTransport = new(HTTPTransport)
+	}
+
+	// NTLM requires HTTP/1.1
+	n.HTTPTransport.Versions = []string{"1.1"}
+
+	// NLTM requires keep-alive
+	if n.HTTPTransport.KeepAlive != nil {
+		enabled := true
+		n.HTTPTransport.KeepAlive.Enabled = &enabled
+	}
+
+	// set up the underlying transport, since we
+	// rely on it for the heavy lifting
+	err := n.HTTPTransport.Provision(ctx)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// RoundTrip implements http.RoundTripper. It basically wraps
+// the underlying HTTPTransport.Transport in a way that preserves
+// NTLM context by mapping transports/connections. Note that this
+// method does not call n.HTTPTransport.RoundTrip (our own method),
+// but the underlying n.HTTPTransport.Transport.RoundTrip (standard
+// library's method).
+func (n *NTLMTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+	n.HTTPTransport.setScheme(req)
+
+	// when the upstream connection is closed, make sure
+	// we close the downstream connection with the client
+	// when this request is done; we only do this if
+	// using a bound transport
+	closeDownstreamIfClosedUpstream := func() {
+		n.transportsMu.Lock()
+		if _, ok := n.transports[req.RemoteAddr]; !ok {
+			req.Close = true
+		}
+		n.transportsMu.Unlock()
+	}
+
+	// first, see if this downstream connection is
+	// already bound to a particular transport
+	// (transports are abstractions over connections
+	// to our upstream, and NTLM auth requires
+	// preserving authentication state for separate
+	// connections over multiple roundtrips, sigh)
+	n.transportsMu.Lock()
+	transport, ok := n.transports[req.RemoteAddr]
+	if ok {
+		n.transportsMu.Unlock()
+		defer closeDownstreamIfClosedUpstream()
+		return transport.RoundTrip(req)
+	}
+
+	// otherwise, start by assuming we will use
+	// the default transport that carries all
+	// normal/non-NTLM-authenticated requests
+	transport = n.HTTPTransport.Transport
+
+	// but if this request begins the NTLM authentication
+	// process, we need to pin it to a specific transport
+	if requestHasAuth(req) {
+		var err error
+		transport, err = n.newTransport()
+		if err != nil {
+			return nil, fmt.Errorf("making new transport for %s: %v", req.RemoteAddr, err)
+		}
+		n.transports[req.RemoteAddr] = transport
+		defer closeDownstreamIfClosedUpstream()
+	}
+	n.transportsMu.Unlock()
+
+	// finally, do the roundtrip with the transport we selected
+	return transport.RoundTrip(req)
+}
+
+// newTransport makes an NTLM-compatible transport.
+func (n *NTLMTransport) newTransport() (*http.Transport, error) {
+	// start with a regular HTTP transport
+	transport, err := n.HTTPTransport.newTransport()
+	if err != nil {
+		return nil, err
+	}
+
+	// we need to wrap upstream connections so we can
+	// clean up in two ways when that connection is
+	// closed: 1) destroy the transport that housed
+	// this connection, and 2) use that as a signal
+	// to close the connection to the downstream.
+	wrappedDialContext := transport.DialContext
+
+	transport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
+		conn2, err := wrappedDialContext(ctx, network, address)
+		if err != nil {
+			return nil, err
+		}
+		req := ctx.Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
+		conn := &unbinderConn{Conn: conn2, ntlm: n, clientAddr: req.RemoteAddr}
+		return conn, nil
+	}
+
+	return transport, nil
+}
+
+// Cleanup implements caddy.CleanerUpper and closes any idle connections.
+func (n *NTLMTransport) Cleanup() error {
+	if err := n.HTTPTransport.Cleanup(); err != nil {
+		return err
+	}
+
+	n.transportsMu.Lock()
+	for _, t := range n.transports {
+		t.CloseIdleConnections()
+	}
+	n.transports = make(map[string]*http.Transport)
+	n.transportsMu.Unlock()
+
+	return nil
+}
+
+// deleteTransportsForClient deletes (unmaps) transports that are
+// associated with clientAddr (a req.RemoteAddr value).
+func (n *NTLMTransport) deleteTransportsForClient(clientAddr string) {
+	n.transportsMu.Lock()
+	for key := range n.transports {
+		if key == clientAddr {
+			delete(n.transports, key)
+		}
+	}
+	n.transportsMu.Unlock()
+}
+
+// requestHasAuth returns true if req has an Authorization
+// header with values "NTLM" or "Negotiate".
+func requestHasAuth(req *http.Request) bool {
+	for _, val := range req.Header["Authorization"] {
+		if val == "NTLM" || val == "Negotiate" {
+			return true
+		}
+	}
+	return false
+}
+
+// unbinderConn is used to wrap upstream connections
+// so that we know when they are closed and can clean
+// up after that.
+type unbinderConn struct {
+	net.Conn
+	clientAddr string
+	ntlm       *NTLMTransport
+}
+
+func (uc *unbinderConn) Close() error {
+	uc.ntlm.deleteTransportsForClient(uc.clientAddr)
+	return uc.Conn.Close()
+}
+
+// Interface guards
+var (
+	_ caddy.Provisioner  = (*NTLMTransport)(nil)
+	_ http.RoundTripper  = (*NTLMTransport)(nil)
+	_ caddy.CleanerUpper = (*NTLMTransport)(nil)
+)
diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go
index f1e9144e2..77dc00544 100644
--- a/modules/caddyhttp/reverseproxy/reverseproxy.go
+++ b/modules/caddyhttp/reverseproxy/reverseproxy.go
@@ -311,15 +311,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
 // This assumes that no mutations of the request are performed
 // by h during or after proxying.
 func (h Handler) prepareRequest(req *http.Request) error {
-	// as a special (but very common) case, if the transport
-	// is HTTP, then ensure the request has the proper scheme
-	// because incoming requests by default are lacking it
-	if req.URL.Scheme == "" {
-		req.URL.Scheme = "http"
-		if ht, ok := h.Transport.(*HTTPTransport); ok && ht.TLS != nil {
-			req.URL.Scheme = "https"
-		}
-	}
+	// most of this is borrowed from the Go std lib reverse proxy
 
 	if req.ContentLength == 0 {
 		req.Body = nil // Issue golang/go#16036: nil Body for http.Transport retries