# Auto-Unsealing HashiCorp Vault with GCP KMS and Deploying to Cloud Run

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1746175251765/e617cd58-89e5-4d9b-9edd-e3c575a6d675.gif align="center")

## **Introduction**

HashiCorp Vault is a powerful secrets management tool that helps organizations secure, store, and control access to tokens, passwords, certificates, and encryption keys. One challenge with managing Vault is the need to unseal it after each restart, which can be cumbersome in automated environments. This article demonstrates how to automate the Vault unsealing process using Google Cloud KMS and deploy the solution to Google Cloud Run for a serverless, scalable, and cost-effective setup.

I'll walk through the entire process, including:

1. Setting up GCP resources with Terraform
    
2. Configuring Vault for auto-unsealing with GCP KMS
    
3. Creating a Docker container for Vault
    
4. Deploying to Cloud Run
    
5. Automating deployment with GitHub Actions
    
6. Migrating from Shamir key shares to GCP KMS auto-unsealing
    

## **Prerequisites**

* Google Cloud Platform account with a project
    
* GCP service account with appropriate permissions
    
* Basic knowledge of Terraform, Docker, and Vault
    
* HashiCorp Vault CLI installed locally
    
* GitHub repository for CI/CD (optional)
    

## **Setting Up Environment Variables**

Start by setting up environment variables for your deployment:

```yaml
export PROJECT_ID=gcp_project_id
export GCP_LOCATION=europe-west1
export GCP_ARTIFACT_REGISTRY_NAME=docker-repository
export DOCKER_IMAGE=vault-server
export CLOUD_RUN_SERVICE_NAME=vault-server
```

## **GCP Resources with Terraform**

### **Required IAM Roles for Vault Service Account**

The Vault service account needs the following roles to interact with GCP services:

```yaml
roles/cloudkms.viewer
roles/cloudkms.cryptoKeyEncrypterDecrypter or roles/cloudkms.signerVerifier
roles/secretmanager.secretAccessor
roles/storage.objectAdmin
```

### **Terraform Configuration**

Create a Terraform configuration file to set up the necessary GCP KMS resources:

```yaml
resource "google_kms_key_ring" "keyring" {
  name     = "${var.name}-keyring"
  location = var.location
  project  = var.project
}
​
resource "google_kms_crypto_key" "key" {
  name            = "${var.name}-key"
  key_ring        = google_kms_key_ring.keyring.id
  rotation_period = var.rotation_period
  purpose         = var.purpose
}
​
resource "google_kms_crypto_key_iam_binding" "iam" {
  crypto_key_id = google_kms_crypto_key.key.id
  role          = "roles/cloudkms.cryptoKeyEncrypterDecrypter"
​
  members = [
    "serviceAccount:${var.vault_service_account}@${var.project}.iam.gserviceaccount.com"
  ]
}
​
variable "name" {
  default = "vault-unseal"
}
variable "location" {
  default = "global"
}
variable "project" {}
variable "rotation_period" {
  default = "7776000s"  # 90 days
}
variable "purpose" {
  default = "ENCRYPT_DECRYPT"
}
variable "vault_service_account" {}
```

You'll also need to create a Google Cloud Storage bucket for Vault's storage backend:

```yaml
resource "google_storage_bucket" "storage-bucket" {
  name          = var.bucket_name
  location      = var.location
  force_destroy = var.force_destroy
  uniform_bucket_level_access = var.uniform_bucket_level_access
  public_access_prevention = var.public_access_prevention
​
  storage_class = var.storage_class
  versioning {
    enabled = true
  }
}
​
​
variable "bucket_name" {
  default = "vault-server-bucket"
}
variable "location" {
  default = "EU"
}
variable "uniform_bucket_level_access" {
  type    = bool
  default = true
}
variable "storage_class" {
  default = "STANDARD"
}
​
variable "force_destroy" {
  type    = bool
  default = false
}
​
variable "public_access_prevention" {
  default = "enforced"
}
```

## **Configuring Vault for Auto-Unsealing**

Create a `vault-config.hcl` file for your Vault configuration:

```yaml
seal "gcpckms" {
  project     = "gcp_project_id"
  region      = "global"
  key_ring    = "vault-unseal-keyring"
  crypto_key  = "vault-unseal-key"
}
​
storage "gcs" {
  bucket = "vault-server-bucket"
}
​
listener "tcp" {
  address     = "0.0.0.0:8080"
  tls_disable = 0  # Enabling TLS
}
```

