Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

☁️ Azure Trusted Signing Account Terraform Module

Manages an Azure Trusted Signing account β€” code signing with the signing keys held in Microsoft's HSMs rather than in a certificate file you have to protect (azurerm_trusted_signing_account). Targets hashicorp/azurerm ~> 4.0.

Terraform Provider Module Type Resources Caveat

🧩 Overview

  • ✍️ A code-signing account, with the signing keys held in Microsoft's HSMs β€” so there is no private key for you to store, protect or leak.
  • πŸ”΄ An account alone signs nothing. Signing needs a certificate profile, and the provider models no such resource β€” that step happens outside Terraform.
  • πŸ”΄ And the account_uri grants nothing. A build agent also needs an Azure role assignment on this account, which no plan here can check.
  • ℹ️ The ARM type is Microsoft.CodeSigning/codeSigningAccounts β€” the Resource ID never says "trusted signing".
  • βœ… sku_name updates in place, which is unusual for a tier argument in this provider.
  • βœ… Ordinary Azure resource β€” real map(string) tags.

πŸ’‘ Why it matters: This module gets you a correct account and cannot get you a working signing pipeline. The two missing pieces β€” a certificate profile and a role assignment β€” are both invisible to a plan, so both are emitted or documented rather than left to fail at signing time.

❀️ Support this project

If this module saves you time, please consider supporting its continued development:


πŸ—ΊοΈ Where this fits in the family

flowchart TB
  rg["terraform-azurerm-resource-group"]
  me["terraform-azurerm-trusted-signing-account"]
  ra["terraform-azurerm-role-assignments, which is how a build agent is actually granted the right to sign"]
  pipeline["a CI pipeline signing artifacts through the account_uri"]
  profile["a CERTIFICATE PROFILE, which the provider does not model at all - the account alone signs nothing, and the profile is created outside Terraform"]
  hsm["and the signing keys live in MICROSOFT's HSMs, not yours. That is the security benefit - no private key for you to leak - and it also means the key is never exportable, so this is not a certificate authority you control."]

  rg -->|"name, location"| me
  me -->|"account_uri"| pipeline
  ra -->|"the grant that lets the agent sign"| pipeline
  me -->|"incomplete without"| profile
  me -->|"key custody"| hsm

  classDef mine fill:#0078D4,stroke:#004578,color:#fff;
  classDef keystone fill:#004578,stroke:#001f3f,color:#fff;
  classDef sib fill:#eef2f7,stroke:#b8c4d0,color:#1b1b1b;
  class me mine;
  class profile keystone;
  class rg,ra,pipeline,hsm sib;
Loading

ℹ️ This resource has no siblings β€” azurerm_trusted_signing_account is the only resource of its kind in the provider, so the diagram above shows the modules it actually composes with rather than an invented family.

🧬 What this module builds

flowchart TB
  inputs["name, resource_group_name, location - all force-new"]
  sku["sku_name, REQUIRED, either Basic or Premium - and it UPDATES IN PLACE, which makes it the only editable field besides tags"]
  arm["the ARM type is Microsoft.CodeSigning/codeSigningAccounts, so the Resource ID does not say trustedSigning anywhere - worth knowing before you write an import command or a role-assignment scope"]
  incomplete["AN ACCOUNT ON ITS OWN SIGNS NOTHING. Signing needs a CERTIFICATE PROFILE, and the provider models no such resource - so this module gets you the account and the last step happens outside Terraform."]
  uri["account_uri is the endpoint a build agent signs through, and it is the one output a pipeline actually consumes"]
  grant["but the URI grants nothing. A build agent also needs an Azure RBAC role assignment on this account, which is a sibling module's job and is invisible to any plan here."]
  hsm["and the signing key itself lives in Microsoft's HSMs rather than yours: that is the security benefit, and it also means there is no private key for this module to accept, emit or leak"]
  this["azurerm_trusted_signing_account.this"]
  tags["tags, a real map(string) - an ordinary Azure resource"]
  outputs["id, name, account_uri, sku_name, location, is_premium_sku"]

  inputs -->|"identity"| this
  sku -->|"tier"| this
  arm -->|"naming"| this
  this -->|"caveat"| incomplete
  this -->|"exports"| uri
  uri -->|"insufficient alone"| grant
  hsm -->|"key custody"| this
  tags -->|"universal tail"| this
  this -->|"exports"| outputs

  classDef mine fill:#0078D4,stroke:#004578,color:#fff;
  classDef keystone fill:#004578,stroke:#001f3f,color:#fff;
  classDef sib fill:#eef2f7,stroke:#b8c4d0,color:#1b1b1b;
  class this mine;
  class incomplete keystone;
  class inputs,sku,arm,uri,grant,hsm,tags,outputs sib;
