macOS · Identity-Aware Proxy
Reach a VM with no public IP, without running a bastion host.
Ostgate is a native macOS client for Google Compute Engine over Identity-Aware Proxy: SSH terminals, RDP desktops and arbitrary TCP forwards into private instances. It runs entirely on your Mac and talks to Google's APIs as you. There is no Ostgate server in the path.
Requirements
- Hardware
- Apple Silicon Mac, arm64 only
- Operating system
- macOS 26.3 or later
- IAM role
roles/iap.tunnelResourceAccessoron the target project- Project setup
- IAP TCP forwarding enabled, with firewall ingress from
35.235.240.0/20
What it does
One window for every way into a private instance.
Project and instance browser
Sign in once and browse the projects, zones and instances your Google account can already see. Multiple Google accounts run side by side as separate profiles, each with its own window, tokens and tunnels.
SSH terminal tabs
A full terminal per instance, opened over an IAP tunnel. Keys are generated on your Mac and published through OS Login or instance metadata; nothing needs a public IP.
Embedded RDP
Windows desktops open as tabs in the same window, with shared-folder redirection, clipboard sync and resize that follows the window.
Tunnel manager
Forward arbitrary TCP ports from an instance to 127.0.0.1: a database,
an admin console, an internal web app. Every open tunnel shows up in one list.
And your existing ssh, when you want it
The app is the point, but it does not want to be in the way. One click in
Settings writes a single block into your ~/.ssh/config that
points at the app bundle, and from then on the alias resolves anywhere
ssh does: a terminal, an IDE's remote session, scp,
a script, with Ostgate closed.
$ ssh web-01.europe-west1-b.acme-prod.gcp you@web-01:~$
Bastion forwards, local ports, hosts
Reach anything a chosen instance can see, inspect and clear stuck loopback
listeners, and manage /etc/hosts aliases, without dropping to a shell.
Today, with gcloud
$ gcloud compute start-iap-tunnel web-01 22 \ --local-host-port=localhost:2222 \ --zone=europe-west1-b --project=acme-prod # leave that running, open another terminal $ ssh -p 2222 you@localhost
One terminal held hostage per tunnel, the port picked by hand, and nothing to tell you why it failed when it does.
With Ostgate
$ ssh web-01.europe-west1-b.acme-prod.gcp
Or double-click the instance in the browser. Ports are allocated and released for you, every tunnel is listed in one place, and a failure names the IAM role you are missing.
What this actually unlocks
Anything one VM can see, you can reach from localhost.
The tunnel ends at an instance, but it does not have to stop there. Pick a VM as a jump host and Ostgate forwards whatever that VM can route to, several ports at once, over a single SSH session through IAP. No jump box to maintain, no VPN, nothing exposed to the internet.
Open an internal-only service in your own browser.
Internal Application Load Balancers have no public address by design, and that is exactly the posture Cloud Run with ingress: internal and private GKE services are supposed to have.
Point the SQL client you already use at 127.0.0.1.
Cloud SQL on a private IP, Memorystore, or a self-hosted Postgres on an instance with no external address. Nothing about your tooling has to change.
Bring your own key where OS Login does not apply.
Firewall and network appliances, a Check Point Gaia gateway among them, run local accounts and do not speak OS Login.
instance browser
terminal session tab
How it works
Three steps, none of which pass through a server of ours.
01
You sign in
Ostgate opens your system browser for Google's own consent screen, using a loopback redirect with PKCE. There is no embedded web view. The refresh token it receives is stored in your macOS Keychain.
02
A relay opens as you
To reach an instance, the app opens a WebSocket to Google's IAP tunnel endpoint at
tunnel.cloudproxy.app, authorised with your own access token. Google
checks your IAM permissions on that instance, not ours.
03
A local port is bound
The tunnel surfaces as a listener on 127.0.0.1. Every connection to it is
screened by peer process before it is wired through, so another program on your Mac
cannot quietly ride your tunnel.
Under the hood
The relay protocol, written down.
Google does not document the IAP TCP forwarding relay. The two open-source
clients that speak it, the gcloud SDK and IAP Desktop, are the
de-facto specification, and Ostgate is implemented from them. This is the
reference we work from. It can change without notice, so our codec is
table-driven and ignores tags it does not know.
Frame layout · DATA
Everything is big-endian. A whole message is therefore at
most 6 + 16384 bytes. There is no multiplexing: one WebSocket
carries exactly one local TCP connection.
| Tag | Name | Payload |
|---|---|---|
| 1 | CONNECT_SUCCESS_SID | uint32 length, then the opaque session id |
| 2 | RECONNECT_SUCCESS_ACK | uint64 ack |
| 4 | DATA | uint32 length, then 1 … 16384 bytes |
| 7 | ACK | uint64 cumulative bytes received |
| 10 | LONG_CLOSE | uint32 close code, uint32 length, UTF-8 reason |
Tags 0, 3, 5, 6 and 8 are unused. Flow control counts cumulative bytes, not messages: an ACK rides along with outgoing data, and a standalone one is forced once the unacknowledged count grows past the window.
Close codes, triaged
How a close code is handled is the difference between a session that silently dies and one that tells you what to fix.
-
Fatal
40014002400340334047405140744033is the one you will actually meet: it means you are missingroles/iap.tunnelResourceAccessor, and Ostgate says so in those words instead of reporting a closed socket. -
Normal end of stream
100040094010The destination stopped reading or writing. That is a logout, not a failure, and it must not trigger a reconnect storm.
-
Retryable
1006400040044005–40084013Reconnect with the session id and the byte count received so far, then replay everything the server has not acknowledged.
4004means the token expired; the tunnel survives a refresh.
Data and permissions
Every scope Ostgate asks for, and exactly what it does with it.
Ostgate requests three OAuth scopes at sign-in. It uses them to call Google's APIs as you, from your Mac.
| Scope | What it is used for |
|---|---|
| openid | Identifies which Google account signed in, so the app can keep several accounts apart as separate profiles. |
| Reads your account email address. It is used as the OS Login username when opening an SSH session, and shown in the account list so you can tell profiles apart. | |
| .../auth/cloud-platform | Calls Compute Engine, Resource Manager, OS Login and IAP TCP forwarding on your
behalf: list the projects, zones and instances you already have access to; open
IAP tunnels to an instance; publish your SSH public key through OS Login or
instance metadata; and, for Windows RDP, write windows-keys instance
metadata and read serial-port output in order to set a session password. |
Where your data goes: nowhere but Google. Ostgate has no backend. Your tokens are held in the macOS Keychain on your own machine, every API call goes directly from your Mac to Google, and no usage data, telemetry or crash reporting is collected by us. Signing a profile out deletes its stored credentials from the Keychain.
The single Google Cloud scope is broad because it is the scope that authorises the Compute Engine, Resource Manager, OS Login and IAP TCP forwarding calls listed above.
Security
The parts that would be embarrassing to get wrong.
Credentials in the Keychain
Refresh tokens and key references live in the macOS Keychain, namespaced per profile. Never in preference files, never on disk in the clear, never in logs.
Keys that cannot be exported
SSH user keys are generated in the Secure Enclave as P-256 where the hardware allows it, so the private key never exists in a form any process can copy.
Loopback only, and screened
Tunnel listeners bind 127.0.0.1 and nothing else. Each accepted
connection is attributed to a peer process and checked against a policy before it
reaches the relay; sharing a tunnel with other programs takes an explicit,
per-tunnel confirmation.
OAuth the way Google asks for it
Desktop-type client, your real system browser, loopback redirect, PKCE and a
state check. No embedded web views and no custom URI schemes.
Auditable supply chain
The SSH stack resolves from owner-controlled mirrors rather than upstream tags that can move, and every bundled third-party licence ships with the app.
Will ship signed and notarized
Release builds will be signed with a Developer ID certificate and notarized by Apple, so macOS can verify the download before it ever runs.
Download
Ostgate for macOS.
A disk image for Apple Silicon Macs running macOS 26.3 or later. Drag the app to Applications, launch it, and sign in with the Google account that already has access to your projects.
Coming with the first release
Licensing
Free while in beta.
Ostgate is free to download and use during the beta. If that changes, existing installations will keep working and the terms will be updated here first.
Questions
The six things people ask first.
Does this replace gcloud?
No. It covers the connectivity part (tunnels, SSH, RDP, port forwards) with a native interface instead of a command line. Everything else you do with gcloud, you still do with gcloud, and the two can be signed in to the same account at once.
Do I still need a bastion host?
No, and that is the point. Identity-Aware Proxy carries the connection to instances that have no external IP, so there is no jump box to run, patch or pay for.
What IAM permissions do I need?
roles/iap.tunnelResourceAccessor on the instances you want to reach, plus
whatever read access you already have to list projects and instances. For SSH, the
usual OS Login roles. Ostgate cannot grant you anything you do not already have. It
acts strictly as you.
Does it work with context-aware access?
The relay supports the mutual-TLS tunnel endpoint that context-aware access requires. Whether a given policy admits your device is decided by your organisation's access rules, not by Ostgate.
Can you see my data, my sessions or my machines?
No. There is no Ostgate server for anything to pass through. The app talks straight from your Mac to Google's APIs, and we collect no telemetry.
Where do my tokens live, and how do I get rid of them?
In the macOS Keychain, under a per-profile entry. Signing the profile out removes it. You can also revoke the app's access from your Google Account permissions page at any time, which invalidates the token regardless of what is on disk.