Attaches a single, HMAC-signed
webwebhook to one GitHub repository β least-event subscriptions, TLS verification on by default, and a fully sensitive delivery configuration. Built for integrations/github v6.x.
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.secretso GitHub signs every payload withX-Hub-Signature-256; the receiver verifies authenticity instead of trusting the source IP. - π TLS verified by default β
insecure_ssldefaults tofalse; certificate validation is never silently disabled. - ποΈ Narrow event scope β
eventsis required and validated (non-empty, no blanks, no duplicates); subscribe only to what the endpoint actually processes. - π§° Sensitive end-to-end β the whole
configurationobject issensitive = true, andurl/payload_urloutputs are marked sensitive because the provider treats them as secret. - βΈοΈ Stage-then-cutover β
active = falseregisters 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.
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!
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
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.
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
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
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
}
}βΉοΈ
repositorymust be the bare repo name (platform-api), notorg/platform-apiand not a URL β the module validates this.
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 |
π =
sensitiveoutput β will not render in plan/apply console output orterraform outputwithout-json/ explicit name.
idformat and what it identifies.idis the numeric hook id assigned by GitHub (e.g.292430177), unique within the repository β not globally. There is nonode_id,repo_id, orslugon 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. Changingrepositorymoves 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 newid). This is documented inline invariables.tf. Wirerepositoryfrommodule.repository.nameso it only changes deliberately. configurationis a required, exactly-one block. The provider modelsconfigurationas a one-of block, so the module declares it statically inmain.tf(nodynamic), withtry(...)on the optional nested fields (content_type,secret,insecure_ssl) so omitted inputs fall back to secure defaults.- Sensitive secret handling. The webhook
secretis the shared HMAC key, not an encryptable value β GitHub has noencrypted_valueequivalent for hooks (unlike Actions secrets). It is therefore plaintext at the Terraform input boundary: the entireconfigurationobject issensitive = 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 sourcesecretfrom a secret store and keep it stable. - Why
url/payload_urloutputs are sensitive. The provider marks the hook's APIurlattribute as secret because a hook URL can embed basic-auth credentials.payload_url(the deliveryconfiguration.url) is emitted as sensitive for the same reason; the HMACsecretitself 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 thefor_eachexample). - Rulesets vs. webhooks β orthogonal. Webhooks are notification plumbing, not enforcement. our preference for
github_repository_rulesetover legacygithub_branch_protectionis 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).
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
}
}
β οΈ Settinginsecure_ssl = truedisables 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 }
}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, defaulttrue) β whether the hook delivers payloads.
Delivery configuration β configuration (object, required, sensitive):
url(string, required) β destination endpoint. Validatedhttp(s)://; https strongly preferred.content_type(string, default"json") β"json"or"form". Validated.secret(string, optional, sensitive) β shared HMAC signing key.insecure_ssl(bool, defaultfalse) β disable TLS verification (avoid).
| 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 HMACsecretis never emitted as an output.
- π Signed by default mindset β the schema makes
secreta first-class, sensitive field so HMAC signing is the obvious path. - π TLS verified by default β
insecure_ssldefaults tofalse; disabling it is an explicit, documented opt-out. - ποΈ Least subscription β
eventsis required and de-duplicated; the README steers callers to the narrowest viable set. - π Sensitive by construction β the full
configurationobject and the credential-bearing outputs aresensitive. - π§© Single responsibility β one hook named
this; scale by composition, never by hidden authority over sibling hooks. - π« No provider concerns leak in β no
owner,token, orapp_authvariables; auth and org are the caller's provider block.
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.
404 Not Foundon create β the provider identity lacks repository admin rights, or therepositoryname is wrong/mistyped (must be the bare name, noorg/prefix). Confirmadmin: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. Keepsecretstable from a secret store; don't omit it after first apply. 403/ secondary rate limit on bulkfor_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 = trueon internet-facing endpoints. - Receiver rejects all payloads β signature mismatch. Ensure the receiver verifies
X-Hub-Signature-256with the samesecretand the matchingcontent_type. - Hook recreated unexpectedly β
repositorychanged. It is ForceNew; the old hook is destroyed and a newidissued. Verify the wired-in repo name didn't shift.
- keystone module β terraform-github-repository (emits the
namethis module consumes). - org-scope counterpart β terraform-github-organization-webhook.
- integrations/github provider β Resource: github_repository_webhook.
- GitHub REST reference β Webhook events and payloads (valid
eventsnames). - GitHub docs β Securing your webhooks (validating
X-Hub-Signature-256).