Skip to content

microsoftexpert/tf-mod-github-dependabot

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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.

Terraform GitHub provider module type resources


🧩 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 / selected visibility 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["tf-mod-github-repository<br/>(keystone)"]
 dep["tf-mod-github-dependabot<br/>(THIS module)"]
 actions["tf-mod-github-actions-repository"]
 env["tf-mod-github-repository-environment"]
 orgset["tf-mod-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 tf-mod-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

Requirement Value
Terraform >= 1.12.0
Provider integrations/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

tf-mod-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/tf-mod-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

Input Type Source
repo_secrets[].repository string (repo name) tf-mod-github-repositoryid output
org_secrets[].selected_repository_ids list(number) tf-mod-github-repositoryrepo_id output
org_secret_repositories[].selected_repository_ids list(number) tf-mod-github-repositoryrepo_id output

Emits

Output Description Consumed by
repo_secret_ids Map of repo-secret handle → github_dependabot_secret resource id Audit / downstream wiring
repo_secret_names Set of repository Dependabot secret names Audit
org_secret_ids Map of org-secret handle → github_dependabot_organization_secret resource id Audit / downstream wiring
org_secret_names Set of organization Dependabot secret names Audit
org_secret_repository_ids Map of allow-list handle → github_dependabot_organization_secret_repositories resource id Audit

🔒 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/tf-mod-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 tf-mod-github-repository
module "repository" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-github-repository?ref=v1.0.0"
  name       = "payments-api"
  visibility = "private"
}

module "dependabot" {
  source = "git::https://github.com/microsoftexpert/tf-mod-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/tf-mod-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/tf-mod-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/tf-mod-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 tf-mod-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/tf-mod-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_ids OR org_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/tf-mod-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/tf-mod-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/tf-mod-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 = {
    for k, v in var.feed_credentials : k => {
      repository      = v.repo
      secret_name     = v.name
      value_encrypted = v.encrypted
    }
  }
}

module "dependabot" {
  source = "git::https://github.com/microsoftexpert/tf-mod-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/tf-mod-github-dependabot?ref=v1.0.0"

  org_secrets = {
    for k, v in var.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/tf-mod-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/tf-mod-github-repository?ref=v1.0.0"
  name       = "payments-api"
  visibility = "private"
}

module "ledger" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-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/tf-mod-github-dependabot?ref=v1.0.0"

  # repository name wired from keystone id
  repo_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_id
  org_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: tf-mod-github-repository.idrepository, and tf-mod-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 handle
map(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 handle
map(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 handle
map(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

Output Description
repo_secret_ids Map of repo-secret handle → github_dependabot_secret resource id
repo_secret_names Set of repository Dependabot secret names managed by this module
org_secret_ids Map of org-secret handle → github_dependabot_organization_secret resource id
org_secret_names Set of organization Dependabot secret names managed by this module
org_secret_repository_ids Map 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_name replaces 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 tf-mod-github-repository-ruleset / tf-mod-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

Symptom Likely cause Resolution
403 Resource not accessible by integration on org secrets Token lacks admin:org (classic) or org Dependabot secrets: read/write (fine-grained); or identity is not an org owner Grant org admin scope and use an org-owner identity
404 Not Found creating an org secret Plan is Free, or org Dependabot not enabled Upgrade to Team/Enterprise; enable Dependabot for the org
Permanent diff on a selected org secret's repo list Allow-list owned in two places (inline selected_repository_ids and org_secret_repositories) Pick one owner; remove the other
Invalid for_each argument / sensitive value error Passing a sensitive map directly into for_each in a caller The module already handles this internally; do not re-wrap inputs in a way that strips/keeps sensitivity inconsistently
Intermittent 403/422 during bulk apply GitHub secondary rate limit on bursty writes Re-run terraform apply; reduce parallelism with -parallelism=N; stagger large rollouts
Secret "rotated" outside Terraform but plan shows no change Secret values are write-only — no drift detection on value Change the *_value input to force the update
selected_repository_ids rejected Passing repo names instead of numeric ids Wire numeric repo_id from tf-mod-github-repository, not the repo name

🔗 Related Docs

  • This module's design contract — SCOPE.md
  • Keystone repository module — tf-mod-github-repository
  • Sibling Actions secrets modules — tf-mod-github-actions-repository, tf-mod-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

Packages

 
 
 

Contributors

Languages