Skip to content

Repository files navigation

🐙 GitHub Dependabot Secrets Terraform Module

Aggregation composite that governs Dependabot secrets at both repository and organization scope — typed map(object) collections, write-only sensitive secret values, value_encrypted-first, secure private visibility default, and single-ownership allow-lists for selected-visibility org secrets. Built for integrations/github v6.x.

TerraformGitHub providermoduletyperesources


🧩 Overview

This module is an aggregation composite — there is no single dominant resource. It groups the three tightly-coupled Dependabot-secret resources behind one boundary so a caller can govern its entire Dependabot credential surface from one module call:

  • 🔐 Repository Dependabot secrets (github_dependabot_secret.repo) — per-repo secrets that Dependabot uses when updating dependencies from private registries.
  • 🏢 Organization Dependabot secrets (github_dependabot_organization_secret.org) — org-wide secrets with all / private / selectedvisibility control.
  • 🎯 Selected-repository allow-lists (github_dependabot_organization_secret_repositories.allow) — the repo allow-list backing a selected-visibility org secret, managed independently of the secret definition.
  • 🧬 Every collection is a typed map(object) keyed on a stable caller handle (never a list index), so adds/removes never churn unrelated secrets.
  • 🛡️ Secret-bearing inputs are sensitive = true and write-only — no plaintext ever lands in outputs, plan diffs, or state-readable attributes you would surface.

💡 Why it matters: Dependabot can only pull from private package feeds if its secrets exist and are scoped correctly. This module makes that credential surface declarative, least-exposure by default, and auditable — instead of click-ops in the org Security settings.


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

flowchart LR
repo["terraform-github-repository<br/>(keystone)"]
dep["terraform-github-dependabot<br/>(THIS module)"]
actions["terraform-github-actions-repository"]
env["terraform-github-repository-environment"]
orgset["terraform-github-organization-settings"]
repo -- "repository (repo name)" --> dep
repo -- "repo_id (numeric)" --> dep
orgset -. "Dependabot enabled / plan".-> dep
repo --> actions
repo --> env
style dep fill:#8957E5,color:#fff
style repo fill:#24292F,color:#fff
Loading

This module consumes repository names and numeric repo ids from the keystone terraform-github-repository, and emits maps of managed secret ids/names for audit and downstream wiring. See the Cross-Module Contract.


🧬 What this module builds

flowchart TD
rs["var.repo_secrets<br/>map(object)"]
os["var.org_secrets<br/>map(object)"]
osr["var.org_secret_repositories<br/>map(object)"]
rs --> repo["github_dependabot_secret.repo<br/>(primary collection · for_each)"]
os --> org["github_dependabot_organization_secret.org<br/>(for_each)"]
osr --> allow["github_dependabot_organization_secret_repositories.allow<br/>(for_each)"]
org -. "selected-visibility allow-list<br/>(own in ONE place)".-> allow
style repo fill:#8957E5,color:#fff
style org fill:#8957E5,color:#fff
style allow fill:#8957E5,color:#fff
Loading

Resource inventory

  • github_dependabot_secret.repo — the nominal primary collection; one repository-scoped Dependabot secret per repo_secrets entry.
  • github_dependabot_organization_secret.org — one organization-scoped Dependabot secret per org_secrets entry, with visibility and optional inline selected_repository_ids.
  • github_dependabot_organization_secret_repositories.allow — one selected-visibility allow-list per org_secret_repositories entry, for callers who prefer to own the allow-list separately from the secret.

ℹ️ This is an aggregation composite — by standard it has no this resource; every resource is named by its concise role (repo, org, allow).


✅ Provider / Versions

RequirementValue
Terraform>= 1.12.0
Providerintegrations/github~> 6.0 (current 6.12.1)

Schema notes that bite:

  • Provider source is integrations/githubnever the deprecated hashicorp/github.
  • github_dependabot_organization_secret and github_dependabot_organization_secret_repositories require a GitHub Team or Enterprise plan and an org-owner identity.
  • Secret values (value / value_encrypted) are write-only — the provider cannot read them back, so there is no remote drift detection on the value.

📁 Module Structure

terraform-github-dependabot/
├── providers.tf # terraform{} + required_providers (integrations/github ~> 6.0)
├── variables.tf # repo_secrets, org_secrets, org_secret_repositories (typed maps)
├── main.tf # 3 role-named for_each resources (repo, org, allow)
├── outputs.tf # id/name maps per collection (no secret values)
├── SCOPE.md # design contract: scope, consumes/emits, token scopes, prerequisites
└── README.md # this file

