This Terraform module deploys a single-instance Vault runtime on Google Cloud Run, a separately identified public Vault Proxy v2 service, and a one-shot initializer job. It is a fork of kelseyhightower/serverless-vault-with-cloud-run with explicit workload identities, immutable image inputs, and recovery material isolated from the long-running Vault process.
The module creates three service accounts with separate responsibilities:
- The Vault runtime can administer objects only in the Vault data bucket and
view, encrypt, or decrypt with the configured KMS key. Vault's
gcpckmsauto-unseal integration needscryptoKeys.getin addition to cryptographic operations. - The initializer can create and view objects only in the recovery bucket and encrypt or decrypt with the same KMS key. It cannot delete or overwrite recovery objects.
- The public proxy has no GCS or KMS role. It can invoke only the IAM-protected
Vault runtime service and authenticates upstream with a short-lived metadata
ID token in
X-Serverless-Authorization, preserving the client's separate Vault authorization header.
Cloud Run assigns one service identity to a revision, so Vault and the public
proxy intentionally run as separate services. The Vault runtime is not publicly
invokable; only the proxy service account receives roles/run.invoker. The
proxy remains publicly invokable because it is the application-layer boundary.
Vault Proxy v2 permits unauthenticated access only to canonical route patterns
in public_routes. Every other route requires an X-Admin-Token Google access
token whose verified email is either explicitly listed in admin_emails or is
the initializer service account. The Vault runtime service account is not a
proxy administrator. Email comparisons and duplicate checks are
case-insensitive to match Vault Proxy's identity normalization.
Vault still enforces its own tokens and policies after the proxy check.
The Vault runtime must pass its port-specific health probe on 8200. Terraform
then creates the public proxy, which must pass /healthz on 8080, before the
initializer runs. The Vault health probe treats an uninitialized server as
ready so the initializer job can complete the first deployment.
Enable these APIs in the target project before using the module:
- Cloud Run API
- Cloud Key Management Service API
- Cloud Storage API
- Identity and Access Management API
Because Terraform creates and immediately executes the initializer, the applying identity also needs permission to run that Cloud Run job in addition to its resource-management permissions.
The caller must supply three Artifact Registry image references pinned by manifest digest:
- Vault server
- Vault Proxy v2
- Vault initializer
The module intentionally has no mutable image defaults. Review and promote the three GAR digests together before changing a deployment.
The initializer job uses the google-beta provider only because Cloud Run's
Terraform run_execution_token remains absent from the stable provider. Every
other resource uses the stable google provider. This keeps initialization
Terraform-managed and makes terraform apply wait for successful completion,
without introducing a credentialed local-exec or manual deployment step.
module "vault" {
source = "git::https://github.com/libops/terraform-vault-cloudrun.git?ref=1.0.0"
project = "example-project"
region = "us-central1"
vault_image = "us-docker.pkg.dev/example-project/public/vault@sha256:REVIEWED_DIGEST"
vault_proxy_image = "us-docker.pkg.dev/example-project/public/vault-proxy@sha256:REVIEWED_DIGEST"
vault_init_image = "us-docker.pkg.dev/example-project/public/vault-init@sha256:REVIEWED_DIGEST"
recovery_pgp_keys = [
filebase64("custodian-1-public.pgp"),
filebase64("custodian-2-public.pgp"),
filebase64("custodian-3-public.pgp"),
filebase64("custodian-4-public.pgp"),
filebase64("custodian-5-public.pgp"),
]
# This module is intentionally blocked by default from being mistaken for
# an HA production topology.
single_instance_preview_acknowledged = true
admin_emails = [
"vault-admin@example.org",
]
}deletion_protection defaults to true for both the service and initializer
job. Set it to false and apply that change before intentionally destroying
the deployment.
The initializer has one task, parallelism one, three retries, and a ten-minute
task timeout. CHECK_INTERVAL=0s makes every task a bounded one-shot attempt.
Its 31-character run_execution_token is a deterministic SHA-256 prefix over
the three image digests, service and identity settings, bucket and KMS IDs,
proxy policy, startup contract, and initializer job settings. A relevant
deployment change therefore runs the idempotent initializer verification
again and keeps the apply open until that execution succeeds. Provider create
and update timeouts allow all bounded retries to finish. Change
initializer_execution_nonce when an operator needs to request the same
verification without otherwise changing the deployment.
init_job_name is limited to 30 characters so the job name, separator, and
31-character execution suffix remain inside Cloud Run's execution-name limit.
Vault Init authenticates protected health and initialization routes as the
initializer service account. The selected Vault Init image must request a
Google metadata access token containing the userinfo.email scope expected by
Vault Proxy v2.
Root-token-free encrypted recovery material is stored in
recovery_bucket_name. Fresh initialization requests five recovery shares with
a threshold of three, enables the cloudrun/ JSON audit device on Vault stdout,
revokes and verifies the initial root token, and records a non-secret completion
marker. Treat access to the bucket, KMS key, recovery shares, and Cloud Logging
as privileged disaster-recovery/audit access. Assign recovery shares to
independent custodians; bucket/KMS access alone is not a custody quorum. Do not
copy decrypted material into Terraform, CI, logs, tickets, chat, or command
arguments.
recovery_pgp_keys is required and must contain five distinct binary PGP
public keys encoded with base64. Vault encrypts one recovery share to each key;
the initializer stores only those encrypted shares. Keep the corresponding
private keys in separate custodian systems. Public keys may appear in Terraform
state, but private keys and decrypted recovery shares must not.
The defaults expose the minimum OIDC discovery, OIDC callback, and userpass
login paths used by common clients. /v1/sys/health is always added even when
public_routes is empty. Vault Proxy v2 path patterns are explicit:
- A literal path matches exactly.
*matches one path segment.- A final
/**matches a subtree.
Legacy trailing-slash prefixes such as /v1/auth/userpass/ are rejected. Add a
secret-engine subtree only when Vault policy is intentionally the sole
authorization boundary for that path.
Direct VPC egress is deliberately OFF and is not exposed as a module input.
This deployment uses Google APIs and the public Cloud Run service URL, so it
does not need a Serverless VPC Access connector or Direct VPC attachment.
Keeping networking outside this module also avoids silently expanding the
Vault trust boundary. A platform that requires private egress should compose
and review that network path separately rather than enabling it implicitly
here.
This remains a single-serving-instance Vault deployment, not an HA failover topology. The pinned Cloud Run module applies a service-level maximum of one across traffic-serving revisions. Because Cloud Run can still briefly exceed a configured maximum during rollout, the GCS backend enables its HA lock to fence overlapping revisions so only one Vault server becomes active. Clustering stays disabled because Cloud Run services cannot address individual instance cluster listeners. Revision changes can therefore cause transient request failures; quiesce clients and use a maintenance window. The separate proxy may scale independently but does not make the Vault storage runtime HA. Production availability claims remain blocked until an explicitly selected HA topology and a hosted failover and recovery drill are promoted.
The repository Dockerfile is a development-only way to exercise the included
Vault configuration template. Terraform never builds it and it is not a
default image source. Production callers must supply a reviewed,
digest-pinned GAR image through vault_image.
Version 1 changes identities, IAM, image inputs, proxy routes, and initialization behavior. Read UPGRADING.md and review the full Terraform plan before applying it to an existing Vault deployment.
Terraform never builds or pushes images. The repository Dockerfile is the
reviewed source for the independently released, multi-platform vault-server
image. It checks out the exact upstream Vault 2.0.3 commit, rebuilds the
UI-enabled target with a digest-pinned patched Go toolchain, and copies only the
binary and license into a numeric non-root runtime. The entrypoint renders the
seal configuration from KMS_KEY_RING and KMS_CRYPTO_KEY at startup.
Image pull requests build and scan both native architectures without publisher
credentials. After the exact commit passes protected main CI, the shared
LibOps workflow publishes, scans, signs, and verifies the same multi-platform
manifest in GHCR and us-docker.pkg.dev/libops-images/public. Its unique tag
records the upstream version, LibOps packaging revision, source commit, and
workflow run. Deployments must still resolve and use the verified GAR digest.
Image payload pull requests and image-trust-only pull requests must retain
[skip-release] in the title so they cannot cut a Terraform module release. A
pull request that changes both module payload and image trust must carry an
explicit release marker. The release workflow independently suppresses
unmarked image/trust-only changes as a second guard. Keep the Terraform CI
workflow name, path, protected-main trigger, and image-contract validation
synchronized with the Vault image workflow and shared WIF allowlist.
| Name | Version |
|---|---|
| terraform | >= 1.7.0, < 2.0.0 |
| ~> 7.22 | |
| google-beta | ~> 7.22 |
| Name | Version |
|---|---|
| ~> 7.22 | |
| google-beta | ~> 7.22 |
| Name | Type |
|---|---|
| google-beta_google_cloud_run_v2_job.vault-init | resource |
| google_kms_crypto_key.key | resource |
| google_kms_crypto_key_iam_member.initializer | resource |
| google_kms_crypto_key_iam_member.vault | resource |
| google_kms_key_ring.vault-server | resource |
| google_service_account.initializer | resource |
| google_service_account.proxy | resource |
| google_service_account.runtime | resource |
| google_storage_bucket.vault | resource |
| google_storage_bucket_iam_member.initializer_recovery | resource |
| google_storage_bucket_iam_member.member | resource |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| admin_emails | Explicit human or automation emails allowed to access protected Vault routes. The initializer service account is added automatically. | list(string) |
n/a | yes |
| country | GCS location for the Vault data and recovery buckets. | string |
"us" |
no |
| create_kms | Whether to create the KMS key ring and crypto key. | bool |
true |
no |
| data_bucket_name | Bucket name for Vault data storage. Defaults to a name derived from project and service name. | string |
"" |
no |
| deletion_protection | Protect both the Vault Cloud Run service and initializer job from accidental deletion. | bool |
true |
no |
| gsa_account_id | Service account ID for the Vault runtime. Defaults to a truncated form of name. | string |
"" |
no |
| init_job_name | Cloud Run job name used to initialize Vault. | string |
"vault-init" |
no |
| initializer_execution_nonce | Optional operator-controlled nonce included in the initializer execution-contract hash. Change it to deliberately request another idempotent verification. | string |
"" |
no |
| initializer_gsa_account_id | Service account ID for the one-shot Vault initializer. Defaults to the service name plus -init. | string |
"" |
no |
| key_bucket_name | Bucket name for encrypted Vault recovery material. Defaults to a name derived from project and service name. | string |
"" |
no |
| kms_key_name | KMS crypto key name used for auto-unseal and recovery-material encryption. | string |
"vault" |
no |
| kms_key_ring_name | KMS key ring name used for auto-unseal. | string |
"vault-server" |
no |
| name | Cloud Run service name for the Vault server. | string |
"vault-server" |
no |
| project | GCP project in which to deploy Vault. | string |
n/a | yes |
| proxy_gsa_account_id | Service account ID for the public Vault proxy. Defaults to the service name plus -proxy. | string |
"" |
no |
| public_routes | Optional canonical Vault Proxy v2 path patterns accessible without X-Admin-Token. /v1/sys/health is always added. | list(string) |
[ |
no |
| recovery_pgp_keys | Exactly five distinct base64-encoded binary PGP public keys for independent recovery-share custodians. Public keys are passed to Vault initialization; private keys must never enter Terraform. | list(string) |
n/a | yes |
| region | GCP region in which to deploy the Cloud Run service and initializer job. | string |
"us-east5" |
no |
| single_instance_preview_acknowledged | Required explicit acknowledgement that this module is a single-serving-instance preview, not an HA Vault topology. Set true only for an approved limited deployment; this is not risk acceptance or production evidence. | bool |
n/a | yes |
| vault_image | Digest-pinned GAR image reference for the Vault server container. | string |
n/a | yes |
| vault_init_image | Digest-pinned GAR image reference for the Vault initializer container. | string |
n/a | yes |
| vault_proxy_image | Digest-pinned GAR image reference for the Vault Proxy v2 container. | string |
n/a | yes |
| Name | Description |
|---|---|
| data_bucket_name | Bucket containing the Vault GCS storage backend. |
| gsa | Deprecated compatibility alias for runtime_service_account_email. |
| initializer_execution_token | Deterministic 31-character run-to-completion token derived from the initializer-relevant deployment contract. |
| initializer_job_name | Name of the one-shot Vault initializer Cloud Run job. |
| initializer_service_account_email | Service account used by the one-shot Vault initializer job. |
| key_bucket | Deprecated compatibility alias for recovery_bucket_name. |
| kms_key_id | Full resource ID of the KMS key used by Vault and the initializer. |
| proxy_service_account_email | Least-privileged service account used by the public Vault proxy. |
| recovery_bucket_name | Bucket containing encrypted Vault recovery material. |
| runtime_service_account_email | Service account used by the long-running Vault service. |
| vault-url | Deprecated compatibility alias for vault_url. |
| vault_runtime_url | IAM-protected URL of the Vault runtime Cloud Run service. |
| vault_url | Public URL of the Vault proxy Cloud Run service. |