> **Note:** Make sure to replace `gcp_project_id` with your actual GCP project ID. For production deployments, you should properly configure TLS with certificates rather than using `tls_disable = 0`.

## **Creating the Vault Docker Container**

### **Dockerfile**

Create a Dockerfile for the Vault container:

```yaml
FROM hashicorp/vault:1.19.0
​
# Create a non-root user and group
RUN addgroup -S vaultgroup && adduser -S vaultuser -G vaultgroup
​
RUN mkdir -p /vault/config
​
COPY config/vault-config.hcl /vault/config/vault-config.hcl
​
# Set proper ownership for Vault directories and files
RUN chown -R vaultuser:vaultgroup /vault
​
# Use the non-root user
USER vaultuser
​
ENTRYPOINT ["vault", "server", "-config=/vault/config/vault-config.hcl"]
```

Make sure your project structure looks like this:

```yaml
project/
├── config/
│   └── vault-config.hcl
├── Dockerfile
└── terraform/
    └── main.tf
```

## **Building and Pushing the Docker Image Manually**

If you're not using CI/CD, you can build and push the Docker image manually:

```yaml
# Build Container
docker build -t "${GCP_LOCATION}-docker.pkg.dev/${PROJECT_ID}/${GCP_ARTIFACT_REGISTRY_NAME}/${DOCKER_IMAGE}:latest" .
​
# Authenticate with Artifact Registry
gcloud auth configure-docker ${GCP_LOCATION}-docker.pkg.dev
​
# Push Container
docker push "${GCP_LOCATION}-docker.pkg.dev/${PROJECT_ID}/${GCP_ARTIFACT_REGISTRY_NAME}/${DOCKER_IMAGE}:latest"
```

## **Automating Deployment with GitHub Actions**

For a more robust deployment process, you can use GitHub Actions to automate the build and deployment of your Vault server. Create a file named `.github/workflows/deploy-vault.yml` with the following content:

```yaml
name: Vault Server Deployment
​
on:
  push:
    branches: [ vault-server ]
​
env:
  GCP_WIF_PROJECT_ID: "org-wif-project"
  GCP_WIF_PROJECT_NUMBER: ${{ secrets.GCP_WIF_PROJECT_NUMBER }}
  GCP_WIF_POOL: "auth-server-gh-pool"
  GCP_WIF_PROVIDER: "auth-server-gh-prov"
  GCP_WIF_SA: "github-org-auth-sa"
  RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
​
  PROJECT_LOCATION: europe-west1
  PROJECT_ID: org-env-project
  GCP_ARTIFACT_REGISTRY_NAME: docker-repository
  DOCKER_IMAGE: vault-server
  GCP_IMPERSONATED_SA_LIFETIME_TOKEN: 300 # 5 minutes
​
jobs:
  deploy:
    name: "Deploy Vault"
    runs-on: ubuntu-latest
    environment: test
    permissions:
      contents: 'read'
      id-token: 'write'
​
    steps:
      - name: Checkout
        uses: actions/checkout@v4
​
      - name: Set up Cloud SDK
        uses: google-github-actions/setup-gcloud@v1
​
      - id: 'auth'
        name: 'Authenticate to Google Cloud'
        uses: 'google-github-actions/auth@v2'
        with:
          create_credentials_file: true
          workload_identity_provider: 'projects/${{ env.GCP_WIF_PROJECT_NUMBER }}/locations/global/workloadIdentityPools/${{ env.GCP_WIF_POOL }}/providers/${{ env.GCP_WIF_PROVIDER }}'
          service_account: '${{ env.GCP_WIF_SA }}@${{ env.GCP_WIF_PROJECT_ID }}.iam.gserviceaccount.com'
​
      - name: Retrieve information on existing releases
        id: get_release_info
        run: |
          RELEASE_TAG=$(curl -L -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" -H "X-GitHub-Api-Version: 2022-11-28" https://api.github.com/repos/${{ github.repository }}/releases/latest |  jq -r '.tag_name')
          echo "Latest release tag: $RELEASE_TAG"
          MAJOR=$(echo $RELEASE_TAG | awk -F. '{print $1}')
          MINOR=$(echo $RELEASE_TAG | awk -F. '{print $2}')
          PATCH=$(echo $RELEASE_TAG | awk -F. '{print $3}')
          PATCH=$((PATCH + 1))
          NEXT_VERSION="${MAJOR}.${MINOR}.${PATCH}"
          NEXT_VERSION=$(echo $NEXT_VERSION | sed 's/^v//')  # Remove the "v" from the beginning of the version
          echo "NEXT_VERSION=${NEXT_VERSION}" >> $GITHUB_ENV
          echo "Next version: $NEXT_VERSION"
​
      - name: Build Container
        working-directory: ./vault-server
        run: |-
          docker build -t "${PROJECT_LOCATION}-docker.pkg.dev/${PROJECT_ID}/${GCP_ARTIFACT_REGISTRY_NAME}/${DOCKER_IMAGE}:${{ env.NEXT_VERSION }}" .
​
      - name: Authenticate Artifact Registry
        run: |-
          gcloud -q auth configure-docker ${{ env.PROJECT_LOCATION }}-docker.pkg.dev
​
      - name: Push Container
        run: |-
          docker push "${PROJECT_LOCATION}-docker.pkg.dev/${PROJECT_ID}/${GCP_ARTIFACT_REGISTRY_NAME}/${DOCKER_IMAGE}:${{ env.NEXT_VERSION }}"
​
      - name: Deploy to cloud run
        working-directory: ./vault-server
        run: |-
          gcloud run deploy vault-server \
            --image "${PROJECT_LOCATION}-docker.pkg.dev/${PROJECT_ID}/${GCP_ARTIFACT_REGISTRY_NAME}/${DOCKER_IMAGE}:${{ env.NEXT_VERSION }}" \
            --platform managed \
            --project ${PROJECT_ID} \
            --region ${PROJECT_LOCATION} \
            --no-allow-unauthenticated \
            --service-account=vault-server-sa@${PROJECT_ID}.iam.gserviceaccount.com \
            --update-secrets=/vault/credentials/gcp-vault-agent-sa.json=VAULT_AGENT_SA:latest \
            --memory=1024Mi \
            --cpu 1 \
            --min-instances=0 \
            --max-instances=3 \
            --timeout=3600s \
            --cpu-boost \
            --port=8080 \
            --ingress=all \
            --execution-environment=gen2
​
      - name: Create a release
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
        with:
          tag_name: ${{ env.NEXT_VERSION }}
          release_name: Version ${{ env.NEXT_VERSION }}
          body: Release notes for vault version ${{ env.NEXT_VERSION }}
```

