Skip to content

Repository files navigation

truemtls

Mutual TLS done properly — minus the operational tax.

truemtls puts mandatory mutual TLS in front of any HTTP service, with a trust model you administer by moving PEM files around instead of running a CA appliance. It is a transparent reverse proxy and a small Go library, with zero third-party dependencies (Go stdlib only).

  • Transparent — forwards method, path, query, headers, cookies, and body unchanged. Your backend speaks plain HTTP and needs no changes.
  • Mandatory mTLS — every connection must present a client certificate that is pinned or chains to a trusted authority, or the TLS handshake is rejected.
  • Hand-manageable trust — trusted CAs and pins are just .pem files in a directory. Trust one by dropping it in; revoke it by deleting it.
  • TOFU approval — an unknown client cert is captured to a pending queue and rejected; you approve it out of band (truemtls trust approve …), then it works. Like known_hosts, for client CAs.

Prerequisites: a working Go environment

If you don't do Go development day to day, read this first — it's the #1 reason a freshly go installed tool reports "command not found".

go install writes binaries to $GOBIN (or $GOPATH/bin when GOBIN is unset). If that directory isn't on your PATH, the installed truemtls (and task, below) exist but your shell can't find them. Set this up once in ~/.bashrc:

export GOROOT=/usr/local/go            # the Go toolchain (provides `go`)
export GOPATH="$HOME/go"               # your Go workspace (module cache, etc.)
export GOBIN="$GOPATH/bin"             # where `go install` puts binaries
export PATH="$GOROOT/bin:$GOPATH/bin:$HOME/.local/bin:$PATH"

Then reload and verify:

source ~/.bashrc
go env GOROOT GOPATH GOBIN
  • GOROOT — where the toolchain lives. Usually auto-detected; set it only if go isn't already on your PATH.
  • GOPATH — your workspace, default ~/go.
  • GOBIN — where go install drops binaries. This is the one that must be on PATH, or nothing you install is runnable.

If ~/.bashrc already has Go lines, edit those instead of adding duplicates.

Install

go install github.com/brywil/truemtls/cmd/truemtls@latest

Quick start

Front a service listening on 127.0.0.1:8080 with mandatory mTLS on :8443:

truemtls serve --backend http://127.0.0.1:8080 --listen 0.0.0.0:8443

On first connect, an untrusted client is rejected and queued:

truemtls trust list                       # see the pending cert + fingerprint
truemtls trust approve authority <fp>     # trust its issuing CA, or…
truemtls trust pin <fp>                    # …pin just that one leaf certificate

To trust an existing corporate CA up front, either drop its PEM into ~/.config/truemtls/trust/authorities/ or pass --client-ca /path/to/ca.pem.

Trust model

~/.config/truemtls/trust/
  authorities/   trusted CA certs (one PEM per CA) — a client chaining to any is authenticated
  pinned/        exact leaf certs — self-authenticating, no CA needed
  pending/       unknown certs captured at handshake, awaiting approval

Everything is a file. There is no database and no daemon state to back up.

Optional flags

Flag Meaning
--client-ca FILES comma-separated CA PEMs to trust in addition to the directory
--client-id-header H set header H to the verified client CN before forwarding (any inbound value is stripped first, so it can't be spoofed)
--no-xforwarded do not add X-Forwarded-* headers (byte-for-byte transparency)
--backend-insecure skip TLS verification to an https backend

The server provisions its own self-signed server certificate on first run (~/.config/truemtls/server.{crt,key}); replace those files to use your own.

Library use

store, _ := trust.Load("~/.config/truemtls/trust", nil, log.Default())
cert, _ := tls.LoadX509KeyPair("server.crt", "server.key")

srv := &http.Server{
    Addr:      ":8443",
    Handler:   myHandler,
    TLSConfig: truemtls.ServerTLSConfig(store, cert), // requires + enforces mTLS
}
srv.ListenAndServeTLS("", "")

store.Verify is a drop-in tls.Config.VerifyPeerCertificate: unknown certs are queued to pending/ and rejected; trusted ones pass. Authorization (what an authenticated principal may do) is intentionally out of scope — layer it on top by the client-cert CN. (See mymcp for an example that gates MCP tools per CN.)

Build & run as a user service

Uses go-task. Install it in one line — to /usr/local/bin, which is always on PATH, so no Go or PATH setup is needed:

sudo sh -c "$(curl -sL https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin
task --version

(If curl is missing: sudo apt install -y curl. Alternatively, with a working Go env — see Prerequisitesgo install github.com/go-task/task/v3/cmd/task@latest.)

Then:

task build            # -> build/truemtls
task test             # go test ./...
task install          # -> ~/.local/bin/truemtls
task install-unit     # systemd --user service (no sudo); enables but doesn't start
# edit ~/.config/truemtls/truemtls.env (BACKEND, LISTEN), then:
systemctl --user start truemtls
task deploy           # build + test + install + restart the user service if present

Everything is per-user: the binary lands in ~/.local/bin, the unit in ~/.config/systemd/user/, config in ~/.config/truemtls/. No root required.

Security notes

  • Because a CN may be honored regardless of which trusted CA issued the cert (a common desire so re-issuing a user's token/CAC does not require re-onboarding), only put CAs you control in authorities/ — any of them can assert a CN.
  • A pinned leaf is trusted by exact certificate bytes; its validity window is still enforced, but it is not tied to any issuer.
  • mTLS is mandatory: there is no unauthenticated mode.

Status

Early but working: transparent proxy, directory trust store, TOFU approval, and the library API are implemented and tested. MIT licensed.

About

Wrap anything in real mtls goodness

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages