Hi all,
we've been using zssh for zero-trust SSH and really like how it works. One thing that bugs us is the disconnect between Ziti identity management and SSH authentication. Right now, Ziti handles the network layer (who can reach the SSH service), but SSH authentication is completely separate; users still need to manage SSH keys, authorized_keys files, and so on. If you revoke someone's Ziti access, their SSH key might still be sitting in authorized_keys on the server.
What we want:
If Ziti says a user has access to the SSH service, that should be the only thing needed. No separate SSH key distribution, no per-user authorized_keys maintenance, and immediate revocation when Ziti access is removed.
We've been thinking through a few approaches and wanted to get input from the community.
zssh currently works in two phases:
- Authenticate to Ziti, dial the SSH service through the overlay
- Authenticate to sshd with a completely separate SSH key
These two credential sets are independent. Adding or removing a user means touching both Ziti policies and SSH authorized_keys on every server. We'd like phase 2 to just follow from phase 1.
In my opinion there are 3 approaches to that:
Approach A: Controller-Signed SSH Certificates
The Ziti controller already has a CA and signs x509 certificates for every identity. The same key could sign SSH-format certificates. If the controller exposed an endpoint for SSH certificate signing (same CA key, different serialization), the flow would be:
- zssh authenticates to Ziti
- zssh requests a short-lived SSH certificate from the controller
- zssh dials the SSH service and authenticates with the SSH certificate
- sshd validates against the controller's CA public key via
TrustedUserCAKeys
Revocation is immediate and the controller refuses to sign if the identity is revoked. Server setup is one line in sshd_config, not per-user.
This would require a new controller endpoint, which is why we're bringing it up here. This one is my favorite, and I believe also what cloudflare does: SSH with Access for Infrastructure ยท Cloudflare One docs
Approach B: Custom Go SSH Server as Ziti Host
Instead of using ziti-edge-tunnel + sshd, replace both with a single Go binary that's a Ziti host and SSH server (using x/crypto/ssh). It accepts Ziti connections via the SDK's Listen API, reads the caller's x509 certificate directly from the mTLS handshake, and makes SSH authorization decisions based on that.
No format conversion needed, the server has the x509 cert, trusts the Ziti CA, and authorizes accordingly. Revocation works because Ziti won't let a revoked identity dial the service in the first place, and even if the connection somehow arrived, the cert validation would fail.
The downside is replacing sshd with a custom implementation. That's a meaningful tradeoff in terms of audit surface and feature completeness.
Approach C: Change ziti-edge-tunnel to bridge Identity to sshd
Keep sshd, but extend ziti-edge-tunnel for SSH services. This ziti-edge-tunnel:
- Accepts Ziti connections via the SDK's Listen API
- Extracts the connecting identity's x509 cert and public key
- Converts the public key to SSH format and writes it to a per-connection authorized keys source
- Proxies the connection to sshd, which uses
AuthorizedKeysCommandto read from that source
The identity context and the information that a client connects to an ssh server should be available because the connection gets terminated locally. sshd stays standard, and revocation works because the Ziti host only proxies connections from identities that pass Ziti's authentication and authorization.
More moving parts than Approach B, but keeps OpenSSH in the picture.
Has anyone solved this or something similar?
For Approach A: is there appetite for an SSH cert signing endpoint in the controller? It's should be the same CA key that already signs x509 certs, just a different output format.
For Approaches B/C: are there other ways to get the connecting identity's certificate information through the SDK that we might be missing?
Any thoughts appreciated, I'd be open to provide testing & code to a PR ![]()
Best regards
Dominik