From ad304f88719b7f4f9447dd0c4ed67b2b398ecd9d Mon Sep 17 00:00:00 2001
From: Niek Palm <niek.palm@philips.com>
Date: Tue, 28 Jan 2025 18:47:23 +0100
Subject: [PATCH] feat: Add option to let the module manage the webhook secret

---
 docs/configuration.md                |  1 +
 docs/getting-started.md              |  4 ++--
 examples/default/.terraform.lock.hcl | 20 ++++++++++++++++++++
 examples/default/main.tf             | 22 +++-------------------
 examples/default/outputs.tf          |  6 ------
 main.tf                              | 21 ++++++++++++++++++++-
 modules/multi-runner/main.tf         | 11 +++++++++++
 modules/multi-runner/ssm.tf          |  2 +-
 modules/multi-runner/variables.tf    | 14 ++++++++++----
 modules/multi-runner/webhook.tf      |  8 ++++++++
 modules/rotating-random/README.md    |  6 ++++++
 modules/rotating-random/main.tf      | 10 ++++++++++
 modules/rotating-random/outputs.tf   |  3 +++
 modules/rotating-random/variables.tf |  5 +++++
 modules/rotating-random/versions.tf  | 14 ++++++++++++++
 outputs.tf                           |  1 -
 variables.tf                         | 14 ++++++++++----
 17 files changed, 124 insertions(+), 38 deletions(-)
 create mode 100644 modules/rotating-random/README.md
 create mode 100644 modules/rotating-random/main.tf
 create mode 100644 modules/rotating-random/outputs.tf
 create mode 100644 modules/rotating-random/variables.tf
 create mode 100644 modules/rotating-random/versions.tf

diff --git a/docs/configuration.md b/docs/configuration.md
index be0ea03975..eaa3da3c7d 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -14,6 +14,7 @@ To be able to support a number of use-cases, the module has quite a lot of confi
 - Spot vs on-demand. The runners use either the EC2 spot or on-demand life cycle. Runners will be created via the AWS [CreateFleet API](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CreateFleet.html). The module (scale up lambda) will request via the CreateFleet API to create instances in one of the subnets and of the specified instance types.
 - ARM64 support via Graviton/Graviton2 instance-types. When using the default example or top-level module, specifying `instance_types` that match a Graviton/Graviton 2 (ARM64) architecture (e.g. a1, t4g or any 6th-gen `g` or `gd` type), you must also specify `runner_architecture = "arm64"` and the sub-modules will be automatically configured to provision with ARM64 AMIs and leverage GitHub's ARM64 action runner. See below for more details.
 - Disable default labels for the runners (os, architecture and `self-hosted`) can achieve by setting `runner_disable_default_labels` = true. If enabled, the runner will only have the extra labels provided in `runner_extra_labels`. In case you on own start script is used, this configuration parameter needs to be parsed via SSM.
+- Managed vs self-managed webhook secret. The module can manage the webhook secret for you. In that case simply do not provide a value for `github_app.webhook_secret`. If you want to manage the secret yourself, provide a value for `github_app.webhook_secret`. The secret will be managed and a rotation is triggered once running terraform apply again after `github_app.webhook_secret_rotation_days` days. **Important note**: THe managed webhook secret depends on a local-exec (bash) to update the secret in GitNub. It will also update the webhook url.
 
 ## AWS SSM Parameters
 
diff --git a/docs/getting-started.md b/docs/getting-started.md
index cfd8f7b88b..37872d7eb7 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -85,7 +85,7 @@ module "github-runner" {
   github_app = {
     key_base64     = "base64string"
     id             = "1"
-    webhook_secret = "webhook_secret"
+    webhook_secret = "webhook_secret" # optional, if not set the module will manage the secret.
   }
 
   webhook_lambda_zip                = "lambdas-download/webhook.zip"
@@ -109,7 +109,7 @@ The lambda for syncing the GitHub distribution to S3 is triggered via CloudWatch
 ### Setup the webhook / GitHub App (part 2)
 
 At this point you have two options. Either create a separate webhook (enterprise,
-org, or repo), or create a webhook in the App.
+org, or repo), or create a webhook in the App. In case you have not provided a Webhook secret the module will create one and update the GitHub app with both the secret and the webhook url.
 
 #### Option 1: Webhook
 
