Aggregation composite that governs Dependabot secrets at both repository and organization scope — typed
map(object)collections, write-onlysensitivesecret values,value_encrypted-first, secureprivatevisibility default, and single-ownership allow-lists forselected-visibility org secrets. Built for integrations/github v6.x.
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 withall/private/selectedvisibility control. - 🎯 Selected-repository allow-lists (
github_dependabot_organization_secret_repositories.allow) — the repo allow-list backing aselected-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 = trueand 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.
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!
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
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.
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
Resource inventory
github_dependabot_secret.repo— the nominal primary collection; one repository-scoped Dependabot secret perrepo_secretsentry.github_dependabot_organization_secret.org— one organization-scoped Dependabot secret perorg_secretsentry, withvisibilityand optional inlineselected_repository_ids.github_dependabot_organization_secret_repositories.allow— oneselected-visibility allow-list perorg_secret_repositoriesentry, for callers who prefer to own the allow-list separately from the secret.
ℹ️ This is an aggregation composite — by standard it has no
thisresource; every resource is named by its concise role (repo,org,allow).
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
| Provider | integrations/github ~> 6.0 (current 6.12.1) |
Schema notes that bite:
- Provider source is
integrations/github— never the deprecatedhashicorp/github. github_dependabot_organization_secretandgithub_dependabot_organization_secret_repositoriesrequire 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.
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
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.
| Input | Type | Source |
|---|---|---|
repo_secrets[].repository |
string (repo name) |
tf-mod-github-repository → id output |
org_secrets[].selected_repository_ids |
list(number) |
tf-mod-github-repository → repo_id output |
org_secret_repositories[].selected_repository_ids |
list(number) |
tf-mod-github-repository → repo_id output |
| 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.
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
idoutput is the repository name — exactly whatrepositoryexpects.
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 ofvalueorvalue_encryptedper secret. Prefervalue_encrypted(setkey_idwith 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
}
}
}🔒
visibilitydefaults toprivate— never toall. Opt intoallexplicitly.
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_idsare numeric repo ids — wire them fromtf-mod-github-repository'srepo_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 eachselectedsecret's allow-list in exactly one place — inlineselected_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/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
}
}
}🔒
allexposes 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
}
⚠️ Bulkfor_eachacross 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_encryptedovervalue;private/selectedoverall; 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.id→repository, andtf-mod-github-repository.repo_id→selected_repository_ids.
High-level:
repo_secrets(sensitive, default{}) — repository-scoped Dependabot secrets.org_secrets(sensitive, default{}) — organization-scoped Dependabot secrets withvisibilitycontrol.org_secret_repositories(default{}) — standaloneselected-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.
| 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.
- No
id/node_id/slugscalar. Dependabot secret resources expose none of those — theiridis a composite ofrepository: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_eachinteraction.repo_secretsandorg_secretsaresensitive = true, and Terraform refuses to use a sensitive value infor_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 withnonsensitiveso plan output and outputs stay useful, while secret values flow straight through to the schema-sensitive arguments.- ForceNew fields. Changing
repositoryorsecret_namereplaces the secret. Keep map keys (handles) stable — renaming a handle is also a replace. - Write-only secret values.
value/value_encryptedcannot 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_encryptedvsvalue. Supply exactly one.value_encryptedis a Base64 secret pre-encrypted with the target's LibSodium public key (pair it withkey_id, the id of that public key), so cleartext never transits Terraform — preferred.valuelets the provider encrypt for you but the cleartext passes through state/plan handling.- Authoritative allow-lists.
github_dependabot_organization_secret_repositoriesis authoritative for theselectedrepo set of its secret — it overwrites, not appends. Combined with inlineselected_repository_idson 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_secretand..._secret_repositoriesfail 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_eachcollections 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.
- 🔒 Private by default — org secret
visibilitydefaults toprivate;allandselectedare explicit opt-ins. - 🔐 Encrypted-first —
value_encryptedis the documented preference overvalue. - 🙈 No secret leakage — secret inputs are
sensitive = true; no output emits a secret value. - 🎯 Least exposure —
selectedvisibility with minimal allow-lists is the recommended pattern for shared secrets. - 🗝️ Single ownership — each
selectedallow-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
ownerare provider concerns.
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.
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 validateandterraform fmt -checkcleanly.
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 = {}
| 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 |
- 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/githubprovider reference —github_dependabot_secret,github_dependabot_organization_secret,github_dependabot_organization_secret_repositories- GitHub Dependabot secrets — official GitHub Docs