Secure SSH with Pomerium Zero
What this guide does
You'll put your SSH servers behind Pomerium Zero so users authenticate with your identity provider and connect over standard SSH, with no VPN, no bastion host, and no long-lived keys to manage. Pomerium acts as a native SSH reverse proxy: it terminates the SSH connection, runs the user through OAuth, and presents a short-lived, certificate-based identity to the upstream server. Your target servers only have to trust one certificate authority (CA) that Pomerium holds.
The whole flow is driven from the Zero Console, which manages the SSH listener address, host keys, and signing CA for your cluster. You generate a few keys locally once, paste them into the Console, trust the CA on your servers, and from then on access is policy-controlled from one place.
Try the Native SSH Access with Pomerium interactive tutorial on iximiuz labs for a hands-on walkthrough. It's free to use, but you'll need to create an account.
When to use this guide
Use it when you want browser-based single sign-on in front of SSH and short-lived certificate auth on the wire, without changing how people invoke ssh. If you only need a raw, encrypted tunnel to a host on a private network and don't want OAuth in the path, a plain TCP route (covered in TCP routes in Zero) is a simpler fit.
Prerequisites
This guide assumes you've completed the Quickstart, so you already have a Pomerium Zero cluster running and signing users in through the hosted authenticate service.
You also need:
- A Zero cluster on Pomerium 0.30 or higher, since native SSH lands in 0.30.
- Administrative access to the Zero Console.
- An OpenSSH client (
ssh,ssh-keygen) on user machines. - One or more target SSH servers you control, where you can edit
sshdconfiguration as root.
Generate the SSH keys
Native SSH needs two things from you: a User CA key pair that Pomerium uses to sign per-session user certificates, and a set of host keys that Pomerium presents to clients as its own SSH server identity. Generate them once on a trusted workstation.
Create the User CA key pair. The private key goes into the Zero Console; the public key gets distributed to your servers:
ssh-keygen -N "" -f pomerium_user_ca_key -C "Pomerium User CA"
This writes pomerium_user_ca_key (private) and pomerium_user_ca_key.pub (public).
Then generate host keys. Generating all three algorithms gives the broadest client compatibility, but ED25519 alone is fine for modern clients:
ssh-keygen -N "" -t ed25519 -f pomerium_ssh_host_ed25519_key
ssh-keygen -N "" -t rsa -b 3072 -f pomerium_ssh_host_rsa_key
ssh-keygen -N "" -t ecdsa -b 256 -f pomerium_ssh_host_ecdsa_key
The -N "" flag generates the keys without a passphrase, which is required because Pomerium loads them non-interactively.
Configure target SSH servers
On every server you want to reach, tell sshd to trust certificates signed by your User CA. Copy the public CA key to the server, then point TrustedUserCAKeys at it.
Place the public key in any root-owned location, for example /etc/ssh/pomerium_user_ca_key.pub:
sudo cp pomerium_user_ca_key.pub /etc/ssh/pomerium_user_ca_key.pub
sudo chown root:root /etc/ssh/pomerium_user_ca_key.pub
sudo chmod 644 /etc/ssh/pomerium_user_ca_key.pub
This is a public key, so it does not need to be secret; the important part is that it is owned by root and not writable by anyone else. Next, add a drop-in sshd config so you don't edit the main file:
TrustedUserCAKeys /etc/ssh/pomerium_user_ca_key.pub
PubkeyAuthentication yes
Reload sshd so the change takes effect, then confirm it's healthy:
sudo systemctl restart sshd # use "ssh" instead of "sshd" on Ubuntu/Debian
sudo systemctl status sshd
The server now accepts any user certificate signed by your CA. Pomerium controls who actually gets a certificate, and for which username, through policy.
Configure SSH in the Zero Console
In the Zero Console, native SSH is set up through a guided flow that walks you through the cluster-wide SSH settings and your first route together.
Open Manage → Routes and select New Route, then choose Guided SSH Route.

The first time you create an SSH route, the Console prompts for the global SSH settings that apply to the whole cluster:

-
SSH Address is where Pomerium listens for SSH connections, for example
0.0.0.0:22. Pick a different port if something else already owns 22 on the data plane.
-
SSH Host Keys are the host private keys you generated. Paste the full contents of the first key (including the
-----BEGIN OPENSSH PRIVATE KEY-----/-----END OPENSSH PRIVATE KEY-----lines), then use the + button to add the RSA and ECDSA keys. -
SSH User CA Key is the private half of the CA you generated (
pomerium_user_ca_key). Pomerium uses it to sign each session's user certificate.
Both fields appear in the Configure Global SSH Settings step shown above. Paste the private keys, not the .pub files, into the host-key and CA fields. The Console has a How to generate SSH keys? helper if you need the commands again. Select Next to move on to the route.

Now configure the route itself:
- Route Name: a descriptive label, such as
prod-server-ssh. - From: the hostname users will connect to, in
ssh://hostnameform, for examplessh://prod-server. This name resolves to your cluster (the built-in starter domain handles this for you, see below). - To: the upstream server in
ssh://host:portform, for examplessh://10.0.1.100:22.

The Console shows the exact ssh command users will run. Select Next to attach a policy.
Configure access policies
The final guided step secures the route. You can attach an existing cluster policy or create a new one with Allow and Deny blocks. For SSH, leave Public Access off; Any Authenticated User is a reasonable starting point that you can tighten later.

By default an authenticated user can request any Linux username on the target (ssh root@route@... as easily as ssh ubuntu@route@...). Constrain that with SSH-specific Pomerium Policy Language (PPL) criteria in the policy:
ssh_usernamepins the allowed username(s), e.g.ssh_username: ubuntu.ssh_username_matches_claimties the username to an OAuth token claim.ssh_username_matches_emailforces the username to match the local part of the user's email.
For example, ssh_username_matches_email: true ensures each person can only log in as the account matching their own email and never as root. Apply the policy and select Next to finish. The cluster-wide SSH settings are now saved, so additional SSH routes can be created from either the guided flow or a custom route.
DNS and connecting
Your cluster ships with a built-in *.pomerium.app starter domain, so there's nothing to configure to start connecting:
ssh myuser@myroute@your-cluster.pomerium.app
In that command, myuser is the account on the target server, myroute is the route's From hostname, and the final field is your cluster address, which Pomerium uses to route to the upstream. The first connection opens a browser for OAuth login; after that Pomerium issues a short-lived certificate and connects you, with no special SSH client config required.
To use your own domain instead, follow adding a custom domain in Zero. A common convention is an ssh. prefix, so the command becomes ssh myuser@myroute@ssh.company.com.
Verify the setup
- The route requires authentication. From a user machine, run
ssh myuser@myroute@your-cluster.pomerium.app. Pomerium should print a URL (or open a browser) asking you to sign in, rather than connecting straight through. - An allowed user gets a shell. Complete the OAuth login as a user your policy allows. Pomerium signs a short-lived certificate and drops you onto the target server's shell.
- Policy is enforced. If you set a username restriction, try connecting as a disallowed username (for example
ssh root@myroute@...when onlyubuntuis allowed) and confirm Pomerium refuses it.
Common failure modes
- Certificate authentication fails on the target. Confirm
pomerium_user_ca_key.pubis installed on the server, thatTrustedUserCAKeyspoints at it, and that you restartedsshd. Runssh -vvvto see whether a certificate is presented and accepted, and setLogLevel DEBUG3insshd_configplusjournalctl -u sshdfor server-side detail. - You reach the wrong SSH server. If the data plane listens on a non-standard port, make sure you're connecting to Pomerium's port and that the route's From hostname resolves to the cluster, not directly to the upstream.
- Key format errors in the Console. The host-key and CA fields take the full private key with its header and footer lines, generated without a passphrase (
-N ""). Paste the private keys, not the.pubfiles. - Username restriction has no effect. Re-check the
ssh_username*criteria in the route policy, and remember the username must also exist as a real account on the target server. Pomerium decides which usernames are allowed; the Linux account still has to be present.
Security considerations
- The User CA is the trust anchor. Any server that trusts
pomerium_user_ca_key.pubwill accept any certificate Pomerium signs. Keep the private CA key only in the Zero Console, store your local copy securely, and rotate the CA and host keys on your organization's schedule (regenerate, update under Settings → SSH, and redistribute the new public CA key to servers). - Scope usernames with policy. Without
ssh_username*criteria, any allowed user can requestroot. Usessh_username_matches_emailor an explicitssh_usernamelist to prevent privilege escalation to shared system accounts. - Harden the upstream too. Pomerium's certificate auth complements, but doesn't replace, normal server hardening (disable password auth, keep the OS patched). Avoid
fail2banin front of a Pomerium SSH route, since connections arrive from Pomerium's address and you can lock everyone out at once.
Next steps
- Build additional routes for your other servers.
- Build policies and advanced policies for granular, per-route access.
- Review the Native SSH Access capability page for the full configuration reference.
- See the general security best practices for monitoring, segmentation, and policy hygiene.