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, except
for the generate-root endpoint family needed to recover an interrupted
bootstrap after the initial root token has been revoked. Vault permits that
single family without a Vault token. The runtime remains private behind Cloud
Run IAM, and the public proxy still requires a valid initializer
X-Admin-Token before forwarding any generate-root request.
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. It also treats
an initialized standby as ready so an overlapping Cloud Run revision can pass
startup while the prior revision holds the GCS HA lease; sealed Vault remains
unhealthy.
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
- Cloud Logging API
- Cloud Monitoring 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 tagged Artifact Registry image references:
- Vault server
- Vault Proxy v2
- Vault initializer
The module intentionally has no implicit image defaults. LibOps deployments use
the publishers' managed main tags; signatures, SBOMs, provenance, and registry
digests remain generated release evidence rather than hand-maintained inputs.
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=main"
project = "example-project"
region = "us-central1"
vault_image = "us-docker.pkg.dev/libops-images/public/vault-server:main"
vault_proxy_image = "us-docker.pkg.dev/libops-images/public/vault-proxy:main"
vault_init_image = "us-docker.pkg.dev/libops-images/public/vault-init:main"
# 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",
]
audit_log_viewer_members = [
"group:vault-audit-reviewers@example.org",
]
audit_log_location = "us-central1"
audit_alert_notification_channels = [
"projects/example-project/notificationChannels/vault-audit-oncall",
]
}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 references, 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.
The module routes only structured Vault API audit records to a dedicated, deletion-protected Cloud Logging bucket with at least 365 days of retention. A least-privilege log view has explicit readers, and a critical export-error alert pages the supplied notification channels when Cloud Logging reports a sink routing error. See Vault audit evidence for the identity boundary, access review, outage drill, and evidence requirements.
By default Vault returns five recovery shares to the initializer, which removes
the initial root token, encrypts the recovery bundle with Google KMS, and stores
only ciphertext in the create-only recovery object in GCS. Set
recovery_pgp_keys to five distinct base64-encoded binary PGP public keys only
when independent human custody is required; Vault then encrypts one share to
each key before the KMS/GCS protection layer. Private keys and decrypted
recovery shares must not enter Terraform, CI, logs, tickets, or chat.
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 the managed tagged 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 Renovate-managed 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 as vault-server:main in GHCR and
us-docker.pkg.dev/libops-images/public. The publisher records the exact
source digest, signature, provenance, and workflow run as generated release
evidence; callers do not maintain a LibOps image SHA pin by hand.
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_logging_log_view.vault_audit | resource |
| google_logging_log_view_iam_member.vault_audit | resource |
| google_logging_project_bucket_config.vault_audit | resource |
| google_logging_project_sink.vault_audit | resource |
| google_monitoring_alert_policy.vault_audit_sink_error | 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 |
| audit_alert_notification_channels | One or more existing Cloud Monitoring notification-channel resource names paged when the Vault audit sink reports a routing error. | set(string) |
n/a | yes |
| audit_log_bucket_locked | Irreversibly lock the audit bucket retention policy. Leave false until the retention period has explicit business and Legal approval; locking is not reversible. | bool |
false |
no |
| audit_log_location | Explicit supported Cloud Logging bucket location approved for the customer's audit evidence. This is intentionally independent from the Cloud Run region. | string |
n/a | yes |
| audit_log_retention_days | Retention period for the dedicated Vault audit log bucket. The minimum is 365 days; choose a longer period only from an approved customer or legal obligation. | number |
365 |
no |
| audit_log_viewer_members | One or more explicit IAM principals granted roles/logging.viewAccessor on only the Vault audit log view. Review inherited project-level Logging access separately. | set(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 | Optional set of five distinct base64-encoded binary PGP public keys for independent recovery-share custodians. When omitted, Vault returns the shares to the initializer for KMS encryption and create-only GCS storage. | list(string) |
[] |
no |
| 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 | Tagged GAR image reference for the Vault server container. | string |
n/a | yes |
| vault_init_image | Tagged GAR image reference for the Vault initializer container. | string |
n/a | yes |
| vault_proxy_image | Tagged GAR image reference for the Vault Proxy v2 container. | string |
n/a | yes |
| Name | Description |
|---|---|
| audit_log_bucket_name | Full resource name of the protected Vault audit Logging bucket. |
| audit_log_view_name | Name of the least-privilege Vault audit log view. |
| audit_sink_error_alert_name | Resource name of the critical Vault audit sink-error alert policy. |
| audit_sink_name | Name of the project sink routing Vault audit records into the protected bucket. |
| 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. |