README
¶
spnego-proxy
An HTTP proxy that handles SPNEGO (Kerberos) authentication on behalf of
clients that don't support it. It sits between the application and the
real proxy, forwarding requests with a Proxy-Authorization: Negotiate
header. It does not alter nor inspect traffic between the client and the
real proxy.
This fork adds native macOS GSS-API support, allowing passwordless
operation using Kerberos tickets from the macOS credential cache
(including the Keychain-based API: cache type). The existing
password-based authentication via gokrb5 is preserved as a fallback
for Linux/Windows or when explicitly requested on macOS.
Installation
Homebrew (macOS)
brew tap andrewesweet/spnego-proxy
brew install spnego-proxy
The formula builds with CGO_ENABLED=1 to link against the native GSS.framework
for SPNEGO authentication.
Docker
docker run --rm ghcr.io/andrewesweet/spnego-proxy -version
For typical usage, pass proxy flags and expose the listening port:
docker run --rm -p 127.0.0.1:3128:3128 ghcr.io/andrewesweet/spnego-proxy \
-proxy proxy.corp.example.com:8080 \
-addr :3128
Multi-arch images are available for linux/amd64 and linux/arm64.
From source (requires Go 1.25+)
go install github.com/andrewesweet/spnego-proxy@latest
Build from this repository
git clone https://github.com/andrewesweet/spnego-proxy.git
cd spnego-proxy
# macOS (with CGO for GSS-API support)
CGO_ENABLED=1 go build -o spnego-proxy .
# Linux (pure Go, gokrb5 fallback)
CGO_ENABLED=0 go build -o spnego-proxy .
For development setup including formatting, linting, and testing, see CONTRIBUTING.md.
Usage
macOS (GSS-API mode — passwordless)
On macOS, spnego-proxy uses the native GSS-API framework (Heimdal) to
acquire SPNEGO tokens from the default Kerberos credential cache. This
works with tickets obtained via kinit or the macOS Kerberos SSO
extension automatically.
Only two flags are required:
# Ensure you have a valid Kerberos ticket
kinit user@REALM.COM
klist
# Run the proxy
./spnego-proxy \
-addr 127.0.0.1:3128 \
-proxy upstream-proxy.example.com:8080
# Test
curl -x http://127.0.0.1:3128 https://example.com
Optional flags for macOS GSS-API mode:
-spn— Override the service principal name (default:HTTP@<proxy-hostname>)-debug— Enable debug logging
Linux/Windows (password-based mode)
On non-macOS platforms, or when -user is specified on macOS, the proxy
uses the pure-Go gokrb5 library with password-based authentication.
Required flags:
./spnego-proxy \
-addr 127.0.0.1:3128 \
-proxy upstream-proxy.example.com:8080 \
-config /etc/krb5.conf \
-user myuser \
-realm EXAMPLE.COM \
-password-file /path/to/password
All flags:
| Flag | Description | Required |
|---|---|---|
-addr |
Listen address (default: 127.0.0.1:8080) |
No |
-proxy |
Upstream proxy address | Yes |
-spn |
Service principal name (default: HTTP@<proxy-host> on macOS, HTTP/<proxy-host> on Linux) |
No |
-debug |
Enable debug logging | No |
-config |
Kerberos config file path | Password mode only |
-user |
Kerberos username (triggers password-based auth on macOS) | Password mode only |
-realm |
Kerberos realm | Password mode only |
-password-file |
Path to password file (prompts if omitted) | No |
macOS with explicit password
If you provide -user on macOS, the proxy will use the gokrb5
password-based path instead of the native GSS-API:
./spnego-proxy \
-addr 127.0.0.1:3128 \
-proxy upstream-proxy.example.com:8080 \
-config /etc/krb5.conf \
-user myuser \
-realm EXAMPLE.COM \
-password-file /path/to/password
Standards compliance
spnego-proxy implements RFC 9110 (HTTP Semantics) and RFC 9209
(Proxy-Status) throughout its request and response handling:
-
Via header (RFC 9110 §7.6.3): The proxy appends a
Viaentry to both forwarded requests and responses, using a randomly generated pseudonym to identify each proxy instance. Incoming requests whoseViaheader already contains the proxy's own pseudonym are rejected with502 proxy_loop_detectedto prevent routing loops. -
Proxy-Status header (RFC 9209): All error responses include a structured
Proxy-Statusheader (e.g.,spnego-proxy; error=connection_timeout) for machine-readable diagnostics.
Known deviation: response Via fallback
RFC 9110 §7.6.3 states that a proxy MUST add a Via header to each
message it forwards. spnego-proxy complies with this requirement for
all parseable HTTP responses. However, if the upstream proxy sends a
response that cannot be parsed as valid HTTP (e.g., a severely malformed
status line or truncated headers), injecting a Via header is
impossible because there are no structured headers to modify.
In this edge case the proxy falls back to raw byte relay — forwarding
the unparseable data to the client without a Via header. This
trade-off was chosen deliberately:
- Client impact: Returning a
502error would discard whatever data the upstream sent. Raw relay preserves the original bytes, which may still be useful to the client or to downstream debugging tools. - Impossibility of injection: Without parseable headers there is no safe byte offset at which to insert a header line. Attempting to splice bytes could corrupt the stream.
- Operator visibility: The proxy emits a
slog.Warn-level log entry whenever the fallback triggers, including the parse error and client address, so the condition is always visible in operational monitoring.
In practice, this fallback should rarely (if ever) fire because conforming HTTP proxies always send well-formed responses. If it does fire, the log entry provides the information needed to investigate the upstream proxy.
Architecture
The project uses Go build tags to separate platform-specific authentication:
main.go— Shared proxy logic,TokenProviderinterface, CLI flagsauth_gss_darwin.go— macOS: CGO-based GSS-API token acquisitiongss_darwin.c/gss_darwin.h— C helpers for GSS-API callsauth_gokrb5.go— Pure-Go gokrb5 password-based auth (all platforms)auth_notdarwin.go— Non-macOS: returns error when native GSS is unavailable
Security considerations
Deployment model
This proxy is designed to run as a per-user localhost tool, similar to
cntlm and
px-proxy. It accepts connections
from any process that can reach its listener (default 127.0.0.1:8080).
When binding to a non-loopback address, use -allowed-ips to restrict
access.
CONNECT tunneling
CONNECT tunnels are restricted to port 443 by default. Use
-connect-ports with a comma-separated list to allow additional ports,
or -connect-ports '*' to allow all ports.
Idle tunnel timeout
CONNECT tunnels are closed after 5 minutes of inactivity by default.
Use -idle-timeout to adjust or -idle-timeout 0 to disable.
Upstream TLS
By default, proxy-to-upstream connections use plain TCP (matching cntlm
and px-proxy behavior). Use -upstream-tls to encrypt the upstream
link. A custom CA can be provided with -upstream-ca.
Kerberos configuration
- Ensure
krb5.confis read-only and owned by root or the service account. - Be aware that
KRB5_CONFIGandKRB5CCNAMEenvironment variables affect authentication behavior. - Run the proxy in an environment where unauthorized modification of these files and variables is not possible.
Password handling
When using password-based authentication (-user flag), the password
bytes are zeroed in memory immediately after being passed to the
Kerberos client. The Kerberos library retains its own copy for TGT
re-authentication; use a keytab in environments where this is a concern.
CGo boundary (macOS)
The macOS GSS-API integration uses C code that has been reviewed for
buffer safety. All buffers are bounds-checked (snprintf, explicit
length limits) and resource cleanup is handled in all code paths.
Logging
The proxy logs structured JSON via slog. Security-relevant events
(authentication failures, CONNECT attempts, circuit breaker state
changes, TE/CL conflicts) are logged but not classified separately from
operational events.
License
MIT — see LICENSE.
Based on montag451/spnego-proxy.