diff --git a/examples/default/.terraform.lock.hcl b/examples/default/.terraform.lock.hcl
index 045fb7350a..c236658c15 100644
--- a/examples/default/.terraform.lock.hcl
+++ b/examples/default/.terraform.lock.hcl
@@ -83,3 +83,23 @@ provider "registry.terraform.io/hashicorp/random" {
     "zh:eff58323099f1bd9a0bec7cb04f717e7f1b2774c7d612bf7581797e1622613a0",
   ]
 }
+
+provider "registry.terraform.io/hashicorp/time" {
+  version     = "0.12.1"
+  constraints = "~> 0.7"
+  hashes = [
+    "h1:JzYsPugN8Fb7C4NlfLoFu7BBPuRVT2/fCOdCaxshveI=",
+    "zh:090023137df8effe8804e81c65f636dadf8f9d35b79c3afff282d39367ba44b2",
+    "zh:26f1e458358ba55f6558613f1427dcfa6ae2be5119b722d0b3adb27cd001efea",
+    "zh:272ccc73a03384b72b964918c7afeb22c2e6be22460d92b150aaf28f29a7d511",
+    "zh:438b8c74f5ed62fe921bd1078abe628a6675e44912933100ea4fa26863e340e9",
+    "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
+    "zh:85c8bd8eefc4afc33445de2ee7fbf33a7807bc34eb3734b8eefa4e98e4cddf38",
+    "zh:98bbe309c9ff5b2352de6a047e0ec6c7e3764b4ed3dfd370839c4be2fbfff869",
+    "zh:9c7bf8c56da1b124e0e2f3210a1915e778bab2be924481af684695b52672891e",
+    "zh:d2200f7f6ab8ecb8373cda796b864ad4867f5c255cff9d3b032f666e4c78f625",
+    "zh:d8c7926feaddfdc08d5ebb41b03445166df8c125417b28d64712dccd9feef136",
+    "zh:e2412a192fc340c61b373d6c20c9d805d7d3dee6c720c34db23c2a8ff0abd71b",
+    "zh:e6ac6bba391afe728a099df344dbd6481425b06d61697522017b8f7a59957d44",
+  ]
+}
diff --git a/examples/default/main.tf b/examples/default/main.tf
index 42608fae40..7ff2a97ac9 100644
--- a/examples/default/main.tf
+++ b/examples/default/main.tf
@@ -3,10 +3,6 @@ locals {
   aws_region  = var.aws_region
 }
 
-resource "random_id" "random" {
-  byte_length = 20
-}
-
 module "base" {
   source = "../base"
 
@@ -27,9 +23,9 @@ module "runners" {
   }
 
   github_app = {
-    key_base64     = var.github_app.key_base64
-    id             = var.github_app.id
-    webhook_secret = random_id.random.hex
+    key_base64 = var.github_app.key_base64
+    id         = var.github_app.id
+    # webhook_secret = random_id.random.hex
   }
 
   # configure the block device mappings, default for Amazon Linux2
@@ -143,18 +139,6 @@ module "runners" {
   # kms_key_arn = aws_kms_key.github.arn
 }
 
-module "webhook_github_app" {
-  source     = "../../modules/webhook-github-app"
-  depends_on = [module.runners]
-
-  github_app = {
-    key_base64     = var.github_app.key_base64
-    id             = var.github_app.id
-    webhook_secret = random_id.random.hex
-  }
-  webhook_endpoint = module.runners.webhook.endpoint
-}
-
 # enable CMK instead of aws managed key for encryptions
 # resource "aws_kms_key" "github" {
 #   is_enabled = true
diff --git a/examples/default/outputs.tf b/examples/default/outputs.tf
index c50214f566..2709fc69b3 100644
--- a/examples/default/outputs.tf
+++ b/examples/default/outputs.tf
@@ -7,9 +7,3 @@ output "runners" {
 output "webhook_endpoint" {
   value = module.runners.webhook.endpoint
 }
-
-output "webhook_secret" {
-  sensitive = true
-  value     = random_id.random.hex
-}
-
diff --git a/main.tf b/main.tf
index 3f3c9808b4..19e5c61036 100644
--- a/main.tf
+++ b/main.tf
@@ -12,6 +12,17 @@ locals {
   runner_labels         = (var.runner_disable_default_labels == false) ? sort(concat(local.default_runner_labels, var.runner_extra_labels)) : var.runner_extra_labels
 
   ssm_root_path = var.ssm_paths.use_prefix ? "/${var.ssm_paths.root}/${var.prefix}" : "/${var.ssm_paths.root}"
+
+  github_app = merge(var.github_app, {
+    webhook_secret = var.github_app.webhook_secret != null ? var.github_app.webhook_secret : module.rotating_random[0].random.hex
+  })
+}
+
+module "rotating_random" {
+  count  = var.github_app.webhook_secret == null ? 1 : 0
+  source = "./modules/rotating-random"
+
+  rotation_days = var.github_app.webhook_secret_rotation_days
 }
 
 resource "random_string" "random" {
@@ -91,10 +102,18 @@ module "ssm" {
 
   kms_key_arn = var.kms_key_arn
   path_prefix = "${local.ssm_root_path}/${var.ssm_paths.app}"
-  github_app  = var.github_app
+  github_app  = local.github_app
   tags        = local.tags
 }
 
+module "webhook_github_app" {
+  count  = var.github_app.webhook_secret == null ? 1 : 0
+  source = "./modules/webhook-github-app"
+
+  github_app       = local.github_app
+  webhook_endpoint = "${module.webhook.gateway.api_endpoint}/${module.webhook.endpoint_relative_path}"
+}
+
 module "webhook" {
   source = "./modules/webhook"
 
diff --git a/modules/multi-runner/main.tf b/modules/multi-runner/main.tf
index 22ec0df3ba..ecdf79eb9a 100644
--- a/modules/multi-runner/main.tf
+++ b/modules/multi-runner/main.tf
@@ -16,6 +16,17 @@ locals {
   unique_os_and_arch                   = { for i, v in local.tmp_distinct_list_unique_os_and_arch : "${v.os_type}_${v.architecture}" => v }
 
   ssm_root_path = "/${var.ssm_paths.root}/${var.prefix}"
+
+  github_app = merge(var.github_app, {
+    webhook_secret = var.github_app.webhook_secret != null ? var.github_app.webhook_secret : module.rotating_random[0].random.hex
+  })
+}
+
+module "rotating_random" {
+  count  = var.github_app.webhook_secret == null ? 1 : 0
+  source = "./../rotating-random"
+
+  rotation_days = var.github_app.webhook_secret_rotation_days
 }
 
 resource "random_string" "random" {
diff --git a/modules/multi-runner/ssm.tf b/modules/multi-runner/ssm.tf
index 6b2591f465..ae914e7471 100644
--- a/modules/multi-runner/ssm.tf
+++ b/modules/multi-runner/ssm.tf
@@ -3,6 +3,6 @@ module "ssm" {
 
   kms_key_arn = var.kms_key_arn
   path_prefix = "${local.ssm_root_path}/${var.ssm_paths.app}"
-  github_app  = var.github_app
+  github_app  = local.github_app
   tags        = local.tags
 }
diff --git a/modules/multi-runner/variables.tf b/modules/multi-runner/variables.tf
index f962d1ea8c..07359e6624 100644
--- a/modules/multi-runner/variables.tf
+++ b/modules/multi-runner/variables.tf
@@ -1,9 +1,15 @@
 variable "github_app" {
-  description = "GitHub app parameters, see your github app. Ensure the key is the base64-encoded `.pem` file (the output of `base64 app.private-key.pem`, not the content of `private-key.pem`)."
+  description = <<EOF
+    GitHub app parameters, see your github app. Ensure the key is the base64-encoded `.pem` file (the output of `base64 app.private-key.pem`, not the content of `private-key.pem`)."
+
+    If `webhook_secret` is not set, a random secret will be generated and stored in SSM. The secret is used to validate the webhook events. If you want to use your own secret, set the `webhook_secret` parameter.
+    When the secret is managed by the module, it will be rotated every `webhook_secret_rotation_days` days.
+  EOF
   type = object({
-    key_base64     = string
-    id             = string
-    webhook_secret = string
+    key_base64                   = string
+    id                           = string
+    webhook_secret               = optional(string)
+    webhook_secret_rotation_days = optional(number, 30)
   })
 }
 
diff --git a/modules/multi-runner/webhook.tf b/modules/multi-runner/webhook.tf
index c819b763e0..fca912cf08 100644
--- a/modules/multi-runner/webhook.tf
+++ b/modules/multi-runner/webhook.tf
@@ -1,3 +1,11 @@
+module "webhook_github_app" {
+  count  = var.github_app.webhook_secret == null ? 1 : 0
+  source = "./../webhook-github-app"
+
+  github_app       = local.github_app
+  webhook_endpoint = "${module.webhook.gateway.api_endpoint}/${module.webhook.endpoint_relative_path}"
+}
+
 module "webhook" {
   source                              = "../webhook"
   prefix                              = var.prefix
diff --git a/modules/rotating-random/README.md b/modules/rotating-random/README.md
new file mode 100644
index 0000000000..313338345e
--- /dev/null
+++ b/modules/rotating-random/README.md
@@ -0,0 +1,6 @@
+# Module - Rotating Random
+
+> This module is treated as internal module, breaking changes will not trigger a major release bump.
+
+<!-- BEGIN_TF_DOCS -->
+<!-- END_TF_DOCS -->
diff --git a/modules/rotating-random/main.tf b/modules/rotating-random/main.tf
new file mode 100644
index 0000000000..c6235ac79a
--- /dev/null
+++ b/modules/rotating-random/main.tf
@@ -0,0 +1,10 @@
+resource "time_rotating" "rotation_days" {
+  rotation_days = var.rotation_days
+}
+
+resource "random_id" "random" {
+  byte_length = 20
+  keepers = {
+    rotation = time_rotating.rotation_days.id
+  }
+}
diff --git a/modules/rotating-random/outputs.tf b/modules/rotating-random/outputs.tf
new file mode 100644
index 0000000000..5d4e64e6d9
--- /dev/null
+++ b/modules/rotating-random/outputs.tf
@@ -0,0 +1,3 @@
+output "random" {
+  value = random_id.random
+}
diff --git a/modules/rotating-random/variables.tf b/modules/rotating-random/variables.tf
new file mode 100644
index 0000000000..f68bab0c04
--- /dev/null
+++ b/modules/rotating-random/variables.tf
@@ -0,0 +1,5 @@
+variable "rotation_days" {
+  description = "Number of days before rotating the random."
+  type        = number
+  default     = 30
+}
diff --git a/modules/rotating-random/versions.tf b/modules/rotating-random/versions.tf
new file mode 100644
index 0000000000..db2e796d6f
--- /dev/null
+++ b/modules/rotating-random/versions.tf
@@ -0,0 +1,14 @@
+terraform {
+  required_version = ">= 1.3.0"
+
+  required_providers {
+    random = {
+      source  = "hashicorp/random"
+      version = "~> 3"
+    }
+    time = {
+      source  = "hashicorp/time"
+      version = "~> 0.12"
+    }
+  }
+}
diff --git a/outputs.tf b/outputs.tf
index 699867ec2e..403402e495 100644
--- a/outputs.tf
+++ b/outputs.tf
@@ -47,7 +47,6 @@ output "ssm_parameters" {
   value = module.ssm.parameters
 }
 
-
 output "queues" {
   description = "SQS queues."
   value = {
diff --git a/variables.tf b/variables.tf
index 5c57606edf..543b532ad4 100644
--- a/variables.tf
+++ b/variables.tf
@@ -32,11 +32,17 @@ variable "enable_organization_runners" {
 }
 
 variable "github_app" {
-  description = "GitHub app parameters, see your github app. Ensure the key is the base64-encoded `.pem` file (the output of `base64 app.private-key.pem`, not the content of `private-key.pem`)."
+  description = <<EOF
+    GitHub app parameters, see your github app. Ensure the key is the base64-encoded `.pem` file (the output of `base64 app.private-key.pem`, not the content of `private-key.pem`)."
+
+    If `webhook_secret` is not set, a random secret will be generated and stored in SSM. The secret is used to validate the webhook events. If you want to use your own secret, set the `webhook_secret` parameter.
+    When the secret is managed by the module, it will be rotated every `webhook_secret_rotation_days` days.
+  EOF
   type = object({
-    key_base64     = string
-    id             = string
-    webhook_secret = string
+    key_base64                   = string
+    id                           = string
+    webhook_secret               = optional(string)
+    webhook_secret_rotation_days = optional(number, 30)
   })
 }