Skip to main content

Gate on GCP as Cloud Run Service

This guide will show you how to deploy Gate on GCP as a Cloud Run container. If you aren't already familiar with Gate, we recommend reading our getting started page.

UserGCP VPC networkGatedestination serviceHTTP requestHTTP request(if authorized)

Deploying

In this guide, we will deploy Gate as a Cloud Run service to validate JSON Web Token (JWT) tokens in requests sent to another Cloud Run service. This allows us to add authorization to a service without custom middleware. You can easily adapt this example to protect server-side rendered applications by using Authentication Proxy plugin instead.

This is a basic example designed to demonstrate the deployment steps. For a list of use cases supported by Gate, see our use cases page.

In this tutorial, we will use Terraform to deploy Gate.

First, let's configure the Terraform GCP provider.

main.tf
provider "google" {
project = var.gcp_project
}

terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "4.24.0"
}
}
}

resource "google_project_service" "service" {
count = length(var.project_services)
project = var.gcp_project
service = element(var.project_services, count.index)

disable_on_destroy = false
}

and define variables that we will need later:

vars.tf
variable "region" {
type = string

default = "europe-west1"
description = "The region to deploy to."
}

variable "gcp_project" {
type = string
description = "The GCP project to deploy to."
}

variable "slashid_organization_id" {
type = string
description = "The SlashID organization ID. You can find it in the dashboard."
}

variable "project_services" {
type = list(string)
default = [
"cloudresourcemanager.googleapis.com",
"compute.googleapis.com",
"servicenetworking.googleapis.com",
"vpcaccess.googleapis.com",
"run.googleapis.com",
"iam.googleapis.com",
]
description = "List of GCP services to enable on the project."
}
tip

This guide uses JWTs issued by SlashID. If you don't have a SlashID organization yet, you can create one for free in 30 seconds by visiting SlashID Console.

Later, we will define a dummy Cloud Run service that we will protect with Gate.

cloudrun_service.tf
resource "google_service_account" "service" {
account_id = "service-cloudrun"
display_name = "Service Cloud Run service service account"
}

resource "google_cloud_run_service" "service" {
name = "service"
location = var.region

template {
spec {
service_account_name = google_service_account.service.email

containers {
image = "us-docker.pkg.dev/cloudrun/container/hello"
}
}

metadata {
annotations = {
"autoscaling.knative.dev/maxScale" = 10

"run.googleapis.com/client-name" = "terraform"
"run.googleapis.com/vpc-access-connector" = "${google_vpc_access_connector.gate_vpc_connector.id}"
"run.googleapis.com/vpc-access-egress" = "private-ranges-only"
"autoscaling.knative.dev/maxScale" = "10"
}
}
}

metadata {
annotations = {
# Allow inbound requests only from from VPC network.
# Change to "external" would allow access from the Internet.
"run.googleapis.com/ingress" = "internal"
}
}

traffic {
percent = 100
latest_revision = true
}

autogenerate_revision_name = true

lifecycle {
ignore_changes = [
metadata[0].annotations["client.knative.dev/user-image"],
metadata[0].annotations["run.googleapis.com/client-name"],
metadata[0].annotations["run.googleapis.com/client-version"],
metadata[0].annotations["run.googleapis.com/operation-id"],

template[0].metadata[0].annotations["run.googleapis.com/client-name"],
template[0].metadata[0].annotations["run.googleapis.com/client-version"],
template[0].metadata[0].annotations["run.googleapis.com/operation-id"],
template[0].metadata[0].annotations["client.knative.dev/user-image"],

template[0].metadata[0].labels["run.googleapis.com/startupProbeType"],
]
}
}

Next, let's define the Gate Cloud Run service. It will be publicly accessible and will proxy requests to the protected service.

gate_service.tf
resource "google_service_account" "gate_service" {
account_id = "cloudrun-gate"
display_name = "Gate Cloud Run service service account"
}

resource "google_cloud_run_service" "gate" {
name = "gate"

location = var.region

template {
spec {
service_account_name = google_service_account.gate_service.email

containers {
image = "slashid/gate-free:latest"

env {
name = "GATE_LOG_LEVEL"
value = "info"
}
env {
name = "GATE_PORT"
value = "8080"
}

# In this tutorial we will use just one target service, but it's possible to use multiple targets.
# You can find more information here: https://developer.slashid.dev/docs/gate/configuration#url-configuration
env {
name = "GATE_DEFAULT_TARGET"
value = google_cloud_run_service.service.status[0].url
}

# You can find all available plugins here: https://developer.slashid.dev/docs/gate/plugins
env {
name = "GATE_PLUGINS_0_TYPE"
value = "validate-jwt"
}
env {
name = "GATE_PLUGINS_0_ENABLED"
value = "true"
}

# Full plugin configuration reference can be found here: https://developer.slashid.dev/docs/gate/plugins/validate-jwt
env {
name = "GATE_PLUGINS_0_PARAMETERS_JWKS_URL"
value = "https://api.slashid.com/.well-known/jwks.json"
}
env {
name = "GATE_PLUGINS_0_PARAMETERS_JWT_ALLOWED_ALGORITHMS"
value = "RS256"
}
env {
name = "GATE_PLUGINS_0_PARAMETERS_JWT_EXPECTED_ISSUER"
value = "https://api.slashid.com"
}
env {
name = "GATE_PLUGINS_0_PARAMETERS_JWT_EXPECTED_AUDIENCE"
value = var.slashid_organization_id
}
}
}

metadata {
annotations = {
"autoscaling.knative.dev/maxScale" = 10

"run.googleapis.com/client-name" = "terraform"
"run.googleapis.com/vpc-access-connector" = google_vpc_access_connector.gate_vpc_connector.id

# all egress from the service should go through the VPC Connector
# it allows calling other Cloud Run instances with "run.googleapis.com/ingress" = "internal"
# as calling `google_cloud_run_service.service.status[0].url` is considered as external traffic
"run.googleapis.com/vpc-access-egress" = "all-traffic"
}
}
}

metadata {
annotations = {
# Allow inbound requests only from from VPC network and external load balancer
"run.googleapis.com/ingress" = "all"
}
}

autogenerate_revision_name = true

lifecycle {

ignore_changes = [
metadata[0].annotations["client.knative.dev/user-image"],
metadata[0].annotations["run.googleapis.com/client-name"],
metadata[0].annotations["run.googleapis.com/client-version"],
metadata[0].annotations["run.googleapis.com/operation-id"],

template[0].metadata[0].annotations["run.googleapis.com/client-name"],
template[0].metadata[0].annotations["run.googleapis.com/client-version"],
template[0].metadata[0].annotations["client.knative.dev/user-image"],
template[0].metadata[0].annotations["run.googleapis.com/operation-id"],

template[0].metadata[0].labels["run.googleapis.com/startupProbeType"],
]
}
}

Next, we want to define a network configuration for Gate. The service will be accessible from the vpc network, so it could be called only from the gate service.

net.tf
resource "google_compute_network" "gate_private_network" {
provider = google-beta

name = "gate-network"

depends_on = [google_project_service.service]
}

resource "google_compute_global_address" "gate_private_ip_address" {
provider = google-beta

name = "gate-net-ip-address"
purpose = "VPC_PEERING"
address_type = "INTERNAL"
prefix_length = 16
network = google_compute_network.gate_private_network.self_link
}

resource "google_service_networking_connection" "gate_private_vpc_connection" {
provider = google-beta

network = google_compute_network.gate_private_network.self_link
service = "servicenetworking.googleapis.com"
reserved_peering_ranges = [google_compute_global_address.gate_private_ip_address.name]
}

resource "google_vpc_access_connector" "gate_vpc_connector" {
name = "gate-vpc-connector"
region = var.region
ip_cidr_range = "10.8.0.0/28"
network = google_compute_network.gate_private_network.name
}

# Assign static outbound IP for Cloud Run. It's required for Gate to be able to get JWKS.

resource "google_compute_address" "nat_ip" {
name = "nat-gateway-${var.region}"
region = var.region
}

resource "google_compute_router" "router" {
name = "router-${var.region}"
region = var.region
network = google_compute_network.gate_private_network.name
}

resource "google_compute_router_nat" "nat" {
name = "router-nat-${var.region}"
router = google_compute_router.router.name
region = var.region

nat_ip_allocate_option = "MANUAL_ONLY"
nat_ips = google_compute_address.nat_ip.*.self_link
source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES"

log_config {
enable = true
filter = "ERRORS_ONLY"
}
}

Later, we need to define IAM permissions for the Gate service. We don't want GCP authentication for Gate, as it won't be publicly accessible. It's also unnecessary to use GCP authentication for the protected service, as it will be called only from Gate.

iam.tf
# Do not require GCP authentication for Cloud Run service.
data "google_iam_policy" "noauth" {
binding {
role = "roles/run.invoker"
members = ["allUsers"]
}
}

resource "google_cloud_run_service_iam_policy" "noauth" {
location = google_cloud_run_service.service.location
project = google_cloud_run_service.service.project
service = google_cloud_run_service.service.name
policy_data = data.google_iam_policy.noauth.policy_data
}

resource "google_cloud_run_service_iam_policy" "noauth_gate" {
location = google_cloud_run_service.gate.location
project = google_cloud_run_service.gate.project
service = google_cloud_run_service.gate.name

policy_data = data.google_iam_policy.noauth.policy_data
}

Finally, let's define Terraform outputs with the Gate public URL, which Terraform will print after deployment.

outputs.tf
output "public_gate_url" {
value = google_cloud_run_service.gate.status[0].url
}

Applying changes

If you are using the new Terraform configuration, you need to initialize Terraform first:

terraform init

After that, you can apply changes:

terraform apply -var gcp_project=<GCP PROJECT> -var slashid_organization_id=<SLASHID ORGANIZATION ID>
tip

This guide uses JWTs issued by SlashID. If you don't have a SlashID organization yet, you can create one for free in 30 seconds by visiting SlashID Console.

Verifying deployment

Let's ensure that requests without a token are rejected:

> curl -I <public_gate_url from Terraform output>

HTTP/2 400
via: 1.0 gate
content-type: text/html
server: Google Frontend

The request was served by Gate (via header), and it returned a 400 error code.

Now, we can verify that Gate allows requests with a valid token. First, we need to create a valid token.

We will use the SlashID mint token endpoint.

tip

You may have no persons in your organization yet. You can create a new person by using the create person endpoint or in the SlashID Console.

> curl --location --request POST 'https://api.slashid.com/persons/<PERSON ID>/mint-token' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'SlashID-API-Key: <YOUR API KEY>' \
--header 'SlashID-OrgID: <YOUR ORG ID>'

{
"result": "<TOKEN>"
}

Now, let's include the generated token in a request to the Cloud Run:

> curl -I <public_gate_url from Terraform output> \
--header 'Authorization:Bearer <TOKEN>'

HTTP/2 200
content-type: text/html; charset=utf-8
via: 1.0 gate
server: Google Frontend

Response code 200 means that the request was allowed by Gate.

Entire Terraform configuration

If you were using Terraform, this is the entire configuration that you should have at the end.

main.tf
provider "google" {
project = var.gcp_project
}

terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "4.24.0"
}
}
}

resource "google_project_service" "service" {
count = length(var.project_services)
project = var.gcp_project
service = element(var.project_services, count.index)

disable_on_destroy = false
}
variable "region" {
type = string

default = "europe-west1"
description = "The region to deploy to."
}

variable "gcp_project" {
type = string
description = "The GCP project to deploy to."
}

variable "slashid_organization_id" {
type = string
description = "The SlashID organization ID. You can find it in the dashboard."
}

variable "project_services" {
type = list(string)
default = [
"cloudresourcemanager.googleapis.com",
"compute.googleapis.com",
"servicenetworking.googleapis.com",
"vpcaccess.googleapis.com",
"run.googleapis.com",
"iam.googleapis.com",
]
description = "List of GCP services to enable on the project."
}
resource "google_service_account" "service" {
account_id = "service-cloudrun"
display_name = "Service Cloud Run service service account"
}

resource "google_cloud_run_service" "service" {
name = "service"
location = var.region

template {
spec {
service_account_name = google_service_account.service.email

containers {
image = "us-docker.pkg.dev/cloudrun/container/hello"
}
}

metadata {
annotations = {
"autoscaling.knative.dev/maxScale" = 10

"run.googleapis.com/client-name" = "terraform"
"run.googleapis.com/vpc-access-connector" = "${google_vpc_access_connector.gate_vpc_connector.id}"
"run.googleapis.com/vpc-access-egress" = "private-ranges-only"
"autoscaling.knative.dev/maxScale" = "10"
}
}
}

metadata {
annotations = {
# Allow inbound requests only from from VPC network.
# Change to "external" would allow access from the Internet.
"run.googleapis.com/ingress" = "internal"
}
}

traffic {
percent = 100
latest_revision = true
}

autogenerate_revision_name = true

lifecycle {
ignore_changes = [
metadata[0].annotations["client.knative.dev/user-image"],
metadata[0].annotations["run.googleapis.com/client-name"],
metadata[0].annotations["run.googleapis.com/client-version"],
metadata[0].annotations["run.googleapis.com/operation-id"],

template[0].metadata[0].annotations["run.googleapis.com/client-name"],
template[0].metadata[0].annotations["run.googleapis.com/client-version"],
template[0].metadata[0].annotations["run.googleapis.com/operation-id"],
template[0].metadata[0].annotations["client.knative.dev/user-image"],

template[0].metadata[0].labels["run.googleapis.com/startupProbeType"],
]
}
}
resource "google_service_account" "gate_service" {
account_id = "cloudrun-gate"
display_name = "Gate Cloud Run service service account"
}

resource "google_cloud_run_service" "gate" {
name = "gate"

location = var.region

template {
spec {
service_account_name = google_service_account.gate_service.email

containers {
image = "slashid/gate-free:latest"

env {
name = "GATE_LOG_LEVEL"
value = "info"
}
env {
name = "GATE_PORT"
value = "8080"
}

# In this tutorial we will use just one target service, but it's possible to use multiple targets.
# You can find more information here: https://developer.slashid.dev/docs/gate/configuration#url-configuration
env {
name = "GATE_DEFAULT_TARGET"
value = google_cloud_run_service.service.status[0].url
}

# You can find all available plugins here: https://developer.slashid.dev/docs/gate/plugins
env {
name = "GATE_PLUGINS_0_TYPE"
value = "validate-jwt"
}
env {
name = "GATE_PLUGINS_0_ENABLED"
value = "true"
}

# Full plugin configuration reference can be found here: https://developer.slashid.dev/docs/gate/plugins/validate-jwt
env {
name = "GATE_PLUGINS_0_PARAMETERS_JWKS_URL"
value = "https://api.slashid.com/.well-known/jwks.json"
}
env {
name = "GATE_PLUGINS_0_PARAMETERS_JWT_ALLOWED_ALGORITHMS"
value = "RS256"
}
env {
name = "GATE_PLUGINS_0_PARAMETERS_JWT_EXPECTED_ISSUER"
value = "https://api.slashid.com"
}
env {
name = "GATE_PLUGINS_0_PARAMETERS_JWT_EXPECTED_AUDIENCE"
value = var.slashid_organization_id
}
}
}

metadata {
annotations = {
"autoscaling.knative.dev/maxScale" = 10

"run.googleapis.com/client-name" = "terraform"
"run.googleapis.com/vpc-access-connector" = google_vpc_access_connector.gate_vpc_connector.id

# all egress from the service should go through the VPC Connector
# it allows calling other Cloud Run instances with "run.googleapis.com/ingress" = "internal"
# as calling `google_cloud_run_service.service.status[0].url` is considered as external traffic
"run.googleapis.com/vpc-access-egress" = "all-traffic"
}
}
}

metadata {
annotations = {
# Allow inbound requests only from from VPC network and external load balancer
"run.googleapis.com/ingress" = "all"
}
}

autogenerate_revision_name = true

lifecycle {

ignore_changes = [
metadata[0].annotations["client.knative.dev/user-image"],
metadata[0].annotations["run.googleapis.com/client-name"],
metadata[0].annotations["run.googleapis.com/client-version"],
metadata[0].annotations["run.googleapis.com/operation-id"],

template[0].metadata[0].annotations["run.googleapis.com/client-name"],
template[0].metadata[0].annotations["run.googleapis.com/client-version"],
template[0].metadata[0].annotations["client.knative.dev/user-image"],
template[0].metadata[0].annotations["run.googleapis.com/operation-id"],

template[0].metadata[0].labels["run.googleapis.com/startupProbeType"],
]
}
}
resource "google_compute_network" "gate_private_network" {
provider = google-beta

name = "gate-network"

depends_on = [google_project_service.service]
}

