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.
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 β
eventsis 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.secretand GitHub signs every delivery (X-Hub-Signature-256) so your receiver can verify authenticity. - π TLS verified by default β
insecure_ssl = falseunless you explicitly opt out for an internal self-signed endpoint. - π’ Live on create β
active = trueby default; stage a hook silently withactive = false. - π€« Secret-aware β the entire
configurationinput issensitive, 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.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- β Star this repository to help others discover this Terraform module.
- π€ Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
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!
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
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.
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
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
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'sprovider "github" {}block.
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 norepository/team_idwire-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.
idis the numeric hook ID, not a node ID. GitHub returns a numeric identifier (e.g.12345678) for org webhooks. That is theidoutput and the value you pass toterraform import. This resource does not exposenode_id,repo_id,full_name, orslugβ those exist on repository/team resources, not ongithub_organization_webhook. Don't invent them.urloutput β delivery endpoint. Theurloutput is the GitHub API URL of the hook resource itself (https://api.github.com/orgs/<org>/hooks/<id>). The endpoint GitHub POSTs to ispayload_url(sourced fromconfiguration.url), which is marked sensitive.- No ForceNew / immutable fields. Unlike repository
nameor environmentrepository, every argument on an org webhook (events,active, allconfigurationkeys) updates in place β changing the URL or event list does not destroy and recreate the hook. The optionalnameargument is fixed to"web"(the only valid type) and is not exposed by this module. - Sensitive secret handling β plaintext only.
configuration.secretis the HMAC signing secret. The provider accepts it as a plaintext value (there is noencrypted_valuevariant 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 wholeconfigurationinput and thepayload_urloutput aresensitive = trueto 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_rulesetover legacy branch protection in the governance modules β seetf-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_webhookis 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_eachat 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 manyPOST /orgs/{org}/hookscalls and can trip GitHub's secondary rate limit (HTTP 403 with aRetry-After). For large fleets, apply in waves or set the provider'sread_delay_ms/write_delay_ms.
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 narrowesteventslist. 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 = truedisables 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'swrite_delay_msif you see HTTP 403Retry-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
}
}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 behttp(s)://; https strongly preferred.content_type(string, default"json") β"json"or"form"(validated).secret(string, optional) β sensitive HMAC signing secret.insecure_ssl(bool, defaultfalse) β TLS verification toggle (keepfalse).
Optional
activeβbool, defaulttrue. Whether the hook delivers events.
| 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_urlis marked sensitive because the provider treats the configuration URL as secret. The HMACsecretitself is never emitted as an output.
- Least-event-privilege.
eventsis required and validated β no accidental org-wide firehose; subscribe to what the receiver processes. - Authenticated delivery.
configuration.secretenables 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_authvariables β auth and the target org are caller concerns. - No tags, no timeouts tail. GitHub has neither; the module stops at its real inputs.
terraform init -backend=false
terraform validate
terraform fmt -check
terraform plan
terraform apply
terraform outputImport 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.
404 Not Found/403on create β the provider identity lacks org-owner rights or theadmin:org_hookscope. Org webhooks require an organization owner. See the token scopes table inSCOPE.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 = truefor 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 bulkfor_each. Apply in waves or set the provider'swrite_delay_ms. insecure_sslshows 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".
- integrations/github provider β Organization Webhook resource reference
- GitHub REST API β Organization Webhooks
- GitHub Docs β Webhook events and payloads (valid
eventsnames) - 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