This workflow does the following:

1. Authenticates to Google Cloud using Workload Identity Federation
    
2. Retrieves the latest release version and increments it
    
3. Builds the Vault Docker image
    
4. Pushes the image to Google Artifact Registry
    
5. Deploys the image to Cloud Run
    
6. Creates a new GitHub release
    

> **Note:** To use this workflow, you'll need to set up Workload Identity Federation and add the required secrets to your GitHub repository.

## **Deploying to Cloud Run Manually**

If you're not using CI/CD, you can deploy to Cloud Run manually:

```yaml
gcloud run deploy ${CLOUD_RUN_SERVICE_NAME} \
  --image "${GCP_LOCATION}-docker.pkg.dev/${PROJECT_ID}/${GCP_ARTIFACT_REGISTRY_NAME}/${DOCKER_IMAGE}:latest" \
  --platform managed \
  --project ${PROJECT_ID} \
  --region ${GCP_LOCATION} \
  --no-allow-unauthenticated \
  --service-account=vault-server-sa@${PROJECT_ID}.iam.gserviceaccount.com \
  --update-secrets=/vault/credentials/gcp-vault-agent-sa.json=VAULT_AGENT_SA:latest \
  --memory=1024Mi \
  --cpu 1 \
  --min-instances=0 \
  --max-instances=3 \
  --timeout=3600s \
  --cpu-boost \
  --port=8080 \
  --ingress=all \
  --execution-environment=gen2
```

> **Security Note:** In a production environment, you should keep `--no-allow-unauthenticated` for proper authentication for your Vault server rather than using `--allow-unauthenticated`. Consider setting up Identity-Aware Proxy (IAP) or another authentication mechanism.

## **Setting Up and Using the Vault CLI**

Install the Vault CLI locally to interact with your deployed Vault server:

```yaml
# For macOS
brew tap hashicorp/tap
brew install hashicorp/tap/vault
​
# Verify installation
vault version
​
# Configure CLI to talk to your Vault server
export VAULT_ADDR="https://vault.yourdomain.com"
​
# Check Vault status
vault status
```

