Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ™ GitHub Repository Webhook Terraform Module

Attaches a single, HMAC-signed web webhook to one GitHub repository β€” least-event subscriptions, TLS verification on by default, and a fully sensitive delivery configuration. Built for integrations/github v6.x.

Terraform GitHub provider module type resources


🧩 Overview

This module manages exactly one github_repository_webhook β€” the per-repository hook GitHub uses to POST event payloads to an external endpoint (CI trigger, ChatOps bridge, audit sink, deployment listener).

  • 🎯 Single responsibility β€” one webhook, one repository, named this.
  • πŸ” Signed by default β€” pass configuration.secret so GitHub signs every payload with X-Hub-Signature-256; the receiver verifies authenticity instead of trusting the source IP.
  • πŸ”’ TLS verified by default β€” insecure_ssl defaults to false; certificate validation is never silently disabled.
  • 🎚️ Narrow event scope β€” events is required and validated (non-empty, no blanks, no duplicates); subscribe only to what the endpoint actually processes.
  • 🧰 Sensitive end-to-end β€” the whole configuration object is sensitive = true, and url / payload_url outputs are marked sensitive because the provider treats them as secret.
  • ⏸️ Stage-then-cutover β€” active = false registers a hook without delivering, so an endpoint can be wired before it goes live.

Why it matters: webhooks are an outbound trust boundary on your source supply chain. A signed, TLS-verified, narrowly-scoped hook keeps payloads authentic and limits the blast radius if a receiving endpoint is ever compromised.


❀️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!


πŸ—ΊοΈ Where this fits in the family

terraform-github-repository-webhook is a repo-scoped integration module β€” it consumes a repository name from the module that owns the repo and delivers selected events to an external endpoint; nothing downstream in this catalog consumes its outputs today.

flowchart LR
 repo["terraform-github-repository<br/>(repository name)"]
 webhook["terraform-github-repository-webhook<br/>(THIS MODULE)"]
 orgwebhook["terraform-github-organization-webhook<br/>(org-scope sibling)"]
 endpoint["External receiving endpoint<br/>(out of suite)"]

 repo -->|"name"| webhook
 webhook -->|"configuration.url (HMAC-signed payloads)"| endpoint
 orgwebhook -.-|"org-scope sibling (no shared wiring)"| webhook

 style webhook fill:#8957E5,color:#fff
 style repo fill:#24292F,color:#fff
Loading

This module consumes repository (from terraform-github-repository's name); it emits id (primary) and repository/url/active/events for import and operational tooling β€” no module in this catalog consumes them yet, see the Typical wiring section.


🧬 What this module builds

A single resource with one required, statically-declared configuration block (not dynamic β€” the provider models it as exactly-one).

flowchart TD
 subgraph mod["terraform-github-repository-webhook"]
 THIS["github_repository_webhook.this<br/>(keystone)<br/>repository + active + events"]
 CONFIG["configuration { }<br/>(required, static block)<br/>url Β· content_type Β· secret Β· insecure_ssl"]
 end

 THIS --> CONFIG

 style THIS fill:#8957E5,color:#fff
Loading

πŸ“ Module Structure

terraform-github-repository-webhook/
β”œβ”€β”€ providers.tf # terraform >= 1.12.0; integrations/github ~> 6.0
β”œβ”€β”€ variables.tf # repository, events, active, configuration
β”œβ”€β”€ main.tf # github_repository_webhook.this
β”œβ”€β”€ outputs.tf # id, repository, url, active, events, payload_url, content_type, insecure_ssl
β”œβ”€β”€ SCOPE.md # resource scope, token scopes, prerequisites, gotchas
└── README.md

βš™οΈ Quick Start

module "ci_webhook" {
  source = "git::https://github.com/microsoftexpert/terraform-github-repository-webhook?ref=v1.0.0"

  repository = module.repository.name # wire from the keystone module β€” never hard-code
  events     = ["push", "pull_request"]

  configuration = {
    url    = "https://ci.example.internal/github/events"
    secret = var.webhook_secret # sensitive β€” sourced from a secret store, never literal
  }
}

ℹ️ repository must be the bare repo name (platform-api), not org/platform-api and not a URL β€” the module validates this.


πŸ”Œ Typical wiring

Derived from the SCOPE.md Emits table β€” one row per output in outputs.tf.

Output Type Typically consumed by
id string Import / state references; downstream modules referencing this hook (import form <repository>/<id>)
repository string Cross-checks against terraform-github-repository (name)
url πŸ”’ string Operational tooling reconciling the hook via the GitHub API
active bool Conditional logic gating dependent automation on hook liveness
events list(string) Audit / drift dashboards confirming the subscribed event set
payload_url πŸ”’ string Receiver-side verification that the endpoint matches expectation
content_type string Receiver payload-decoding configuration (json / form)
insecure_ssl bool Compliance scans asserting TLS verification is on

πŸ”’ = sensitive output β€” will not render in plan/apply console output or terraform output without -json / explicit name.


🧠 Architecture Notes

  • id format and what it identifies. id is the numeric hook id assigned by GitHub (e.g. 292430177), unique within the repository β€” not globally. There is no node_id, repo_id, or slug on this resource; GitHub's REST hooks API does not expose a GraphQL node id for webhooks. The cross-resource key that matters is the import address <repository>/<id> (e.g. platform-api/292430177).
  • ForceNew: repository. Changing repository moves the hook to a different repo, which the provider implements as destroy-and-recreate β€” the hook here is deleted and a new one is created under the new repository (a new id). This is documented inline in variables.tf. Wire repository from module.repository.name so it only changes deliberately.
  • configuration is a required, exactly-one block. The provider models configuration as a one-of block, so the module declares it statically in main.tf (no dynamic), with try(...) on the optional nested fields (content_type, secret, insecure_ssl) so omitted inputs fall back to secure defaults.
  • Sensitive secret handling. The webhook secret is the shared HMAC key, not an encryptable value β€” GitHub has no encrypted_value equivalent for hooks (unlike Actions secrets). It is therefore plaintext at the Terraform input boundary: the entire configuration object is sensitive = true, and on import GitHub returns the secret as ********, which causes a perpetual diff if you later read state without re-supplying the same secret. Always source secret from a secret store and keep it stable.
  • Why url / payload_url outputs are sensitive. The provider marks the hook's API url attribute as secret because a hook URL can embed basic-auth credentials. payload_url (the delivery configuration.url) is emitted as sensitive for the same reason; the HMAC secret itself is never emitted.
  • Single hook, not authoritative. This resource manages one webhook addressed by its own id; it is neither a plural nor an authoritative resource. It will not detect or remove other webhooks on the same repository β€” only the one it created. To manage many hooks, instantiate the module multiple times (see the for_each example).
  • Rulesets vs. webhooks β€” orthogonal. Webhooks are notification plumbing, not enforcement. our preference for github_repository_ruleset over legacy github_branch_protection is a separate concern β€” rulesets govern what may merge; webhooks only announce that something happened. Don't conflate them.
  • Secondary rate limits on bulk for_each. Creating many hooks across many repos in one apply can trip GitHub's secondary rate limit (HTTP 403, abuse-detection). The provider retries with backoff, but very large fan-outs may still fail mid-apply. For wide rollouts, batch the apply or use -parallelism=2 (see Troubleshooting).

πŸ“š Example Library

1 Β· Minimal (signed, TLS-verified defaults).
module "webhook_minimal" {
  source     = "git::https://github.com/microsoftexpert/terraform-github-repository-webhook?ref=v1.0.0"
  repository = module.repository.name
  events     = ["push"]
  configuration = {
    url    = "https://hooks.example.internal/push"
    secret = var.webhook_secret
  }
}
2 Β· Secure / hardened variant (explicit narrow scope, signed, verified, JSON).
module "webhook_hardened" {
  source     = "git::https://github.com/microsoftexpert/terraform-github-repository-webhook?ref=v1.0.0"
  repository = module.repository.name
  events     = ["push", "pull_request"]
  active     = true
  configuration = {
    url          = "https://secure.example.internal/github"
    content_type = "json"
    secret       = var.webhook_secret
    insecure_ssl = false # explicit β€” TLS verification on
  }
}
3 Β· Form-encoded payloads.
module "webhook_form" {
  source     = "git::https://github.com/microsoftexpert/terraform-github-repository-webhook?ref=v1.0.0"
  repository = module.repository.name
  events     = ["issues", "issue_comment"]
  configuration = {
    url          = "https://chatops.example.internal/github"
    content_type = "form"
    secret       = var.webhook_secret
  }
}
4 Β· Subscribe to every event (`*`).
module "webhook_all_events" {
  source     = "git::https://github.com/microsoftexpert/terraform-github-repository-webhook?ref=v1.0.0"
  repository = module.repository.name
  events     = ["*"] # firehose β€” use only for a trusted audit sink
  configuration = {
    url    = "https://audit-sink.example.internal/github"
    secret = var.webhook_secret
  }
}
5 Β· Staged (registered but not delivering).
module "webhook_staged" {
  source     = "git::https://github.com/microsoftexpert/terraform-github-repository-webhook?ref=v1.0.0"
  repository = module.repository.name
  events     = ["deployment", "deployment_status"]
  active     = false # register now, flip to true at cutover
  configuration = {
    url    = "https://deploy-listener.example.internal/github"
    secret = var.webhook_secret
  }
}
6 Β· CI build trigger.
module "webhook_ci" {
  source     = "git::https://github.com/microsoftexpert/terraform-github-repository-webhook?ref=v1.0.0"
  repository = module.repository.name
  events     = ["push", "pull_request", "create", "delete"]
  configuration = {
    url    = "https://jenkins.example.internal/github-webhook/"
    secret = var.webhook_secret
  }
}
7 Β· Release / publish notifications.
module "webhook_release" {
  source     = "git::https://github.com/microsoftexpert/terraform-github-repository-webhook?ref=v1.0.0"
  repository = module.repository.name
  events     = ["release", "registry_package"]
  configuration = {
    url    = "https://release-bot.example.internal/github"
    secret = var.webhook_secret
  }
}
8 Β· Security event sink (secret scanning + Dependabot alerts).
module "webhook_security" {
  source     = "git::https://github.com/microsoftexpert/terraform-github-repository-webhook?ref=v1.0.0"
  repository = module.repository.name
  events     = ["secret_scanning_alert", "dependabot_alert", "code_scanning_alert"]
  configuration = {
    url    = "https://siem.example.internal/github"
    secret = var.webhook_secret
  }
}
9 Β· Workflow / Actions observability.
module "webhook_workflow" {
  source     = "git::https://github.com/microsoftexpert/terraform-github-repository-webhook?ref=v1.0.0"
  repository = module.repository.name
  events     = ["workflow_run", "workflow_job", "check_run", "check_suite"]
  configuration = {
    url    = "https://ci-metrics.example.internal/github"
    secret = var.webhook_secret
  }
}
10 Β· Internal endpoint with a self-signed certificate (last resort).
module "webhook_self_signed" {
  source     = "git::https://github.com/microsoftexpert/terraform-github-repository-webhook?ref=v1.0.0"
  repository = module.repository.name
  events     = ["push"]
  configuration = {
    url          = "https://10.20.30.40/github"
    secret       = var.webhook_secret
    insecure_ssl = true # ⚠️ only for internal, self-signed endpoints β€” NEVER internet-facing
  }
}

⚠️ Setting insecure_ssl = true disables certificate verification and exposes payloads to MITM. Use a properly issued certificate instead wherever possible.

11 Β· `for_each` at scale from a `map(object)`.
variable "webhooks" {
  type = map(object({
    events       = list(string)
    url          = string
    content_type = optional(string, "json")
    active       = optional(bool, true)
  }))
}

module "webhooks" {
  source   = "git::https://github.com/microsoftexpert/terraform-github-repository-webhook?ref=v1.0.0"
  for_each = var.webhooks

  repository = module.repository.name
  events     = each.value.events
  active     = each.value.active
  configuration = {
    url          = each.value.url
    content_type = each.value.content_type
    secret       = var.webhook_secret
  }
}

πŸ’‘ Keys are stable strings, so adding or removing a hook never re-creates the others. For large maps, see the secondary rate-limit note in Troubleshooting.

12 Β· Cross-module wiring with the keystone repository.
module "repository" {
  source     = "git::https://github.com/microsoftexpert/terraform-github-repository?ref=v1.0.0"
  name       = "platform-api"
  visibility = "private"
}

module "webhook" {
  source     = "git::https://github.com/microsoftexpert/terraform-github-repository-webhook?ref=v1.0.0"
  repository = module.repository.name # wired output β†’ input
  events     = ["push", "pull_request"]
  configuration = {
    url    = "https://ci.example.internal/github/events"
    secret = var.webhook_secret
  }
}
13 Β· Multiple distinct hooks on one repo (separate concerns).
module "webhook_ci" {
  source        = "git::https://github.com/microsoftexpert/terraform-github-repository-webhook?ref=v1.0.0"
  repository    = module.repository.name
  events        = ["push", "pull_request"]
  configuration = { url = "https://ci.example.internal/github", secret = var.ci_secret }
}

module "webhook_audit" {
  source        = "git::https://github.com/microsoftexpert/terraform-github-repository-webhook?ref=v1.0.0"
  repository    = module.repository.name
  events        = ["*"]
  configuration = { url = "https://audit.example.internal/github", secret = var.audit_secret }
}

πŸ“¦ Inputs (high-level)

Identity / parent reference

  • repository (string, required) β€” bare repo name the hook attaches to. ForceNew. Validated: non-empty, no /.

Subscription

  • events (list(string), required) β€” event types that trigger the hook. Validated: non-empty, no blank entries, no duplicates. ["*"] = all events.
  • active (bool, default true) β€” whether the hook delivers payloads.

Delivery configuration β€” configuration (object, required, sensitive):

  • url (string, required) β€” destination endpoint. Validated http(s)://; https strongly preferred.
  • content_type (string, default "json") β€” "json" or "form". Validated.
  • secret (string, optional, sensitive) β€” shared HMAC signing key.
  • insecure_ssl (bool, default false) β€” disable TLS verification (avoid).

🧾 Outputs

Output Description
id Numeric webhook id (import form <repository>/<id>).
repository Repository name the hook is attached to.
url πŸ”’ GitHub API URL of the hook resource (sensitive β€” may embed credentials).
active Whether the hook is delivering events.
events Subscribed event types.
payload_url πŸ”’ Configured delivery endpoint (configuration.url, sensitive).
content_type Payload encoding (json / form).
insecure_ssl Whether TLS verification is disabled (secure default false).

πŸ”’ = sensitive. The HMAC secret is never emitted as an output.


🧱 Design Principles

  • πŸ” Signed by default mindset β€” the schema makes secret a first-class, sensitive field so HMAC signing is the obvious path.
  • πŸ”’ TLS verified by default β€” insecure_ssl defaults to false; disabling it is an explicit, documented opt-out.
  • 🎚️ Least subscription β€” events is required and de-duplicated; the README steers callers to the narrowest viable set.
  • πŸ™ˆ Sensitive by construction β€” the full configuration object and the credential-bearing outputs are sensitive.
  • 🧩 Single responsibility β€” one hook named this; scale by composition, never by hidden authority over sibling hooks.
  • 🚫 No provider concerns leak in β€” no owner, token, or app_auth variables; auth and org are the caller's provider block.

πŸš€ Runbook

terraform init -backend=false
terraform validate
terraform fmt -check
terraform plan
terraform apply
terraform output

⚠️ Always pin the module to a tag β€” ?ref=v1.0.0 β€” never a branch. Branch sources drift silently between applies.


πŸ” Troubleshooting

  • 404 Not Found on create β€” the provider identity lacks repository admin rights, or the repository name is wrong/mistyped (must be the bare name, no org/ prefix). Confirm admin:repo_hook (classic) or fine-grained Webhooks: read & write is granted.
  • Perpetual diff on configuration.secret β€” GitHub returns the secret as ******** on read, so Terraform sees a change every plan if you don't re-supply the same value. Keep secret stable from a secret store; don't omit it after first apply.
  • 403 / secondary rate limit on bulk for_each β€” GitHub's abuse-detection throttles rapid hook creation. Re-run, or lower throughput: terraform apply -parallelism=2. Batch very large rollouts across multiple applies.
  • Deliveries failing with TLS errors β€” endpoint certificate isn't trusted by GitHub. Fix the certificate; do not reach for insecure_ssl = true on internet-facing endpoints.
  • Receiver rejects all payloads β€” signature mismatch. Ensure the receiver verifies X-Hub-Signature-256 with the same secret and the matching content_type.
  • Hook recreated unexpectedly β€” repository changed. It is ForceNew; the old hook is destroyed and a new id issued. Verify the wired-in repo name didn't shift.

πŸ”— Related Docs

  • keystone module β€” terraform-github-repository (emits the name this module consumes).
  • org-scope counterpart β€” terraform-github-organization-webhook.
  • integrations/github provider β€” Resource: github_repository_webhook.
  • GitHub REST reference β€” Webhook events and payloads (valid events names).
  • GitHub docs β€” Securing your webhooks (validating X-Hub-Signature-256).

Releases

Packages

Contributors

Languages