Point an Express.js App at Google Storage Bucket Files for Let's Encrypt Authentication

The previous Tutorial (http://itvagabonds.com/tutorials/manually-deploy-expressjs-app-google-app-engine-lets-encrypt-cert) did a great job of following the entire Let's Encrypt process, allowing you to manually authenticate with Let's Encrypt, but it isn't exactly set up to be automated like you want it to be. In this post, I'll show you how to store the keys in a Google Cloud Storage Bucket instead of statically in the code. This will allow you to update your certificate authentication with Let's Encrypt without deploying a new version of your app (thank goodness), and is a great first step towards automating this process entirely. 

First, we need to make a Google Storage Bucket for keys. Go to Google Cloud Platform > Storage > Browser, and create a new bucket. I would recommend a multi-regional bucket and a name like: "acme-challenges-a98hg3feed7ghnb5gfdyosdhp9p29pnmenb".

Once your bucket is made, we are going to assign permissions to the service account of your Google App Engine Instance. Edit Bucket Permissions for your new bucket, and grant the service account (for the default project app, the name should be in the format 'your-project@appspot.gserviceaccount.com') access to the Storage Object Viewer Role. Now Google Apps Engine will automatically allow requests originated from your app to read information from your bucket!

Now make some new text files (with no file extension), and upload them to your bucket. The name should match the 'path' we used as the url previously, and the contents should match the 'data' from the previous post. 

Once we have the bucket and files ready, we need to install the Google Cloud Storage and fast-crc32c Node.js modules. From within your project folder:

npm install --save @google-cloud/storage

npm install --save fast-crc32c

Now we just need to do is update the contents of the letsEncrypt.js file from last time. It should now look like this (you will need to change the gcsBucket const to match your bucket name) :

//import express and express router
var express = require('express');
var router = require('express').Router();

//URL for gcloud documentation = https://googlecloudplatform.github.io/google-cloud-node/#/docs/storage/1.2.0/storage/file
//import gcloud storage module
var gcs = require('@google-cloud/storage')();

//set bucket google bucket
const gcsBucket = gcs.bucket('acme-challenges-a98hg3feed7ghnb5gfdyosdhp9p29pnmenb');

//function to return the contents of the google file
var returnGcloudFileContents = (path) => {
  return new Promise((resolve, reject) => {
    var file = gcsBucket.file(path);
    file.download().then((fileData) => {
        resolve(fileData[0]);
      }).catch((err) => {
        reject(`Attempt to access file returned error: ${err.code} - ${err.message}`);
      });
  });
};

//turn path into param, and pass it to the returnGcloudFileContents function
router.get(`/:path`, (req, res) => {
  returnGcloudFileContents(req.params.path).then((gcloudFile) => {
    console.log(`Returned letsencrypt key: ${gcloudFile.toString()}`);
    res.send(gcloudFile.toString());
  }).catch((err) => {
    console.log(err);
    res.send(err);
  });
});

//export express routes
module.exports = router;

Your entry point file should still just have the following: NOTE: Where you put the middleware (app.use) line is relevant, I'm trusting you to place it appropriately.

//import letsencrypt
const letsEncrypt = require('./config/letsEncrypt.js');

//middleware for letsencrypt
app.use('/.well-known/acme-challenge', letsEncrypt);

The process still loads as a middleware like before, but now it pulls the path in as a parameter on the request and allows you to load a file matching the name from your Google Storage Bucket. This is a lot cleaner than the manual coding of paths and data, but it does make testing a little more complicated. To test before redeploying your app, you will need to run the following command (from your project directory), and accept the prompt in your browser, to simulate authentication of your app as if it were in the Google App Engine environment:

gcloud auth application-default login

Redeploy your app and test. The behavior from the perspective of Let's Encrypt should be unchanged, but you now have a bucket that you can add new authentication files to without making any updates to your app!