A secure-by-default AWS Signer code-signing profile — finite signature validity, explicit (never-wildcard) cross-account grants, and optional Terraform-managed signing jobs, all from one module call. Built for the AWS provider v6.x.
- ✍️ Creates an AWS Signer signing profile (
aws_signer_signing_profile) — the trusted-publisher configuration used to produce verifiable code/firmware/container-image signatures. - ⏳ Finite signature validity by default (
{ value = 1, type = "YEARS" }) — never relies on an unbounded/platform-default validity window. - 🎯 Platform selection is validated against a known, documented AWS Signer platform-ID allow-list (Lambda, IoT Device Management, FreeRTOS, container/OCI Notation) so typos and unsupported platforms fail at
plantime. - 🔏 Optional bring-your-own signing certificate (
signing_material) for platforms that require one (e.g.Notation-OCI-SHA384-ECDSA) — wired fromtf-mod-aws-acm. - 🔐 Optional cross-account permissions (
map(object),for_each) — every grant names an explicit AWS account ID or IAM principal ARN; wildcard ("*") principals are rejected outright. - 📦 Optional, narrowly-scoped Terraform-managed signing jobs for the rare static/infrastructure-provisioned signing use case — real signing workflows almost always belong in a CI/CD pipeline instead.
💡 Why it matters: code signing is how downstream consumers (Lambda, IoT OTA fleets, container runtimes) verify that an artifact came from a trusted publisher and hasn't been tampered with. This module makes the finite-validity, explicit-principal, never-wildcard posture the path of least resistance for a regulated FI's signing infrastructure.
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
acm["tf-mod-aws-acm"]
s3["tf-mod-aws-s3-bucket"]
iam["tf-mod-aws-iam-role"]
signer["tf-mod-aws-signer"]
lambda["tf-mod-aws-lambda"]
acm -->|"certificate_arn (optional)"| signer
s3 -->|"bucket name (signing jobs)"| signer
iam -->|"principal arn (permissions)"| signer
signer -->|"profile arn"| lambda
style signer fill:#FF9900,color:#fff
Signer is a near-foundation module: it consumes only an optional ACM certificate ARN and referenced S3 bucket names, and is consumed by the compute/containers/IoT layer (Lambda code-signing configs, OTA firmware pipelines, container image signing) that needs a verifiable, trusted publisher identity.
flowchart TD
subgraph module["tf-mod-aws-signer"]
profile["aws_signer_signing_profile.this<br/>(keystone)"]
perm["aws_signer_signing_profile_permission.this<br/>(for_each, optional)"]
job["aws_signer_signing_job.this<br/>(for_each, optional)"]
end
profile -->|profile_name| perm
profile -->|profile_name| job
style profile fill:#FF9900,color:#fff
| Resource | Role | Created when |
|---|---|---|
aws_signer_signing_profile.this |
Keystone signing profile | Always |
aws_signer_signing_profile_permission.this |
Cross-account permission statement (for_each) |
permissions non-empty |
aws_signer_signing_job.this |
Terraform-managed signing job (for_each) |
signing_jobs non-empty |
ℹ️
signing_jobsis deliberately the exception, not the pattern — real code-signing workflows callStartSigningJobper build artifact from a CI/CD pipeline, not from Terraform state. See Architecture Notes.
| Requirement | Version |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/aws |
>= 6.0, < 7.0 |
No configuration_aliases — AWS Signer is a single-Region service with no us-east-1 global-service coupling, so the module inherits the caller's default provider only.
The Terraform identity needs the following actions. Scope to the signing profile ARN where possible.
| Action | Required for | Notes |
|---|---|---|
signer:PutSigningProfile, signer:GetSigningProfile |
Profile lifecycle / read-back | — |
signer:CancelSigningProfile |
Destroy | Cancellation, not hard-delete — see Architecture Notes |
signer:ListSigningProfiles, signer:ListTagsForResource |
Read-back / plan refresh | — |
signer:TagResource, signer:UntagResource |
Tagging | — |
signer:AddProfilePermission, signer:RemoveProfilePermission, signer:ListProfilePermissions |
Cross-account permissions (aws_signer_signing_profile_permission) |
— |
signer:StartSigningJob, signer:DescribeSigningJob |
Terraform-managed signing jobs (aws_signer_signing_job) |
Only needed if signing_jobs is used |
acm:DescribeCertificate |
Validating a caller-supplied signing_material.certificate_arn |
Only when signing_material is set |
ℹ️ No
iam:PassRoleis required by the Terraform identity — AWS Signer uses its own service principal (signer.amazonaws.com) to read the source object and write the signed object. The source/destination bucket policies (owned bytf-mod-aws-s3-bucket, out of scope here) must separately authorize that service principal fors3:GetObject/s3:GetObjectVersion/s3:PutObject.
⚠️ No service-linked role is auto-created by AWS Signer.
- Service: AWS Signer is enabled in every account and Region by default — no account-level opt-in required.
- Service-linked role: none required.
- Region constraints: Signer is a regional service — no
us-east-1global-service coupling (unlike CloudFront/WAFv2/ACM-for-CloudFront). Confirm Regional availability before use in GovCloud or newer Regions. - Platform support: confirm the account/Region actually supports the chosen
platform_id— runaws signer list-signing-platformsfor the authoritative, current list; the module's variable validation covers the platforms documented at authoring time (Lambda, IoT Device Management, FreeRTOS, Notation-OCI). - Source bucket versioning:
aws_signer_signing_jobrequires the source S3 bucket to have versioning enabled —source.s3.versionis a required argument. - Quotas: signing profiles, permission statements per profile, and concurrent signing jobs are subject to per-account/Region service quotas — check the Service Quotas console before large-scale rollout.
tf-mod-aws-signer/
├── providers.tf # required_providers only — no provider {} block
├── variables.tf # name, platform_id, signature_validity_period, signing_material,
│ # signing_parameters, permissions, signing_jobs, tags
├── main.tf # aws_signer_signing_profile.this + permission + job (for_each)
├── outputs.tf # id, arn, name, version(_arn), status, revocation_record,
│ # permission_*, signing_job_*, tags_all
├── README.md # this file
└── SCOPE.md # in/out-of-scope, IAM, prerequisites, gotchas, design decisions
Smallest working call — a Lambda code-signing profile with a finite (1-year) signature validity:
module "signer" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-signer?ref=v1.0.0"
name = "app_lambda_signing"
platform_id = "AWSLambda-SHA384-ECDSA"
tags = {
Environment = "prod"
CostCenter = "1234"
}
}
# Wire the profile ARN into a Lambda code-signing config:
# allowed_publishers { signing_profile_version_arns = [module.signer.version_arn] }
⚠️ Always pin the source with?ref=v1.0.0— never a branch.
| Input | Type | Source module |
|---|---|---|
signing_material.certificate_arn |
string (ACM ARN) |
tf-mod-aws-acm |
permissions[*].principal |
string (IAM ARN or account ID) |
tf-mod-aws-iam-role / tf-mod-aws-iam-user |
signing_jobs[*].source.s3.bucket / destination.s3.bucket |
string (bucket name) |
tf-mod-aws-s3-bucket |
Signer is a near-foundation module — it consumes only an optional ACM cert ARN and referenced bucket names.
| Output | Description | Consumed by |
|---|---|---|
id |
Signing profile id (the profile name) | Signer API references |
arn |
Signing profile ARN — cross-resource reference type | tf-mod-aws-lambda (code-signing config), IAM policies |
name |
Signing profile name | profile_name inputs on any sibling Signer resource |
version / version_arn |
Current profile version / version-qualified ARN | Version-pinned consumers (e.g. IAM signer:ProfileVersion conditions) |
status |
Profile status (Active/Canceled) | Drift/health monitoring |
revocation_record |
Revocation metadata, if revoked | Security monitoring |
permission_ids / permission_statement_ids |
Maps of cross-account permission resource ids / statement ids | Audit of granted cross-account access |
signing_job_ids / signing_job_statuses / signed_object_locations |
Maps for any Terraform-managed signing jobs | Release/build pipeline tracking |
tags_all |
All tags incl. provider default_tags |
Governance / audit |
1 · Minimal — Lambda code-signing profile
module "signer" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-signer?ref=v1.0.0"
name = "app_lambda_signing"
platform_id = "AWSLambda-SHA384-ECDSA"
}2 · With tags (merges with provider default_tags)
provider "aws" {
region = "us-east-1"
default_tags {
tags = {
ManagedBy = "Terraform"
Owner = "platform-team"
}
}
}
module "signer" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-signer?ref=v1.0.0"
name = "app_lambda_signing"
platform_id = "AWSLambda-SHA384-ECDSA"
# Resource tags MERGE with default_tags; resource tags win on key conflict.
tags = {
Environment = "prod"
DataClass = "internal"
}
}
# module.signer.tags_all == { ManagedBy, Owner, Environment, DataClass }3 · Custom signature validity (extend beyond the 1-year default)
module "signer" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-signer?ref=v1.0.0"
name = "long_lived_signing"
platform_id = "AWSLambda-SHA384-ECDSA"
# Always finite — no "unlimited" option is exposed by this module.
signature_validity_period = {
value = 5
type = "YEARS"
}
}4 · IoT Device Management OTA firmware signing
module "iot_signer" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-signer?ref=v1.0.0"
name = "iot_ota_signing"
platform_id = "AWSIoTDeviceManagement-SHA256-ECDSA"
signature_validity_period = {
value = 6
type = "MONTHS"
}
}5 · FreeRTOS OTA firmware signing (generic platform)
module "freertos_signer" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-signer?ref=v1.0.0"
name = "freertos_ota_signing"
platform_id = "AmazonFreeRTOS-Default"
}6 · Customer-managed signing certificate (Notation-OCI container image signing)
module "acm_cert" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-acm?ref=v1.0.0"
domain_name = "signer.internal.example.com"
#... validation config...
}
module "container_signer" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-signer?ref=v1.0.0"
name = "oci_image_signing"
platform_id = "Notation-OCI-SHA384-ECDSA"
# Notation-OCI requires a bring-your-own signing certificate.
signing_material = {
certificate_arn = module.acm_cert.arn
}
}7 · Signing parameters (opaque platform-specific metadata)
module "signer" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-signer?ref=v1.0.0"
name = "device_signing"
platform_id = "AmazonFreeRTOS-TI-CC3220SF"
signing_parameters = {
certname = "/device/cert/path"
}
}8 · Cross-account permission for a CI/CD role (recommended pattern)
module "ci_role" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-iam-role?ref=v1.0.0"
name = "ci-signing-role"
assume_role_policy = data.aws_iam_policy_document.ci_trust.json
}
module "signer" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-signer?ref=v1.0.0"
name = "app_lambda_signing"
platform_id = "AWSLambda-SHA384-ECDSA"
permissions = {
ci-pipeline = {
action = "signer:StartSigningJob"
principal = module.ci_role.arn
}
}
}9 · Cross-account permission scoped to a specific profile version
module "signer" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-signer?ref=v1.0.0"
name = "prod_signing"
platform_id = "AWSLambda-SHA384-ECDSA"
permissions = {
version-pinned = {
action = "signer:RevokeSignature"
principal = "111122223333"
profile_version = module.signer.version # not literally self-referential in real use — pass a known prior version
}
}
}10 · Multiple cross-account permissions (for_each pattern)
locals {
signing_grantees = {
ci-pipeline = { action = "signer:StartSigningJob", principal = module.ci_role.arn }
release-audit = { action = "signer:GetSigningProfile", principal = module.audit_role.arn }
}
}
module "signer" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-signer?ref=v1.0.0"
name = "app_lambda_signing"
platform_id = "AWSLambda-SHA384-ECDSA"
permissions = local.signing_grantees
}11 · Static, Terraform-managed signing job (narrow exception case)
# Only for a deliberately static, infrastructure-provisioned artifact.
# Most signing belongs in a CI/CD pipeline — see Architecture Notes.
module "signer" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-signer?ref=v1.0.0"
name = "release_pinned_signing"
platform_id = "AWSLambda-SHA384-ECDSA"
signing_jobs = {
release-2026-07 = {
source = {
s3 = { bucket = "casey-build-artifacts", key = "app.zip", version = "abc123def456" }
}
destination = {
s3 = { bucket = "casey-signed-artifacts", prefix = "signed/" }
}
}
}
}12 · Import an existing signing profile
import {
to = module.signer.aws_signer_signing_profile.this
id = "app_lambda_signing_DdW3Mk1foYL88fajut4mTVFGpuwfd4ACO6ANL0D1uIj7lrn8adK"
}
module "signer" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-signer?ref=v1.0.0"
name = "app_lambda_signing"
platform_id = "AWSLambda-SHA384-ECDSA"
}13 · Secure-by-default opt-out — long signature validity (EXCEPTION)
# ⚠️ Weakens the baseline (long exposure window for a compromised signature).
# Document the exception and get sign-off before use.
module "signer" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-signer?ref=v1.0.0"
name = "legacy_compat_signing"
platform_id = "AWSLambda-SHA384-ECDSA"
signature_validity_period = {
value = 10
type = "YEARS" # still finite — no "unlimited" option exists in this module
}
}14 · End-to-end composition — IAM → Signer → Lambda code signing (mandatory finale)
# 1) Role that the CI/CD pipeline assumes to start signing jobs
module "ci_role" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-iam-role?ref=v1.0.0"
name = "ci-signing-role"
assume_role_policy = data.aws_iam_policy_document.ci_trust.json
}
# 2) The signing profile — cross-account permission delegated to the CI role
module "signer" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-signer?ref=v1.0.0"
name = "app_lambda_signing"
platform_id = "AWSLambda-SHA384-ECDSA"
permissions = {
ci-pipeline = {
action = "signer:StartSigningJob"
principal = module.ci_role.arn
}
}
tags = { Environment = "prod", DataClass = "internal" }
}
# 3) Lambda function trusts signatures produced under this profile
resource "aws_lambda_code_signing_config" "this" {
allowed_publishers {
signing_profile_version_arns = [module.signer.version_arn]
}
policies {
untrusted_artifact_on_deployment = "Enforce"
}
}
module "app_lambda" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-lambda?ref=v1.0.0"
function_name = "app"
code_signing_config_arn = aws_lambda_code_signing_config.this.arn
#... runtime, handler, package, role, etc....
}ℹ️ High-level grouping below.
- Core (FORCE-NEW):
name(primary identity),platform_id. - Signature validity (FORCE-NEW):
signature_validity_period— object{ value, type }; defaults to finite{ value = 1, type = "YEARS" }. - Signing certificate (FORCE-NEW):
signing_material— nullable object{ certificate_arn }; required only by bring-your-own-cert platforms (e.g. Notation-OCI). - Signing parameters (FORCE-NEW):
signing_parameters— opaquemap(string), platform-specific. - Cross-account permissions:
permissions—map(object)keyed by stable name;principalmay never be"*". - Signing jobs (narrow use case):
signing_jobs—map(object)keyed by stable name; see Architecture Notes before using. - Universal:
tags. (Notimeouts— none of the three Signer resources expose a configurable timeouts block.)
id— the signing profile id (the profile name).arn— the signing profile ARN (the canonical cross-resource reference).name— the profile name.version,version_arn— current profile version and version-qualified ARN.status— profile status (e.g. Active, Canceled).platform_display_name— human-readable platform name.revocation_record— revocation metadata (empty unless the profile was revoked).permission_ids,permission_statement_ids— maps keyed byvar.permissionsentry (empty when no permissions configured).signing_job_ids,signing_job_statuses,signed_object_locations— maps keyed byvar.signing_jobsentry (empty when no jobs configured).tags_all— merged resource + providerdefault_tags.
No output in this module is sensitive = true — Signer does not emit secret material.
- ARN / id formats.
- Signing profile ARN:
arn:aws:signer:<region>:<account>:/signing-profiles/<name> - Profile id: the profile name itself (same value as
id). - Version ARN: the profile ARN suffixed with the version identifier — use to pin a consumer to an exact profile version.
- Force-new / immutable fields.
name,platform_id,signature_validity_period, andsigning_materialare all FORCE-NEW — changing any of them destroys and recreates the profile. Signing profile names are permanently retired once canceled — AWS Signer has no hard-delete, only cancellation, and a canceled name can never be reused in that account/Region. signature_validity_perioduses avalue+typepair, not a duration string (typeis one ofDAYS/MONTHS/YEARS). This module defaults to a finite{ value = 1, type = "YEARS" }rather than deferring to the platform's own default validity.- Signing jobs are typically pipeline-invoked, not Terraform-managed. Real code-signing workflows call
StartSigningJobonce per build artifact via CI/CD, the AWS CLI, or an SDK — that access pattern doesn't fit Terraform's declarative, converge-to-a-fixed-state model well. Thesigning_jobsinput exists only for a single, static, infrastructure-provisioned signing job; aterraform applyre-runs the job if any input (source key/version, etc.) changes. aws_signer_signing_profile_permissionexports no additional attributes beyond the baseid— there is noarnoutput for a permission statement.tags↔tags_all↔default_tags. Only the signing profile is taggable —aws_signer_signing_jobandaws_signer_signing_profile_permissionare not.var.tagsflows to the profile only;tags_allreflects the merge with providerdefault_tags(resource tags win on key conflict).- No
us-east-1global-resource coupling. Unlike CloudFront/WAFv2/ACM-for-CloudFront, AWS Signer is a regional service with no special-Region requirement. - Eventual consistency. A freshly created signing profile may take a moment to be resolvable by
aws_signer_signing_profile_permission/aws_signer_signing_jobin the same apply; Terraform's implicit dependency graph (viaaws_signer_signing_profile.this.name) orders these correctly.
Secure-by-default posture — each hardened default and the exact variable that relaxes it:
| Posture | Hardened default | Opt-out (variable) |
|---|---|---|
| Signature validity | finite { value = 1, type = "YEARS" }, never unlimited |
supply a longer explicit signature_validity_period — always still finite; no "unlimited" option is exposed |
| Cross-account permission principal | must be an explicit AWS account ID/ARN | non-negotiable — wildcard ("*") principals are rejected by variable validation |
| Platform selection | validated against a documented, known platform-ID allow-list | extend the platform_id variable's validation list when a legitimately new AWS Signer platform is required |
| Signing job failures | ignore_signing_job_failure = false (fail the apply loudly) |
set true per signing-job entry, only with a documented reason to inspect status/reason instead of failing outright |
Additional principles:
- One composite, one profile. The profile plus its optional cross-account permissions and optional static signing jobs come from a single call.
signing_jobsis the documented exception, not the pattern. Real signing workflows belong in a CI/CD pipeline invoking the Signer API per build, not in Terraform state — see Architecture Notes.- Optional features are absent unless configured. Permissions (
{}) and signing jobs ({}) materialize only when supplied.
terraform init -backend=false
terraform validate
terraform fmt -check
# plan/apply require valid AWS credentials (profile / SSO / OIDC) and a region:
terraform plan
terraform apply
terraform output
⚠️ Pin the module source with?ref=v1.0.0— never a branch.
terraform init -backend=false && terraform validate— offline schema validation.terraform fmt -check— formatting gate.terraform planagainst a sandbox account — confirm the profile renders with the expectedplatform_idandsignature_validity_period.- Verify
permission_ids/permission_statement_idspopulate correctly for eachpermissionsentry, and confirm no plan ever accepts a"*"principal (the variable validation should reject it beforeplaneven runs). - For
signing_jobs: confirmsigning_job_statusesshowsSucceededandsigned_object_locationspoints at the expected destination key.
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
arn = "arn:aws:signer:us-east-1:111122223333:/signing-profiles/app_lambda_signing"
id = "app_lambda_signing"
name = "app_lambda_signing"
platform_display_name = "AWS Lambda"
status = "Active"
tags_all = {
"DataClass" = "internal"
"Environment" = "prod"
"ManagedBy" = "Terraform"
}
version = "6QavYnEsxD3ir4DAAAAA"
version_arn = "arn:aws:signer:us-east-1:111122223333:/signing-profiles/app_lambda_signing/6QavYnEsxD3ir4DAAAAA"
- Tag drift from
default_tagsoverlap. A key set in bothvar.tagsand providerdefault_tagsshows the resource value intags_all(resource wins). If a plan keeps re-adding a tag, it's defined in both places with different values — remove the duplicate. - Credential-chain failures.
NoCredentialProviders/ExpiredTokenon plan/apply means no valid AWS credentials — setAWS_PROFILE, refresh SSO, or confirm the OIDC role assumption succeeded in CI. - Region / availability errors.
UnknownOperationExceptionor similar for a platform in an unexpected Region usually means the chosenplatform_idisn't available there — verify withaws signer list-signing-platformsin the target Region (nous-east-1requirement, but not every platform is available everywhere). - IAM permission denials. A
signer:PutSigningProfile/signer:StartSigningJobdenial means the Terraform identity lacks the action — see Required IAM Permissions. Note thats3:GetObject/s3:PutObjectdenials on a signing job come from the bucket policy, not the Terraform identity's IAM policy — thesigner.amazonaws.comservice principal needs bucket-level access. - Profile name reuse fails. Attempting to recreate a profile with a previously canceled name errors — AWS Signer never allows reusing a retired profile name in the same account/Region. Pick a new name.
ValidationExceptiononpermissions[*].principal = "*". This module's variable validation rejects wildcard principals before the plan even reaches AWS — supply an explicit account ID or ARN.- Signing job "hangs" or times out. Signing jobs are asynchronous; check
signing_job_statusesand, on failure withignore_signing_job_failure = true, inspect the job'sstatus_reasonvia the AWS Signer console/CLI (this module does not exposestatus_reasonas an output since it is only meaningful on failure). - Destroy leaves the profile in
Canceled, not gone. Signer has no hard-delete for profiles —terraform destroycancels the profile; it remains visible (as canceled) in the console/API, and its name can never be reused.
- Terraform Registry —
aws_signer_signing_profile,aws_signer_signing_job,aws_signer_signing_profile_permission - AWS Signer Developer Guide — signing platforms, profile permissions, and revocation
- AWS Lambda Developer Guide — code signing for Lambda functions and code-signing configs
- AWS IoT / FreeRTOS Developer Guide — OTA firmware update code signing
- module suite —
tf-mod-aws-acm,tf-mod-aws-s3-bucket,tf-mod-aws-iam-role,tf-mod-aws-lambda
🧡 "Infrastructure as Code should be standardized, consistent, and secure."