Google Cloud Run
The recommended deployment for most users. Cloud Run provides a public HTTPS endpoint, scales to zero between CI runs (typically staying within the free tier), and stores the CA key in Secret Manager. There are no state files or load balancers to manage.
The container image is built from the repository
Dockerfile
and contains only the binary. The policy and the CA key are both delivered
through Secret Manager: the key as an environment variable, the policy as a
mounted file.
1. Create the CA key and policy
ssh-keygen -t ed25519 -N "" -f ca_key -C "oidc-ssh-ca"
Write your policy.yaml (see the policy reference) and
validate it:
oidc-ssh-ca check-config policy.yaml
2. Store both in Secret Manager
The policy is not confidential, but Secret Manager is the simplest way to deliver a runtime file to Cloud Run, and it gives policy changes a versioned history for free.
gcloud secrets create oidc-ssh-ca-key --data-file=ca_key
gcloud secrets create oidc-ssh-ca-policy --data-file=policy.yaml
Create a dedicated service account so the CA runs with no permissions beyond reading its own two secrets:
gcloud iam service-accounts create oidc-ssh-ca
PROJECT_ID=$(gcloud config get-value project)
for s in oidc-ssh-ca-key oidc-ssh-ca-policy; do
gcloud secrets add-iam-policy-binding "$s" \
--member="serviceAccount:oidc-ssh-ca@${PROJECT_ID}.iam.gserviceaccount.com" \
--role=roles/secretmanager.secretAccessor
done
3. Deploy
From the repository root (Cloud Build uses the Dockerfile automatically):
gcloud run deploy oidc-ssh-ca \
--source . \
--region asia-northeast1 \
--service-account "oidc-ssh-ca@${PROJECT_ID}.iam.gserviceaccount.com" \
--allow-unauthenticated \
--set-secrets "OIDC_SSH_CA_KEY=oidc-ssh-ca-key:latest,/etc/oidc-ssh-ca/policy.yaml=oidc-ssh-ca-policy:latest" \
--max-instances 1
Notes:
--allow-unauthenticatedis intentional:/signauthenticates callers by verifying their OIDC token, the same model as running the server on any public host.--max-instances 1bounds the cost of unauthenticated traffic — a single instance serves up to 80 concurrent requests by default, far more than this workload needs.The server listens on
:8080by default, which matches Cloud Run’s default container port.The command prints the service URL — that is the
OIDC_SSH_CA_URLfor the GitHub Actions workflow.
Check the startup log (the CA logs its public key fingerprint, the policy path, and the rule count — never key material):
gcloud run services logs read oidc-ssh-ca --region asia-northeast1 --limit 20
Updating the policy
Add a new secret version, then roll a new revision (secret versions are resolved when a revision starts, so a deploy is required either way):
gcloud secrets versions add oidc-ssh-ca-policy --data-file=policy.yaml
gcloud run deploy oidc-ssh-ca --source . --region asia-northeast1
There is no SIGHUP reload on Cloud Run; revisions are the reload mechanism.
An invalid policy fails the new revision’s startup and Cloud Run keeps
routing to the previous revision — the same fail-safe behavior as the
standalone server, enforced by the platform.
Emergency stop
The fastest stop is removing public access (immediate, no rebuild):
gcloud run services remove-iam-policy-binding oidc-ssh-ca \
--region asia-northeast1 \
--member=allUsers --role=roles/run.invoker
Then follow the standard procedure: wait out max_valid_for_seconds, and
rotate the CA key only if the key itself may have leaked. To stop issuance
but keep the endpoint answering (HTTP 503), set disabled: true in the
policy and deploy a new revision.