A mutual-TLS (mTLS) authentication server written in Go, built as a portfolio and learning project.
Every user authenticates with a client certificate that the server cryptographically verifies, instead of relying on a password or token alone. It runs as a single Go binary with a small web UI, and a setup wizard that gets you from a fresh clone to a running server in a few commands.
- Certificate-based login — users authenticate with a CA-signed client certificate plus a password.
- Device enrollment — users generate a key and CSR, submit a request, and an admin approves and signs it. The private key never leaves the user's machine.
- Permission-based access — granular permissions (
system_admin,manage_devices,manage_users,manage_pending,view_audit_logs) gate every page and endpoint. - Instant device revocation — revoke a device with one toggle and it's denied on its next request.
- User & device management — review and remove users and devices from the browser.
- Audit logging — every login and access event is logged as structured JSON, with a built-in filterable, color-coded log viewer.
The client certificate is the identity anchor. Everything is derived from it on every request:
client certificate ──► SHA256 fingerprint ──► device ──► owner ──► permissions
Logging in:
- The browser presents a client certificate during the TLS handshake. If it isn't signed by the server's CA, the connection is refused so unenrolled clients can't get in at all.
- The server hashes the certificate into a fingerprint and looks it up to find the device, its owner, and that user's permissions.
- The user submits their password. The server checks it and checks that the certificate belongs to the same user, so a valid certificate can't be used to log in as someone else.
- On success, a session is created server side. The session stores the certificate's fingerprint, and the browser gets only a random session ID in a cookie.
Every request after that:
- The server recomputes the fingerprint from the certificate presented on this request and confirms it matches the one bound to the session. A stolen cookie is useless without the matching certificate and key.
- Permissions are looked up fresh from the certificate every time, never cached in the session.
Because authorization is re-derived from the live certificate on every request, revoking a device takes effect immediately, the fingerprint stops resolving, and the next request is denied with no sessions to clear.
Requirements: Go 1.22+. Run all commands from the project root (the folder containing
data/,certs/, andstatic/).
git clone https://github.com/powplowdevs/zero-trust-mtls-server.git
cd zero-trust-mtls-serverThe wizard will handle creating a first admin and its certificates along with the Certificate Authority and the server's own TLS certificate.
go build -o setup ./setup
./setupThe wizard will:
- Generate a Certificate Authority (
certs/ca.crt+certs/ca.key), the root of trust for every certificate. (Re-running reuses the existing CA by default; pass--new-cato regenerate, which invalidates all existing certificates.) - Generate the server's TLS certificate, automatically valid for
localhost,127.0.0.1, and your machine's detected LAN IP. - Create the first admin account, prompts for a device name and password (minimum 8 characters, with at least one uppercase letter and one number).
It writes the admin's certificate bundle to certs/admin.p12 and prints import instructions.
The admin (and every user) logs in with a certificate held by their browser. Import certs/admin.p12:
- Firefox: Settings → Privacy & Security → Certificates → View Certificates → Your Certificates → Import → select
certs/admin.p12 - Chrome / Edge: Settings → Privacy and security → Security → Manage certificates → Import → select
certs/admin.p12
The
.p12has an empty export password for convenience. After importing, you can deletecerts/admin.crtandcerts/admin.p12from the server, keepcerts/admin.keyprivate and never commit it.
When an admin approves an enrollment, the signed certificate is emailed to the user. Set your SMTP details in config.json:
Use an App Password, not your real password. Most providers (Gmail included) won't accept your normal account password for SMTP. Enable 2-factor authentication on the account, generate a dedicated App Password, and use that as
smtp_password.
You can skip this for local testing, but enrollment approvals won't be able to deliver certificates until SMTP is configured.
go build -o zts .
./ztsThe server starts on https://localhost:8443. Open it, present the admin certificate when prompted, and log in as admin with the password you set.
Once you're logged in as admin, other users enroll through the web UI:
- They visit
/enroll, follow the steps to generate a key and CSR, and submit the request. - You review it under Pending, grant permissions, and approve, the server signs their certificate and emails it.
- They import their certificate and log in.

{ "smtp_host": "smtp.gmail.com", // your SMTP server "smtp_port": "587", // typically 587 (TLS) or 465 (SSL) "smtp_username": "you@example.com", // the sending account "smtp_password": "your-app-password" // see note below }