Search code examples
node.jsgoogle-cloud-functionsgoogle-cloud-storagegoogle-cloud-kms

Synchronously load secrets in an HTTP Google Cloud Function (NodeJS)


I have a Google Cloud Function that provides a login API endpoint. The function signs a JWT using a private key.

Now, when deploying the function I need to securely load the private key used to sign the JWT. I cannot use environment variables or embed it in any part of the code, of course, as that is visible to anyone who can read the function in gcloud.

The options I've considered are:

  1. Create a dedicated service account for the function, plus a GCS bucket that only that service account can access, and store the secret in plain text in there. When the function loads, load the secrets (await loadMySecrets(...)) and carry on.

  2. The recommended way: create a KMS key, encrypt the secrets with that key and upload the ciphertext along with the function code. At runtime, ask KMS to decrypt the key (await decryptSecret(...)).

Problem is: as far as I can see, when an HTTP function is loaded, the entire loading process has to be synchronous.

Your function returns a request handler and GCF then executes it. There is no opportunity to await a Promise before you return your request handler, and GCF does not support return Promises for HTTP functions. The GCS and KMS APIs are Promise-based, there are no *Sync() calls supported.

How have others gotten around this problem? I cannot synchronously wait on my Promise to resolve (e.g. via sleep()), as that will block the Node event loop. Am I forced to somehow provision the secrets synchronously or is there a way to do it async that plays nice with GCF?

Note: this is plain Google Cloud Functions, not Firebase.

Note 2: there is a nuclear option, which is to move the async aspect of this into Express middleware. I really, really don't want to do that as I have to wrap things like cookie-parser and passport, which expect secrets to be available when the middleware is first created, in async middleware which loads my secrets then delegates. Ugly and might affect performance.


Solution

  • You can make the entire function handler async (requires node 8+):

    const decrypt = async (ciphertext) => {
      result = await client.decrypt({
          name: cryptoKeyID,
          ciphertext: ciphertext,
      });
      return result.plaintext;
    }
    
    exports.F = async (req, res) => {
      const username = await decrypt(process.env.DB_USER);
      const password = await decrypt(process.env.DB_PASS);
      res.send(`${username}:${password}`)
    }
    

    I saw your note about middleware. If you post a code sample, I’ll try to update this to match better.