SSH into a Cloud Run container from a Mac
Since 7 October 2026 you can SSH into a running Cloud Run container, as a Preview
feature. Turn SSH on for the service with --ssh, run it in the second
generation execution environment, and run gcloud in Terminal. gcloud tunnels the session through Identity-Aware Proxy
and logs you in as root with a certificate that expires after five minutes. You get a shell
even when the image has none, but only the shell: in Google's
hello sample image, ls and id did not exist.
By Ostgate · Updated
Preview. Google lists SSH for Cloud Run services and instances as Preview in the
Cloud Run release notes
of 7 October 2026. Pre-GA features “are available "as is" and might have limited support”
(SSH for Cloud
Run), and the gcloud commands sit under gcloud beta.
The test
We ran this on 12 November 2026 from a Mac with Google Cloud SDK 590.0.0 (beta component
2026.10.16) installed by Homebrew. The service was Google's sample image
us-docker.pkg.dev/ in europe-central2,
deployed with --no-allow-unauthenticated, minimum instances 0 and maximum 1. The
account had the Owner role on the test project. We deleted the service seven minutes after
creating it. Project IDs in the output below are replaced with acme-prod.
Turn on SSH for the service
SSH needs the Cloud Run Admin, Cloud Resource Manager, Identity-Aware Proxy and Cloud OS Login APIs in the project:
gcloud services enable run.googleapis.com \
cloudresourcemanager.googleapis.com \
iap.googleapis.com \
oslogin.googleapis.com \
--project=acme-prod
Then turn SSH on for the service. It is a service setting, the annotation
run.googleapis.com/, so the update below created no new
revision in our test. gcloud beta run deploy takes the same --ssh
flag, and --no-ssh turns it off again:
gcloud beta run services update example-api --ssh \
--region=europe-central2 --project=acme-prod
SSH works only in the second generation execution environment. gcloud accepted
--ssh on our first generation service without a warning; the error came at
connect time (see Symptoms). Switching creates a new revision:
gcloud beta run services update example-api --execution-environment=gen2 \
--region=europe-central2 --project=acme-prod
Cloud Run instances, the other resource type, have SSH on by default and are reached with
gcloud beta run instances ssh INSTANCE
(SSH for Cloud
Run).
Connect to a Cloud Run container with gcloud
gcloud beta run services ssh example-api --region=europe-central2 --project=acme-prod
Our session opened with this banner. The same banner came back when we connected with plain
ssh below, so it comes from the container side, not from gcloud. The instance
ID, shortened here, was printed on its own line:
Project: acme-prod Region: europe-central2 Service: seo-cr-ssh Revision: seo-cr-ssh-00002-vzn Instance: 00e8a408bd638039904a45c1c5b50599e6147e10… Container: hello-1 Image: us-docker.pkg.dev/cloudrun/container/hello@sha256:f70f8f41…
Our Mac already had ~/.ssh/google_compute_engine from earlier
gcloud compute ssh use, so gcloud asked for no passphrase. Google's page says a
passphrase prompt can appear, and that you can leave it empty; if you set one, reuse it.
Type exit to leave. Ending the session doesn't restart the container, and
“Any changes you made on the container persist for as long as the container is kept
alive” (SSH for Cloud
Run).
Several instances or a traffic split need a choice:
--instance=INSTANCE_IDpicks one instance. The ID is in thelabelsof the service's log entries, and in the Console under Logs › an entry › SSH into this instance.--revision=REVISIONpicks a revision. Without it, a service that splits traffic makes gcloud ask “There is a traffic split. Please specify which revision to SSH into:”, or fail with the list of revisions when it can't prompt.--container=NAMEpicks a container in a multi-container service.
The command takes no remote command argument. Piped input works: gcloud prints
Pseudo-terminal and the
shell runs the lines.
printf 'echo /*\nexit\n' | gcloud beta run services ssh example-api \
--region=europe-central2 --project=acme-prod
The Console has the same route: Cloud Run › the service › SSH. It opens Cloud Shell with the gcloud command filled in.
What you can run inside the container
Cloud Run adds sshd, its certificate checks and the MirBSD Korn Shell
(mksh) while a session is open, and removes them when it ends. Other debugging
tools “(such as curl or package managers) aren't provisioned automatically”
(SSH for Cloud
Run). In the hello image, where PID 1 is /server, our session looked
like this:
shell=/bin/mksh /bin/mksh: <stdin>[2]: id: inaccessible or not found /bin/mksh: <stdin>[3]: uname: inaccessible or not found /bin/mksh: <stdin>[5]: ls: inaccessible or not found no sh no bash no curl no wget no ps
mksh builtins still work, so you can list and read files without ls or
cat:
$ echo "uid=$USER_ID ksh=$KSH_VERSION" uid=0 ksh=@(#)MIRBSD KSH R59 2020/10/31 $ echo /* /bin /dev /etc /proc /server /sys /tmp /usr /var /workspace $ echo /bin/* /bin/mksh $ echo "cmdline=$(</proc/1/cmdline)" cmdline=/server $ echo "K_SERVICE=$K_SERVICE PORT=$PORT" K_SERVICE= PORT=
The shell ran as root (uid 0) and could read /proc. It did not inherit the
service's environment: K_SERVICE and PORT, which Cloud Run sets for
the container's own process, were empty.
Plain ssh and ~/.ssh/config on a Mac
Run gcloud beta run services ssh once first: it writes the certificate to
~/.ssh/.
Then ssh can use a host entry like Google's in
~/.ssh/config. On a Mac with Homebrew, gcloud's
Python and SDK folder differ from the paths in Google's sample; this prints both:
gcloud info --format="value(basic.python_location,installation.sdk_root)"
The entry we used, with those two paths in the last line. We added --account
because our default gcloud account had no role on the project:
Host cloud-run-ssh
HostName cloud-run-default
User root
IdentityFile ~/.ssh/google_compute_engine
CertificateFile ~/.ssh/google_compute_engine_cert/acme-prod_europe-central2_service_example-api-cert.pub
CheckHostIP no
HashKnownHosts no
HostKeyAlias cloud-run-default
IdentitiesOnly yes
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
ProxyUseFdpass no
ProxyCommand PYTHON -S SDK_ROOT/lib/gcloud.py beta run start-iap-tunnel --account=alice@example.com --project_number=PROJECT_NUMBER --project_id=acme-prod --workload_type=service --deployment_name=example-api/revisions/REVISION --region=europe-central2 --iap-tunnel-url-override=wss://europe-central2.ssh.run.app/v4
ssh cloud-run-ssh
The entry names one revision, so it needs editing after each deploy. The certificate is the other limit. Ours was valid for exactly five minutes:
$ ssh-keygen -L -f ~/.ssh/google_compute_engine_cert/…-cert.pub
Type: ssh-rsa-cert-v01@openssh.com user certificate
Valid: from 2026-11-12T09:41:09 to 2026-11-12T09:46:09
Those times are the Mac's local time (CET). ssh cloud-run-ssh worked at 08:46:06
UTC. Started at 08:46:15, six seconds after the certificate ran out, it failed:
root@cloud-run-default: Permission denied (publickey).
Run gcloud beta run services ssh again for a new certificate. In our test a
second run within the five minutes reused the certificate it already had.
StrictHostKeyChecking no comes from Google's sample, and gcloud passes the same
option when it runs ssh itself.
Roles and who can turn SSH off
Google's page lists these roles for the person who connects:
- Service Account User (
roles/) on the service accountiam.serviceAccountUser - Cloud Run SSH Root Access (
roles/), IAP-secured Tunnel User (run.sshRoot roles/) and Cloud Run Developer (iap.tunnelResourceAccessor roles/) on the servicerun.developer - Logs View Accessor (
roles/), Browser (logging.viewAccessor roles/) and Service Usage Consumer (browser roles/) on the projectserviceusage.serviceUsageConsumer - IAP Policy Admin (
roles/) on the project, for IAP management operationsiap.admin
The tunnel role can be granted on one service:
gcloud beta iap tcp add-iam-policy-binding \
--resource-type=cloud-run \
--service=example-api \
--region=europe-central2 \
--member=user:alice@example.com \
--role=roles/iap.tunnelResourceAccessor \
--project=acme-prod
Someone outside the project's organization also needs
roles/, or Cloud Run blocks the session. Our
account had Owner, so we did not see the refusal for a missing role. gcloud's source turns
it into 4033:
An organization can forbid SSH with the custom constraint
custom., which denies creating or updating a service whose
run.googleapis.com/ annotation is "true"
(Cloud Run
custom constraints). Under VPC Service Controls the tunnel is checked as
iaptunnel.googleapis.com, not run.googleapis.com. SSH logins appear in
the Cloud OS Login API's Data Access audit logs once those are turned on.
Limits and billing
- “If no instance of the service is running, Cloud Run starts a new instance for the SSH session”, and it tries to keep that instance active for the session. “You are billed for the instance as usual while it is active.” SSH also bills for network egress.
- A session ends after 24 hours, and IAP disconnects it after one hour without activity.
- SSH replaces the image's host keys, and they stay replaced until the instance restarts.
- On a multi-container service with container startup order, an SSH connection that cold
starts an instance gets an HTTP
500and the new instance is stopped. Send a request first, or set minimum instances.
All four come from SSH for Cloud Run; we did not run a session long enough to test them.
How Ostgate does it
Ostgate does not open SSH sessions to Cloud Run containers. Use
gcloud beta run services ssh as above. What Ostgate does with Cloud Run, from
its release notes:
- “Resources lists instances, Cloud SQL instances and Cloud Run services in one table, grouped by account and project” (0.0.1).
- “Cloud Run: internal-only services and functions open in your browser through a local proxy” (0.0.1). How that works, and its limits, is in Open an internal Cloud Run service from a Mac.
Symptoms and causes
| You see | Cause | Fix |
|---|---|---|
SSH |
The service has no ssh-enabled annotation |
gcloud |
Error |
gcloud prints this for relay code 4003 on any service; ours ran in the first generation environment | --execution- |
4033: |
No IAP tunnel role on the service (from gcloud's source, not reproduced) | roles/ |
root@cloud-run-default: from plain ssh |
The five-minute certificate ran out | Run gcloud again |
/bin/mksh: |
The image has no such binary; only mksh is added |
Use mksh builtins, or add the tools to the image |
There |
More than one revision serves traffic | Pick one, or pass --revision |
HTTP 500 and the gcloud command fails on a multi-container service |
The connection cold started an instance with container startup order (not reproduced) | Start an instance with a request first, or set minimum instances |
Questions
Can you SSH into a Cloud Run container?
Yes, since 7 October 2026, as a Preview feature. Turn it on with gcloud , run the service in the second generation execution environment, then run gcloud . gcloud tunnels the session through Identity-Aware Proxy and logs you in as root with a certificate that expires after five minutes.
Do I need a shell or sshd in my container image?
No. Cloud Run adds sshd and the MirBSD Korn Shell (mksh) while a session is open and removes them when it ends. Other tools are not added. In the hello sample image our session had only /bin/mksh, so id and ls answered inaccessible or not found, curl was missing, and mksh builtins such as echo /* still worked.
Why does plain ssh say Permission denied (publickey) a few minutes later?
The certificate gcloud writes for the session lasts five minutes. Ours was valid from 08:41:09 to 08:46:09 UTC; ssh with the same ~/.ssh/config worked at 08:46:06 and was refused at 08:46:15. Run gcloud beta run services ssh again to get a new certificate.
Does an SSH session start or keep a Cloud Run instance running?
Yes. If no instance is running, Cloud Run starts one for the session, and it tries to keep the instance alive while the session lasts. The instance is billed as usual, and SSH also bills for network egress. A session ends after 24 hours, or after one hour without activity.
Ostgate runs on Apple Silicon Macs with macOS 15 or later, with a 7-day trial and no sign-up. It reaches Compute Engine VMs over IAP for SSH, files, Remote Desktop and tunnels; setup is in the docs.