Skip to content

microsoftexpert/tf-mod-github-organization-webhook

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

3 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ™ GitHub Organization Webhook Terraform Module

Registers a single, HMAC-signed, organization-scoped webhook that delivers GitHub event payloads to an endpoint you control β€” secure-by-default (TLS verified, narrow event subscription, signing secret marked sensitive). Built for integrations/github v6.x.

Terraform GitHub provider module type resources


🧩 Overview

This module wraps the github_organization_webhook resource β€” one webhook, registered at the organization level, that fires for every repository in the org.

  • πŸ“‘ Org-wide delivery β€” one hook receives events from all repositories in the organization (no per-repo wiring).
  • 🎯 Narrow event subscription β€” events is required and validated to be non-empty, de-duplicated, and free of blank entries. Subscribe only to what the receiver processes.
  • πŸ” HMAC-signed payloads β€” set configuration.secret and GitHub signs every delivery (X-Hub-Signature-256) so your receiver can verify authenticity.
  • πŸ”’ TLS verified by default β€” insecure_ssl = false unless you explicitly opt out for an internal self-signed endpoint.
  • 🟒 Live on create β€” active = true by default; stage a hook silently with active = false.
  • 🀫 Secret-aware β€” the entire configuration input is sensitive, and the delivery URL output is sensitive because the provider treats it as secret.

πŸ’‘ Why it matters: A single org webhook is the supply-chain tripwire for source events (pushes, ruleset bypasses, member changes, secret-scanning alerts). Getting it signed, TLS-verified, and scoped narrowly is the difference between a reliable audit feed and an unauthenticated firehose.


❀️ 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

This module is a peer among the Org Governance modules β€” it consumes no repository or sibling-module output, and no GitHub module in this suite consumes it by reference. Its role is to feed org-wide events to an external audit/automation endpoint.

flowchart LR
 subgraph GOV["Org Governance peers"]
 settings["tf-mod-github-organization-settings"]
 ruleset["tf-mod-github-organization-ruleset"]
 membership["tf-mod-github-membership"]
 webhook["tf-mod-github-organization-webhook<br/>(THIS MODULE)"]
 end
 SIEM["Audit / SIEM / automation endpoint<br/>(outside Terraform)"]

 webhook -->|"events + payload_url"| SIEM

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

This module consumes nothing from sibling modules (organization-scoped; no repository_id or identity input); it emits id, events, active, and the sensitive payload_url for external audit/SIEM tooling β€” see the Typical wiring section. No GitHub module in this suite consumes an org webhook by reference.


🧬 What this module builds

A single organization webhook with its one required configuration block, rendered statically with secure-default fallbacks for omitted fields.

flowchart TD
 this["github_organization_webhook.this<br/>(keystone)<br/>org-wide event delivery"]
 config["configuration<br/>(required block Β· url / content_type / secret / insecure_ssl)"]

 this --> config

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

πŸ“ Module Structure

tf-mod-github-organization-webhook/
β”œβ”€β”€ providers.tf # terraform >= 1.12.0; integrations/github ~> 6.0
β”œβ”€β”€ variables.tf # events, active, configuration (sensitive object)
β”œβ”€β”€ main.tf # github_organization_webhook.this
β”œβ”€β”€ outputs.tf # id, url, active, events, payload_url, content_type, insecure_ssl
β”œβ”€β”€ SCOPE.md # resource scope, token scopes, prerequisites, emits
└── README.md # this file

βš™οΈ Quick Start

module "audit_webhook" {
  source = "git::https://github.com/microsoftexpert/tf-mod-github-organization-webhook?ref=v1.0.0"

  events = ["push", "pull_request"]

  configuration = {
    url    = "https://hooks.example.internal/github/org"
    secret = var.org_webhook_secret # sensitive β€” pass via TF_VAR / Key Vault
  }
}

ℹ️ The target organization is a provider concern (owner / GITHUB_OWNER), never a module variable. Configure it in the caller's provider "github" {} block.


πŸ”Œ Typical wiring

Every output in outputs.tf gets one row. This is an organization-scoped module β€” it consumes no repository and emits org-level signals consumed by audit/automation, not by other Terraform modules.

Output Type Typically consumed by
id string (numeric hook ID) terraform import, GitHub API hook-management calls, audit inventory
url string runbooks / scripts that ping the GitHub API hook resource
active bool drift dashboards confirming the hook is delivering
events list(string) governance reports auditing org event coverage
payload_url string (sensitive) the receiving service's allow-list / config (delivery endpoint)
content_type string the receiver's payload parser (json vs form)
insecure_ssl bool compliance checks asserting TLS verification is on

