Cloudfront Software Installers
This module allows for Fleet software installers to be served via AWS Cloudfront instead of directly from Fleet.
This should improve the performance of software installer delivery. For general information about Fleet and Cloudfront see the following:
https://fleetdm.com/guides/cdn-signed-urls https://victoronsoftware.com/posts/cloudfront-signed-urls/
The second link includes a script that can be used to test and see if signed URLs are working outside of Fleet for troubleshooting purposes.
AWS GovCloud Scope
This addon is intentionally out of scope for AWS GovCloud compatibility. CloudFront is not available inside the AWS GovCloud (US) partition, so using it with GovCloud-hosted resources is a cross-partition architecture that should be designed and reviewed separately.
S3 Bucket Policy
This addon no longer manages an S3 bucket policy on the software installers bucket. The bucket policy is now managed by the core module (byo-ecs), which automatically denies non-HTTPS requests and includes the CloudFront allow statement when cloudfront_distribution_arn is set.
Other module requirements
These are the minimum versions of modules required if used:
- tf-mod-root-v1.30.0
- tf-mod-byo-vpc-v1.31.0
- tf-mod-byo-db-v1.21.0
- tf-mod-byo-ecs-v1.21.0
- tf-mod-addon-logging-alb-v1.3.0
Previous versions do not allow for proper interaction with both the software installers and logging s3 buckets.
Blue-Green Key Rotation
This module supports rotating CloudFront signing keys without invalidating existing signed URLs. Instead of a single public_key / private_key pair, you can provide a map of named keypairs and select which one is active:
module "cloudfront-software-installers" {
source = "..."
customer = "fleet"
s3_bucket = module.main.byo-vpc.byo-db.byo-ecs.fleet_s3_software_installers_config.bucket_name
keypairs = {
current = {
public_key = data.aws_kms_secrets.cloudfront.plaintext["public_key"]
private_key = data.aws_kms_secrets.cloudfront.plaintext["private_key"]
}
next = {
public_key = data.aws_kms_secrets.cloudfront_next.plaintext["public_key"]
private_key = data.aws_kms_secrets.cloudfront_next.plaintext["private_key"]
}
}
active_keypair_name = "current"
}
All keypairs in the map are added to the CloudFront key group, so signed URLs created by any retained key remain valid. Only the active keypair populates the Secrets Manager secret that Fleet reads.
Rotation procedure
- Upgrade the module with the existing key only (or via the legacy
public_key/private_keyinputs). Confirm Terraform plans only a resource move (aws_cloudfront_public_key.software_installers[0]→["current"]) with no create/update/delete. - Add the new keypair under any new map name (e.g.
next), leavingactive_keypair_name = "current". Apply and wait for the CloudFront key group update to deploy. - Flip the active key by changing
active_keypair_nameto the new key name and applying. This creates a new Secrets Manager secret version. - Redeploy Fleet ECS tasks so the container receives the updated secret values.
- Retire the old keypair after the maximum signed URL lifetime has passed. Remove it from the map and apply. If CloudFront reports the old public key is still in use, wait for propagation and apply again.
Legacy inputs
The public_key and private_key variables are deprecated but still supported. When keypairs is not set, they are normalized into a single current keypair internally. Migrate to the keypairs map at your convenience.
External key groups
Blue-green key rotation is not supported when key_group_id is set. When using an external key group, the module cannot manage the public key resources, so active_keypair_name cannot select the correct public key ID. Use keypairs only when the module manages the key group (i.e., key_group_id is not set).
Configuration considerations for other modules
tf-mod-root/tf-mod-byo-vpc/tf-mod-byo-db/tf-mod-byo-ecs
For any of these modules, software installers requires a customer-managed KMS key whenever KMS encryption is used at all. CloudFront access requires a key policy statement on that key, and the AWS-managed default key cannot be modified to accept that policy.
Breaking change: this addon no longer manages the software installers KMS key policy. That policy now lives in tf-mod-byo-ecs, which means CloudFront access to the CMK must be configured there by setting software_installers.cloudfront_distribution_arn to the static distribution ARN.
This is the relevant configuration starting at the software installers configuration block:
software_installers = {
bucket_prefix = "fleet-software-installers-"
create_kms_key = true
kms_alias = "fleet-software-installers"
cloudfront_distribution_arn = "arn:aws:cloudfront::<account-id>:distribution/<distribution-id>"
}
The required configuration items here are create_kms_key, kms_alias, and cloudfront_distribution_arn when CloudFront must read from an SSE-KMS bucket.
tf-mod-addon-logging-alb
No changes required if using at least version tf-mod-addon-logging-alb-v1.3.0. Bucket ACLs are changed to allow for the alb logging bucket to accept Cloudfront logs via ACLs.
Configuration Example
This example assumes that you used the following commands to create your public and private key for consumption by the module:
openssl genrsa -out cloudfront.key 2048
openssl rsa -pubout -in cloudfront.key -out cloudfront.pem
To be able to store these in source control in a sane manner, the objects will be KMS encrypted for storage at rest. This can happen by having a KMS key as follows:
resource "aws_kms_key" "customer_data_key" {
description = "key used to encrypt sensitive data stored in terraform"
}
resource "aws_kms_alias" "alias" {
name = "alias/fleet-terraform-encrypted"
target_key_id = aws_kms_key.customer_data_key.id
}
output "kms_key_id" {
value = aws_kms_key.customer_data_key.id
}
Then with the key, the following encrypt.sh script encrypt the objects:
#!/bin/bash
set -e
function usage() {
cat <<-EOUSAGE
Usage: $(basename ${0}) <KMS_KEY_ID> <SOURCE> <DESTINATION> [AWS_PROFILE]
This script encrypts an plaintext file from SOURCE into an
AWS KMS encrypted DESTINATION file. Optionally you
may provide the AWS_PROFILE you wish to use to run the aws kms
commands.
EOUSAGE
exit 1
}
[ $# -lt 3 ] && usage
if [ -n "${4}" ]; then
export AWS_PROFILE=${4}
fi
aws kms encrypt --key-id "${1:?}" --plaintext fileb://<(cat "${2:?}") --output text --query CiphertextBlob > "${3:?}"
We can do the following with that script to encrypt the objects:
./encrypt.sh <KMS_KEY_ID> cloudfront.key cloudfront.key.encrypted
./encrypt.sh <KMS_KEY_ID> cloudfront.pem cloudfront.pem.encrypted
Now with those encrypted we could setup the module with something like the following to populate the module (assuming we add the files to a /resources folder):
module "cloudfront-software-installers" {
source = "github.com/fleetdm/fleet-terraform/addons/cloudfront-software-installers?ref=tf-mod-addon-cloudfront-software-installers-v3.0.0"
customer = "fleet"
s3_bucket = module.main.byo-vpc.byo-db.byo-ecs.fleet_s3_software_installers_config.bucket_name
# OPTIONAL
# If you'd like to use an existing key_group_id for your new Cloudfront distribution, uncomment key_group_id and supply the value for the key_group_id
# If you're using an existing key_group_id, public_key_id is required. The public_key_id is the value of a public_key that is also part of the key group that you define.
# key_group_id = ""
# public_key_id = ""
public_key = data.aws_kms_secrets.cloudfront.plaintext["public_key"]
private_key = data.aws_kms_secrets.cloudfront.plaintext["private_key"]
enable_logging = true
logging_s3_bucket = module.logging_alb.log_s3_bucket_id
}
data "aws_kms_secrets" "cloudfront" {
secret {
name = "public_key"
key_id = aws_kms_key.customer_data_key.id
payload = file("${path.module}/resources/cloudfront.pem.encrypted")
}
secret {
name = "private_key"
key_id = aws_kms_key.customer_data_key.id
payload = file("${path.module}/resources/cloudfront.key.encrypted")
}
}
Then we need to include outputs from this module once applied back into the main fleet-config under the extra_secrets and extra_execution_roles:
This addon also outputs cloudfront_distribution_arn. Do not feed that output back into tf-mod-byo-ecs, because that creates a Terraform cycle. Instead, set the same static ARN directly in software_installers.cloudfront_distribution_arn.
Under the fleet_config section. If not using the mdm module, that could be omitted but was included to show how to include multiple extra items:
fleet_config = {
...
extra_execution_iam_policies = concat(
module.mdm.extra_execution_iam_policies,
module.cloudfront-software-installers.extra_execution_iam_policies,
)
extra_secrets = merge(
module.mdm.extra_secrets,
module.cloudfront-software-installers.extra_secrets
)
}
Requirements
No requirements.
Providers
| Name | Version |
|---|---|
| aws | 6.54.0 |
Modules
| Name | Source | Version |
|---|---|---|
| cloudfront_software_installers | terraform-aws-modules/cloudfront/aws | 5.2.0 |
Resources
| Name | Type |
|---|---|
| aws_cloudfront_key_group.software_installers | resource |
| aws_cloudfront_public_key.software_installers | resource |
| aws_iam_policy.software_installers_secret | resource |
| aws_secretsmanager_secret.software_installers | resource |
| aws_secretsmanager_secret_version.software_installers | resource |
| aws_iam_policy_document.software_installers_secret | data source |
| aws_s3_bucket.logging | data source |
| aws_s3_bucket.software_installers | data source |
Inputs
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| active_keypair_name | Name of the keypair in keypairs (or "current" when using legacy inputs) whose keys populate the Secrets Manager secret. All keypairs in the map are added to the CloudFront key group so signed URLs from any retained key remain valid. |
string |
"current" |
no |
| customer | Customer name for the cloudfront instance | string |
"fleet" |
no |
| enable_logging | Enable optional logging to s3 | bool |
false |
no |
| key_group_id | Cloudfront key group id | string |
null |
no |
| keypairs | Map of named keypairs for blue-green key rotation. Each value must contain public_key and private_key. When set, public_key and private_key variables are ignored. |
map(object({ |
null |
no |
| logging_s3_bucket | s3 bucket to log to | string |
null |
no |
| logging_s3_prefix | logging s3 bucket prefix | string |
"cloudfront" |
no |
| private_key | Private key used for signed URLs. Deprecated: use keypairs instead. |
string |
null |
no |
| public_key | Public key used for signed URLs. Deprecated: use keypairs instead. |
string |
null |
no |
| public_key_id | Cloudfront public key id. Required when passing in a key_group_id | string |
null |
no |
| s3_bucket | Name of the S3 bucket that Cloudfront will point to | string |
n/a | yes |
Outputs
| Name | Description |
|---|---|
| cloudfront_arn | n/a |
| cloudfront_distribution_arn | n/a |
| extra_execution_iam_policies | n/a |
| extra_secrets | n/a |