⚠️ Installers live on Cloudflare R2 (download-osh.moninotes.com) · iOS TestFlight link pending Beta Review approval
Zero-Trust SSH

Log in to servers
with no passwords.

Short-lived SSH certificates — requested by your laptop, approved on your iPhone, signed by a YubiKey-backed CA. Nothing static to steal.

zero-trust-flow
[ OSH Approver · iPhone ] [ CA Signer · YubiKey ] │ approve (Face ID) │ sign cert (PIV 9c) ▼ ▼ [ OSH Client ] ──▶ [ Gateway ] ──────────────┘ │ request cert │ issue short-lived cert └──────────────────┴──────────▶ [ Target server ] # TrustedUserCAKey

Download

Pick your role in the system

macOS builds are signed & notarized — open them directly. Always download through a browser to keep the signature intact.

Gateway Available

Server (Linux) — the broker that issues certs

Ubuntu / Debian · amd64 .deb Download
Ubuntu / Debian · arm64 .deb Download
RHEL / Rocky · x86_64 .rpm Download
RHEL / Rocky · aarch64 .rpm Download

OSH Approver Pending review

iOS — approves & signs requests (Secure Enclave + Face ID)

iPhone · TestFlight TestFlight

Public build is pending Beta App Review (~24–48h). Until then, invite testers via Internal (send your Apple ID).

CA Signer Cross-platform

Signs certificates with a YubiKey (PIV slot 9c)

macOS · Apple Silicon .dmg Download
Windows .exe Download
Ubuntu · amd64 .deb Download

OSH Client Cross-platform

Workstation — where you run ssh to get a cert

macOS · Apple Silicon .pkg · GUI + CLI Download
Windows .exe Download
Ubuntu · amd64 .deb Download

OSH CLI Terminal

Just the osh command — servers / headless / scripts (no GUI)

Ubuntu/Debian · amd64 .deb Download
Ubuntu/Debian · arm64 .deb Download
RHEL/Rocky · x86_64 .rpm Download
RHEL/Rocky · aarch64 .rpm Download
Windows · x64 .zip Download

macOS: osh is already inside the OSH Client .pkg above.

Install

Get running in a few steps

Four pieces on four machines. The Gateway is a Linux server (the broker); the OSH Client, CA Signer & iPhone Approver are cross-platform — see the apps block below. Copy-paste per platform.

gateway — linux server
# ═══ Option A · ONE COMMAND (recommended) — installs, writes a SQLite config, auto-generates a CA token, starts ═══ # YOUR_SERVER = the address clients reach this box at — its LAN or public IP, or a domain. NOT localhost / 127.0.0.1. # e.g. http://192.168.1.50:8443 (same LAN) · http://203.0.113.10:8443 (public IP) · https://gw.example.com (domain) curl -fsSL https://download-osh.moninotes.com/install.sh | sudo sh -s -- --public-url http://YOUR_SERVER:8443 # SQLite single-node · no PostgreSQL. Public HTTPS domain? add: --autocert gw.example.com --public-url https://gw.example.com # (bare IP = http only — Let's Encrypt needs a domain.) Then watch for the root-enroll QR: sudo journalctl -u pam-zta-gateway -o cat --no-pager -n 300 # full QR: -o cat strips the log prefix, -n 300 shows past lines (-f alone tails only 10 → QR cut off) · widen the window if it wraps # ═══ Option B · MANUAL (advanced) — if you downloaded the .deb / .rpm above ═══ # 1) Install — --no-install-recommends keeps PostgreSQL OUT (SQLite needs no DB server) sudo apt install --no-install-recommends ./pam-zta-gateway_0.2.0_amd64.deb # Ubuntu / Debian sudo dnf install --setopt=install_weak_deps=False ./pam-zta-gateway-0.2.0-1.x86_64.rpm # RHEL / Rocky / Fedora # 2) Point it at SQLite + set a CA token (writes the whole config) sudo sh -c 'cat > /etc/pam-zta/gateway.toml' <<EOF [server] listen_addr = ":8443" public_url = "http://YOUR_SERVER:8443" # IP or domain clients reach (NOT localhost) — goes into the enroll QR [ca] token = "$(openssl rand -hex 32)" [database] driver = "sqlite" path = "/var/lib/pam-zta/gateway.db" [cert] ttl = 300 [gateway_key] key_path = "/var/lib/pam-zta/gateway_key.pem" EOF sudo chown root:pam-zta /etc/pam-zta/gateway.toml && sudo chmod 640 /etc/pam-zta/gateway.toml # 3) Start + watch for the root-enroll QR sudo systemctl enable --now pam-zta-gateway sudo journalctl -u pam-zta-gateway -o cat --no-pager -n 300 # full QR: -o cat strips the log prefix, -n 300 shows past lines (-f alone tails only 10 → QR cut off) · widen the window if it wraps grep token /etc/pam-zta/gateway.toml # hand this CA token to the CA operator # ─── Connect to this gateway ─── # OSH client → osh enroll --force --gateway http://SERVER:8443 (first time: register the device) # osh connect root@SERVER --gateway http://SERVER:8443 # CA Signer → ws://SERVER:8443/v1/ca/ws + the [ca].token above # HA / multi-node? Use PostgreSQL instead of SQLite ([database] host/user/... ).
desktop & ios — apps
# macOS — CA Signer (.dmg) Open the .dmg ▸ drag "PAM-ZTA CA" to Applications ▸ open ▸ plug YubiKey ▸ enter PIN (PIV 9c) ▸ "Save PIN to Keychain". # macOS — OSH Client (.pkg · all-in-one: GUI app + `osh` command) Double-click OSH-1.2.0-arm64.pkg ▸ Install. # notarized · installs the OSH app + the `osh` CLI into PATH osh connect dev@your-server # or open the OSH app and click Connect · (new terminal: run `rehash`) # Windows — CA Signer / OSH Client (.exe · not yet code-signed) Run the .exe ▸ on SmartScreen "Windows protected your PC" ▸ More info ▸ Run anyway. # Ubuntu/Debian — CA Signer / OSH Client (.deb · amd64) sudo apt install ./CA-0.1.0-amd64.deb # CA Signer (Operator Console) sudo apt install ./OSH-0.1.0-amd64.deb # OSH Client # iOS (OSH Approver) 1. Install TestFlight from the App Store. 2. Open the TestFlight link above ▸ Install OSH. 3. Open OSH ▸ scan the enrollment QR from the Gateway ▸ approve with Face ID.
target server — trust the CA (TrustedUserCAKeys)
# ONE command from your laptop — curl runs LOCALLY and pipes the script over ssh (target needs no internet). # Fill in 2 things: # ubuntu@YOUR_TARGET = the ssh login you ALREADY use on that server (needs sudo) — root@... works too # BASE64_FROM_GUI = CA Signer ▸ Settings ▸ Export Public Key ▸ Copy — paste as-is (no ssh-ed25519 prefix) curl -fsSL https://download-osh.moninotes.com/install-pam-zta-ca.sh | ssh ubuntu@YOUR_TARGET 'sudo bash -s BASE64_FROM_GUI' # e.g. — server 203.0.113.10, you normally ssh in as root, key copied from the GUI: curl -fsSL https://download-osh.moninotes.com/install-pam-zta-ca.sh | ssh root@203.0.113.10 'sudo bash -s AAAAC3NzaC1lZDI1NTE5AAAAIG43Man3DUHiddGccOJM897B/BaSm3tgSh09z3blL/hz' # Installs /etc/ssh/pam-zta-ca.pub + a sshd drop-in, validates (sshd -t), then RELOADS — no restart, # no dropped sessions, authorized_keys untouched. Idempotent — safe to re-run. # Expect: trustedusercakeys /etc/ssh/pam-zta-ca.pub · the CA fingerprint (match it in the GUI) · INSTALL_OK # To log in, the cert user must EXIST on the target (e.g. ubuntu) and match an access rule — and port 22 reachable. # Undo: sudo rm -f /etc/ssh/sshd_config.d/50-pam-zta.conf /etc/ssh/pam-zta-ca.pub && sudo systemctl reload ssh
using osh — enroll once, then connect
# 1) First time on a gateway: point osh at it, then enroll this device (an admin approves it once). export OSH_GATEWAY=https://your-gateway:8443 # add to ~/.bashrc → skip --gateway every time osh enroll --force # registers this device · an admin approves it in the App Signer osh status # optional: expect status:ok, ca_connected:true # 2) Log in — ONE command. The login user must MATCH an access rule (e.g. root — not just any name): osh root@10.0.0.5 # short for: osh connect root@10.0.0.5 # • osh asks the Gateway for a certificate for root@10.0.0.5 # • your iPhone buzzes → review who / where / what → Approve with Face ID # • the CA (YubiKey) signs a short-lived cert → you land in the SSH session ✅ # (session recorded to ~/.osh/sessions/ · cert expires in minutes · nothing static left behind) # Rejected? 401 = device not enrolled on THIS gateway → re-run osh enroll --force # 403 = device not approved yet, or the login user doesn't match a rule
🔐CA Signer requires a YubiKey (hardware-only, fail-closed). Pull the YubiKey and signing stops — exactly by zero-trust design.