⚠️ No GitHub module consumes an org webhook by reference β€” there is no repository/team_id wire-in here. It sits alongside the other Org Governance modules (tf-mod-github-organization-settings, tf-mod-github-organization-ruleset, tf-mod-github-membership) as a peer, not a child.


🧠 Architecture Notes

  • id is the numeric hook ID, not a node ID. GitHub returns a numeric identifier (e.g. 12345678) for org webhooks. That is the id output and the value you pass to terraform import. This resource does not expose node_id, repo_id, full_name, or slug β€” those exist on repository/team resources, not on github_organization_webhook. Don't invent them.
  • url output β‰  delivery endpoint. The url output is the GitHub API URL of the hook resource itself (https://api.github.com/orgs/<org>/hooks/<id>). The endpoint GitHub POSTs to is payload_url (sourced from configuration.url), which is marked sensitive.
  • No ForceNew / immutable fields. Unlike repository name or environment repository, every argument on an org webhook (events, active, all configuration keys) updates in place β€” changing the URL or event list does not destroy and recreate the hook. The optional name argument is fixed to "web" (the only valid type) and is not exposed by this module.
  • Sensitive secret handling β€” plaintext only. configuration.secret is the HMAC signing secret. The provider accepts it as a plaintext value (there is no encrypted_value variant for webhooks, unlike Actions/Dependabot secrets). On read/import GitHub returns it masked as ********, so Terraform cannot detect drift on the secret value β€” rotating it out-of-band will not be noticed; rotate it through this module. The whole configuration input and the payload_url output are sensitive = true to keep secrets out of plan output and logs.
  • Rulesets vs branch protection β€” not applicable here. This is event delivery, not policy enforcement; there is no ruleset/branch-protection choice to make. (prefers github_organization_ruleset over legacy branch protection in the governance modules β€” see tf-mod-github-organization-ruleset.) A common pairing: subscribe this hook to ruleset-related events to feed an audit pipeline.
  • Single resource, not an authoritative collection. Each github_organization_webhook is a discrete hook; there is no plural "authoritative" variant that owns all org hooks. To manage several hooks, call this module once per endpoint (for_each at the module level) β€” each instance owns exactly its own hook and ignores others.
  • Secondary rate limits on bulk for_each. Fanning many module instances out in one apply issues many POST /orgs/{org}/hooks calls and can trip GitHub's secondary rate limit (HTTP 403 with a Retry-After). For large fleets, apply in waves or set the provider's read_delay_ms / write_delay_ms.

πŸ“š Example Library

1 Β· Minimal
module "webhook_min" {
  source = "git::https://github.com/microsoftexpert/tf-mod-github-organization-webhook?ref=v1.0.0"

  events = ["push"]
  configuration = {
    url = "https://hooks.example.internal/github"
  }
}
2 Β· Secure / hardened variant (recommended baseline)
module "webhook_hardened" {
  source = "git::https://github.com/microsoftexpert/tf-mod-github-organization-webhook?ref=v1.0.0"

  events = ["push", "pull_request", "repository_ruleset"]
  configuration = {
    url          = "https://hooks.example.internal/github/secure"
    content_type = "json"
    secret       = var.org_webhook_secret # HMAC signing β€” always set this
    insecure_ssl = false                  # TLS verification ON (default)
  }
}

πŸ”’ The hardened baseline = HMAC secret set + insecure_ssl = false + the narrowest events list. Everything else is the module default.

3 Β· Subscribe to all events with `["*"]`
module "webhook_all" {
  source = "git::https://github.com/microsoftexpert/tf-mod-github-organization-webhook?ref=v1.0.0"

  events = ["*"] # every event type
  configuration = {
    url    = "https://siem.example.internal/github/firehose"
    secret = var.org_webhook_secret
  }
}

⚠️ ["*"] maximizes payload volume and the blast radius of a compromised endpoint. Prefer an explicit list unless the receiver is a hardened SIEM that genuinely needs everything.

4 Β· `form` content type
module "webhook_form" {
  source = "git::https://github.com/microsoftexpert/tf-mod-github-organization-webhook?ref=v1.0.0"

  events = ["push"]
  configuration = {
    url          = "https://legacy.example.internal/github"
    content_type = "form" # x-www-form-urlencoded instead of JSON
    secret       = var.org_webhook_secret
  }
}
5 Β· Staged (inactive) endpoint before cutover
module "webhook_staged" {
  source = "git::https://github.com/microsoftexpert/tf-mod-github-organization-webhook?ref=v1.0.0"

  active = false # registered but delivers nothing yet
  events = ["push", "pull_request"]
  configuration = {
    url    = "https://newstack.example.internal/github"
    secret = var.org_webhook_secret
  }
}
6 Β· Internal endpoint with self-signed TLS (explicit opt-out)
module "webhook_selfsigned" {
  source = "git::https://github.com/microsoftexpert/tf-mod-github-organization-webhook?ref=v1.0.0"

  events = ["push"]
  configuration = {
    url          = "https://internal-ci.corp.local/github"
    secret       = var.org_webhook_secret
    insecure_ssl = true # ONLY for internal self-signed hosts, never internet-facing
  }
}

⚠️ insecure_ssl = true disables certificate verification β€” GitHub will deliver to a host presenting any cert. Use only on private, non-routable endpoints.

7 Β· Security / audit event feed
module "webhook_security" {
  source = "git::https://github.com/microsoftexpert/tf-mod-github-organization-webhook?ref=v1.0.0"

  events = [
    "secret_scanning_alert",
    "code_scanning_alert",
    "dependabot_alert",
    "repository_vulnerability_alert",
  ]
  configuration = {
    url    = "https://soc.example.internal/github/alerts"
    secret = var.soc_webhook_secret
  }
}
8 Β· Membership / org-governance feed
module "webhook_governance" {
  source = "git::https://github.com/microsoftexpert/tf-mod-github-organization-webhook?ref=v1.0.0"

  events = ["member", "membership", "organization", "team", "team_add"]
  configuration = {
    url    = "https://iam.example.internal/github/membership"
    secret = var.iam_webhook_secret
  }
}
9 Β· CI trigger feed
module "webhook_ci" {
  source = "git::https://github.com/microsoftexpert/tf-mod-github-organization-webhook?ref=v1.0.0"

  events = ["push", "pull_request", "create", "delete"]
  configuration = {
    url    = "https://ci.example.internal/github/trigger"
    secret = var.ci_webhook_secret
  }
}
10 Β· Secret sourced from a variable (no hardcoding)
variable "org_webhook_secret" {
  type      = string
  sensitive = true
}

module "webhook_from_var" {
  source = "git::https://github.com/microsoftexpert/tf-mod-github-organization-webhook?ref=v1.0.0"

  events = ["push"]
  configuration = {
    url    = "https://hooks.example.internal/github"
    secret = var.org_webhook_secret
  }
}
11 Β· `for_each` at scale from a `map(object)`
locals {
  org_webhooks = {
    ci = {
      url    = "https://ci.example.internal/github"
      events = ["push", "pull_request"]
    }
    soc = {
      url    = "https://soc.example.internal/github"
      events = ["secret_scanning_alert", "code_scanning_alert"]
    }
    iam = {
      url    = "https://iam.example.internal/github"
      events = ["member", "membership", "team"]
    }
  }
}

module "org_webhooks" {
  source   = "git::https://github.com/microsoftexpert/tf-mod-github-organization-webhook?ref=v1.0.0"
  for_each = local.org_webhooks

  events = each.value.events
  configuration = {
    url    = each.value.url
    secret = var.org_webhook_secrets[each.key] # one secret per hook
  }
}

⚠️ Several hooks created in one apply can hit the secondary rate limit. Apply in waves or tune the provider's write_delay_ms if you see HTTP 403 Retry-After.

12 Β· Wiring the delivery URL from a sibling module's output
# Endpoint provisioned elsewhere (e.g. an Azure Function / API Gateway module)
module "ingest_endpoint" {
  source = "git::https://github.com/microsoftexpert/tf-mod-function-app?ref=v1.0.0"
  #...
}

module "webhook_wired" {
  source = "git::https://github.com/microsoftexpert/tf-mod-github-organization-webhook?ref=v1.0.0"

  events = ["push", "pull_request"]
  configuration = {
    url    = module.ingest_endpoint.https_endpoint # consumed from a sibling
    secret = var.org_webhook_secret
  }
}
13 Β· Alongside the org-governance peers
module "org_settings" {
  source = "git::https://github.com/microsoftexpert/tf-mod-github-organization-settings?ref=v1.0.0"
  #...secure org defaults (2FA required, least-privilege member perms)...
}

module "org_ruleset" {
  source = "git::https://github.com/microsoftexpert/tf-mod-github-organization-ruleset?ref=v1.0.0"
  #...enforced org-wide ruleset...
}

# Feed every ruleset/governance change to the audit pipeline
module "org_audit_webhook" {
  source = "git::https://github.com/microsoftexpert/tf-mod-github-organization-webhook?ref=v1.0.0"

  events = ["repository_ruleset", "organization", "member", "repository"]
  configuration = {
    url    = "https://audit.example.internal/github/governance"
    secret = var.audit_webhook_secret
  }
}

πŸ“¦ Inputs (high-level)

Required

  • events β€” list(string). The event types that trigger the hook. Validated: non-empty, no blank entries, no duplicates. Use ["*"] for all events.
  • configuration β€” object({...}), sensitive. The delivery configuration:
  • url (string, required) β€” destination endpoint. Validated to be http(s)://; https strongly preferred.
  • content_type (string, default "json") β€” "json" or "form" (validated).
  • secret (string, optional) β€” sensitive HMAC signing secret.
  • insecure_ssl (bool, default false) β€” TLS verification toggle (keep false).

Optional

  • active β€” bool, default true. Whether the hook delivers events.

🧾 Outputs

Output Description
id Numeric hook ID β€” the import key and primary handle for this hook.
url GitHub API URL of the hook resource (.../orgs/<org>/hooks/<id>), not the delivery endpoint.
active Whether the hook is delivering events.
events The list of subscribed event types.
payload_url πŸ”’ sensitive β€” the configured delivery endpoint (configuration.url).
content_type Payload encoding ("json" or "form").
insecure_ssl Whether TLS verification is disabled (secure default false).

πŸ”’ payload_url is marked sensitive because the provider treats the configuration URL as secret. The HMAC secret itself is never emitted as an output.


🧱 Design Principles

  • Least-event-privilege. events is required and validated β€” no accidental org-wide firehose; subscribe to what the receiver processes.
  • Authenticated delivery. configuration.secret enables HMAC signing so receivers can reject forged payloads; the whole config object is sensitive.
  • TLS by default. insecure_ssl = false β€” verification must be explicitly turned off, and only for internal hosts.
  • Secrets stay secret. Sensitive inputs and the delivery-URL output are marked sensitive; the signing secret is never output.
  • Provider owns identity & org. No owner / token / app_auth variables β€” auth and the target org are caller concerns.
  • No tags, no timeouts tail. GitHub has neither; the module stops at its real inputs.

πŸš€ Runbook

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

Import an existing org webhook (the ID is the numeric hook ID from the GitHub API / org settings):

terraform import github_organization_webhook.this 12345678

⚠️ Always pin the module source to a tag β€” ?ref=v1.0.0, never a branch. Branch refs drift and break reproducible plans.


πŸ” Troubleshooting

  • 404 Not Found / 403 on create β€” the provider identity lacks org-owner rights or the admin:org_hook scope. Org webhooks require an organization owner. See the token scopes table in SCOPE.md.
  • Secret never shows drift β€” expected. GitHub returns the secret as ******** on read, so Terraform can't compare values. Rotate the secret through this module, not out-of-band.
  • Deliveries failing TLS β€” if your endpoint uses a self-signed cert you'll see delivery failures in the org β†’ Settings β†’ Webhooks β†’ "Recent Deliveries" panel. Either install a trusted cert (preferred) or set insecure_ssl = true for the internal host.
  • 422 Validation Failed: Hook already exists β€” an identical hook (same URL) already exists. Import it (terraform import... <id>) instead of creating a duplicate.
  • HTTP 403 You have exceeded a secondary rate limit β€” too many hook writes at once from a bulk for_each. Apply in waves or set the provider's write_delay_ms.
  • insecure_ssl shows as a string in plan β€” GitHub's API models this as "0"/"1"; the provider normalizes it to a bool. If you see churn, ensure you pass a real boolean, not "true".

πŸ”— Related Docs

  • integrations/github provider β€” Organization Webhook resource reference
  • GitHub REST API β€” Organization Webhooks
  • GitHub Docs β€” Webhook events and payloads (valid events names)
  • GitHub Docs β€” Securing your webhooks (HMAC X-Hub-Signature-256)
  • Org Governance peers β€” tf-mod-github-organization-settings, tf-mod-github-organization-ruleset, tf-mod-github-membership

About

No description or website provided.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages