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 beta run services ssh SERVICE --region=REGION 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/cloudrun/container/hello 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/ssh-enabled: "true", 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_ID picks one instance. The ID is in the labels of the service's log entries, and in the Console under Logs › an entry › SSH into this instance.
  • --revision=REVISION picks 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=NAME picks a container in a multi-container service.

The command takes no remote command argument. Piped input works: gcloud prints Pseudo-terminal will not be allocated because stdin is not a 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/google_compute_engine_cert/PROJECT_REGION_service_SERVICE-cert.pub. 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/iam.serviceAccountUser) on the service account
  • Cloud Run SSH Root Access (roles/run.sshRoot), IAP-secured Tunnel User (roles/iap.tunnelResourceAccessor) and Cloud Run Developer (roles/run.developer) on the service
  • Logs View Accessor (roles/logging.viewAccessor), Browser (roles/browser) and Service Usage Consumer (roles/serviceusage.serviceUsageConsumer) on the project
  • IAP Policy Admin (roles/iap.admin) on the project, for IAP management operations

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/compute.osLoginExternalUser, 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: not authorized. Missing iap.tunnelServices.accessViaIAP permission.

An organization can forbid SSH with the custom constraint custom.disableCloudRunSSH, which denies creating or updating a service whose run.googleapis.com/ssh-enabled 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 500 and 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 seeCauseFix
SSH is not enabled for this deployment. Use the --ssh flag to enable SSH for this deployment. The service has no ssh-enabled annotation gcloud beta run services update SERVICE --ssh
Error connecting to the service. Please ensure your execution environment is set to gen2. gcloud prints this for relay code 4003 on any service; ours ran in the first generation environment --execution-environment=gen2
4033: not authorized. Missing iap.tunnelServices.accessViaIAP permission. No IAP tunnel role on the service (from gcloud's source, not reproduced) roles/iap.tunnelResourceAccessor
root@cloud-run-default: Permission denied (publickey). from plain ssh The five-minute certificate ran out Run gcloud beta run services ssh again
/bin/mksh: <stdin>[2]: id: inaccessible or not found The image has no such binary; only mksh is added Use mksh builtins, or add the tools to the image
There is a traffic split. Please specify which revision to SSH into: 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 beta run services update SERVICE --ssh, run the service in the second generation execution environment, then run gcloud beta run services ssh SERVICE --region=REGION. 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.