IBM Cloud Docs
Part 2: Create a GitHub issue when your certificates are about to expire

Part 2: Create a GitHub issue when your certificates are about to expire

This tutorial is part 2 of a 3-part tutorial series that guides you through setting up alerts for your expiring secrets.

In part 1 of this tutorial series, you learned how to set up IBM Cloud® Secrets Manager and Event Notifications to route events to a test webhook URL. Part 2 shows you how to replace your test webhook with a Cloud Functions webhook URL so that you can open a GitHub issue that contains your Secrets Manager notification content.

The diagram shows the basic flow between the Secrets Manager, Event Notifications, and Cloud Functions services to create a GitHub issue.
Figure 1. Notifications flow

Before you begin

Be sure that you've completed Part 1: Enabling notifications for Secrets Manager. Before you get started with part 2, you also need the following prerequisites:

Create a webhook with Cloud Functions

Let's start by creating a Cloud Functions action. A Cloud Functions action is a piece of code that can be run in response to a trigger, such as an incoming notification from the Event Notifications service. Your Cloud Functions action can contain code that takes an incoming payload from Event Notifications, which originated as an event in Secrets Manager, and uses it to create a GitHub issue.

Create a Cloud Functions action

To create a Cloud Functions action, you can use the Functions UI.

  1. In the IBM Cloud console, click the Menu icon Menu icon > Functions.
  2. In the navigation, click Actions > Create.
  3. Give the action a name. For example, sm-notifications-github.
  4. Choose the default package.
  5. Select a Node.js runtime.
  6. Click Create.

Obtain your new webhook URL

In part 1 of this tutorial series, you used Webhook.site to test your notifications workflow for the first time. Now that you've set up a Cloud Functions action, you can also use it to receive any incoming notifications.

  1. From the Cloud Functions action menu, click Endpoints.
  2. Select Enable as Web Action.
  3. Select Raw HTTP handling.
  4. Click Save.
  5. Copy the URL. This URL will be your new webhook URL that you can add as an Event Notifications destination in the next step.

Update your Event Notifications settings

Next, update your Event Notifications settings so that the service can begin to forward any incoming events from Secrets Manager to your new Cloud Functions action.

Add your new webhook URL as a destination

Start by adding your Cloud Functions webhook URL as a new Event Notifications destination.

  1. In the console, click the Menu icon Menu icon > Resource list.

  2. From the list of resources, select your Event Notifications instance.

  3. In the Event Notifications UI, go to Destinations.

  4. Create a destination so that your alerts can be forwarded to the Cloud Functions webhook that you created the previous step.

    1. From the navigation, click Destinations > Add.
    2. Provide a name for your destination. For example, GitHub.
    3. Select Webhook as the destination type.
    4. Paste the URL that you copied from the Cloud Functions UI.
    5. From the list of HTTP verbs, select POST.
    6. Click Add.

    After you create your destination, copy your destination ID. You use this value to prepare your Functions code in a later step.

Enable webhook signing

By default, the notification content that is delivered to a webhook URL arrives in raw JSON format. Event Notifications can optionally send notifications as signed JSON Web Tokens (JWT). In this step, you enable signing for the webhook subscription that you created in part 1.

With webhook signing, you verify that the notification payload is sent by Event Notifications and not by a third party. For more information, check out the Event Notifications documentation.

  1. Create a subscription with signing enabled between your existing Secrets Manager topic and your new Cloud Functions destination.

    1. In the Event Notifications UI, click Subscriptions > Create.
    2. Provide a name for your subscription. For example, GitHub.
    3. Select the Secrets Manager topic that you created in part 1.
    4. Select the destination that you created in the previous step.
    5. In the Security section, select the Signing enabled option.
    6. Click Create.
  2. Update your existing subscription (between your Secrets Manager topic and Webhook.site URL) to enable signing.

    1. In the row for subscription that you created in part 1, click the Actions menu Actions icon > Edit.
    2. In the Security section, select the Signing enabled option.
    3. Click Save.

    Now you're all set to securely deliver notifications to your webhook URL. Continue to the next step.

Verify the notification payload

Next, test that your notifications are now delivered as a signed JWT objects to your Webhook.site page. You can use the Settings > Event Notifications section in the Secrets Manager UI to send a test event. The output of a signed notification payload looks similar to the following example.

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXQiOnsiYXV0aG9yIjp7...(truncated)

To verify the signature and decrypt the message, you'll need to obtain the public key that is generated by Event Notifications. Continue to the next step.

Prepare your sample code

Next, prepare the sample code for your Cloud Functions action.

  1. Copy the following JavaScript code and update the placeholder values.

    const axios = require('axios');
    const jwtVerify = require('jsonwebtoken').verify;
    
    // Generate an IAM token
    async function getIAMToken() {
       const options = {
          url: 'https://iam.cloud.ibm.com/identity/token',
          method: 'POST',
          header: 'Content-Type: application/x-www-form-urlencoded',
          data: 'grant_type=urn:ibm:params:oauth:grant-type:apikey&apikey=API_KEY'
       };
    
       const response = await axios(options);
       const responseData = response.data;
       console.log("\nGenerated an IAM token..");
       return responseData.access_token;
    }
    
    // Get a public key to verify notifications
    async function getPublicKey() {
       const accessToken = await getIAMToken();
    
       const options = {
          method: 'GET',
          url: '<event_notifications_endpoint_url>/destinations/<destination_id>/public_key',
          headers: {
                'authorization': 'Bearer ' + accessToken
          }
       };
    
       const response = await axios(options);
       const responseData = response.data;
       console.log('\nRetrieved a public key from Event Notifications..');
       return responseData.public_key;
    }
    
    function getDate(timestamp) {
       return new Date(timestamp).toDateString();
    }
    
    // Build a GitHub issue description according to the event type
    function createIssueBody(decodedNotification) {
       if (decodedNotification.event_type === "test_event") // Remove this option later if you don't want to create issues for test events
          return `This is a test notification from ${decodedNotification.source_service}.`; 
       if (decodedNotification.event_type === "secret_about_to_expire")
          return `The following certificate(s) expire on ${getDate(decodedNotification.secrets_expiration_date)}:
       ${decodedNotification.secrets.reduce((accumulator, currentValue) => {
                return accumulator + `
       > Domain(s): ${currentValue.domains}
       Secret ID: ${currentValue.secret_id}
       Secret name: ${currentValue.secret_name}
       `;
          }, "")}`;
       if (decodedNotification.event_type === "secret_expired")
          return `The following certificate(s) have expired:
       ${decodedNotification.secrets.reduce((accumulator, currentValue) => {
                return accumulator + `
       > Domain(s): ${currentValue.domains}
       Secret ID: ${currentValue.secret_id}
       Secret name: ${currentValue.secret_name}
       `;
          }, "")}`;
    }
    
    async function main(params) {
       try {
          const publicKey = await getPublicKey();
    
          // Verify the notification data using the retrieved public key
          const decodedNotification = await jwtVerify(params.__ow_body, publicKey).data.data;
          console.log(`\nReceived the following event notification from Secrets Manager:\n${JSON.stringify(decodedNotification)}`);
    
          const body = createIssueBody(decodedNotification);
          if (!body) {
                console.log(`\nNo action required for this notification. Event type: ${decodedNotification.event_type}`);
                return;
          }
    
          // Create a GitHub issue with notification data
          const options = {
                method: 'POST',
                url: 'https://api.github.com/repos/<org_name>/<repo_name>/issues',
                headers: {
                   accept: 'application/vnd.github.v3+json',
                   authorization: '<github_token>'
                },
                data: {
                   title: 'Test issue',
                   body: body,
                   labels: ['<label>', '<label>'],
                   assignees: ['<assignee>']
                }
          };
    
          await axios(options);
          console.log('\nNotification successfully forwarded to GitHub!');
    
       } catch (error) {
          console.error(error);
       }
    }
    
    Table 1. Variables to replace in your Functions code
    Variable Description
    API_KEY An IBM Cloud API key with Manager access on the Event Notifications service.
    <event_notifications_endpoint_url> The base URL of your Event Notifications service instance. For more information, see the Event Notifications API documentation.
    <destination_id> The ID of the Event Notifications destination that you created in step 2.
    <github_org>
    <github_repo>
    The values that are associated with the GitHub repository where you would like to open issues. Replace <owner> and <repo> with the values that apply to repository.
    <github_token> Your GitHub personal access token, prefixed by the word Token. For example, Token 35890433325cv...(truncated).
  2. Paste the code into your Functions action.

    1. In the console, go Menu icon Menu icon > Functions > Actions to return to the Functions UI.
    2. From your list of actions, select the action that you created in step 1.
    3. Paste the code that you modified.
    4. Click Save.

Test your connection to GitHub

Finally, verify that you're able to post your notifications to GitHub. You can use the Settings > Event Notifications section in the Secrets Manager UI to send a test event. Then, check your GitHub repository to view the results.

The image shows the Event Notifications screen in the Secrets Manager UI.
Figure 2. Sending a test event to Event Notifications

  1. In the IBM Cloud console, go to your Secrets Manager instance.

  2. Go to Settings > Event Notifications, and click Send test event.

  3. Check the GitHub repository that you defined in the Functions action to verify the results.

    If the Functions action completed successfully, you see a GitHub issue created that is similar to the following example:

    Test issue
    
    This is a test notification from SecretsManager.
    

    If you have an existing certificate in your Secrets Manager instance that expires soon, a GitHub issue is created similar to the following example:

    Test issue
    
    The following certificate(s) expire on Thu Mar 10 2022:
    
    Domain(s): example.com
    Secret ID: 538f7d7e-bef8-2b9a-704f-f4a897219df6
    Secret name: example-certificate
    
  4. Optional. Check your Cloud Functions activations logs if you encounter issues.

    From the Cloud Functions activations dashboard, you can check to see your activated action. Notification successfully forwarded to Slack! is displayed in the response details if the code runs successfully.

    If you see HTTP 4XX errors in the logs, make sure that the API key in your action code has the correct level of access. For JWT signature errors, verify that the Event Notifications destination ID in the action code corresponds with your Cloud Functions webhook.

Next steps

Great job! In part 2 of this tutorial series, you learned how to set up a Cloud Functions action that takes your incoming notification content and uses it to open an issue in your GitHub repository. From now on, a GitHub issue is created each time that a certificate in your Secrets Manager service instance expires or is about to expire. In part 3, you create another Functions action that sends your notifications to Slack.