Creating a Password Protected Website with IAP and Google App Engine
-

Creating a Password Protected Website with IAP and Google App Engine

April 30, 2020
Categories:
Tutorial
Tags:
Google Cloud Platform, Identity Aware Proxy, Google App Engine

TL;DR: If you have a static website that needs to be password protected, using Google’s Identity Aware Proxy along with App Engine is one of the simplest ways to accomplish this.

If you want to follow along with a site of your own, I have provided a working example in this GitHub repo.

Note: I have also seen S3 + Lambda used to accomplish this

images/read-the-docs.png

Table of Contents:

The Need

A company I have been working with (Gauntlet Networks) needed to set up a website containing documentation for the simulation SDK they are building but wanted to restrict access to only their clients and employees.

Other static hosting solutions

There are a plethora of high-quality, affordable options (GitHub Pages, Netlify, a Free Tier Cloud VM, or even an S3 bucket), but adding a layer of authentication makes the task significantly more challenging.

Netlify does offer password protection using basic authentication for their paid plans, but using this would still require distributing the password to the authorized individuals and rotating it whenever it was necessary to revoke anyone’s access.

Handling passwords

Historically, implementing authentication would require adding significant complexity to our website’s architecture. We would need a database of some kind to store the (hashed) passwords and server-side processing to check if a login attempt is valid. We would also probably need to add some form of email confirmation/password reset functionality because people are terrible at remembering their passwords. All of the sudden the one-hour task of getting the documentation site set up just ballooned into something that could take weeks to accomplish!

Luckily, we don’t need to take on all of this complexity ourselves. A standard called OAuth allows us to leverage the authentication system of another entity (for example, Google) to provide"secure delegated access" to our content.


Solution (IAP + App engine)

OAuth by itself would allow us to set up a sign-in flow using an external authentication provider, but for this use case, we want the process to be completely hands-off.

This is where Google’s Identity Aware Proxy (IAP) comes in. Cloud IAP uses Google Sign-In and GCP’s Identity and Access Management (IAM) to handle authentication and authorization to control access to GCP resources. Granting access to the site can then be managed using individual Google accounts and/or Google groups.

Given that Gauntlet was already using Google Cloud Platform, leveraging IAP in conjunction with Google App Engine to host the static site provided a simple, clean solution to satisfy the need.

images/iap-app.png

Example Site

Because a documentation website was the use case that inspired this post, I am using the Sphinx Python documentation generator quick start as the basis for this demo. The static site files will be hosted using App Engine, with CircleCI for CI/CD.

Configuring App Engine

Google has documentation outlining how to use app engine to create a static site by adding an app.yaml file to the codebase that tells App Engine how to interpret URL request paths. Sphinx stores its output in the /docs/build/html directory by default, so I used the following app.yaml:

runtime: python27
api_version: 1
threadsafe: true

handlers:
- url: /
  static_files: docs/build/html/index.html
  upload: docs/build/html/index.html

- url: /(.*)
  static_files: docs/build/html/\1
  upload: docs/build/html/(.*)

With this in place, running the gcloud app deploy` command within the root of the project deploys the application.

Setting up IAP

With the website deployed, the next step was to follow Google’s guide to enabling IAP for App Engine.

images/iap-page.png
Just click the slider!

This involves configuring a few things for the Oauth Consent page (app name, support email, etc…), but the process is relatively painless.

The final step is to grant the IAP-secured Web App User access to all authorized individuals. In the screenshot below, I granted access to allAuthenticatedUsers which allows anyone with a Google account to access it. (Test it out here!)

images/iap-access.png
Access can be granted to individuals or groups

Configuring CI/CD with CircleCI

At this point, the website is live and access controlled, but to minimize future work, it is useful to set up a system to handle continuous integration and deployment.

CircleCi offers simple integration with GitHub and a free tier that will easily handle a small project like this. The first step is to create a CircleCI account and then grant access to the GitHub repo containing the source files using their GitHub marketplace app.

Workflow

To define our workflow, we create a file at .circleci/config.yml containing two parts, a workflow definition and a job definition.

We define the workflow using version 2 of the CircleCI API. This workflow tells CircleCi to run the build_and_deploy job on any push to the master branch.

workflows:
  version: 2
  build_and_deploy:
    jobs:
      - build_and_deploy:
        filters:
          branches:
            only:
              - master

Next, we define the build_and_deploy job. Each CircleCI job runs within a Docker image, and because the goal is to deploy to Google App Engine, the google/cloud-sdk provides a good starting point.

version: 2.1
jobs:
  build_and_deploy:     
    docker:
        - image: google/cloud-sdk:slim
    steps:

The job is broken down into four steps:

  1. checkout,
  2. Sphinx installation,
  3. Generating the website
  4. Deploying to App Engine

1) checkout

      - checkout

As its name suggests, this step retrieves the source code within the repo and stores it in the working directory.

2) Install Sphinx

      - run:
          name: Install Sphinx
          command: |
            apt install -y python-pip && \
            python3 -m pip install -r requirements.txt            

The google/cloud-sdk:slim container image we are using is based on Debian, so we can install Sphinx and its dependencies by first installing pip with apt and then using pip to install the requirements.txt file.

3) Make Docs

      - run:
          name: Make Docs
          command: |
            cd docs && make html            

The Sphinx quickstart creates a Makefile within the /docs directory which can be used to generate the documentation site. This step changes directory into /docs and executes this make target passing html as the destination for Sphinx to store its output.

Note: html here needs to match the directory used in the App Engine app.yaml config (make html will build the site into docs/build/html where App Engine is configured to find them)

4) Deploy Docs

      - run: 
          name: Deploy Docs
          command: |
            echo ${GCLOUD_SERVICE_KEY} > /tmp/sa_key.json
            gcloud auth activate-service-account --key-file=/tmp/sa_key.json
            rm /tmp/sa_key.json
            gcloud --quiet config set project ${GOOGLE_PROJECT_ID}
            gcloud --quiet config set compute/zone ${GOOGLE_COMPUTE_ZONE}
            gcloud --quiet app deploy            

The final step in the job is to deploy the site. To do this, we first need to create a service account (https://console.cloud.google.com/iam-admin/serviceaccounts) and grant it the necessary permissions. Although it seems as though App Engine Deployer would be sufficient, it turns out there are actually a few additional permissions involved with deploying the site. The screenshot below shows the 5 roles I needed to add to achieve a successful deployment.

images/sa-roles.png

At this point I created a key for the service account (.json format) and stored its content as an environment variable in CircleCI. I also stored the Zone and Project ID as environment variables to ensure the deployment was targeting the correct app engine environment.

There was one final “gotcha” having to do with the service account key. My first attempt tried to pass it into the gcloud auth command by piping it in as follows:

echo ${GCLOUD_SERVICE_KEY} | gcloud auth activate-service-account --key-file=-

This caused gcloud to assume the key was a .p12 type and fail.

ERROR: (gcloud.auth.activate-service-account) Missing required argument [ACCOUNT]: An account is required when using .p12 keys

As shown in the full example at the beginning of the step, I was able to solve this by redirecting the echo output to a temporary file containing the JSON key and passing the file path to gcloud auth.

images/green-build.png
Success!

That’s it! We now have a password-protected website, with CI/CD, that falls within the free usage tiers of GCP + CircleCI. If this site had heavy traffic or data egress it could exceed the free tier but for this internal documentation use case that is unlikely.


Next steps

There are a couple of additional steps that I will leave as an exercise to the reader.

Custom Domain

By default, App Engine assigns a domain such as https://devops-directive-project.uc.r.appspot.com/ to the website. It is possible to map a custom domain to the site by following this guide from Google.

Custom OAuth screen

The default OAuth consent screen is fairly boring but can be customized with a logo image, application homepage link, etc… from Google Cloud Console

images/oauth-screen.png

© DevOps Directive 2023