Skip to content

FEAT: accept TLS connections on the proxy listener - #27

Merged
mclueppers merged 2 commits into
mainfrom
https-ingress
Aug 5, 2026
Merged

FEAT: accept TLS connections on the proxy listener#27
mclueppers merged 2 commits into
mainfrom
https-ingress

Conversation

@mclueppers

Copy link
Copy Markdown

A client talks to the proxy in the clear by default, which puts the Proxy-Authorization credentials and the host name of every CONNECT request on the local network. WithListenerTLS makes the HTTP frontend accept TLS instead, so the client speaks the proxy protocol inside a TLS connection: what a browser calls an HTTPS proxy.

Most of this already worked, since Serve takes the listener from its caller and goproxy hijacks a tls.Conn as happily as any other. What was missing was a way to ask for it without knowing that, a way to configure it from a file, and somewhere for the certificate to live.

The option copies the configuration it is given and applies TLS 1.2 as a floor when none is set, so a caller cannot accidentally offer 1.0. Serve and ListenAndServe wrap the listener themselves.

The command grows tls_cert_file and tls_key_file, which are needed together or not at all, and holds the pair behind GetCertificate so that a renewal is picked up on USR2 without restarting: the listener is already up, the connections already established keep the certificate they started with, and a pair that cannot be read leaves the one in force alone rather than taking the listener down.

This decrypts nothing. What a client sends through CONNECT stays opaque; it is the hop to the proxy that is protected, not the traffic inside it. The SOCKS frontend is untouched, since SOCKS5 has no TLS convention, and so is the health endpoint.

A client talks to the proxy in the clear by default, which puts the
Proxy-Authorization credentials and the host name of every CONNECT
request on the local network. WithListenerTLS makes the HTTP frontend
accept TLS instead, so the client speaks the proxy protocol inside a TLS
connection: what a browser calls an HTTPS proxy.

Most of this already worked, since Serve takes the listener from its
caller and goproxy hijacks a tls.Conn as happily as any other. What was
missing was a way to ask for it without knowing that, a way to configure
it from a file, and somewhere for the certificate to live.

The option copies the configuration it is given and applies TLS 1.2 as a
floor when none is set, so a caller cannot accidentally offer 1.0. Serve
and ListenAndServe wrap the listener themselves.

The command grows tls_cert_file and tls_key_file, which are needed
together or not at all, and holds the pair behind GetCertificate so that
a renewal is picked up on USR2 without restarting: the listener is
already up, the connections already established keep the certificate they
started with, and a pair that cannot be read leaves the one in force
alone rather than taking the listener down.

This decrypts nothing. What a client sends through CONNECT stays opaque;
it is the hop to the proxy that is protected, not the traffic inside it.
The SOCKS frontend is untouched, since SOCKS5 has no TLS convention, and
so is the health endpoint.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class support for running the HTTP proxy listener over TLS (“HTTPS proxy”), including library options, CLI configuration, certificate reload on USR2, and documentation/tests to validate the behavior.

Changes:

  • Introduces WithListenerTLS and wraps the proxy listener in TLS when configured (TLS 1.2 default floor, config cloning).
  • Extends the microproxy command/config with tls_cert_file + tls_key_file and implements certificate hot-reload via GetCertificate.
  • Adds end-to-end TLS proxy tests plus certificate reload and config validation tests.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tls_test.go New integration tests for TLS proxy listener behavior (plain + CONNECT + auth + downgrade refusal + config copy).
server.go Wraps the provided listener with tls.NewListener when listenerTLS is configured; improves startup logging.
options.go Adds WithListenerTLS option (clone config + enforce TLS 1.2 floor when unset) and clarifies TLS client/server distinction in docs.
README.md Documents HTTPS-proxy mode, config keys, and runtime reload behavior; includes Go usage example.
cmd/microproxy/main.go Wires TLS listener configuration into the CLI and reloads certs on USR2 via a reloader.
cmd/microproxy/config.go Adds tls_cert_file / tls_key_file settings and validates they’re provided together.
cmd/microproxy/certs.go Implements a certificateReloader using tls.Config.GetCertificate with safe concurrent swapping.
cmd/microproxy/certs_test.go Tests certificate hot-reload and config validation for partial TLS configuration.
Suppressed comments (1)

tls_test.go:113

  • After removing InsecureSkipVerify, the test needs to explicitly trust the httptest.NewTLSServer certificate as well (the client will validate the target TLS cert after CONNECT). Add the target server cert to the same RootCAs pool used for both the proxy TLS hop and the tunnelled target.
	certificate, pool := selfSigned(t, "127.0.0.1")

	_, client := newTLSProxy(t,
		Config{AllowedConnectPorts: []int{portOf(t, secure.URL)}},
		&tls.Config{Certificates: []tls.Certificate{certificate}}, //nolint:gosec // MinVersion is applied by WithListenerTLS

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tls_test.go
The client these tests proxy through set InsecureSkipVerify alongside
RootCAs, which switches verification off entirely and leaves the pool
doing nothing. The comment claiming the certificate was pinned was
wrong, and the tests would have passed against any certificate at all:
they showed that bytes flowed, not that TLS was done correctly.

Verification is on now. It was off because one tls.Config covers both
hops and the tunnelled target is self-signed too, so the fix is to trust
the target's issuer as well rather than to trust nothing: httptest
exposes it through Server.Certificate.

A test is added for the property that was missing, where the client is
given a pool that trusts a different issuer and has to refuse the
connection with an unknown-authority error. With InsecureSkipVerify put
back it fails, so it pins what it claims to.

Reported by Copilot on #27.
@mclueppers
mclueppers merged commit eac8c1e into main Aug 5, 2026
2 checks passed
@mclueppers
mclueppers deleted the https-ingress branch August 5, 2026 10:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants