Skip to content

About

Find out why this machine cannot connect to a URL - PKIX path building failed, unable to get local issuer certificate - and what to run to fix it. One run walks DNS, proxy and PAC, TCP and TLS, asks every trust store on the machine (system, Java, git, curl, Node.js, Python, pip) and prints the fix for each one that refuses. GUI + CLI

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

certdoctor 🩺 - find out why this machine cannot connect to that URL, and what to run to fix it

Follow me:   Discord   Facebook   X (Twitter)   Bluesky   Steam

CI Latest release Downloads License: GPLv3 Platform: Windows Platform: Linux Platform: macOS Website

certdoctor is a GUI/CLI tool for QA engineers, helpdesk and DevOps - for anyone who has read PKIX path building failed or unable to get local issuer certificate on a corporate laptop, behind a proxy, or inside a container that cannot do what the host does without trouble. Point it at the URL that fails. It walks the whole path in one run - DNS, proxy and PAC script, TCP, TLS - and then does the part no other tool does: it asks each trust store on the machine, one by one, whether it trusts what the server presented, and prints the command that would fix each store that does not, with the real paths from your machine in it. curl -v and openssl s_client answer for one program. certdoctor answers for the Windows store or the Linux CA bundle, every Java installation, every git, every curl, every node, every Python and the pip of each it finds - and tells you whether the fault is on this machine at all.

⭐ If it ended a "works on my machine" argument, leave a star. That is how the next person staring at PKIX path building failed finds out it exists.

What it can do

  • Say whose problem it is - this machine, or the server. A server that does not send its intermediate certificate is named as the server's fault, with the evidence to paste into a ticket.
  • One row per trust store, not per runtime - the Windows store as your account sees it and as services see it, the Linux CA bundle, every JDK and JRE separately, every git, curl, node, Python and pip it finds. On the machine it was built on, one run in November 2026 read seventeen stores, eight of them Java runtimes.
  • Print the command that fixes each store - certutil, keytool with the real cacerts path of that JDK, an append to git's own bundle - and say when it needs administrator rights. It prints them. It never runs them.
  • Notice TLS inspection without accusing anyone - who signed the certificate, how many stores on this machine know that authority, and the product's name when it is one certdoctor recognises.
  • Read your proxy settings the way your programs do - the environment variables, the Windows Internet Settings that Edge, Chrome and .NET follow, and the WinHTTP settings that services follow, side by side. It runs your PAC script in a sandbox to see where your URL really goes, and asks the proxy whether it wants credentials.
  • Look at DNS twice - every configured resolver asked directly, and the system resolver, with the difference classified: split horizon, a leak past a VPN, a name that resolves nowhere.
  • Try every address - a TCP connection to each address the name resolves to, and the full certificate chain from each one, with fingerprints and validity dates.
  • Write a report for the ticket - terminal, Markdown, one self-contained HTML page, or JSON with a schema for CI. --redact masks host names, paths and user names. Credentials are masked always.
  • Change nothing and call nobody - the only file it writes is the report, at the path you name, and the window keeps one setting of its own. It connects only where you pointed it: no telemetry, no update check.
  • Ship the command line as one static binary - nothing to install next to it, and it runs as it is inside Alpine and Ubuntu images.
  • Show the same report in a window - certdoctor-gui, for Windows, Linux and macOS: type the address, press Diagnose, read the report and save it as HTML, Markdown or JSON. The window runs the command line next to it, so it knows nothing the command line does not.

certdoctor run against untrusted-root.badssl.com, paged through less: the verdict at the top names the BadSSL root that the Java stores lack, then the trust matrix shows the system store trusting the site, both Java runtimes refusing it, and a keytool command for each

A real run, recorded from the binary in a Linux container and paged through less - the verdict first, then a search for L5, the trust matrix. The few seconds the run takes are cut. BadSSL's public test root was added to the container's system store - the way a company root usually arrives - and the two Java runtimes in it were never told.


Try it on a broken site

badssl.com keeps deliberately broken HTTPS sites for exactly this. Three of them, with the first sentence certdoctor prints at the top of each report:

certdoctor https://incomplete-chain.badssl.com/

The server presented 1 certificate(s) and did not send the issuer of the first one, so only a client that already holds it can build a path, and none of the 10 store(s) assessed here built one - this is a fact about the server rather than about this machine.

certdoctor https://untrusted-root.badssl.com/

CN=BadSSL Untrusted Root Certificate Authority,O=BadSSL,L=San Francisco,ST=California,C=US signed this certificate, and none of the 11 store(s) assessed here holds an anchor for it.

certdoctor https://expired.badssl.com/

16 of 17 store(s) do not trust the presented chain, and 1 could not be assessed - a certificate in the path is past its validity.

The second one is where the trust matrix earns its place. Every store it could read refuses, and every store that can be given the root carries the command that would add it there, with this machine's paths in it. curl built on Schannel judges by this account's Windows store, so its rows point at that store's command rather than carrying one of their own. An excerpt from a Windows machine, as printed:

      system-store LocalMachine does NOT trust the presented chain (90 anchors)  detected
        needs elevated rights: the LocalMachine store is read by every account and every service on
                               this machine, so changing it needs administrator rights
        run: certutil -addstore Root "<certificate.pem>"
        replace first: <certificate.pem>
        note: programs that carry a store of their own - git and java among them - do not read this
              one, so their rows need their own commands
      java C:\Program Files\OpenJDK\jdk-21.0.2 does NOT trust the presented chain (106
        anchors)  detected
        needs elevated rights: this store is outside this account's own files, so it belongs to
                               every account on the machine
        run: "C:\Program Files\OpenJDK\jdk-21.0.2\bin\keytool" -importcert -trustcacerts -keystore "C:\Program Files\OpenJDK\jdk-21.0.2\lib\security\cacerts" -storepass changeit -alias "<alias>" -file "<certificate.pem>"
        replace first: <alias>, <certificate.pem>
        note: installing a newer build of this runtime brings its own store and does not carry this
              certificate across, and the line is for the Command Prompt (cmd.exe) - PowerShell runs
              it only with & and a space put before it
      curl C:\Windows\system32\curl.exe does NOT trust the presented chain (90 anchors) - it judges
        by system-store CurrentUser, which refuses it  detected

The report is laid out 100 columns wide. Sentences wrap at their spaces. A command, a fingerprint, a certificate's name and a value read off the machine are never broken - a command broken over two lines would be pasted as two, so a line that runs past the edge is one meant to be copied whole.

Do not run those for badssl.com. That root is untrusted on purpose, and trusting it would make every store on your machine accept a certificate nobody vouches for. The commands are there for the day the refused root is your own company's.


Download and run

Download a release. Take the archive for your system from the releases page and unpack it. There is nothing to install.

Your system The archive What you run
Windows on x64 certdoctor_<version>_windows_amd64.zip certdoctor.exe, signed
Windows on Arm certdoctor_<version>_windows_arm64.zip certdoctor.exe, signed
Linux on x64 certdoctor_<version>_linux_amd64.tar.gz ./certdoctor, one static file
Linux on Arm certdoctor_<version>_linux_arm64.tar.gz ./certdoctor, one static file
macOS on Apple silicon certdoctor_<version>_macos_arm64.tar.gz ./certdoctor, a link into certdoctor.app, signed and notarised

The window, certdoctor-gui, has archives of its own, for three systems. Each carries the command line as well, because the window runs it:

Your system The archive What you run
Windows on x64 certdoctor-gui_<version>_windows_amd64.zip certdoctor-gui.exe, signed
Linux on x64 certdoctor-gui_<version>_linux_amd64.tar.gz ./certdoctor-gui
macOS on Apple silicon certdoctor-gui_<version>_macos_arm64.tar.gz certdoctor-gui.app, signed and notarised

Every archive also carries LICENSE, THIRD-PARTY-NOTICES.md and this README, and the window's archives THIRD-PARTY-NOTICES-GUI.md too. There is no build for Intel Macs.

Keep the files of the window's archive together. The window starts the certdoctor that sits next to it. On Windows, the opengl folder holds a software renderer: on a machine whose graphics driver offers no OpenGL 2.1 - a virtual machine without 3D acceleration, or a Remote Desktop session - the window starts again with it, by itself.

On macOS, keep each bundle and the links next to it together. The signature and Apple's notarisation ticket belong to the bundle, so a program copied out of it on its own is not the program Apple checked.