resource "google_compute_global_address" "gate_private_ip_address" {
provider = google-beta

name = "gate-net-ip-address"
purpose = "VPC_PEERING"
address_type = "INTERNAL"
prefix_length = 16
network = google_compute_network.gate_private_network.self_link
}

resource "google_service_networking_connection" "gate_private_vpc_connection" {
provider = google-beta

network = google_compute_network.gate_private_network.self_link
service = "servicenetworking.googleapis.com"
reserved_peering_ranges = [google_compute_global_address.gate_private_ip_address.name]
}

resource "google_vpc_access_connector" "gate_vpc_connector" {
name = "gate-vpc-connector"
region = var.region
ip_cidr_range = "10.8.0.0/28"
network = google_compute_network.gate_private_network.name
}

# Assign static outbound IP for Cloud Run. It's required for Gate to be able to get JWKS.

resource "google_compute_address" "nat_ip" {
name = "nat-gateway-${var.region}"
region = var.region
}

resource "google_compute_router" "router" {
name = "router-${var.region}"
region = var.region
network = google_compute_network.gate_private_network.name
}

resource "google_compute_router_nat" "nat" {
name = "router-nat-${var.region}"
router = google_compute_router.router.name
region = var.region

nat_ip_allocate_option = "MANUAL_ONLY"
nat_ips = google_compute_address.nat_ip.*.self_link
source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES"

log_config {
enable = true
filter = "ERRORS_ONLY"
}
}
# Do not require GCP authentication for Cloud Run service.
data "google_iam_policy" "noauth" {
binding {
role = "roles/run.invoker"
members = ["allUsers"]
}
}

resource "google_cloud_run_service_iam_policy" "noauth" {
location = google_cloud_run_service.service.location
project = google_cloud_run_service.service.project
service = google_cloud_run_service.service.name
policy_data = data.google_iam_policy.noauth.policy_data
}

resource "google_cloud_run_service_iam_policy" "noauth_gate" {
location = google_cloud_run_service.gate.location
project = google_cloud_run_service.gate.project
service = google_cloud_run_service.gate.name

policy_data = data.google_iam_policy.noauth.policy_data
}
output "public_gate_url" {
value = google_cloud_run_service.gate.status[0].url
}

Further reading

In this tutorial, we covered a simple use case. You can learn about other Gate use cases in Use Cases page.