⚙️ Quick Start

module"dependabot" {
source="git::https://github.com/microsoftexpert/terraform-github-dependabot?ref=v1.0.0"repo_secrets={
npm_token = {
repository = module.repository.id # repo name from the keystone module
secret_name ="NPM_REGISTRY_TOKEN"
value_encrypted = var.npm_token_encrypted # prefer value_encrypted
}
}
}

⚠️ Always pin the source to a tag (?ref=v1.0.0) — never a branch.


🔌 Cross-Module Contract

Consumes

InputTypeSource
repo_secrets[].repositorystring (repo name)terraform-github-repositoryid output
org_secrets[].selected_repository_idslist(number)terraform-github-repositoryrepo_id output
org_secret_repositories[].selected_repository_idslist(number)terraform-github-repositoryrepo_id output

Emits

OutputDescriptionConsumed by
repo_secret_idsMap of repo-secret handle → github_dependabot_secret resource idAudit / downstream wiring
repo_secret_namesSet of repository Dependabot secret namesAudit
org_secret_idsMap of org-secret handle → github_dependabot_organization_secret resource idAudit / downstream wiring
org_secret_namesSet of organization Dependabot secret namesAudit
org_secret_repository_idsMap of allow-list handle → github_dependabot_organization_secret_repositories resource idAudit

🔒 No secret value is ever emitted — values are write-only inputs.


📚 Example Library

1️⃣ Minimal — one repository secret
module"dependabot" {
source="git::https://github.com/microsoftexpert/terraform-github-dependabot?ref=v1.0.0"repo_secrets={
npm = {
repository ="payments-api"
secret_name ="NPM_REGISTRY_TOKEN"
value_encrypted = var.npm_token_encrypted
}
}
}
2️⃣ Repository secret wired from terraform-github-repository
module"repository" {
source="git::https://github.com/microsoftexpert/terraform-github-repository?ref=v1.0.0"name="payments-api"visibility="private"
}
module"dependabot" {
source="git::https://github.com/microsoftexpert/terraform-github-dependabot?ref=v1.0.0"repo_secrets={
npm = {
repository = module.repository.id # repo name is the keystone id
secret_name ="NPM_REGISTRY_TOKEN"
value_encrypted = var.npm_token_encrypted
}
}
}

💡 The keystone's id output is the repository name — exactly what repository expects.

3️⃣ Plaintext value (provider encrypts on your behalf)
module"dependabot" {
source="git::https://github.com/microsoftexpert/terraform-github-dependabot?ref=v1.0.0"repo_secrets={
artifactory = {
repository ="payments-api"
secret_name ="ARTIFACTORY_TOKEN"
value = var.artifactory_token # provider encrypts with the repo public key
}
}
}

⚠️ Supply exactly one of value or value_encrypted per secret. Prefer value_encrypted (set key_id with it) so plaintext never enters state.

4️⃣ Organization secret — secure private visibility (default)
module"dependabot" {
source="git::https://github.com/microsoftexpert/terraform-github-dependabot?ref=v1.0.0"org_secrets={
shared_npm = {
secret_name ="ORG_NPM_TOKEN"# visibility defaults to "private" — all private/internal repos can read it
value_encrypted = var.org_npm_encrypted
}
}
}

🔒 visibility defaults to private — never to all. Opt into all explicitly.

5️⃣ Organization secret — selected visibility, inline allow-list
module"dependabot" {
source="git::https://github.com/microsoftexpert/terraform-github-dependabot?ref=v1.0.0"org_secrets={
shared_feed = {
secret_name ="ORG_PRIVATE_FEED"
visibility ="selected"
value_encrypted = var.feed_encrypted
selected_repository_ids = [module.payments.repo_id, module.ledger.repo_id]
}
}
}

💡 selected_repository_ids are numeric repo ids — wire them from terraform-github-repository's repo_id, not the repo name.

6️⃣ Organization secret — selected visibility, allow-list owned separately
module"dependabot" {
source="git::https://github.com/microsoftexpert/terraform-github-dependabot?ref=v1.0.0"org_secrets={
shared_feed = {
secret_name ="ORG_PRIVATE_FEED"
visibility ="selected"
value_encrypted = var.feed_encrypted
# NOTE: no selected_repository_ids here — owned below
}
}
org_secret_repositories={
shared_feed_allow = {
secret_name ="ORG_PRIVATE_FEED"
selected_repository_ids = [module.payments.repo_id, module.ledger.repo_id]
}
}
}

⚠️ Own each selected secret's allow-list in exactly one place — inline selected_repository_idsORorg_secret_repositories, never both for the same secret, or the two will fight on every apply.

7️⃣ Org secret visible to all repositories (incl. public)
module"dependabot" {
source="git::https://github.com/microsoftexpert/terraform-github-dependabot?ref=v1.0.0"org_secrets={
public_mirror = {
secret_name ="MIRROR_READONLY_TOKEN"
visibility ="all"# explicit opt-out of the private default
value_encrypted = var.mirror_encrypted
}
}
}

🔒 all exposes the secret to public repositories too. Use it only for genuinely low-sensitivity, read-only credentials.

8️⃣ Mixed scope — repository and organization secrets together
module"dependabot" {
source="git::https://github.com/microsoftexpert/terraform-github-dependabot?ref=v1.0.0"repo_secrets={
api_npm = {
repository ="payments-api"
secret_name ="NPM_REGISTRY_TOKEN"
value_encrypted = var.api_npm_encrypted
}
}
org_secrets={
shared_npm = {
secret_name ="ORG_NPM_TOKEN"
visibility ="private"
value_encrypted = var.org_npm_encrypted
}
}
}
9️⃣ Multiple repository secrets on the same repo
module"dependabot" {
source="git::https://github.com/microsoftexpert/terraform-github-dependabot?ref=v1.0.0"repo_secrets={
npm = { repository ="payments-api", secret_name ="NPM_REGISTRY_TOKEN", value_encrypted = var.npm_enc }
maven = { repository ="payments-api", secret_name ="MAVEN_TOKEN", value_encrypted = var.maven_enc }
nuget = { repository ="payments-api", secret_name ="NUGET_TOKEN", value_encrypted = var.nuget_enc }
}
}

💡 Map keys (npm, maven, nuget) are stable handles — renaming a handle replaces that secret, so keep them constant.

🔟 for_each at scale — repo secrets from a map(object)
locals {
# Built from your inventory of repo => feed credentials.dependabot_repo_secrets={
fork, vinvar.feed_credentials:k=> {
repository = v.repo
secret_name = v.name
value_encrypted = v.encrypted
}
}
}
module"dependabot" {
source="git::https://github.com/microsoftexpert/terraform-github-dependabot?ref=v1.0.0"repo_secrets=local.dependabot_repo_secrets
}

⚠️ Bulk for_each across many repos can hit GitHub secondary rate limits. See Troubleshooting.

1️⃣1️⃣ for_each at scale — org secrets with per-secret visibility
module"dependabot" {
source="git::https://github.com/microsoftexpert/terraform-github-dependabot?ref=v1.0.0"org_secrets={
fork, vinvar.org_feeds:k=> {
secret_name = v.name
visibility = v.visibility # "all" | "private" | "selected"
value_encrypted = v.encrypted
selected_repository_ids =try(v.repo_ids, []) # only used when visibility = "selected"
}
}
}
1️⃣2️⃣ 🔒 Secure / hardened variant — encrypted-only, private/selected, least exposure
module"dependabot" {
source="git::https://github.com/microsoftexpert/terraform-github-dependabot?ref=v1.0.0"# Repo secrets: value_encrypted only, never plaintext.repo_secrets={
api_feed = {
repository = module.payments.id
secret_name ="PRIVATE_FEED_TOKEN"
value_encrypted = var.api_feed_encrypted
}
}
# Org secrets: private by default; selected where the blast radius must be minimal.org_secrets={
shared_feed = {
secret_name ="ORG_PRIVATE_FEED"
visibility ="selected"# narrowest practical visibility
value_encrypted = var.shared_feed_encrypted
selected_repository_ids = [module.payments.repo_id]
}
}
}

🔒 Hardening checklist: value_encrypted over value; private/selected over all; allow-lists scoped to the minimum repo set; secret material sourced from a vault data source, never committed.

1️⃣3️⃣ 🏗️ End-to-end composition (mandatory) — full suite wired outputs → inputs
# 1) Keystone repositories.module"payments" {
source="git::https://github.com/microsoftexpert/terraform-github-repository?ref=v1.0.0"name="payments-api"visibility="private"
}
module"ledger" {
source="git::https://github.com/microsoftexpert/terraform-github-repository?ref=v1.0.0"name="ledger-core"visibility="private"
}
# 2) Dependabot secrets wired from the repositories' outputs.module"dependabot" {
source="git::https://github.com/microsoftexpert/terraform-github-dependabot?ref=v1.0.0"# repository name wired from keystone idrepo_secrets={
payments_npm = {
repository = module.payments.id
secret_name ="NPM_REGISTRY_TOKEN"
value_encrypted = var.payments_npm_encrypted
}
ledger_maven = {
repository = module.ledger.id
secret_name ="MAVEN_TOKEN"
value_encrypted = var.ledger_maven_encrypted
}
}
# org secret, selected visibility, allow-list wired from keystone repo_idorg_secrets={
shared_feed = {
secret_name ="ORG_PRIVATE_FEED"
visibility ="selected"
value_encrypted = var.shared_feed_encrypted
selected_repository_ids = [module.payments.repo_id, module.ledger.repo_id]
}
}
}
output"dependabot_org_secrets" {
value=module.dependabot.org_secret_names
}

💡 This is the canonical wiring: terraform-github-repository.idrepository, and terraform-github-repository.repo_idselected_repository_ids.


📥 Inputs

High-level:

  • repo_secrets(sensitive, default {}) — repository-scoped Dependabot secrets.
  • org_secrets(sensitive, default {}) — organization-scoped Dependabot secrets with visibility control.
  • org_secret_repositories(default {}) — standalone selected-visibility allow-lists.
Full object schemas
# repo_secrets — map keyed by a stable caller handlemap(object({
repository =string# repo name that owns the secret (ForceNew)
secret_name =string# Dependabot secret name (ForceNew)
value =optional(string) # plaintext value (provider encrypts) — sensitive
value_encrypted =optional(string) # pre-encrypted Base64 value (preferred) — sensitive
key_id =optional(string) # id of the public key used to encrypt value_encrypted; required with it
}))
# org_secrets — map keyed by a stable caller handlemap(object({
secret_name =string# org secret name (ForceNew)
visibility =optional(string, "private") # all | private | selected
value =optional(string) # plaintext value — sensitive
value_encrypted =optional(string) # pre-encrypted Base64 value (preferred) — sensitive
key_id =optional(string) # id of the public key used to encrypt value_encrypted; required with it
selected_repository_ids =optional(list(number), []) # numeric repo ids when visibility = "selected"
}))
# org_secret_repositories — map keyed by a stable caller handlemap(object({
secret_name =string# name of an existing org Dependabot secret (visibility = "selected")
selected_repository_ids =list(number) # numeric repo ids granted access
}))

Validation: each org_secrets[].visibility must be one of all, private, selected.


🧾 Outputs

OutputDescription
repo_secret_idsMap of repo-secret handle → github_dependabot_secret resource id
repo_secret_namesSet of repository Dependabot secret names managed by this module
org_secret_idsMap of org-secret handle → github_dependabot_organization_secret resource id
org_secret_namesSet of organization Dependabot secret names managed by this module
org_secret_repository_idsMap of allow-list handle → github_dependabot_organization_secret_repositories resource id

🔒 No output carries a secret value — values are write-only. Outputs expose ids and names only, safe for audit.


🧠 Architecture Notes

  • No id / node_id / slug scalar. Dependabot secret resources expose none of those — their id is a composite of repository:secret_name (repo scope) or the secret name (org scope). This is an aggregation composite with no keystone, so each collection emits a map of ids keyed like its input rather than a single scalar.
  • sensitive + for_each interaction.repo_secrets and org_secrets are sensitive = true, and Terraform refuses to use a sensitive value in for_each. The module iterates over the non-secret key set (nonsensitive(toset(keys(...)))) and indexes the maps inside each resource; non-secret structural fields (repository, secret_name, visibility) are revealed with nonsensitive so plan output and outputs stay useful, while secret values flow straight through to the schema-sensitive arguments.
  • ForceNew fields. Changing repository or secret_namereplaces the secret. Keep map keys (handles) stable — renaming a handle is also a replace.
  • Write-only secret values.value / value_encrypted cannot be read back from the API, so there is no remote drift detection on the value. If a secret is rotated outside Terraform, Terraform will not notice until the value input changes.
  • value_encrypted vs value. Supply exactly one. value_encrypted is a Base64 secret pre-encrypted with the target's LibSodium public key (pair it with key_id, the id of that public key), so cleartext never transits Terraform — preferred. value lets the provider encrypt for you but the cleartext passes through state/plan handling.
  • Authoritative allow-lists.github_dependabot_organization_secret_repositories is authoritative for the selected repo set of its secret — it overwrites, not appends. Combined with inline selected_repository_ids on the same secret it produces a permanent diff. Own each allow-list in one place.
  • Org resources are Team/Enterprise + org-owner.github_dependabot_organization_secret and ..._secret_repositories fail on Free plans or with a non-owner identity.
  • Eventual consistency / secondary rate limits. The GitHub REST API is eventually consistent and enforces secondary rate limits on bursty writes. Large for_each collections can intermittently 403/422; re-running the apply usually succeeds.
  • Rulesets vs branch protection — N/A here. This module governs secrets only; branch governance lives in terraform-github-repository-ruleset / terraform-github-branch-protection.

🧱 Design Principles

  • 🔒 Private by default — org secret visibility defaults to private; all and selected are explicit opt-ins.
  • 🔐 Encrypted-firstvalue_encrypted is the documented preference over value.
  • 🙈 No secret leakage — secret inputs are sensitive = true; no output emits a secret value.
  • 🎯 Least exposureselected visibility with minimal allow-lists is the recommended pattern for shared secrets.
  • 🗝️ Single ownership — each selected allow-list is owned in exactly one place to avoid apply churn.
  • 🧬 Stable keying — every collection is keyed on a caller handle, never a list index, so changes are surgical.
  • 🚫 No tags, no timeouts, no auth variables — GitHub has no tags; auth and owner are provider concerns.

🚀 Runbook

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

⚠️ Pin the module source to a tag (?ref=v1.0.0) — never a branch — so applies are reproducible.


🧪 Testing

The offline proof gate (no live org required):

terraform fmt -check # zero formatting differences
terraform validate # configuration is valid
tflint # core rules — no dedicated GitHub ruleset exists

✅ This module passes terraform validate and terraform fmt -check cleanly.


💬 Example Output

repo_secret_ids = {
"payments_npm" = "payments-api:NPM_REGISTRY_TOKEN"
"ledger_maven" = "ledger-core:MAVEN_TOKEN"
}
repo_secret_names = toset([
"MAVEN_TOKEN",
"NPM_REGISTRY_TOKEN",
])
org_secret_ids = {
"shared_feed" = "ORG_PRIVATE_FEED"
}
org_secret_names = toset([
"ORG_PRIVATE_FEED",
])
org_secret_repository_ids = {}

🔍 Troubleshooting

SymptomLikely causeResolution
403 Resource not accessible by integration on org secretsToken lacks admin:org (classic) or org Dependabot secrets: read/write (fine-grained); or identity is not an org ownerGrant org admin scope and use an org-owner identity
404 Not Found creating an org secretPlan is Free, or org Dependabot not enabledUpgrade to Team/Enterprise; enable Dependabot for the org
Permanent diff on a selected org secret's repo listAllow-list owned in two places (inline selected_repository_idsandorg_secret_repositories)Pick one owner; remove the other
Invalid for_each argument / sensitive value errorPassing a sensitive map directly into for_each in a callerThe module already handles this internally; do not re-wrap inputs in a way that strips/keeps sensitivity inconsistently
Intermittent 403/422 during bulk applyGitHub secondary rate limit on bursty writesRe-run terraform apply; reduce parallelism with -parallelism=N; stagger large rollouts
Secret "rotated" outside Terraform but plan shows no changeSecret values are write-only — no drift detection on valueChange the *_value input to force the update
selected_repository_ids rejectedPassing repo names instead of numeric idsWire numeric repo_id from terraform-github-repository, not the repo name

🔗 Related Docs

  • This module's design contract — SCOPE.md
  • Keystone repository module — terraform-github-repository
  • Sibling Actions secrets modules — terraform-github-actions-repository, terraform-github-actions-organization
  • integrations/github provider reference — github_dependabot_secret, github_dependabot_organization_secret, github_dependabot_organization_secret_repositories
  • GitHub Dependabot secrets — official GitHub Docs

Releases

Packages

Contributors

Languages