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). Targetshashicorp/azurerm ~> 4.0.
- βοΈ 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_urigrants 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_nameupdates 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.
If this module saves you time, please consider supporting its continued development:
- β Star the repository on GitHub.
- π€ Connect on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
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;
βΉοΈ This resource has no siblings β
azurerm_trusted_signing_accountis 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.
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;
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. |
| 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_nameupdates in place β unusual; most SKU arguments in this provider force replacement. name,resource_group_nameandlocationare 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 planfails outright β for every configuration sharing that state file, not just this module β until someone runsterraform state rmby hand. β οΈ The provider talks toMicrosoft.CodeSigningat API version2024-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.β οΈ nameis 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.lifecycleis not valid inside amoduleblock, so a caller cannot addprevent_destroy.
| 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-assignmentsscoped to this module'sid(example 4).
- 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).
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
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.
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 |
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
idoutput 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
trueis 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
Basicthe 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_eachlives 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
}
}π΄
incompleteis 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).
β οΈ uriinvites 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
idto 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/codeSigningAccountsin 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_nameandlocationare force-new, and all three appear in the Resource ID itself. Asku_namemismatch 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. ACanNotDeletelock is the available protection, since a caller cannot useprevent_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
profiletag 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
ownertag 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. Aprofiletag 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_nameisBasicorPremium, 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 consoleinside the module, which does fire root-module variable validations β unliketerraform validateon a calling configuration. Asku_nameofStandardβ 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_profilerather 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
idrather than a hand-writtenMicrosoft.CodeSigningpath; aprofiletag recording the one dependency that lives outside Terraform; aCanNotDeletelock 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_profilelists every account, always, by construction β which is the point. It is there for whoever assumes a clean apply means a working signing pipeline.
| Input | Type | Default | Notes |
|---|---|---|---|
name |
string |
β | Required. Force-new. |
resource_group_name / location |
string |
β | Required. Force-new. |
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."
}
}| 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. |
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.
-
The module's own incompleteness is emitted as an output.
requires_certificate_profileis a constanttrue, 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_heldis 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
idoutput itself. The service is Trusted Signing and the provider isMicrosoft.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.
| 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.
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_nameandtagsupdate 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'sidfor role scopes and imports β the path saysMicrosoft.CodeSigning.- βΉοΈ Add a
CanNotDeletelock; a caller cannot useprevent_destroy. - βΉοΈ Re-check the signing role name against Microsoft's documentation before relying on it.
terraform validate and terraform fmt -check are the offline gate. They confirm:
sku_nameisBasicorPremium;name,resource_group_nameandlocationare non-empty;- no output is sensitive, because nothing sensitive is accepted;
- the module declares no
providerblock.
π‘ This was proved by evaluating the condition in
terraform consoleinside the module β which does fire root-module variable validations, unliketerraform validateon a calling configuration. Asku_nameofStandardβ 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.
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 = trueis a constant. Read it as "and nothing here created one" (example 3). It is the most useful line in this output.
βΉοΈ Note
Microsoft.CodeSigningin theidβ not "TrustedSigning" (example 2). This is the string to copy into a role scope or an import command.
β οΈ account_uriis 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.
| 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). |
azurerm_trusted_signing_accountβ provider documentation.- Trusted Signing overview β the service, and the identity validation of example 8.
- Trusted Signing quickstart β the certificate profile of example 3.
- Sibling modules:
terraform-azurerm-resource-group,terraform-azurerm-role-assignments(the grant that lets an agent sign),terraform-azurerm-user-assigned-identity. - This module's
SCOPE.md.
π "Infrastructure as Code should be standardized, consistent, and secure."