Skip to content

xiangshen-dk/securing-gcs-static-assets-iap

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

title description author tags date_published
Protecting static website assets hosted on Cloud Storage using Identity-Aware-Proxy(IAP)
Learn how to protect static website assets on Cloud Storage with Cloud Load Balancing serverless network endpoint groups (NEGs).
xiangshen-dk
authentication, authorization, security, IAP
2022-05-05

It's easy to create and maintain an HTTPS-based static website on Cloud Storage with Cloud CDN, Cloud Load Balancing, managed SSL certificates, and custom domains. This serverless approach is popular because of its flexibility, scalability, and low cost.
However, it can still be challenging to provide authentication and authorization for a static website on Google Cloud in a serverless fashion. This tutorial describes a solution to protect static assets on Cloud Storage using Cloud Load Balancing serverless network endpoint groups (NEGs).

The following diagram shows the high-level architecture of this solution:

gcs-static-arch

The architecture incorporates the following key features:

  1. A load balancer to use a custom domain with a managed SSL certificate. When a request is received, the solution uses routing rules to check whether a signed cookie for Cloud CDN is in the request header. If the cookie doesn't exist, the request is redirected to an app hosted on Cloud Run. If there is a signed cookie, the request is sent to the Cloud CDN of the backend bucket. The Cloud Run app is necessary since IAP can be enabled for Cloud Run but not directly for GCS.

  2. IAP performs the authentication when access the Cloud Run app. If the authentication is successful, the Cloud Run app generates a signed cookie and sends it back to the client with an HTTP redirect. The cookie works because the login app and the bucket backends are behind the same load balancer. Therefore, they are considered as the same origin.

    Important: Even if a user can access the default Cloud Run endpoint directly and log in from there, they still don't have access to the CDN because the cookie is not from the same origin. Also, notice the cookie has a specific path instead of the default '/'.

  3. The CDN signed cookie is verified by Cloud CDN, providing access to the static assets. You can specify an expiration time for the cookie. When the cookie has expired, modern browsers either delete the cookie or stop sending it. Cloud CDN also rejects expired cookies.

  4. Permit Cloud CDN to read the objects in the private bucket by adding the Cloud CDN service account to Cloud Storage access control lists (ACLs).

Objectives

  • Build a static website and deploy it to a Cloud Storage bucket.
  • Create a Cloud Load Balancing load balancer with a Google-managed SSL certificate.
  • Set up Cloud CDN with a backend bucket.
  • Deploy a service to Cloud Run and enable IAP.
  • Create serverless network endpoint groups for Cloud Load Balancing.
  • Create URL maps and route authenticated and unauthenticated traffic.

Costs

This tutorial uses billable components of Google Cloud, including the following:

Use the pricing calculator to generate a cost estimate based on your projected usage.

Before you begin

For this tutorial, you need a Google Cloud project. You can create a new project or select a project that you already created. When you finish this tutorial, you can avoid continued billing by deleting the resources you created. To make cleanup easiest, you may want to create a new project for this tutorial, so that you can delete the project when you're done. For details, see the "Cleaning up" section at the end of the tutorial.

To complete this tutorial, you need a domain that you own or manage. If you don't yet have a domain, there are many services through which you can register a domain, such as Google Domains. This tutorial uses nip.io for a custom domain name.

  1. Select or create a Google Cloud project.

  2. Enable billing for your project.

  3. Enable the Compute Engine, Cloud Run, Cloud Secret Manager, and Cloud DNS APIs.

  4. Make sure that you have either a project owner or editor role, or sufficient permissions to use the services listed in the previous section.

  5. Verify that you own or manage the domain that you will be using.

    Make sure that you are verifying the top-level domain, such as example.com, and not a subdomain, such as www.example.com.

    Note: If you own the domain that you are associating with a bucket, you might have already performed this step in the past. If you purchased your domain through Google Domains, verification is automatic.

For information about using Cloud DNS to set up your domain, see Set up your domain using Cloud DNS.

Using Cloud Shell

This tutorial uses the following tool packages:

Because Cloud Shell automatically includes these packages, we recommend that you run the commands in this tutorial in Cloud Shell, so that you don't need to install these packages locally.

Preparing your environment

Get the sample code

  1. Clone the repository:

    git clone https://github.com/xiangshen-dk/securing-gcs-static-assets-iap.git
    
  2. Go to the tutorial directory:

    cd securing-gcs-static-assets-iap/static-website
    

Set environment variables

Set environment variables that you use throughout the tutorial:

# Replace [YOUR_PROJECT_ID] with your project ID.
export PROJECT_ID=[YOUR_PROJECT_ID]

export PROJECT_NUM=$(gcloud projects describe ${PROJECT_ID} --format="value(projectNumber)")
export REGION=us-central1
export BUCKET_NAME=${PROJECT_ID}-example-com
export SERVERLESS_NEG_NAME=login-web-serverless-neg
export LOGIN_BACKEND_SVC_NAME=login-backend-service
export STATIC_IP_NAME=private-static-web-external-ip
export CDN_SIGN_KEY=private-static-web-cdn-key
export GCS_PATH=static

Set environment variables

gcloud services enable \
    iap.googleapis.com \
    cloudresourcemanager.googleapis.com \
    cloudidentity.googleapis.com \
    compute.googleapis.com \
    cloudbuild.googleapis.com \
    run.googleapis.com \
    secretmanager.googleapis.com

Implementation steps

Build a static website and deploy it to Cloud Storage

  1. Build the demonstration single-page application (SPA):

    npm install
    npm run build
    

    This is a demonstration app using Vue.js. You can ignore any warnings from npm.

  2. Create a bucket:

    gsutil mb -b on gs://${BUCKET_NAME}
    

    For infomation on the gsutil mb command, see the documentation.

  3. Upload the build artifacts, which are all static files:

    gsutil rsync -R dist/ gs://$BUCKET_NAME/${GCS_PATH}
    

    For infomation on the gsutil rsync command, see the documentation.

  4. Set the MainPageSuffix property with the -m flag and the NotFoundPage with the -e flag:

    gsutil web set -m index.html -e index.html gs://$BUCKET_NAME
    

    For infomation on the gsutil web set command, see the documentation.

Create a load balancer

  1. Reserve an external IP address, which will be used by the load balancer:

    gcloud compute addresses create $STATIC_IP_NAME \
        --network-tier=PREMIUM \
        --ip-version=IPV4 \
        --global
    
  2. Export the IP address:

    export SVC_IP_ADDR=$(gcloud compute addresses list --filter="name=${STATIC_IP_NAME}" \
    --format="value(address)" --global --project ${PROJECT_ID})
    
  3. Confirm that you have an IP address available in the SVC_IP_ADDR environment variable:

    echo ${SVC_IP_ADDR}
    

    Example output:

    34.120.180.189
    
  4. Create a domain using nip.io.

    export DNS_NAME=${SVC_IP_ADDR}.nip.io
    

    Note: You can configure your DNS to use a custom domain name.

  5. Create an HTTPS load balancer, configure the bucket as a backend, and enable Cloud CDN for the load balancer:

    gcloud compute backend-buckets create web-backend-bucket \
        --gcs-bucket-name=$BUCKET_NAME \
        --enable-cdn
    
  6. Create a Google-managed SSL certificate.

    gcloud compute ssl-certificates create www-ssl-cert \
    --domains $DNS_NAME
    
  7. Check the status of your SSL certificate:

    gcloud compute ssl-certificates list | grep ${DNS_NAME}
    

    Example output:

    xxx.xxx.xxx.xxx.nip.io: PROVISIONING
    

    Initially, the status is PROVISIONING. The status will eventually (might take up to 60 minutes) change to ACTIVE. Until it is ACTIVE, you won't be able to access your service.

Configure Cloud CDN

  1. Add a signing key to CDN:

    head -c 16 /dev/urandom | base64 | tr +/ -_ > key_file.txt
    
    gcloud compute backend-buckets \
    add-signed-url-key web-backend-bucket \
    --key-name $CDN_SIGN_KEY \
    --key-file key_file.txt
    
  2. Add the key to Secret Manager:

    gcloud secrets create $CDN_SIGN_KEY --data-file="./key_file.txt"
    
  3. To be safe, remove the data file:

    rm key_file.txt
    
  4. Configure IAM to allow the CDN service account to read the objects in the bucket:

    gsutil iam ch \
    serviceAccount:service-${PROJECT_NUM}@cloud-cdn-fill.iam.gserviceaccount.com:objectViewer gs://$BUCKET_NAME
    

    The Cloud CDN service account [[email protected]] doesn't appear in the list of service accounts in your project because the Cloud CDN service account is owned by Cloud CDN, not your project.

Deploy the login service to Cloud Run

  1. Build the Docker container for the login page and push it to Container Registry:

     cd ../flask_login
    
     docker build -t flask_login .
     docker tag flask_login gcr.io/$PROJECT_ID/flask_login
     docker push gcr.io/$PROJECT_ID/flask_login
    
  2. Deploy the Cloud Run service:

    gcloud run deploy $LOGIN_BACKEND_SVC_NAME \
    --image=gcr.io/$PROJECT_ID/flask_login --platform=managed \
    --region=$REGION --allow-unauthenticated \
    --set-env-vars=WEB_URL=https://$DNS_NAME,PROJECT_ID=$PROJECT_ID,CDN_SIGN_KEY=$CDN_SIGN_KEY,GCS_PATH="/${GCS_PATH}/"
    

    Important: The path is needed here to make sure the CDN signed cookie will be the first in the cookie header string. It takes advantage of the fact that most browsers will sort the cookies from the longest path to the shortest path.

  3. Because the Cloud Run service needs to access the secrets saved in Secret Manager, grant the permission here:

    gcloud projects add-iam-policy-binding \
    --member=serviceAccount:${PROJECT_NUM}[email protected] \
    --role=roles/secretmanager.secretAccessor $PROJECT_ID
    

    This demonstration provides the secretAccessor permission to the default compute service account. In a production environment, you probably want to use a custom service account and only allow access to the needed secrets.

