Skip to content

Repository files navigation

terraform-vault-cloudrun

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.

Security model

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 gcpckms auto-unseal integration needs cryptoKeys.get in 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.

Prerequisites

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.

Usage

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.

Initialization behavior

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.

Public route policy

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.

Networking

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.

Availability and revisions

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.

Local development image

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.

Upgrade

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.

Vault server image publication

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.

Requirements

Name Version
terraform >= 1.7.0, < 2.0.0
google ~> 7.22
google-beta ~> 7.22

Providers

Name Version
google ~> 7.22
google-beta ~> 7.22

Modules

Name Source Version
vault https://github.com/libops/terraform-cloudrun-v2/archive/refs/heads/main.zip//terraform-cloudrun-v2-main n/a
vault_proxy https://github.com/libops/terraform-cloudrun-v2/archive/refs/heads/main.zip//terraform-cloudrun-v2-main n/a

Resources

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

Inputs

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)
[
"/.well-known/",
"/v1/identity/oidc/provider/*/.well-known/
",
"/v1/identity/oidc/provider//authorize",
"/v1/identity/oidc/provider/
/token",
"/v1/identity/oidc/provider//userinfo",
"/ui/vault/identity/oidc/provider/
/authorize",
"/v1/auth/oidc/oidc/auth_url",
"/v1/auth/oidc/oidc/callback",
"/ui/vault/auth/*/oidc/callback",
"/v1/auth/userpass/login/**"
]
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

Outputs

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.

About

Terraform module to run HashiCorp Vault on Google Cloud Run

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages