Skip to content

Instantly share code, notes, and snippets.

@dpkirchner
Last active July 17, 2020 21:58
Show Gist options
  • Save dpkirchner/006d0796ca88e79fdebaab238f2e84e2 to your computer and use it in GitHub Desktop.
Save dpkirchner/006d0796ca88e79fdebaab238f2e84e2 to your computer and use it in GitHub Desktop.
Relay Github webhook to a Google IAP-protected service
Dockerfile
node_modules
npm-debug.log
FROM node:12-slim
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install --only=production
COPY . ./
CMD [ "npm", "start" ]
// A simple way to relay webhooks/callbacks into your tools that are sitting
// behind a Google Identity-Aware Proxy (IAP). This can run anywhere, but it was
// written to run in a Google Cloud Run instance that uses a service account to
// make the IAP request.
//
// This is largely based on
// https://gist.github.com/stigok/57d075c1cf2a609cb758898c0b202428
//
// and was initially written to handle webhooks to Argo CD, however there's no
// reason it wouldn't work for other tools.
const { GoogleAuth } = require('google-auth-library');
const auth = new GoogleAuth();
const express = require('express');
const crypto = require('crypto')
const bodyParser = require('body-parser')
const app = express();
const url = 'https://argocd.example.com/api/webhook';
const targetAudience = 'NNNNNN-MMMMMM.apps.googleusercontent.com';
const port = process.env.PORT || 8080;
const secret = 'some shared secret';
app.use(bodyParser.json())
const verifyPostData = (req, res, next) => {
const payload = JSON.stringify(req.body)
if (!payload) {
return next('Request body empty')
}
const sig = req.get('x-hub-signature') || ''
const hmac = crypto.createHmac('sha1', secret)
const digest = Buffer.from('sha1=' + hmac.update(payload).digest('hex'), 'utf8')
const checksum = Buffer.from(sig, 'utf8')
if (checksum.length !== digest.length || !crypto.timingSafeEqual(digest, checksum)) {
return next(`Request body digest (${digest}) did not match x-hub-signature (${checksum})`)
}
return next()
}
app.post('/gh', verifyPostData, async (req, res) => {
console.log(`Received webhook for user ${req.body.pusher.name} id ${req.body.head_commit.id} message '${req.body.head_commit.message}'`);
const client = await auth.getIdTokenClient(targetAudience);
const argoResponse = await client.request({
body: JSON.stringify(req.body),
headers: {
'X-Github-Delivery': req.header('x-github-delivery'),
'X-Github-Event': req.header('x-github-event'),
'X-Hub-Signature': req.header('x-hub-signature'),
},
method: 'POST',
url,
});
console.log(`Received response from argo for user ${req.body.pusher.name} id ${req.body.head_commit.id} message '${req.body.head_commit.message}': ${argoResponse.status}`);
res.status(204).send();
});
app.use((err, req, res, next) => {
if (err) console.error(err)
res.status(403).send('Request body was not signed or verification failed')
});
app.listen(port, () => {
console.log('listening on port', port);
});

This assumes you've already got a IAP-protected service. I'm calling that out of scope for this doc. Although this is mostly something you can just copy, it also assumes that you know your way around Node and expressjs so you understand what is going on.

Before you start you'll need:

Then you'll need to come up with a shared secret string you want to use. See "some shared secret" in index.js.

Security note: You don't need to create a service account key for this. Cloud Run will provide the account information to your application at runtime.

  1. Create the service account:
$ PROJECT=your-project-id
$ SERVICE_ACCOUNT=webhook
$ SERVICE_ACCOUNT_EMAIL=$SERVICE_ACCOUNT@$PROJECT.iam.gserviceaccount.com
$ gcloud iam service-accounts create webhook --project $PROJECT
Created service account [webhook].
  1. Grant IAP access to the service account:
# This will output your entire project's policy bindings, including
# the new entry you're creating here.
$ gcloud projects add-iam-policy-binding $PROJECT--member=serviceAccount:$SERVICE_ACCOUNT_EMAIL --role=roles/iap.httpsResourceAccessor
Updated IAM policy for project [$PROJECT].
bindings:
- members:
  - serviceAccount:$SERVICE_ACCOUNT_EMAIL
  role: roles/iap.httpsResourceAccessor
  1. Paste the files in this gist in a new directory, setting the URL, target audience, and secret appropriately in index.js

  2. Submit the directory to Google Cloud Build:

$ gcloud builds submit --tag gcr.io/$PROJECT/argo-github-webhook
Creating temporary tarball archive of 651 file(s) totalling 4.9 MiB before compression.
Uploading tarball of [.] to [gs://$PROJECT_cloudbuild/source/1595022270.005232-xxx.tgz]
Created [https://cloudbuild.googleapis.com/v1/projects/$PROJECT/builds/a-b-c].
Logs are available at [https://console.cloud.google.com/cloud-build/builds/a-b-c?project=nnnn].
----------------------------------------------- REMOTE BUILD OUTPUT ------------------------------------------------
starting build "a-b-c"
[...]
ID     CREATE_TIME                DURATION  SOURCE                                                                                            IMAGES                                                   STATUS
a-b-c  2020-07-17T21:44:34+00:00  28S       gs://$PROJECT_cloudbuild/source/1595022270.005232-xxx.tgz  gcr.io/$PROJECT/argo-github-webhook (+1 more)  SUCCESS
  1. Deploy a Google Cloud Run process for the image:
$ gcloud run deploy argo-github-webhook --image gcr.io/$PROJECT/argo-github-webhook --platform managed --service-account $SERVICE_ACCOUNT_EMAIL --region us-central1
Deploying container to Cloud Run service [argo-github-webhook] in project [$PROJECT] region [us-central1]
✓ Deploying... Done.
  ✓ Creating Revision...
  ✓ Routing traffic...
Done.
Service [argo-github-webhook] revision [argo-github-webhook-xyz] has been deployed and is serving 100 percent of traffic at https://argo-github-webhook-xyz-uc.a.run.app
  1. Configure Github webhooks to use the URL returned above, using the secret you specified in the index.js file. See https://developer.github.com/webhooks/creating/ to learn how to do that.

The gist of it is:

  • Visit https://github.com/yourusername/deployments/settings/hooks
  • Click "Add webhook" to open the webhook form
  • Enter the URL above (with /gh at the end as you see in index.js), set the Content-Type to JSON (if necessary), enter the secret
  • Ensure that "Just the push event" is selected (unless you need more)
  • Click "Add webhook" to create
  1. Push something to a repo. Watch the Google Cloud Run logs to see how it's going. The URL will be something like: https://console.cloud.google.com/run/detail/us-central1/argo-github-webhook/logs?project=$PROJECT

Github also provides webhook logs so you can see what they sent and what your app returned.

{
"name": "argo-github-webhook",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.19.0",
"express": "^4.17.1",
"google-auth-library": "^6.0.5"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment