Cntlm or Px: which local proxy should get your dev tools through an NTLM or Kerberos corporate proxy?
Px for Windows single sign-on with no stored password, Cntlm for a 0.3 MB C daemon. Which builds really have Kerberos, and how to wire tools to 3128.
Key points
- Use Px on a domain-joined Windows machine: SSPI authenticates as the logged-in user with NTLM or Kerberos and nothing is stored. Use Cntlm for a small C daemon on Linux, with the
PassNTLMv2hash fromcntlm -H. - "Cntlm has no Kerberos" is out of date for versat/cntlm 0.94.0 (19 August 2025), but still true of the package most people install: Debian 13, Ubuntu 24.04 and Homebrew ship 0.92.3, and Ubuntu 26.04's 0.94.0+dfsg-1 was built without GSSAPI.
cntlm -hlists agssauth type only when the binary was built with Kerberos. Nogssin the help means no Kerberos, whatever the version says.- A
cntlm -Hhash keeps the password from being read, not from being used: anyone who copiesPassNTLMv2can authenticate to the proxy as you. - Both listen on
127.0.0.1:3128by default. Pointhttp_proxyandhttps_proxyat it, keepno_proxyto the portable subset, and put CIDR ranges and wildcards in the local proxy's own bypass list.
Use Px on a domain-joined Windows machine: it authenticates as the logged-in Windows user through SSPI, with NTLM or Kerberos, so no password or hash is stored anywhere. Use Cntlm when you want a small C daemon with no runtime, typically on a Linux host where you have to supply a credential anyway, and store the NTLMv2 hash that cntlm -H prints rather than the password. The claim that "Cntlm has no Kerberos" is out of date for the maintained versat/cntlm fork, whose 0.94.0 release (19 August 2025) added Kerberos, Windows SSPI (NTLM only) and PAC files, but it is still true of most Cntlm packages people actually install. Both tools listen on 127.0.0.1:3128 by default, so the tools behind them are wired identically.
Both exist for one job: authenticating to the proxy your own employer runs, on behalf of tools that cannot speak NTLM or Negotiate themselves. They log in as you, so the proxy's policy, filtering and logging apply exactly as they do to your browser. Nothing on this page is a way around that proxy.
| Your situation | Use | Why |
|---|---|---|
| Domain-joined Windows laptop, no stored secret wanted | Px | SSPI uses the logged-in user for NTLM and Kerberos. Cntlm's SSPI does NTLM only |
| Proxy accepts only Negotiate (Kerberos), you are on Windows | Px | Cntlm has no Kerberos path through SSPI |
Linux or macOS workstation that already runs kinit | Either; Px if tickets expire on you | Cntlm uses your ticket cache but never renews it. Px 0.11.0 and later renews it with --kerberos |
| Smallest footprint, nothing but libc | Cntlm | The upstream 0.94.0 .deb is 0.30 MB; the Px 0.11.0 Linux x86_64 binary archive is 26.15 MB |
| "There is no Python on this machine" | Either | Px ships standalone binaries. Python 3.10 or later is needed only for pip install px-proxy |
| Linux CI runner with a service account | Cntlm with PassNTLMv2, or Px with PX_PASSWORD | Both hold a password equivalent. Ask the proxy team for a non-NTLM route for CI first |
| Nothing password-equivalent may be stored | Px with SSPI, or a Kerberos ticket | A cntlm -H hash authenticates without the password |
| The proxy is published only as a PAC URL | Px | --pac takes a URL or a path, and with no proxy set Px reads Windows Internet Options. Cntlm's Pac takes a file path |
| Intranet sites that demand NTLM themselves | Cntlm | In NoProxy direct mode Cntlm also performs the site's own NTLM authentication |
| Only distribution packages are allowed | Check the build first | See the package table below: most ship Cntlm without Kerberos |
Does Cntlm support Kerberos?#
Yes, from 0.94.0, if the binary was built with GSSAPI and a ticket is already in the cache. Each of those three conditions fails often enough in practice that the opposite claim keeps circulating.
The version. Cntlm's last release from its original author is 0.92.3. The maintained fork is versat/cntlm, and its 0.94.0 release notes say it "Added support for SSPI (on Windows, NTLM authentication only), Kerberos authentication, IPv6, proxy PAC files". Distributions have been slow to follow:
| Where Cntlm comes from | Version | gss auth type | PAC (-x) |
|---|---|---|---|
versat/cntlm 0.94.0 .deb (depends on libgssapi-krb5-2) | 0.94.0 | Yes | Yes |
| Debian 12 bookworm | 0.92.3-1.2 | No | No |
| Debian 13 trixie | 0.92.3-1.3 | No | No |
| Debian forky and sid | 0.94.0+dfsg-2 | Yes, links libgssapi_krb5.so.2 | Yes |
| Ubuntu 22.04 jammy | 0.92.3-1ubuntu2 | No | No |
| Ubuntu 24.04 noble | 0.92.3-1.3build1 | No | No |
| Ubuntu 26.04 resolute | 0.94.0+dfsg-1 | No | Yes |
| Homebrew | 0.92.3 | No | No |
The Ubuntu 26.04 row is the trap. It has the new version, so it looks like the answer to "upgrade to get Kerberos", but Debian added "GSS/Kerberos support" to its package only in 0.94.0+dfsg-2 (8 June 2026). On 26.04, cntlm -h lists -a ntlm | nt | lm and the binary does not link GSSAPI.
The build. Kerberos is compiled in only when ./configure detects GSSAPI. Its gss test compiles config/gss.c against gssapi/gssapi.h (the GSS framework on macOS) and checks that libgssapi_krb5.so can be loaded; the Makefile adds -lgssapi_krb5 only if it passed. A source build on a host without the Kerberos development headers prints Checking gss... no and carries on to produce a binary with no Kerberos and no error.
The help text tells you which binary you have, because main.c prints the gss option only inside #if config_gss == 1:
cntlm -h 2>&1 | grep -A2 -- '-a '
# -a ntlm | nt | lm | gss
# Authentication type - combined NTLM, just LM, just NT, or GSS. Default NTLM.
# GSS activates kerberos auth: you need a cached credential.That is Debian sid's 0.94.0. Debian 13's 0.92.3 and Ubuntu 26.04's 0.94.0 both print the line without gss. ldd /usr/sbin/cntlm | grep gssapi gives the same answer from the linker's side.
The ticket. Cntlm never obtains a Kerberos ticket. The manual says "You need a cached credential (see kinit doc): per default the cache file is in /tmp/krb5cc_<uid>, you can override this using the environment variable KRB5CCNAME." With Auth GSS it sends a token on each connection, and "If GSS credentials are not available will fallback to the NTLM discovery procedure". Without an Auth line it uses Kerberos only when a ticket is cached and the proxy asks for Negotiate, and NTLM otherwise. A Cntlm whose ticket has expired, or which cannot read the cache, quietly becomes an NTLM proxy, which looks like "no Kerberos" from the outside.
On Windows the question does not arise in Cntlm's favour: its single sign-on path is SSPI, and the configuration template says plainly "Only NTLM is supported for now."
How Px authenticates#
Px hands each upstream connection to libcurl and, in its README's words, "supports all the authentication mechanisms supported by libcurl". --auth defaults to ANY, which lets libcurl pick from what the proxy offers in its 407; it can be forced to NEGOTIATE, NTLM, DIGEST or BASIC, or narrowed with prefixes (NONTLM is ANY minus NTLM, ONLYNTLM allows only NTLM). How the proxy's list of schemes turns into a choice is covered in proxy authentication.
On Windows Px "automatically authenticates using the currently logged in Windows user account" through SSPI, for NTLM and Kerberos alike. Nothing is stored, and a password change needs no action because the logon session carries the new one. If you name no upstream proxy, Px "will check Internet Options or environment variables for any proxy definitions". That makes the minimal Windows setup px with no arguments, or winget install genotrance.px followed by pxw --install to start it at logon.
Elsewhere Px needs --username=DOMAIN\user and reads the password from the system keyring: Credential Manager on Windows, Keychain on macOS, Gnome Keyring or KWallet on Linux. PX_PASSWORD is the fallback "when keyring is not available", and PX_KEYRING_PLAINTEXT=1 stores it unencrypted on disk, which the documentation marks "not recommended".
Kerberos on Linux and macOS needs a ticket in the cache, as with Cntlm. The difference is what happens when it expires. Without --kerberos, the documentation is explicit: "If the ticket expires, Px will fail to authenticate until a new ticket is obtained and Px is restarted." Px 0.11.0 (3 April 2026) added --kerberos, which runs kinit with the configured user and the keyring password, renews the ticket before it expires, retries after a laptop sleep, and needs a libcurl with GSS-API.
Versions. 0.11.0 is the newest full release on GitHub. 0.12.0 (21 July 2026) is marked pre-release there, yet pip install px-proxy installed 0.12.0 on 12 October 2026, so the version you get depends on the channel. 0.12.0 adds PAC encoding detection for files served as cp1252 or cp1251 without a charset, a real problem with PAC files on IIS. Python 3.10 has been the minimum since 0.10.3 dropped 3.8 and 3.9. Standalone binaries cover Windows amd64, Linux glibc and musl on x86_64 and aarch64, and macOS arm64 only, so an Intel Mac installs through pip.
Where the credential lives#
The decision that matters to a security reviewer is not the tool but what sits on the machine afterwards.
| Method | Tool | What is stored | After a password change |
|---|---|---|---|
| SSPI single sign-on | Px on Windows (NTLM and Kerberos); Cntlm on Windows (NTLM only) | Nothing | Nothing to do |
Kerberos ticket from your own kinit | Cntlm 0.94.0 with GSS; Px without --kerberos | A time-limited ticket in the cache | Next kinit uses the new password |
--kerberos ticket management | Px 0.11.0 and later, Linux and macOS | Password in the keyring, ticket in a per-process cache | Re-run px --password |
| System keyring | Px with --username | Password, protected by the OS keyring | Re-run px --password |
PX_PASSWORD | Px, including the mini Docker image | Password in the environment | Update the variable |
PassNTLMv2 hash | Cntlm, from cntlm -H | Hash bound to one user and domain | Regenerate with -H |
Password | Cntlm | Plaintext in cntlm.conf, mode 0600 | Edit the file |
-I prompt | Cntlm | Nothing; typed at every start | Type the new one |
Px's own documentation warns that a password in the environment "is visible in docker inspect and /proc/<pid>/environ", which is the same leak path as credentials embedded in a proxy URL.
cntlm -H prints the hashes that replace the password in the configuration file. From Debian sid's 0.94.0, with the values replaced:
$ cntlm -H -u alice -d CORP -c /dev/null
Password:
PassLM <32 hex digits>
PassNT <32 hex digits>
PassNTLMv2 <32 hex digits> # Only for user 'alice', domain 'CORP'Keep only the PassNTLMv2 line, set Auth NTLMv2, and delete Password. The manual calls NTLMv2 "the most and possibly the only secure authentication of the NTLM family". If the proxy rejects it, cntlm -M <testurl> tries the known presets strongest first and prints the setting that worked.
Wiring dev tools to 127.0.0.1:3128#
A request can leave for its destination directly at two points, and only one hop carries credentials:
- The tool checks its own
no_proxy. A match goes direct and never reaches the local proxy. - Everything else goes to Cntlm or Px on
127.0.0.1:3128, with no credentials at all on that hop. - The local proxy checks its own bypass list (
NoProxyin Cntlm,--noproxyin Px). A match goes direct from there. - Everything else goes to the corporate proxy, and the local proxy answers its
407with NTLM or Negotiate.
A Cntlm configuration for two parent proxies:
# /etc/cntlm.conf (mode 0600)
Username alice
Domain CORP
PassNTLMv2 <from cntlm -H>
Auth NTLMv2
Proxy proxy1.corp.example:8080
Proxy proxy2.corp.example:8080
NoProxy localhost, 127.0.0.*, 10.*, *.corp.example
Listen 127.0.0.1:3128Cntlm tries the Proxy lines "one after another until one works". The Px equivalent, saved to px.ini:
px --proxy=proxy1.corp.example:8080,proxy2.corp.example:8080 \
--noproxy=10.0.0.0/8,corp.example --saveThen point the tools at it:
export http_proxy=http://127.0.0.1:3128
export https_proxy=http://127.0.0.1:3128
export HTTPS_PROXY=$https_proxy
export no_proxy=localhost,127.0.0.1,::1,corp.example
export NO_PROXY=$no_proxyhttps_proxy uses an http:// URL because that is the scheme of the hop to the local proxy, not of the destination. curl reads http_proxy in lower case only, the httpoxy defence explained in corporate proxies and developer tooling, so do not rely on HTTP_PROXY alone.
Keep no_proxy portable and move everything else into the local proxy. The no_proxy environment variable has no specification, and its portable subset is narrow: comma separated, bare hostnames and literal IPs, no leading dots, no wildcards, no CIDR, both cases set. That page's advice for ranges is to "fix it at a different layer", and the local proxy is that layer. Px's --noproxy accepts IPs, wildcards, ranges, CIDR and domains; Cntlm's NoProxy accepts * and ? wildcards. Each is one parser that every tool shares, so a range written there behaves the same for Go, Python, Java and curl. The cost is that internal traffic outside the portable list passes through the local process before going direct. Loopback addresses still belong in no_proxy itself, and you can check what each runtime will do with a value in the no_proxy tester.
What gets simpler. The hop to 127.0.0.1 asks for no authentication, so no proxy URL carries user:password, nothing leaks into shell history or CI logs, and the JVM's default jdk.http.auth.tunneling.disabledSchemes=Basic never comes into play. The JVM still ignores https_proxy, so it needs -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=3128 and the matching http. properties, and npm needs proxy and https-proxy in .npmrc.
What does not change: certificates. Neither tool terminates TLS. An https:// request reaches the corporate proxy as a CONNECT tunnel, so if that proxy inspects TLS, every tool still needs the corporate root CA, as described in TLS interception and corporate root CAs. A local auth proxy fixes 407, never unable to get local issuer certificate.
Containers and WSL2. Inside a container, 127.0.0.1 is the container. The Docker daemon runs on the host and can use 127.0.0.1:3128 for pulls, but builds and running containers need the host's address: Px's documentation uses http://host.docker.internal:3128 on Docker for Windows with Px started with --hostonly, which accepts connections from every address assigned to the machine. For WSL2 it derives the Windows host address from the nameserver line in /etc/resolv.conf. With Cntlm, add a Listen line on the bridge address and restrict it with Allow and Deny.
Never run either as a shared gateway. Px's documentation describes --gateway as "every client will use your login", and Cntlm's Gateway yes makes it "accessible to other machines on the network". Either turns your identity into an open proxy on the office network, with every request logged under your name. Cntlm's optional SOCKS5 listener carries the same warning: it "accepts all requests, unless you use SOCKS5User".
Failure modes#
Auth GSS is set, but the proxy still sees NTLM. Two causes. The binary has no Kerberos (cntlm -h shows no gss), or Cntlm cannot read your ticket. The Debian packages run Cntlm as a dedicated cntlm user: User=cntlm in the 0.94.0+dfsg-2 systemd unit, RUNAS=cntlm in /etc/default/cntlm on Debian 13. Your ticket in /tmp/krb5cc_<your uid> is not its cache, so it falls back to NTLM. Point KRB5CCNAME in the unit at a cache that user can read, or run Cntlm as yourself.
Px on Linux works all morning, then every request gets a 407. The Kerberos ticket expired and Px was running without --kerberos, so it will not recover until a new ticket exists and Px is restarted. Use --kerberos on 0.11.0 or later.
Px as a Windows service authenticates as nobody. Its installation guide notes that a service running as LocalSystem "does not have access to SSPI for NTLM or Kerberos authentication". Run the service as the user, or use pxw --install, which the same guide recommends for running Px at startup with the current user's SSPI credentials.
Address already in use on 3128. Both tools default to it. Cntlm "fails to start if it cannot bind all of the proxy service ports". Move one with Listen or --port and change http_proxy to match.
Px on a headless Linux box fails with Prompt dismissed. gnome-keyring-daemon 48 and later needs ~/.local/share/keyrings owned by the current user with mode 700, and fails silently otherwise. Fix the permissions, or use PX_PASSWORD and accept the trade-off in the credential table.
Cntlm returns 407 for everything after a password change. The stored hash was computed from the old password. Run cntlm -H again and replace PassNTLMv2. Px through SSPI is unaffected.
A PAC-defined proxy changed and Cntlm did not notice. Cntlm's Pac option takes a file path, so a PAC downloaded once stays as it was. Px fetches --pac by URL. To see which proxy a PAC returns for a host, use the PAC file tester, and see PAC files and WPAD for how browsers find the file in the first place.
Where this answer stops applying#
The Px statements come from its README, usage and configuration documentation, release notes for 0.10.3 to 0.12.0, and px --help from 0.12.0. The Cntlm statements come from the versat/cntlm manual, configuration template, source and 0.94.0 release, plus the Debian 13, Debian sid and Ubuntu 26.04 packages installed in containers on 12 October 2026. Neither tool was run against a live NTLM or Kerberos proxy for this page, so whether a particular proxy accepts NTLMv2, or expects Negotiate first, is for cntlm -M or px --test to answer. Package versions move: re-check the table above before relying on a distribution build. If your proxy team can offer Basic authentication over a TLS-protected proxy hop, or source-address rules for build machines, you may not need either tool.
Frequently asked questions#
Does Cntlm support Kerberos?#
Yes, from version 0.94.0 of the maintained versat/cntlm fork, with Auth GSS and a ticket from kinit. Many packages still lack it: Debian 13, Ubuntu 24.04 and Homebrew ship 0.92.3, and Ubuntu 26.04's 0.94.0 was built without GSSAPI. If cntlm -h does not list gss, the binary has no Kerberos.
What port does Cntlm listen on by default?#
Cntlm listens on 127.0.0.1:3128 by default, set by the Listen line in cntlm.conf. Px uses the same default, so running both on one machine needs one of them moved.
Does Px need Python?#
Only if you install it with pip, which requires Python 3.10 or later. Standalone binaries for Windows, Linux and Apple Silicon Macs are on the GitHub releases page, and Windows users can install with Winget or Scoop.
Is it safe to store the cntlm -H hash instead of the password?#
It is safer than plaintext but not safe to share. The hash is enough to authenticate to the proxy as you, so treat cntlm.conf like a password file with mode 0600. Keep only PassNTLMv2 and regenerate it after every password change.
Is Cntlm still maintained?#
The original project stopped at 0.92.3, but the versat/cntlm fork is maintained and released 0.94.0 on 19 August 2025 with Kerberos, SSPI, IPv6 and PAC support. Many distributions still package 0.92.3.
Can Cntlm or Px use a PAC file?#
Both can. Px takes a URL or local path with --pac, and on Windows reads the system proxy settings when none is given. Cntlm 0.94.0 takes a local file path with Pac or -x; 0.92.3 has no PAC support.
Should I use Px or Cntlm on Linux?#
Cntlm, if you need NTLM with a small daemon and can store an NTLMv2 hash. Px, if you need Kerberos with automatic ticket renewal (--kerberos, 0.11.0 and later) or CIDR ranges in the bypass list. Check that a distribution's Cntlm package was built with GSSAPI before relying on its Kerberos support.
Primary sources#
Every normative claim on this page is checked against the specification or the vendor documentation listed here. Where behaviour is version dependent, the version is named in the text.
- versat/cntlm repository and README
- versat/cntlm releases (0.94.0)
- Cntlm manual page, doc/cntlm.1
- Cntlm configuration template, doc/cntlm.conf
- Cntlm source: configure, config/gss.c and main.c (GSS build detection and help text)
- Debian cntlm package versions and 0.94.0+dfsg-2 changelog
- Ubuntu cntlm package publishing history
- genotrance/px repository and README
- Px releases and changelog
- Px usage and configuration reference
Found something wrong, or behaviour that differs on your version? Report it with the version number and a primary source. Anything substantive is fixed in the page and logged on the corrections page. See editorial standards for how pages are researched, sourced and reviewed.
More in troubleshooting proxies#
- 502 vs 503 vs 504: reading proxy errors correctly
- Access-Control-Allow-Origin multiple values: the fix
- curl through a proxy
- Header and body size limits at the proxy
- nginx 499: what it means, and its ALB, HAProxy, Envoy names
- nginx 502 after Docker restart: fix the stale IP
- nginx worker_connections: the right number for a proxy
- A proxy debugging playbook