Configure a serverless network endpoint group

  1. Create a network endpoint group (NEG) for the Cloud Run service:

    gcloud compute network-endpoint-groups create $SERVERLESS_NEG_NAME \
        --project $PROJECT_ID \
        --region=$REGION \
        --network-endpoint-type=SERVERLESS  \
        --cloud-run-service=$LOGIN_BACKEND_SVC_NAME
    
  2. Create a backend service and add the serverless NEG as a backend to the Cloud Run service.

    gcloud compute backend-services create $LOGIN_BACKEND_SVC_NAME \
        --global
    
    gcloud compute backend-services add-backend $LOGIN_BACKEND_SVC_NAME \
        --global \
        --network-endpoint-group=$SERVERLESS_NEG_NAME \
        --network-endpoint-group-region=$REGION
    

    A serverless NEG is needed here because that's how Cloud Run services can be associated with a load balancer.

Create the URL map and configure forwarding rules

  1. Update the URL mapping file and create the URL map.

    sed -i -e "s/<DNS_NAME>/$DNS_NAME/" web-map-http.yaml
    sed -i -e "s/<PROJECT_ID>/$PROJECT_ID/" web-map-http.yaml
    sed -i -e "s/<LOGIN_BACKEND_SVC_NAME>/$LOGIN_BACKEND_SVC_NAME/" web-map-http.yaml
    
    gcloud compute url-maps import web-map-http --source web-map-http.yaml --global
    

    This step updates the values in the template URL map file and imports it. A final configuration looks like the following:

    defaultService: https://www.googleapis.com/compute/v1/projects/democlound-test/global/backendBuckets/private-web
    kind: compute#urlMap
    name: web-map-http
    hostRules:
    - hosts:
    - 'web.democloud.info'
    pathMatcher: matcher1
    pathMatchers:
    - defaultService: https://www.googleapis.com/compute/v1/projects/democlound-test/global/backendBuckets/private-web
    name: matcher1
    routeRules:
        - matchRules:
            - prefixMatch: /
            headerMatches:
                - headerName: cookie
                prefixMatch: 'Cloud-CDN-Cookie'
        priority: 0
        service: https://www.googleapis.com/compute/v1/projects/democlound-test/global/backendBuckets/private-web
        - matchRules:
            - prefixMatch: /
        priority: 1
        service: https://www.googleapis.com/compute/v1/projects/democlound-test/global/backendServices/flasklogin-backend-service
    

    In this configuration, a route rule is configured to match a cookie starting with Cloud-CDN-Cookie in the request header. If it's matched, the request is forwarded to the backend bucket service. Otherwise, it is forwarded to the login backend service. The cookie Cloud-CDN-Cookie is the signed cookie mentioned earlier. It is set by the Cloud Run service after successful IAP authentication.

    Note: As of 5/2022, regexMatch is not supported for headerMatches in external HTTPS load balancer. Therefore, prefixMatch is used here. For prefixMatch, the Cloud-CDN-Cookie needs to be the first one in the cookie header. That's why a path is introduced to let the browsers sort the cookies and make the Cloud-CDN-Cookie the first one.

  2. Create a target HTTPS proxy with the URL map:

    gcloud compute target-https-proxies create https-lb-proxy --url-map web-map-http --ssl-certificates=www-ssl-cert
    
  3. Create the forwarding rule with the reserved IP address:

    gcloud compute forwarding-rules create private-web-https-rule --address=$STATIC_IP_NAME --global --target-https-proxy=https-lb-proxy --ports=443
    

Enabling IAP on the Load Balancer

Configuring the OAuth consent screen

A brand is the OAuth consent screen that contains branding information for users. Brands might be restricted to internal or public users. An internal brand allows the OAuth flow to be accessed by a member of the same Google Workspace organization as the project. A public brand makes the OAuth flow accessible to anyone with access to the internet.

  1. Create a brand

    export USER_EMAIL=$(gcloud config list account --format "value(core.account)")
    
    gcloud alpha iap oauth-brands create \
        --application_title="Static web site" \
        --support_email=$USER_EMAIL
    

    Example Output

    Created [462858740426].
    applicationTitle: Static web site
    name: projects/462858740426/brands/462858740426
    orgInternalOnly: true
    
  2. Creating an IAP OAuth Client

    Create a client using the brand name from the previous step

    gcloud alpha iap oauth-clients create \
        projects/$PROJECT_ID/brands/$PROJECT_NUM \
        --display_name=static-web-site
    

    Example Output

    Created [xxxxxxxxxfllsd.apps.googleusercontent.com].
    displayName: static-web-site
    name: projects/462858740426/brands/462858740426/identityAwareProxyClients/462858740426-xxxxxfllsd.apps.googleusercontent.com
    secret: [secret-removed]
    
  3. Store the client name, ID and secret

    export CLIENT_NAME=$(gcloud alpha iap oauth-clients list \
        projects/$PROJECT_NUM/brands/$PROJECT_NUM --format='value(name)' \
        --filter="displayName:static-web-site")
    
    export CLIENT_ID=${CLIENT_NAME##*/}
    
    export CLIENT_SECRET=$(gcloud alpha iap oauth-clients describe $CLIENT_NAME --format='value(secret)')
    
  4. In the Cloud Console, select the project from the drop-down project selection menu

  5. Navigate to the OAuth consent screen in the Cloud Console

  6. Click MAKE EXTERNAL under User Type

  7. Select Testing as the Publishing status

  8. Click CONFIRM

  9. Enable IAP on the backend service

    gcloud iap web enable --resource-type=backend-services \
        --oauth2-client-id=$CLIENT_ID \
        --oauth2-client-secret=$CLIENT_SECRET \
        --service=$LOGIN_BACKEND_SVC_NAME
    
  10. Add an IAM policy binding for the role of roles/iap.httpsResourceAccessor for the user created in the previous step

    gcloud iap web add-iam-policy-binding \
        --resource-type=backend-services \
        --service=$LOGIN_BACKEND_SVC_NAME \
        --member=user:$USER_EMAIL \
        --role='roles/iap.httpsResourceAccessor'
    

    Note: you can add addtional Google user or group for access.

Test the website

  1. To make sure that the managed SSL certificate has been successfully provisioned, run the following command:

    gcloud compute ssl-certificates list | grep ${DNS_NAME}
    

    The status should be ACTIVE:

  2. Get the URL:

    echo https://$DNS_NAME/$GCS_PATH/index.html
    

    Note: Both the SSL certificate and the DNS name propagation need some time. Wait a few minutes if the page is not accessible.

  3. Try to open a file hosted in the bucket.

    The first time, you'll be redirected to the login page. For example:

    login

  4. Enter the user email you got from a previous step and its password. You should have the page like the following:

    home-page

  5. Try again to open a file hostead in the bucket:

    static-file

Cleaning up

To avoid incurring charges to your Google Cloud account for the resources used in this tutorial, you can delete the resources that you created. You can either delete the entire project or delete individual resources.

Deleting a project has the following effects:

  • Everything in the project is deleted. If you used an existing project for this tutorial, when you delete it, you also delete any other work you've done in the project.
  • Custom project IDs are lost. When you created this project, you might have created a custom project ID that you want to use in the future. To preserve the URLs that use the project ID, delete selected resources inside the project instead of deleting the whole project.

If you plan to explore multiple tutorials, reusing projects can help you to avoid exceeding project quota limits.

Delete the project

The easiest way to eliminate billing is to delete the project you created for the tutorial.

  1. In the Cloud Console, go to the Manage resources page.
  2. In the project list, select the project that you want to delete and then click Delete.
  3. In the dialog, type the project ID and then click Shut down to delete the project.

Delete the resources

If you don't want to delete the project, you can delete the provisioned resources:

gcloud compute forwarding-rules delete private-web-https-rule --global

gcloud compute target-https-proxies delete https-lb-proxy

gcloud compute url-maps delete web-map-http

gcloud compute backend-services delete $LOGIN_BACKEND_SVC_NAME --global

gcloud compute network-endpoint-groups delete $SERVERLESS_NEG_NAME --region $REGION

gcloud projects remove-iam-policy-binding $PROJECT_ID \
    --member=serviceAccount:${PROJECT_NUM}[email protected] \
    --role=roles/secretmanager.secretAccessor

gcloud run services delete $LOGIN_BACKEND_SVC_NAME \
    --platform=managed --region=$REGION

gcloud container images delete gcr.io/$PROJECT_ID/flask_login

gcloud secrets delete $CDN_SIGN_KEY

gcloud compute ssl-certificates delete www-ssl-cert

gcloud compute backend-buckets delete web-backend-bucket

gcloud compute addresses delete $STATIC_IP_NAME --global

gsutil rm -r gs://$BUCKET_NAME

Finally, remove the DNS record in your DNS registry.

What's next

About

Securing static files hosted on GCS using IAP

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published