## **Migrating from Shamir Keys to GCP KMS Auto-Unsealing**

If you're migrating an existing Vault installation from Shamir key shares to GCP KMS auto-unsealing, follow these steps:

1. Update your Vault configuration to include the `gcpckms` seal stanza
    
2. Restart Vault
    
3. Unseal Vault with the `-migrate` flag:
    

```yaml
# Provide three of your five Shamir unseal keys with the -migrate flag
vault operator unseal -migrate <UNSEAL_KEY_1>
vault operator unseal -migrate <UNSEAL_KEY_2>
vault operator unseal -migrate <UNSEAL_KEY_3>
​
# Verify the migration was successful
vault status
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1746249011930/0929599c-da1e-46b7-880b-6d0c0a4502c1.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1746249022484/d2265d83-ec1a-4dfa-828b-cc59dd2eae31.png align="center")

After successful migration, you should see output similar to:

```yaml
Key                      Value
---                      -----
Seal Type                gcpckms
Recovery Seal Type       shamir
Initialized              true
Sealed                   false
Total Recovery Shares    5
Threshold                3
Version                  1.19.0
Build Date               2025-03-04T12:36:40Z
Storage Type             gcs
Cluster Name             vault-cluster-xxxxxx
Cluster ID               sssss-5a16sc405-89c1-s333333ffffff
HA Enabled               true
```

Note that the Shamir keys are now recovery keys for use in emergency situations.

## **Setting Up Authentication Methods (Optional)**

After your Vault is up and running with auto-unsealing, you might want to configure authentication methods:

### **OIDC Authentication**

```yaml
# Enable OIDC auth method
vault auth enable oidc
​
# Configure OIDC
vault write auth/oidc/config \
  oidc_discovery_url="https://oidc.yourdomain.com/realms/vault" \
  oidc_client_id="vault-client" \
  oidc_client_secret="your-client-secret" \
  default_role="reader"
```

### **AppRole Authentication for Applications**

```yaml
# Enable AppRole auth method
vault auth enable approle
​
# Create a policy for the Vault agent
vault policy write vault-agent-policy - <<EOF
path "secret/data/*" {
  capabilities = ["read"]
}
EOF
​
# Create an AppRole with the policy attached
vault write auth/approle/role/vault-agent \
  token_policies="vault-agent-policy" \
  token_ttl=1h \
  token_max_ttl=2h \
  secret_id_ttl=1h \
  bind_secret_id=true
​
# Get Role ID
vault read auth/approle/role/vault-agent/role-id
​
# Generate a Secret ID
vault write -f auth/approle/role/vault-agent/secret-id
```

## **Conclusion**

By configuring HashiCorp Vault with Google Cloud KMS for auto-unsealing and deploying it to Cloud Run, we've created a serverless, fully managed secrets management solution that automatically unseals itself after restarts. The CI/CD pipeline with GitHub Actions further enhances this setup by automating the deployment process, making version management easy and repeatable, and our use of GCP Secret Manager ensures that sensitive initialization data is securely stored.

This approach eliminates the operational burden of manual unsealing while maintaining the security benefits of Vault's seal mechanism. The combination of GCP KMS for auto-unsealing, Secret Manager for secure storage, Cloud Run for deployment, and GitHub Actions for CI/CD provides a scalable, resilient, and cost-effective secrets management solution that can grow with your organization's needs.

## **Next Steps**

* Set up proper TLS certificates for your Vault instance
    
* Configure additional authentication methods as needed
    
* Implement audit logging
    
* Enhance your CI/CD pipeline with testing
    
* Implement backup and disaster recovery procedures
    
* Consider setting up Vault HA for higher availability
    

## **Resources**

* [HashiCorp Vault Documentation](https://developer.hashicorp.com/vault/docs)
    
* [GCP KMS Auto-Unseal](https://developer.hashicorp.com/vault/docs/configuration/seal/gcpckms)
    
* [Vault on Google Cloud](https://cloud.google.com/solutions/secrets-management)
    
* [Cloud Run Documentation](https://cloud.google.com/run/docs)
    
* [GitHub Actions Documentation](https://docs.github.com/en/actions)
    
* [GCP Secret Manager](https://cloud.google.com/secret-manager/docs)
    

---

*This article represents a practical implementation based on real-world experience deploying HashiCorp Vault on Google Cloud Platform. While this setup works well for many use cases, always assess your specific security requirements before implementing any secrets management solution in production.*