Loading

Resource inventory

Resource Count Notes
azurerm_trusted_signing_account.this 1 The keystone. Only sku_name and tags update in place.
timeouts block 0..1 All four operations exist.

βœ… Provider / Versions

Requirement Value
Terraform >= 1.12.0
hashicorp/azurerm ~> 4.0
Azure resource provider Microsoft.CodeSigning/codeSigningAccounts
Provider block None in this module. The caller configures provider "azurerm", including the mandatory features {} block, and supplies authentication.

Schema notes that bite β€” confirmed against the live provider schema and its documentation:

  • πŸ”΄ The provider models no certificate profile resource, so a complete signing setup is not achievable in Terraform alone.
  • ℹ️ The ARM type is Microsoft.CodeSigning/codeSigningAccounts. Nothing in the Resource ID says "trustedSigning", which surprises people writing an import command or a role-assignment scope.
  • βœ… sku_name updates in place β€” unusual; most SKU arguments in this provider force replacement.
  • name, resource_group_name and location are force-new.
  • ⚠️ Region availability is limited, and the provider cannot tell you where β€” it sends whatever region it is given, and a region without the service fails at apply with an error about the resource provider rather than about the region.
  • πŸ”΄ The provider's read function has NO not-found handling. Every other resource in this library checks for a 404 and marks the resource gone, so one deleted outside Terraform simply reappears in the plan as something to create. This one returns the retrieval error instead, so terraform plan fails outright β€” for every configuration sharing that state file, not just this module β€” until someone runs terraform state rm by hand.
  • ⚠️ The provider talks to Microsoft.CodeSigning at API version 2024-09-30-preview. A preview API can change shape or be withdrawn between provider releases without a breaking-change notice on the resource, so a provider upgrade deserves more care here than on a stable-API resource.
  • ⚠️ name is validated more strictly than it looks: 3–24 characters, must begin with a letter, must end with a letter or digit, no consecutive hyphens, and no underscores or periods at all. It is also the first label of the account's signing endpoint, so a rename replaces the account and changes the URI every pipeline is configured with.
  • lifecycle is not valid inside a module block, so a caller cannot add prevent_destroy.

πŸ”‘ Required Azure RBAC Roles / Permissions

Operation Role Scope
Creating, updating or deleting the account Contributor the resource group
Reading it Reader the resource group
Signing code through the account a Trusted Signing signing role, assigned to the build agent's identity this account β€” a separate grant

πŸ”΄ That third row is a grant this module does not make, and its absence is invisible to a plan. The account applies cleanly and signing fails later. Wire it with terraform-azurerm-role-assignments scoped to this module's id (example 4).

Azure Prerequisites

  • The resource group exists, in a region where Trusted Signing is offered.
  • πŸ”΄ A certificate profile, created outside Terraform (example 3). Without one the account cannot sign.
  • An identity for the build agent β€” preferably workload-identity federation to the CI system, so there is no standing secret β€” plus the signing role assignment on this account.
  • Publisher identity validation. Trusted Signing has an eligibility and validation process that is Microsoft's, not a Terraform step (example 8).

πŸ“ Module Structure

terraform-azurerm-trusted-signing-account/
β”œβ”€β”€ providers.tf   # required_version + the pinned azurerm provider. No provider block.
β”œβ”€β”€ variables.tf   # name, resource_group_name, location, sku_name, tags, timeouts
β”œβ”€β”€ main.tf        # a single resource
β”œβ”€β”€ outputs.tf     # id first, then the resource's own fields, then the derived review flags
β”œβ”€β”€ README.md      # this document
β”œβ”€β”€ SCOPE.md       # the cross-module contract
β”œβ”€β”€ LICENSE        # MIT
└── .gitignore

βš™οΈ Quick Start

provider "azurerm" {
  features {}
}

module "signing" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-trusted-signing-account.git?ref=v1.0.0"

  name                = "sign-contoso-prod"
  resource_group_name = module.rg.name
  location            = module.rg.location

  # Basic or Premium. βœ… Updates in place, so starting small is cheap. See example 5.
  sku_name = "Basic"

  tags = { owner = "release-engineering" }
}

ℹ️ The caller configures the provider, its authentication, and the mandatory features {} block. This module declares none of them.

⚠️ Read example 3 before expecting to sign anything β€” this module gets you an account, and an account alone signs nothing.

πŸ”Œ Cross-Module Contract

Consumes

Input Type Source module
name string caller β€” force-new
resource_group_name / location string terraform-azurerm-resource-group
sku_name string caller β€” required, Basic or Premium, updates in place
tags map(string) caller
timeouts object(...) caller β€” all four operations exist

Emits

Output Description Consumed by
id The Resource ID. ℹ️ Path says Microsoft.CodeSigning. role assignments, imports
name / resource_group_name / location Identity. Force-new. review
sku_name / is_premium_sku The tier. Updates in place. review
account_uri The endpoint a pipeline signs through. the CI pipeline
requires_certificate_profile Always true. completeness review
signing_keys_are_microsoft_held Always true. review

πŸ“š Example Library

1 Β· What Trusted Signing changes about code signing
TRADITIONAL code signing:   you hold a certificate and its PRIVATE KEY
                            -> you must protect it, rotate it, and survive its theft
                            -> a stolen signing key is a supply-chain incident

TRUSTED SIGNING:            Microsoft holds the key in an HSM
                            -> there is no private key for you to store or leak
                            -> access is an Azure role assignment, revocable instantly

βœ… The security argument is straightforward and real: the most damaging thing about a code-signing certificate is that it exists as a file somebody can steal. Removing the file removes that risk, and replaces "protect this key forever" with "manage who can call this endpoint", which is a problem Azure is good at.

⚠️ And the trade-off is equally real: the key is not yours. It is not exportable, not portable to another provider, and not usable outside this service. If your signing story has to survive leaving Azure, that is a decision to make now rather than later.

ℹ️ This module emits both facts as constants, because they cut in opposite directions and both matter:

output "keys" { value = module.signing.signing_keys_are_microsoft_held } # always true

πŸ’‘ Nothing in this module accepts, emits or stores a private key β€” not because of a design choice here, but because there is no such value in the service.

2 Β· ℹ️ The ARM type does not say "trusted signing"
/subscriptions/.../resourceGroups/rg-release/providers/
    Microsoft.CodeSigning/codeSigningAccounts/sign-contoso-prod
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    not "TrustedSigning" anywhere

ℹ️ The service is called Trusted Signing; the ARM resource provider is Microsoft.CodeSigning. The Terraform resource name follows the product, and the Resource ID follows the API β€” so they disagree, and both are correct.

⚠️ Where that bites: writing an import command by hand (example 9), scoping a role assignment, searching Azure Resource Graph, or reading an activity log and looking for the wrong provider name.

βœ… So take the ID from the module rather than constructing it:

module "signing_rbac" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-role-assignments.git?ref=v1.0.0"
  scope  = module.signing.id # βœ… not a hand-written path
  # ...
}

πŸ’‘ This is a small thing that costs real time when you hit it, which is why the module's id output says so in its own description rather than leaving it to be discovered.

3 Β· πŸ”΄ An account alone signs nothing
WHAT YOU NEED TO SIGN                      MANAGED BY
------------------------------------------------------------------
a Trusted Signing account                  βœ… this module
a CERTIFICATE PROFILE on that account      ❌ not modelled by the provider
an identity for the build agent            βœ… a sibling module
a signing role assignment on the account   βœ… a sibling module (example 4)
publisher identity validation              ❌ Microsoft's process (example 8)

πŸ”΄ The certificate profile is the gap that matters most, because it is the one people do not expect. The provider offers no resource for it, so it is created through the portal, the CLI or the API β€” and without it the account exists, the plan is clean, and signing fails.

⚠️ And it fails at signing time, which is to say in a release pipeline, probably out of hours, probably to somebody who did not build the infrastructure.

βœ… So the module states its own incompleteness as an output, which is unusual and earns its place here:

output "incomplete" { value = module.signing.requires_certificate_profile } # always true

πŸ’‘ A constant true is a weak signal in general. It is worth having in this case because nothing else in Terraform state hints that the resource you just created cannot do the job on its own, and the alternative is a comment in a file nobody reads.

ℹ️ Practical advice: create the profile once, by hand, and record the how in your release documentation β€” then treat this module as managing the account's lifecycle and access, which is what it can genuinely own.

4 Β· πŸ”΄ The `account_uri` grants nothing
# The endpoint a build agent signs through.
output "uri" { value = module.signing.account_uri }

πŸ”΄ Holding that URI confers no ability to sign. The build agent's identity needs a Trusted Signing role assignment on the account itself β€” a separate grant, on another module's terms, invisible to any plan here.

module "signing_rbac" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-role-assignments.git?ref=v1.0.0"

  scope = module.signing.id

  role_assignments = {
    ci_signer = {
      role_definition_name = "Trusted Signing Certificate Profile Signer"
      principal_id         = var.ci_identity_object_id
    }
  }
}

⚠️ Check the current role name against Microsoft's documentation rather than trusting the string above indefinitely. Role names for newer services do change, and this library would rather point you at the source of truth than bake in a value that quietly goes stale.

βœ… Prefer workload-identity federation for the CI identity β€” GitHub Actions or Azure DevOps authenticating to Azure with no standing secret. A code-signing pipeline is precisely where you do not want a long-lived client secret sitting in a variable group.

πŸ’‘ And keep the signing grant narrow. Anyone who can sign through this account can produce artifacts your users will trust. That is a small group, and it is not the same group as "people who can deploy".

5 Β· `sku_name` β€” and a pleasant surprise
sku_name = "Basic"   # start here
sku_name = "Premium" # move up when you need to

βœ… It updates in place, which is genuinely unusual: most SKU arguments in this provider force replacement, so a tier change means rebuilding the resource. Here it is an ordinary apply.

πŸ’‘ Which makes starting on Basic the sensible default. There is no penalty for being wrong, and no migration to plan.

ℹ️ The tiers differ in quota and in how many certificate profiles an account may hold. This module deliberately does not restate Microsoft's current limits β€” they change, and a stale number in a module description is worse than a pointer to the source of truth.

Error: Invalid value for variable

  sku_name must be Basic or Premium.

βœ… Validated against the closed set, so a typo or an invented tier is caught at plan.

6 Β· Several accounts, and when that is the right shape
locals {
  signing_accounts = {
    prod = { sku = "Premium", location = "eastus" }
    test = { sku = "Basic", location = "eastus" }
  }
}

module "signing" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-trusted-signing-account.git?ref=v1.0.0"
  for_each = local.signing_accounts

  name                = "sign-contoso-${each.key}"
  resource_group_name = module.rg.name
  location            = each.value.location
  sku_name            = each.value.sku

  tags = { environment = each.key }
}

βœ… Separating production signing from test signing is worth the second account. A test pipeline that can sign with the production account can produce artifacts your users trust, which is exactly the outcome code signing exists to prevent.

πŸ’‘ And the separation is enforced by RBAC, not by the accounts themselves β€” so the real work is giving each pipeline a role assignment on only its own account (example 4).

⚠️ Do not multiply accounts beyond that. Each one needs its own certificate profile created by hand (example 3), so accounts are more expensive to operate than they look in Terraform.

ℹ️ for_each lives in the caller; the module manages one account.

7 Β· What a review should assert
output "signing_review" {
  value = {
    id         = module.signing.id           # ℹ️ says Microsoft.CodeSigning
    uri        = module.signing.account_uri  # grants nothing on its own
    sku        = module.signing.sku_name
    premium    = module.signing.is_premium_sku
    incomplete = module.signing.requires_certificate_profile # always true
    ms_keys    = module.signing.signing_keys_are_microsoft_held # always true
  }
}

πŸ”΄ incomplete is the line worth reading before a release. It is a constant, and it says the thing Terraform state otherwise cannot: this account needs a certificate profile that no plan created (example 3).

⚠️ uri invites the wrong conclusion, which is why its own description says so. An endpoint in an output looks like a capability; the capability is a role assignment (example 4).

πŸ’‘ What no output can tell you is who can actually sign. That is a role-assignment question, on a different module's scope, and it is the most security-relevant fact about this account.

8 · ⚠️ The parts that are not infrastructure at all
Trusted Signing also requires, outside Terraform entirely:
  - an eligible subscription and publisher
  - IDENTITY VALIDATION of the publishing organisation, by Microsoft
  - agreement to the service's terms
  - a certificate profile bound to that validated identity (example 3)

⚠️ Identity validation is a process with a lead time, not a setting. Microsoft verifies the publishing organisation before it will issue certificates on your behalf, and no amount of Terraform makes that faster.

πŸ’‘ So plan the account creation and the validation in parallel, not in sequence. Creating the account is instant; being allowed to sign is not.

ℹ️ This module deliberately does not describe the current requirements or timelines. They are Microsoft's, they change, and a module README repeating them would go stale in a way that is worse than useless β€” it would look authoritative. The service's own documentation is the source of truth.

βœ… What this module can honestly offer is the account, its tier, its tags, and a id to scope the signing grant to. Everything above is somebody's project-management problem, and it is better named than omitted.

9 Β· Importing, and what a destroy removes
terraform import 'module.signing.azurerm_trusted_signing_account.this' \
  "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-release/providers/Microsoft.CodeSigning/codeSigningAccounts/sign-contoso-prod"

ℹ️ Note Microsoft.CodeSigning/codeSigningAccounts in that path (example 2). Constructing it from the product name is the most likely way to get an import wrong here.

βœ… Importing is comfortable otherwise β€” only name, resource_group_name and location are force-new, and all three appear in the Resource ID itself. A sku_name mismatch proposes an in-place change.

βœ… Importing is also the likely path in practice, since the certificate profile has probably already been created by hand against an existing account (example 3).

What a destroy removes

terraform destroy on this module:
  removes the account  ->  and its certificate profiles with it
                       ->  the signing pipeline stops
                       ->  ALREADY-SIGNED artifacts remain valid and trusted

βœ… Existing signatures survive, which is the important reassurance: signatures are verified against a certificate chain, not against the account that produced them. Deleting the account does not invalidate what you shipped.

⚠️ But the certificate profile goes with the account, and recreating it means going through profile creation again β€” and possibly re-validation. This is not a resource to destroy casually while a release is pending.

πŸ’‘ To stop managing the account without deleting it, use terraform state rm. A CanNotDelete lock is the available protection, since a caller cannot use prevent_destroy.

10 Β· Tags β€” and what is worth recording
tags = {
  owner       = "release-engineering"
  environment = "prod"
  profile     = "contoso-public-trust" # the profile created by hand (example 3)
}

βœ… Real Azure resource tags β€” a map(string), updating in place.

πŸ’‘ A profile tag is a genuinely useful convention here. The certificate profile is not in Terraform state (example 3), so nothing in your configuration records which profile this account carries β€” and a tag is the cheapest place to put that link where somebody debugging a release will find it.

ℹ️ An owner tag matters more than usual for a signing account. "Who can sign?" is a question with a security answer (example 4), and "who owns this account?" is the question that gets asked first.

⚠️ Tags are metadata. A profile tag does not create a profile, and none of this makes the account capable of signing on its own.

11 Β· The offline proof gate
terraform init -backend=false
terraform validate
terraform fmt -check

βœ… What that proves: sku_name is Basic or Premium, and the three force-new identity fields are non-empty. This is a small resource and the gate is correspondingly small.

πŸ’‘ Proved by evaluating the condition in terraform console inside the module, which does fire root-module variable validations β€” unlike terraform validate on a calling configuration. A sku_name of Standard β€” a plausible guess, and not a real tier β€” is rejected.

⚠️ What it cannot prove is everything that actually matters here: whether a certificate profile exists, whether the region offers the service, whether the publisher has been validated, and whether any identity is permitted to sign.

ℹ️ That gap is why this module emits requires_certificate_profile rather than relying on documentation alone (example 3). No cloud apply happens in this flow β€” plan-only until a human applies from CI.

12 Β· πŸ—οΈ End-to-end composition
provider "azurerm" {
  features {}
}

module "rg" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"

  name     = "rg-release-prod"
  location = "eastus" # ⚠️ Trusted Signing is not offered everywhere
}

# ── Production and test signing kept apart (example 6) ──────────────────
locals {
  accounts = {
    prod = "Premium"
    test = "Basic"
  }
}

module "signing" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-trusted-signing-account.git?ref=v1.0.0"
  for_each = local.accounts

  name                = "sign-contoso-${each.key}"
  resource_group_name = module.rg.name
  location            = module.rg.location
  sku_name            = each.value # updates in place, so this is cheap (example 5)

  tags = {
    environment = each.key
    owner       = "release-engineering"
    # The profile is NOT in Terraform state (example 3) β€” record the link here.
    profile = "contoso-${each.key}-profile"
  }
}

# ── The grant that actually lets a pipeline sign (example 4) ──────────────
# Each pipeline gets a role on ONLY its own account.
module "signing_rbac" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-role-assignments.git?ref=v1.0.0"
  for_each = local.accounts

  scope = module.signing[each.key].id # βœ… from the module, not hand-built (example 2)

  role_assignments = {
    ci_signer = {
      # ⚠️ Confirm the current role name against Microsoft's documentation.
      role_definition_name = "Trusted Signing Certificate Profile Signer"
      principal_id         = var.ci_identity_object_ids[each.key]
    }
  }
}

resource "azurerm_management_lock" "signing_prod" {
  name       = "signing-no-delete"
  scope      = module.signing["prod"].id
  lock_level = "CanNotDelete"
  notes      = "Deleting this account removes its certificate profile too"
}

output "signing_posture" {
  value = {
    uris = { for k, m in module.signing : k => m.account_uri }
    # Always true, for every account. Read it before trusting the pipeline (example 3).
    needs_profile = [for k, m in module.signing : k if m.requires_certificate_profile]
  }
}

πŸ”’ What the composition gets right: production and test signing on separate accounts with separate role assignments, so a test pipeline cannot sign production artifacts; the scope taken from the module's id rather than a hand-written Microsoft.CodeSigning path; a profile tag recording the one dependency that lives outside Terraform; a CanNotDelete lock on production; and a note to re-check the role name rather than trusting a string indefinitely.

⚠️ What no plan will tell you: whether a certificate profile exists on either account, whether the publisher has passed identity validation, or whether the role name above is still current.

πŸ’‘ needs_profile lists every account, always, by construction β€” which is the point. It is there for whoever assumes a clean apply means a working signing pipeline.

πŸ“₯ Inputs

Input Type Default Notes
name string β€” Required. Force-new.
resource_group_name / location string β€” Required. Force-new. ⚠️ Limited regions.
sku_name string β€” Required. Basic or Premium, validated. βœ… Updates in place.
tags map(string) {} Real Azure tags.
timeouts object(...) null All four operations exist.
Full schemas
variable "sku_name" {
  type = string
  # The tiers differ in quota and certificate-profile capacity. This module does NOT restate Microsoft's
  # current limits β€” they change, and a stale number in a description is worse than a pointer to the
  # source of truth. See example 5.
  validation {
    condition     = contains(["Basic", "Premium"], var.sku_name)
    error_message = "sku_name must be Basic or Premium."
  }
}

🧾 Outputs

Output Description Sensitive
id The Resource ID. ℹ️ Path says Microsoft.CodeSigning. no
name / resource_group_name / location Identity. Force-new. no
sku_name / is_premium_sku The tier. no
account_uri The signing endpoint. ⚠️ Grants nothing. no
tags The account's tags. ℹ️ The only metadata this resource has. no
only_sku_and_tags_update_in_place Always true. no
requires_certificate_profile Always true. no
signing_keys_are_microsoft_held Always true. no
refresh_fails_if_deleted_outside_terraform πŸ”΄ Always true. Refresh ERRORS rather than planning a re-create. no
uses_a_preview_api_version Always true. 2024-09-30-preview. no

πŸ”’ No secret is accepted or emitted β€” and, unusually, there is no private key anywhere to emit.

🧠 Architecture Notes

  • The module's own incompleteness is emitted as an output. requires_certificate_profile is a constant true, and it earns its place because nothing else in Terraform state hints that this resource cannot do its job alone. The failure it guards against surfaces in a release pipeline, out of hours, to somebody who did not build the infrastructure β€” which is the worst possible place to discover a missing prerequisite.

  • account_uri's description states that the URI grants nothing. An endpoint in an output reads as a capability; the capability is a role assignment on another module's scope. Saying so at the point of use is cheaper than expecting the reader to have reached the RBAC table.

  • signing_keys_are_microsoft_held is emitted because it cuts both ways. There is no private key to leak, which is the security benefit and the reason to choose the service β€” and there is also no key you can export or migrate, which is a real constraint on anyone who might leave Azure. A one-sided presentation of that would be marketing.

  • The ARM-type mismatch is documented in the id output itself. The service is Trusted Signing and the provider is Microsoft.CodeSigning; both are correct, they disagree, and the cost of not knowing is a hand-written import path or role scope that does not resolve.

  • Microsoft's quota limits, timelines and role names are deliberately not restated. They change, and a module README repeating them goes stale in a way that is worse than useless because it looks authoritative. The examples point at the source of truth and say to check it β€” including for the role name in the RBAC example, which this library would rather flag as checkable than bake in.

  • The non-infrastructure prerequisites get their own example. Publisher identity validation has a lead time and is not a setting; naming it lets someone plan around it instead of discovering it after the account is built.

🧱 Design Principles

Concern Secure default (empty call) Opt-out (caller must type it)
Silent incompleteness requires_certificate_profile emitted as a constant β€”
Endpoints mistaken for access account_uri documented as granting nothing; the separate grant shown β€”
Over-broad signing rights one account per environment, with a role assignment on each share one account, knowingly
Standing CI secrets workload-identity federation recommended for the build identity use a client secret, knowingly
Stolen signing keys structurally prevented β€” there is no private key to hold β€”
Vendor lock-in hidden signing_keys_are_microsoft_held emitted, with the downside stated β€”
Stale documentation Microsoft's quotas, timelines and role names pointed at rather than restated β€”
Accidental profile loss the destroy semantics documented; a CanNotDelete lock recommended destroy anyway, knowingly
  • Before expecting to sign: create a certificate profile. No plan creates one.
  • Before trusting the pipeline: the build identity needs a role assignment on this account.
  • Before planning a release: publisher identity validation has a lead time, not a setting.
  • Before sharing an account: a test pipeline that can sign production artifacts defeats the purpose.
  • Before choosing the service: the signing key is not exportable. That is the benefit and the constraint.

πŸš€ Runbook

terraform init -backend=false
terraform validate
terraform fmt -check
  • Pin the source to a tag β€” ?ref=v1.0.0 β€” never a branch.
  • Plan-only from here. A human applies from CI.
  • βœ… sku_name and tags update in place; the three identity fields are force-new.
  • πŸ”΄ A destroy takes the account's certificate profiles with it. Already-signed artifacts stay valid.
  • ⚠️ Use the module's id for role scopes and imports β€” the path says Microsoft.CodeSigning.
  • ℹ️ Add a CanNotDelete lock; a caller cannot use prevent_destroy.
  • ℹ️ Re-check the signing role name against Microsoft's documentation before relying on it.

πŸ§ͺ Testing

terraform validate and terraform fmt -check are the offline gate. They confirm:

  • sku_name is Basic or Premium;
  • name, resource_group_name and location are non-empty;
  • no output is sensitive, because nothing sensitive is accepted;
  • the module declares no provider block.

πŸ’‘ This was proved by evaluating the condition in terraform console inside the module β€” which does fire root-module variable validations, unlike terraform validate on a calling configuration. A sku_name of Standard β€” a plausible guess, and not a real tier β€” is rejected.

What only plan and apply exercise:

  • whether the resource group exists and the region offers the service;
  • whether the identity holds the roles in the table above.

What no Terraform command checks at any stage:

  • πŸ”΄ whether a certificate profile exists β€” without one the account cannot sign, and the provider models no such resource;
  • πŸ”΄ whether any identity is permitted to sign β€” that is a role assignment on another module's scope;
  • whether the publisher has passed Microsoft's identity validation;
  • whether the region offers Trusted Signing;
  • whether the signing role name in your configuration is still current.

πŸ’¬ Example Output

Outputs:

account_uri                     = "https://eus.codesigning.azure.net/"
id                              = "/subscriptions/00000000-.../providers/Microsoft.CodeSigning/codeSigningAccounts/sign-contoso-prod"
is_premium_sku                  = true
location                        = "eastus"
name                            = "sign-contoso-prod"
requires_certificate_profile    = true
resource_group_name             = "rg-release-prod"
signing_keys_are_microsoft_held = true
sku_name                        = "Premium"

πŸ”΄ requires_certificate_profile = true is a constant. Read it as "and nothing here created one" (example 3). It is the most useful line in this output.

ℹ️ Note Microsoft.CodeSigning in the id β€” not "TrustedSigning" (example 2). This is the string to copy into a role scope or an import command.

⚠️ account_uri is present and grants nothing. Signing needs a role assignment (example 4).

βœ… signing_keys_are_microsoft_held = true β€” there is no private key in this output, or anywhere else in this module, because the service has none to give.

πŸ” Troubleshooting

Symptom Cause Fix
Plan rejects sku_name Not Basic or Premium β€” Standard is the usual guess. Use one of the two (example 5).
An import path does not resolve It was built from "TrustedSigning". The provider is Microsoft.CodeSigning (example 2).
Signing fails though the account exists Most likely no certificate profile. Create one outside Terraform (example 3).
Signing fails with a permissions error The build identity has no role on the account. Assign one (example 4).
The signing role name is not found Role names for newer services change. Check Microsoft's current documentation (example 4).
Apply fails on the region Trusted Signing is not offered there. Choose a supported region.
Cannot sign yet, and nothing is misconfigured Publisher identity validation may be outstanding. It has a lead time (example 8).
A test pipeline signed a production artifact One account shared across environments. Separate accounts and grants (example 6).
Deleted the account, signatures still valid Signatures verify against a chain, not the account. Expected (example 9).
Need to export the signing key The key is in Microsoft's HSM and is not exportable. Not possible (example 1).
Wanted prevent_destroy lifecycle is not valid inside a module block. Use a CanNotDelete lock (example 9).

πŸ”— Related Docs

πŸ’™ "Infrastructure as Code should be standardized, consistent, and secure."