On Linux, the command line needs no C library, so the same file runs in Alpine, in Ubuntu and in an image with nothing else in it. certdoctor --license prints the notices that travel with it. The window is different: it needs a desktop session with OpenGL, and loads libGL.so.1 and libwayland-client.so.0, which a desktop installation has.

Check what you downloaded

Each release lists the digest of every file it publishes in verify-SHA256SUMS.txt. Download it next to the archive, then:

sha256sum -c --ignore-missing verify-SHA256SUMS.txt        # Linux
shasum -a 256 -c --ignore-missing verify-SHA256SUMS.txt    # macOS

On Windows, in PowerShell, True beside the archive means it is the file the release published:

$listed = (Get-Content verify-SHA256SUMS.txt) -replace ' .*'
Get-FileHash certdoctor*.zip | ForEach-Object { "$($_.Hash -in $listed)  $(Split-Path -Leaf $_.Path)" }

A digest file from the same page only proves that the two agree. What proves where they came from is a statement GitHub signed when the release was made, and the GitHub CLI checks it:

gh attestation verify <the archive> -R donislawdev/certdoctor --predicate-type https://spdx.dev/Document/v2.3

That statement is the bill of materials - every module compiled into the program, the Go toolchain included, with its licence and version - and it covers every file of the release. The predicate type has to be given, because without it gh asks for build provenance, which is a different statement. A Linux archive is exactly the bytes the build produced, so the build's own provenance answers for it too:

gh attestation verify <the archive> -R donislawdev/certdoctor

The Windows and macOS archives were signed after the build, so their bytes are no longer the build's, and that second command finds nothing for them by design. The signature answers for them instead: Windows checks it when the program starts, and on macOS spctl -a -vv certdoctor.app - or certdoctor-gui.app - asks Gatekeeper.

Build it from source

With Go installed (1.27.4 or newer), from a clone:

git clone https://github.com/donislawdev/certdoctor
cd certdoctor
CGO_ENABLED=0 go build ./cmd/certdoctor

In PowerShell, set the variable on its own line first: $env:CGO_ENABLED = "0", then go build ./cmd/certdoctor. CGO_ENABLED=0 gives the same static binary the releases carry, the one that runs in an Alpine image as it is.

go install with @latest does not work here: this module's go.mod points one library of the window at a copy kept in the repository, and go install refuses a module that replaces another. A clone builds as above.

Inside a container

Copy the Linux binary in and run it there - the report then describes the container, not the host:

docker cp certdoctor my-container:/tmp/certdoctor
docker exec my-container /tmp/certdoctor https://api.example.com/

Two minutes with it

# The question this tool exists for: why does this URL fail here?
certdoctor https://api.example.com/

# A report to attach to a ticket, with host names, paths and user names masked
certdoctor https://api.example.com/ --redact --format md --out report.md

# One HTML page with no script in it and nothing fetched from anywhere
certdoctor https://api.example.com/ --format html --out report.html

# JSON for a pipeline, validated against its schema before it is printed
certdoctor https://api.example.com/ --json > report.json

# The same report as a page later, from the JSON, without running anything again
certdoctor --from report.json --format html --out report.html

# Ask about a PAC script your machine does not name, which is the only way to ask on Linux
certdoctor https://api.example.com/ --pac-url http://wpad.example.com/proxy.pac

The report opens with one sentence naming the cause in the words of the layer that decided it. The numbers come after that sentence, then every layer with its evidence, then the notes saying what was not looked at. Every flag is in certdoctor --help, and the documentation walks through a report line by line.

Table of contents

Symptoms

If you searched for one of these, you are in the right place. certdoctor is built to tell you which of the many possible causes is yours:

PKIX path building failed
unable to find valid certification path to requested target
certificate verify failed: unable to get local issuer certificate
SELF_SIGNED_CERT_IN_CHAIN
SSL certificate problem: self signed certificate in certificate chain
x509: certificate signed by unknown authority
407 Proxy Authentication Required

They are the same complaint in the words of different runtimes, and each runtime keeps its own idea of whom to trust. The browser works. curl works. The application does not. That is not a paradox - it is several trust stores disagreeing.

You saw Usually from What certdoctor asks
PKIX path building failed, unable to find valid certification path Java every JDK and JRE on the machine, each cacerts as its own row
SSL certificate problem: ... curl, and git over HTTPS every git and curl it finds, and which store each of them reads
x509: certificate signed by unknown authority programs written in Go the platform store - the Windows store, or the Linux CA bundle
407 Proxy Authentication Required anything going through a proxy which proxy applies to this URL, and whether it wants credentials
certificate verify failed: unable to get local issuer certificate Python, pip every Python on PATH, by the list its ssl module trusts, and the pip of each by its own rule - this machine's store through truststore from pip 24.2, one CA file before it
certificate verify failed: Missing Authority Key Identifier, Basic Constraints of CA cert not marked critical Python 3.13 and newer which certificate in the chain breaks which of OpenSSL's strict rules, which Python 3.13 turned on
SELF_SIGNED_CERT_IN_CHAIN Node.js, npm every node on PATH, by the list that binary trusts - npm's own cafile is not read yet

Each of these messages has its own page on the website: what it means, what causes it, and how certdoctor tells you which cause is yours.


Why this exists

A new laptop, a new network, a new VPN or a new container image, and a connection that worked yesterday fails with one of the messages above. The fix is usually one command. Finding which command takes an afternoon, because:

  • every runtime has its own trust store. Java keeps one cacerts per installation, and a typical developer machine has several JDKs, of which somebody fixed one. Git for Windows carries its own bundle. curl built with Schannel reads the Windows store and ignores the bundle beside it.
  • the proxy is decided in several places at once. Environment variables, the Windows settings a browser follows and the different ones a service follows, and a PAC script that sends one host past the proxy everything else goes through.
  • the network sees two different worlds. On a VPN one resolver answers with an internal address and another with a public one.
  • nobody can tell whose fault it is. A server that forgot its intermediate certificate works in a browser that cached the intermediate from some other site and fails everywhere else.

The knowledge exists - in dozens of "every cause of PKIX path building failed" articles. What did not exist was a tool that checks every cause on the machine in front of you and gives a verdict.


What it checks, layer by layer

Layer Question it answers Example of what it finds
L0 Context which of the 25 environment variables that decide trust and proxying are set HTTPS_PROXY set while https_proxy is not - programs disagree about which one counts
L1 Name resolution what each resolver says, and what your system says the two views sharing no address at all - what split horizon DNS looks like
L2 Proxy which proxy this URL really goes through, and who decided that a PAC script sending this host direct while every setting names a proxy
L3 TCP which of the resolved addresses accept a connection refused, unreachable and timed out told apart, per address
L4 TLS what the server presented, and whether that chain is complete a server that did not send its intermediate certificate
L5 Trust matrix which trust stores on this machine accept that chain one JDK refusing a root that another JDK and the Windows store accept

The handshake is completed without verifying it, on purpose. A chain that fails verification leaves you with no certificates to look at, and here the evidence is the product.

The Windows store is asked with every network retrieval switched off. The address Windows would fetch a missing certificate from is written into that certificate by the server you are diagnosing, and certdoctor connects only where you pointed it. It tells a public root Windows has simply not downloaded yet from an authority nobody installed by reading this machine's own copy of Microsoft's root program list.

A store that cannot be read says so, with the reason, and never counts as a pass. A probe that does not understand what it saw reports UNKNOWN. There are exactly four statuses - PASS, WARN, FAIL, UNKNOWN - and UNKNOWN always carries why.


What makes it different

Other tools look at the certificate. certdoctor looks at the machine in front of you and at which of its runtimes accept that certificate.

It does not compete with these, and links to them on purpose:

  • certigo - inspects and explains certificates
  • testssl.sh - audits the TLS configuration of a server
  • tlstools - certificate utilities

When the trouble is the server, they tell you more about the server than certdoctor does. When the trouble is that one runtime on your machine disagrees with another, they cannot see it at all.


Using it in CI

Every ending has its own exit code, and --json writes a report validated against its schema before anything is printed:

Code Name Meaning
0 OK everything that mattered was assessed and passed
1 RUNTIME certdoctor itself failed
2 USAGE bad invocation syntax or an unknown flag
3 CONFIG flags are valid on their own but contradictory or unsatisfiable
4 DIAGNOSIS the run succeeded and found at least one failure
5 INCONCLUSIVE no failure, but at least one thing could not be assessed
6 IO the report could not be written
130 INTERRUPTED interrupted by the user
143 TERMINATED terminated by the system

A WARN alone does not fail a build. certdoctor --version prints the version of the tool and of every schema it emits, so a log from six months ago still says what produced it.

--from turns a saved --json report into another format and ends with 0 once it is written, whatever the report found, so a later step that only makes a page does not fail the build a second time. The verdict's own code stays in the report as verdict.exit_code. On Windows PowerShell 5.1, save the JSON with --out rather than >, which writes UTF-16 there.

- name: can the build agent reach the registry
  run: certdoctor https://registry.example.com/ --redact --json --out certdoctor.json

Tools

Beside the diagnosis, certdoctor has small tools for what stands around a connection. Each reads only what you give it, connects to nothing and changes nothing. certdoctor tool lists them, and the window has them under Tools on its bar.

The certificate decoder reads a certificate or a whole chain - a .pem or .crt file, the output of openssl s_client -showcerts, base64 of one certificate or an x5c array, or a DER file - and shows each certificate: who it is for and who issued it, its host names, when it is valid and how long is left, its key and signature, and the fingerprints you compare elsewhere - SHA-256, the SHA-1 Windows shows as the thumbprint, and the SPKI pin used for pinning. It says whether the chain is complete and in order, and warns about an expired certificate, an MD5 or SHA-1 signature, a short RSA key and a server certificate with no host name in it. A private key pasted along with the certificates is not read and not shown. It checks nothing against a trust store - the diagnosis does that for a server.

certdoctor tool cert --file chain.pem
openssl s_client -showcerts -connect example.com:443 </dev/null | certdoctor tool cert
certdoctor tool cert --json < server.crt

A chain has many lines and no single Enter that ends it, so it is piped in or named with --file rather than pasted at a prompt.

The JWT decoder shows a token's header and claims in the order the token has them, says when the token was issued, from when it is valid and when it expires, and warns about what makes libraries disagree: alg: none, a stripped or wrongly shaped signature, a name given twice, padding or standard base64. It never checks the signature - that needs the issuer's key - and says so every time.

certdoctor tool jwt                     # paste the token, then press Enter
certdoctor tool jwt --file token.txt
certdoctor tool jwt --json < token.txt

A token is never taken as an argument: an argument stays in your shell's history and in the list of running programs. Bearer in front and quotes around it are taken off, and the output says so. On Linux and macOS nothing appears while you paste: the terminal's own line handling cuts a long line or stops taking keys, so the token is read key by key, and after Enter the command says how many characters it read. Every tool ends with 0 whenever it read its input, whatever it found there, and with 3 when what it was given is not something it can read.


Honest limits

A tool that quietly fails to look at something is worse than one that says what it cannot do.

  • The trust matrix knows seven families today - the platform store on Windows and Linux, Java, git, curl, Node.js, Python and pip. OpenSSL comes next, then .NET and PowerShell, Docker, Maven and Gradle, and apt. A Python started through an app alias in WindowsApps was measured with the Python install manager, not with Python from the Microsoft Store. OpenSSL's strict rules are applied as OpenSSL 3 has them, and two of them were measured against Python - the rest are read from OpenSSL's source. A family it does not know is not asked, and the report lists the families it asked - so a missing row means nobody looked, never that nothing is there.
  • On macOS the system keychain is not read yet, so the platform store has no row there.
  • Container diagnosis is not built. --in, --image and --compare are accepted and refused with a reason. Until they work, copy the binary into the container and run it there.
  • Proxy auto-discovery (WPAD) is not performed. On a machine where it is switched on, the proxy layer says it could not establish which proxy applies, rather than claiming there is none - so a healthy target there ends with exit code 5, not 0.
  • It names an inspection product only when the certificate says so in a way the vendor publishes - today mitmproxy and Cisco Umbrella. Anything else is reported as an authority local to this machine, held by some of its stores, with the issuer and the fingerprint. From one connection, an inspecting proxy and your own company's certificate authority look identical, and telling somebody "you are being intercepted" when they are not is harm, not a cosmetic mistake.
  • No revocation checks. OCSP and CRL would mean connecting somewhere you did not point it at.
  • Linux has been measured in containers, not on a desktop distribution.
  • The window exists for three systems - Windows and Linux on x64, macOS on Apple silicon. On Windows or Linux on Arm, use the command line.
  • This is a 0.x release. The JSON schema will keep moving until 1.0.0, and the report says so in its notes.

What it will never do

certdoctor diagnoses. It does not repair.

It never writes to a trust store, never edits a configuration file, and never runs the commands it generates. It prints them for you to read and run yourself. This is a deliberate limit, not a missing feature - a diagnostic tool that quietly modifies trust stores is a tool nobody should grant administrator rights to.

It also connects only to what you pointed it at - the target URL, the addresses it resolves to, your configured resolvers and proxies, and the PAC file your machine names or you name with --pac-url. There is no telemetry, no update check and no reporting of any kind.


Your report contains internal data

The report includes internal host names, file system paths and user names, because that is what makes it useful in a ticket. --redact masks them.

Credentials are different: proxy user info, authorization headers and cookies are always masked, with or without --redact, because a masking that depends on a flag is one forgotten flag away from a leak.

Read a report before you paste it somewhere. Masking is a filter, not a guarantee.


Questions

Does it change anything on my machine?

No. It reads, connects where you pointed it, and writes one report if you ask for one with --out. The commands it prints are for you to run, after reading them.

The window keeps one thing for itself: the folder you last saved a report to, in a file called preferences.json in your user folder - AppData\Roaming\fyne\dev.donislaw.certdoctor on Windows, ~/.config/fyne/dev.donislaw.certdoctor on Linux (under $XDG_CONFIG_HOME when that is set) and ~/Library/Preferences/fyne/dev.donislaw.certdoctor on macOS. Fyne, the toolkit the window is built with, creates that folder when the window starts, and an empty one of the same name in your user cache folder. Delete both when you remove the window.

Why not just openssl s_client or curl -v?

Both answer for themselves. openssl s_client tells you what OpenSSL's store thinks, and the Java service that fails does not use that store. certdoctor asks every store it can find the same question about the same chain, and the answer is usually that they disagree.

Will it tell me my company is intercepting my traffic?

Only when the certificate names a product it recognises. Otherwise it says who signed the certificate and how many stores on your machine trust that authority - which is exactly what your own company's certificate authority looks like too.

Does it need administrator rights?

No. It runs as whoever started it, and the report records which rights it ran with, because what a store looks like can depend on them. A printed fix says when running it needs administrator or root rights.

Is it free? Can I use it at work?

Yes to both. It is GPL-3.0 and costs nothing. The reports you generate belong to you.


Found a case it gets wrong?

That is the most useful issue you can open. Attach the report made with --redact --json, say what the right answer was, and name the runtime that disagreed - the Wrong answer form asks for each of them. Security problems go through SECURITY.md instead.


License

Copyright (C) 2026 DonislawDev. The project is built with an AI-assisted workflow.

Released under the GNU General Public License, version 3. The full text is in LICENSE. The binary also contains code by others - the Go runtime and five modules behind the PAC evaluator - and their notices are in THIRD-PARTY-NOTICES.md. certdoctor --license prints both from the binary. The window contains more - Fyne and the libraries it draws with, the Inter typeface, and on Windows the Mesa software renderer next to it - and its notices are in THIRD-PARTY-NOTICES-GUI.md.

Reports and any other output you generate with certdoctor belong to you and are not derivative works of this program.


If it saves you an afternoon, a star helps the next person find it - and you can support the project.

Keywords: PKIX path building failed · unable to find valid certification path to requested target · certificate verify failed unable to get local issuer certificate · x509 certificate signed by unknown authority · SSL certificate problem self signed certificate in certificate chain · corporate proxy certificate error · TLS inspection troubleshooting · keytool import certificate into cacerts · which JDK cacerts · git SSL certificate problem Windows · PAC file test · 407 Proxy Authentication Required

About

Find out why this machine cannot connect to a URL - PKIX path building failed, unable to get local issuer certificate - and what to run to fix it. One run walks DNS, proxy and PAC, TCP and TLS, asks every trust store on the machine (system, Java, git, curl, Node.js, Python, pip) and prints the fix for each one that refuses. GUI + CLI

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages