Deploying OAuth2-Proxy as a Cloud Run sidecar container
In this article we will see a step-by-step tutorial on how to secure access to a Cloud Run web application with OAuth2 without making any change to the application code.
Introduction
Cloud Run is a popular serverless platform offered by Google Cloud Platform (GCP) that allows you to run stateless containers invocable via HTTP requests. It’s a fully managed environment, meaning you don’t need to worry about infrastructure management, and it automatically scales your application up or down (even to zero) based on demand. This makes it ideal for deploying microservices, APIs, and web applications.
Cloud Run’s sidecar containers are a powerful feature that allows you to run a second container alongside the main application container in order to decouple tasks such as authentication, access protection, network proxying and mesh, logging and monitoring. The sidecar container shares the same network namespace as the main container, allowing seamless communication between them.
A typical need for real-world applications is to secure access with OAuth authorization. OAuth 2.0 is an industry-standard authorization framework that allows third-party applications to access user data without requiring their username and password. It works by delegating user authentication to a trusted identity provider (IdP), such as Google, Facebook, Okta, Keycloak and many others. The IdP issues an access token to the application, which can then be used to access protected resources on behalf of the user.
A popular open source OAuth implementation is OAuth2-Proxy, a software that can act as either a standalone reverse proxy in front of the application or a middleware component integrated with the existing reverse proxy or load balancer setup.
In this article I will provide you a step-by-step tutorial on how to deploy the OAuth2-Proxy container as a sidecar in front of the Cloud Run microservice, as shown in the diagram below.
The main benefit of this architecture, following a modular and reusable approach, is to decouple the OAuth authorization flow from the web application, where you don’t need to add any custom code to handle the OAuth flow.
Deploy a Cloud Run hello application
In order to start, the pre-requisite is to have a running Cloud Run microservice. For demo purposes, a simple way to deploy a Cloud Run instance is to use the GCP built-in demo container image (of course, any other container image of your choice is fine, just replace it in the command below):
export PROJECT_ID=serverless-playground-368617
export REGION=europe-west4
gcloud config set project $PROJECT
gcloud config set run/region $REGION
gcloud run deploy --image us-docker.pkg.dev/cloudrun/container/hello --allow-unauthenticated hello-demo
After few seconds you can check that the deployed microservice is available (please note that I’ve disabled Cloud Run native authentication):
curl $(gcloud run services list --format="value(URL)" --filter="metadata.name=hello-demo")
Now we’re ready to add the OAuth 2.0 capabilities to our microservice.
Register your app to the Google OAuth2 provider
Among the multiple OAuth providers available and supported by OAuth2Proxy, I’ve chosen to use Google OAuth 2.0.
The first step is to follow the instructions in the OAuth2 Proxy configuration guide in order to register our Cloud Run microservice to the Google OAuth provider. In the Google Cloud console create the OAuth consent screen for your application URI, then create the credentials (select the OAuth client ID type). In the credentials set the authorized Javascript origin to the Cloud Run URI and add the callback path /oauth2/callback to the Cloud Run service URI in the redirect URI:
Download the generated JSON file and take note of the client ID and secret that we will use later.
Prepare the OAuth2-Proxy configuration
Before deploying the OAuth2Proxy sidecar container, we need to make some preparatory work.
First, let’s pull the OAuth2Proxy image from its public repository and push it to our Artifact Registry repository (create one, if not yet existing), from which the Cloud Run service can pull the image. I’ve downloaded the OAuth2-Proxy version based on the Alpine base image instead of the distroless one.
docker pull quay.io/oauth2-proxy/oauth2-proxy:latest-alpine
docker tag quay.io/oauth2-proxy/oauth2-proxy:latest-alpine europe-west4-docker.pkg.dev/$PROJECT_ID/<YOUR REPO>/oauth-proxy:latest-alpine
docker push europe-west4-docker.pkg.dev/$PROJECT_ID/<YOUR REPO>/oauth-proxy:latest-alpine
Let’s now create the oauth2-proxy.cfg config file locally:
## OAuth2 Proxy Config File
## https://github.com/oauth2-proxy/oauth2-proxy
## <addr>:<port> to listen on for HTTP/HTTPS clients
http_address = "0.0.0.0:4180"
## the OAuth Redirect URL.
# defaults to the "https://" + requested host header + "/oauth2/callback"
redirect_url = "https://<CLOUD RUN URI>/oauth2/callback"
## the http url(s) of the upstream endpoint. If multiple, routing is based on path
upstreams = [
"http://127.0.0.1:8080/"
]
## Email Domains to allow authentication for (this authorizes any email on this domain)
## for more granular authorization use `authenticated_emails_file`
## To authorize any email addresses use "*"
email_domains = [
"*"
]
## The OAuth Client ID, Secret
client_id = "<CLIENT ID>"
client_secret = "<CLIENT SECRET>"
## skip SSL checking for HTTPS requests
ssl_insecure_skip_verify = true
## Cookie Settings
## Name - the cookie name
## Secret - the seed string for secure cookies; should be 16, 24, or 32 bytes
## for use with an AES cipher when cookie_refresh or pass_access_token
## is set
## Domain - (optional) cookie domain to force cookies to (ie: .yourcompany.com)
## Expire - (duration) expire timeframe for cookie
## Refresh - (duration) refresh the cookie when duration has elapsed after cookie was initially set.
## Should be less than cookie_expire; set to 0 to disable.
## On refresh, OAuth token is re-validated.
## (ie: 1h means tokens are refreshed on request 1hr+ after it was set)
## Secure - secure cookies are only sent by the browser of a HTTPS connection (recommended)
## HttpOnly - httponly cookies are not readable by javascript (recommended)
cookie_name = "_oauth2_proxy"
cookie_secret = "<COOKIE SECRET GENERATED AS BELOW>"
cookie_expire = "168h"
cookie_secure = true
cookie_httponly = true
I’ve done a simplified configuration that can be made as complex as desired. Please note few things:
- the OAuth2Proxy is listening on the 4180 port (I’ve kept the standard OAuth2Proxy port, but this can be fully customized)
- the upstream container (hello in our demo) is expected to listen on the 8080 port
- I’ve configured the client_id and client_secret previously generated as GCP OAuth 2.0 Credentials
- I’ve added a cookie_secret generated with the command:
dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64 | tr -d -- '\n' | tr -- '+/' '-_'; echo
Let’s upload the config file as a secret into the GCP Secret Manager and give the Cloud Run Service Account the Secret Accessor role for the newly generated secret:
gcloud secrets create oauth2_config --replication-policy="automatic" --data-file="./oauth2-proxy.cfg"
gcloud secrets add-iam-policy-binding oauth2_config --member=serviceAccount:<Cloud Run Service Account> --role='roles/secretmana
ger.secretAccessor'
Deploy the sidecar OAuth2 container
Now let’s deploy the OAuth2Proxy sidecar container. One way to do it is to extract the current Cloud Run configuration with the gcloud export feature, to edit it and to apply the modified version with the replace command (the alternative would be to apply the changes manually through the Google Cloud Console). Let’s extract the current configuration:
gcloud run services describe hello-demo --region $REGION --format export > hellowithoauth2.yaml
Let’s analyze one by one the changes one we will apply. First, we declare the dependency in order for the hello container to wait for the oauth-proxy successful startup:
spec:
template:
metadata:
annotations:
run.googleapis.com/container-dependencies: '{"hello":["oauth-proxy"]}'
Then, we create the volume to be mounted into the containers:
spec:
template:
spec:
volumes:
- name: secret-1
secret:
secretName: oauth2_config
items:
- key: latest
path: oauth2_config.cfg
The the secret-1 module contains the OAuth2 configuration created above.
We can now create the OAuth-Proxy sidecar container, that will be listening on port 4180 and will mount the 2 volumes:
spec:
template:
spec:
containers:
- name: oauth-proxy
image: europe-west4-docker.pkg.dev/<PROJECT_ID>/demos-repo/oauth-proxy:latest-alpine
command:
- /bin/oauth2-proxy
args:
- --config=/etc/oauth2_config.cfg
ports:
- name: http1
containerPort: 4180
resources:
limits:
cpu: 500m
memory: 256Mi
volumeMounts:
- name: secret-1
mountPath: /etc
startupProbe:
timeoutSeconds: 240
periodSeconds: 240
failureThreshold: 1
tcpSocket:
port: 4180
Finally, since the new request entrypoint will be the oauth2-proxy port 4180, we also have to remove the listening port 8080 from the previous declaration of the hello container (a Cloud Run microservice can only have 1 listening port):
spec:
template:
spec:
containers:
ports:
- containerPort: 8080
name: http1
Let’s apply all the above configuration changes with the gcloud replace command:
gcloud run services replace --region $REGION hellowithoauth2.yaml
Please note that, in general, the OAuth flow might require the Cloud Run microservice to communicate with the external IdP over the Internet, make sure that everything is in place to allow this communication (namely Cloud Run egress configuration, routing and firewall configurations if using VPC intermediation and name resolution).
Test the OAuth flow
Now we can test the OAuth2 authorization flow. With your browser reach the microservice URL and you’ll see that this time you are given the OAuth2 Proxy landing page.
Let’s sign in with Google and give our consent. We’ll finally reach the hello webpage:
Summary
In this article I’ve deployed an OAuth2-Proxy container as a sidecar to a Cloud Run web application.
I’ve first configured the OAuth2 flow on the Google OAuth provider, then injected the sidecar container into the Cloud Run web application by applying a configuration change. After that the web application becomes accessible only to authenticated users that are authorized by the OAuth provider. No modification to the web application code has been done.
Please note that this example is to be considered a demo and should not be used in production, but can be used as a starting point for your application setup.