├── .gitignore ├── Assets └── fleet-provisioning-1.jpg ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Config ├── LICENSE ├── README.md ├── SubTemplates └── IoT │ ├── Lambdas │ ├── bootstrap_generator │ │ ├── app.py │ │ └── requirements.txt │ ├── cert_rotation_hook │ │ ├── app.py │ │ └── requirements.txt │ ├── cert_rotation_monitor │ │ ├── app.py │ │ └── requirements.txt │ ├── provision_device │ │ ├── app.py │ │ ├── artifacts │ │ │ ├── bootstrapPolicy.json │ │ │ ├── certRotationTemplate.json │ │ │ ├── models.txt │ │ │ ├── productionPolicy.json │ │ │ └── provisioningTemplate.json │ │ ├── cfnresponse.py │ │ ├── client │ │ │ ├── __pycache__ │ │ │ │ └── provisioning_handler.cpython-37.pyc │ │ │ ├── config.ini │ │ │ ├── machine_config.json │ │ │ ├── main.py │ │ │ ├── provisioning_handler.py │ │ │ ├── requirements.txt │ │ │ └── utils │ │ │ │ ├── __init__.py │ │ │ │ ├── __pycache__ │ │ │ │ ├── __init__.cpython-37.pyc │ │ │ │ └── config_loader.cpython-37.pyc │ │ │ │ └── config_loader.py │ │ └── requirements.txt │ └── provision_hook │ │ ├── app.py │ │ └── requirements.txt │ └── template.yaml ├── buildspec.yaml ├── pipeline.yaml └── template.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | *.msi 2 | *.zip 3 | *.exe 4 | node_modules/ 5 | .idea -------------------------------------------------------------------------------- /Assets/fleet-provisioning-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/automated-iot-fleet-provisioning-by-claim/6c09906b255a3ba3a96896921f638804e32265c9/Assets/fleet-provisioning-1.jpg -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /Config: -------------------------------------------------------------------------------- 1 | package.Fleet-provisioning = { 2 | interfaces = (1.0); 3 | 4 | # Use NoOpBuild. See https://w.amazon.com/index.php/BrazilBuildSystem/NoOpBuild 5 | build-system = no-op; 6 | build-tools = { 7 | 1.0 = { 8 | NoOpBuild = 1.0; 9 | }; 10 | }; 11 | 12 | # Use runtime-dependencies for when you want to bring in additional 13 | # packages when deploying. 14 | # Use dependencies instead if you intend for these dependencies to 15 | # be exported to other packages that build against you. 16 | dependencies = { 17 | 1.0 = { 18 | }; 19 | }; 20 | 21 | runtime-dependencies = { 22 | 1.0 = { 23 | }; 24 | }; 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Automated IoT Fleet Provisioning By Claim using Bootstrap Certificates 5 | AWS IoT Core Fleet Provisioning provides all necessary tools to securely onboard IoT devices. This process includes establishing a unique identity for each device, registering each device within AWS IoT, and managing the device permissions. The image below is from [how-to-automate-onboarding-of-iot-devices-to-aws-iot-core-at-scale-with-fleet-provisioning](https://aws.amazon.com/blogs/iot/how-to-automate-onboarding-of-iot-devices-to-aws-iot-core-at-scale-with-fleet-provisioning/), which provides a detailed description of fleet provisioning. Provisioning by claim is a popular provisioning method that uses a bootstrap certificate (X.509 certificate and a private key) that can be included on edge devices during the manufacturing process. The edge device first connects to IoT Core using the bootstrap certificate to request production certificates that uniquely identify the device. At this point, the device uses the production certificates for future AWS IoT connections. For the first connection with the production certificates, IoT Core references a provisioning template to assign the correct permissions to the device. At this point, the device has full production permissions during communications with AWS IoT Core. 6 | 7 | 8 | 9 | ![Fleet Provisioning by Claim](Assets/fleet-provisioning-1.jpg) 10 | 11 | This repository includes AWS CloudFormation templates that fully automate the process of setting up fleet provisioning by claim. This removes the need to manually create and link AWS resources with the aim to provide a turnkey IoT device onboarding experience. This CloudFormation templates in this repo create: 12 | 13 | 1. All cloud infrastructure for fleet provisioning by claim 14 | 2. A custom account-specific edge client that can be deployed on edge devices (prebaked with bootstrap certificates and your IoT endpoint) 15 | 3. (**New**): The ability to manage and rotate certificates signed by an AWS Root CA with configurable timelines and graceperiods. 16 | 17 | ## Getting Started 18 | 19 | ### Cloud Setup 20 | 21 | These steps only need to be completed once per AWS account. 22 | 23 | 1. Clone this repository to code commit 24 | 2. Install AWS CLI 25 | 3. Use the following command to install infrastructure into your account 26 | 27 | ``` 28 | aws cloudformation create-stack --region PUT-REGION-HERE --stack-name PUT-STACK-NAME-HERE --template-body file://pipeline.yaml --capabilities CAPABILITY_NAMED_IAM --parameters ParameterKey=CodeRepositoryName,ParameterValue=PUT-REPO-NAME-HERE ParameterKey=CodeRepositoryBranch,ParameterValue=PUT-BRANCH-HERE ParameterKey=ResourceTag,ParameterValue=PUT-RESOURCE-TAG-HERE --profile PUT_PROFILE_HERE 29 | ``` 30 | 31 | Example 32 | 33 | ``` 34 | aws cloudformation create-stack --region us-east-1 --stack-name pipeline --template-body file://pipeline.yaml --capabilities CAPABILITY_NAMED_IAM --parameters ParameterKey=CodeRepositoryName,ParameterValue=fleet ParameterKey=CodeRepositoryBranch,ParameterValue=master ParameterKey=ResourceTag,ParameterValue=fleetprov --profile default 35 | ``` 36 | 37 | This template will dynamically create an edge client and bootstrap certificates for your account. These resources are uploaded to an Amazon S3 bucket named {ResourceTag}-{rootstackId}-bootstrapcerts-{stackId}. The full name of the S3 bucket as also provided as an output of the root stack. 38 | 39 | Cloud updates should be performed by updating the AWS Serverless Application Modem (SAM) template.yaml file from this repository or files referenced by the template.yaml file. All updates pushed to AWS CodeCommit will be built and deployed as CloudFormation templates. Pipeline.yaml creates a pipeline using CodeCommit, AWS CodeBuild, and AWS CodeDeploy. After deployment, updates to template.yaml and resources referenced by template.yaml in this CodeCommit repository will push updates to AWS infrastructure via CloudFormation. All template.yaml files in this repository are SAM templates. Codebuild uses instructions in the buildspec.yaml file to transform the SAM templates in this repository to CloudFormation templates for deployment. 40 | 41 | ### Edge Setup 42 | 43 | Completing the cloud setup dynamically creates an edge client for your account in the S3 bucket at {ResourceTag}-{rootstackId}-bootstrapcerts-{stackId}. This mqtt client was adapted from https://github.com/aws-samples/aws-iot-fleet-provisioning and is provided in the /Client folder at the root level of the repo for your reference and for future code changes. Note that the following steps require using the personalized edge client that is uploaded to the S3 folder (client.zip) and not the general client files in the repo itself. For device setup, complete the following steps on each edge device. 44 | 45 | 1. Install Python 3.7 or above 46 | 2. Copy client.zip from the S3 bucket to the edge device and unzip. 47 | 3. Open machine_config.json and set the "serial_num" value to a unique identifier for the device. This will be used as the thing name. 48 | 4. Navigate your command prompt to the Client directory 49 | 5. Install python requirements ```pip3 install -r requirements.txt``` 50 | 6. Run program ```python main.py``` 51 | 52 | The client exchanges the bootstrap certificates for the production certificates with extended permissions. These certificates are then used for all future mqtt communication on topics containing the unique name assigned to this device (described in detail in fleet-provisioning components). 53 | 54 | ### NEW! Cert Rotation 55 | All things created in the AWS IoT registry will contain a *cert_issuance* attribute which will be the trigger for detecting certificates outside of the expiry date. A lambda trigger will run daily to detect expired certs and publish a message to each endpoint within the graceperiod of expiry. Your edge device must subscribe to this message and handle the published alert (arriving each day until compliance is met or certificate expires altogether). By executing the provided *run_provisioning* method (in main.py) and passing in run_provisioning(isRotation=True), the edge will perform a new cert rotation. The provisioning hook will validate if the cert_issuance date is indeed out of compliance and if so, issue a new certificate with an updated date (deactivating the old). The following illustrates a sample test workflow: 56 | 57 | 1) Make sure to enable Fleet Index Settings (Thing Indexing) in IoT Core / Settings. 58 | 2) Execute main.py from the edge to issue your initial production certs (from the bootstrap certs) 59 | 3) From IoT Core, select Manage and click on your newly created thing, and modify the cert issuance date e.g. (20180630). yyyymmdd 60 | 4) In IoT Core, Go to the Test console and subscribe to all topics (#). Note the published message to the device to rotate. 61 | 5) Back at the edge, modify the run_provisioning method by passing in (True) and re-run. 62 | 6) Observe the swap in certificates, and the new date and cert association for the THING in IoTCore. 63 | 64 | ## Fleet-Provisioning Resources 65 | 66 | ### IoT Cloud Resources 67 | The cloud provisioning resources (1-4 below) are created with an AWS Lambda custom resource in CloudFormation SubTemplates\IoT\Lambdas\provision_device\app.py. These assets for fleet provisioning include bootstrap certificates, a provisioning template, production certificates, and a Lambda provisioning hook. 68 | 69 | 1. Bootstrap Certificates: Certificates used with all edge devices that provide limited permissions policy shown below. The bootstrap policy allows the device to connect to the iot endpoint and publish/subsribe to topics to request production certificates via the provisioning template. 70 | 71 | ``` 72 | { 73 | "Version": "2012-10-17", 74 | "Statement": [ 75 | { 76 | "Effect": "Allow", 77 | "Action": [ 78 | "iot:Connect" 79 | ], 80 | "Resource": [ 81 | "*" 82 | ] 83 | }, 84 | { 85 | "Effect": "Allow", 86 | "Action": [ 87 | "iot:Publish", 88 | "iot:Receive" 89 | ], 90 | "Resource": [ 91 | "arn:aws:iot:$REGION:$ACCOUNT:topic/$aws/certificates/create/*", 92 | "arn:aws:iot:$REGION:$ACCOUNT:topic/$aws/provisioning-templates/$PROVTEMPLATE/provision/*" 93 | ] 94 | }, 95 | { 96 | "Effect": "Allow", 97 | "Action": [ 98 | "iot:Subscribe" 99 | ], 100 | "Resource": [ 101 | "arn:aws:iot:$REGION:$ACCOUNT:topicfilter/$aws/certificates/create/*", 102 | "arn:aws:iot:$REGION:$ACCOUNT:topicfilter/$aws/provisioning-templates/$PROVTEMPLATE/provision/*" 103 | ] 104 | } 105 | ] 106 | } 107 | ``` 108 | 109 | 2. Provisioning Template: Device onboarding asset that defines parameters required for production certificate request and defines production policy permissions. Production policy is provided in 3. 110 | 111 | ``` 112 | { 113 | "Parameters": { 114 | "SerialNumber": { 115 | "Type": "String" 116 | }, 117 | "ModelType": { 118 | "Type": "String" 119 | }, 120 | "AWS::IoT::Certificate::Id": { 121 | "Type": "String" 122 | } 123 | }, 124 | "Resources": { 125 | "certificate": { 126 | "Properties": { 127 | "CertificateId": { 128 | "Ref": "AWS::IoT::Certificate::Id" 129 | }, 130 | "Status": "Active" 131 | }, 132 | "Type": "AWS::IoT::Certificate" 133 | }, 134 | "policy": { 135 | "Properties": { 136 | "PolicyDocument": "" 137 | }, 138 | "Type": "AWS::IoT::Policy" 139 | }, 140 | "thing": { 141 | "OverrideSettings": { 142 | "AttributePayload": "MERGE", 143 | "ThingGroups": "DO_NOTHING", 144 | "ThingTypeName": "REPLACE" 145 | }, 146 | "Properties": { 147 | "AttributePayload": { 148 | "model_type": { 149 | "Ref": "ModelType" 150 | } 151 | }, 152 | "ThingGroups": [], 153 | "ThingName": { 154 | "Ref": "SerialNumber" 155 | } 156 | }, 157 | "Type": "AWS::IoT::Thing" 158 | } 159 | } 160 | } 161 | ``` 162 | 163 | 3. Production Certificates: The production certificates receive permissions based on the production policy. This policy grants full publish and subscribe permissions for topic containing device name. 164 | 165 | ``` 166 | { 167 | "Version": "2012-10-17", 168 | "Statement": [ 169 | { 170 | "Effect": "Allow", 171 | "Action": [ 172 | "iot:Connect" 173 | ], 174 | "Resource": [ 175 | "arn:aws:iot:$REGION:$ACCOUNT:client/${iot:Connection.Thing.ThingName}" 176 | ] 177 | }, 178 | { 179 | "Effect": "Allow", 180 | "Action": [ 181 | "iot:Publish" 182 | ], 183 | "Resource": [ 184 | "arn:aws:iot:$REGION:$ACCOUNT:topic/*/${iot:Connection.Thing.ThingName}/*" 185 | ] 186 | }, 187 | { 188 | "Effect": "Allow", 189 | "Action": [ 190 | "iot:Subscribe" 191 | ], 192 | "Resource": [ 193 | "arn:aws:iot:$REGION:$ACCOUNT:topicfilter/*/${iot:Connection.Thing.ThingName}/*" 194 | ] 195 | }, 196 | { 197 | "Effect": "Allow", 198 | "Action": [ 199 | "iot:Receive" 200 | ], 201 | "Resource": "arn:aws:iot:$REGION:$ACCOUNT:topic/*/${iot:Connection.Thing.ThingName}/*" 202 | } 203 | ] 204 | } 205 | ``` 206 | 207 | 4. Lambda Provisioning Hook: Lambda function with custom logic to evaluate if a device should receive the production policy permissions. This function can be used for tasks such as whitelisting devices based on serial number. 208 | 209 | 210 | ### Lambda Functions 211 | 212 | 1. DeviceProvisioningFunction: 213 | Location: SubTemplates\IoT\Lambdas\provision_device\app.py 214 | Trigger: Template creation 215 | Actions: 216 | - Create bootstrap policy 217 | - Create bootstrap certificates 218 | - Save bootstrap certs to S3 219 | - Create provisioning template 220 | - Create edge personalized client for your AWS account 221 | 222 | 2. DeviceProvisioningHookFunction: 223 | Location: SubTemplates\IoT\Lambdas\provision_hook\app.py 224 | Trigger: Fleet-provisioning client publish to topic 225 | Actions: 226 | - Logic to approve/deny device certificate requests 227 | 228 | 229 | ### Edge Client 230 | 231 | The edge client was adapted from https://github.com/aws-samples/aws-iot-fleet-provisioning. 232 | 233 | Edge Client: Python application with bootstrap certificates that runs on the edge device. The app establishes mqtt communication and exchanges the bootstrap certificates for the production certificates with elevated privileges. 234 | 235 | 236 | ### Infrastructure Teardown 237 | Teardown removes all SSM managed instances and IoT things/resources created using the edge client 238 | 239 | 1. Delete the CloudFormation root project assets stack {ResourceTag} 240 | 2. Delete the CloudFormation pipeline stack 241 | 3. Delete things created for project 242 | 4. Delete production policy for project 243 | 5. Delete production policy certificate for project -------------------------------------------------------------------------------- /SubTemplates/IoT/Lambdas/bootstrap_generator/app.py: -------------------------------------------------------------------------------- 1 | import json 2 | import boto3 3 | from datetime import date, datetime 4 | import json 5 | from urllib.request import urlopen 6 | from zipfile import ZipFile, ZIP_DEFLATED 7 | import io 8 | from io import BytesIO 9 | import os 10 | 11 | iotClient = boto3.client('iot') 12 | endpoint = boto3.client('iot-data') 13 | s3Client = boto3.client('s3') 14 | 15 | resourceTag = os.environ['ResourceTag'] 16 | region = os.environ['Region'] 17 | 18 | bootstrapPolicyName = '{}_birth_policy'.format(resourceTag) 19 | BUCKET_NAME = '{}-per-vendor-bootstraps'.format(resourceTag) 20 | rootCertUrl = "https://www.amazontrust.com/repository/AmazonRootCA1.pem" 21 | rootCert = urlopen(rootCertUrl) 22 | 23 | s3Client.create_bucket(Bucket=BUCKET_NAME, CreateBucketConfiguration={'LocationConstraint': region}) 24 | 25 | def handler(event, context): 26 | 27 | response = createModelBootstraps(event["models"]) 28 | 29 | return { 30 | 'statusCode': 200, 31 | 'body': {'models_added' : json.dumps(response)} 32 | } 33 | 34 | 35 | def createModelBootstraps(model_list): 36 | 37 | added_models = [] 38 | 39 | for model in model_list: 40 | items = s3Client.list_objects_v2( 41 | Bucket=BUCKET_NAME, 42 | Prefix=model 43 | ) 44 | 45 | if items["KeyCount"] == 0: 46 | certificates = iotClient.create_keys_and_certificate(setAsActive=True) 47 | iotClient.attach_policy(policyName=bootstrapPolicyName,target=certificates['certificateArn']) 48 | mem_zip = BytesIO() 49 | added_models.append(model) 50 | with ZipFile(mem_zip, mode="w", compression=ZIP_DEFLATED) as archive: 51 | archive.writestr('bootstrap-certificate.pem.crt', certificates['certificatePem']) 52 | archive.writestr('bootstrap-private.pem.key', certificates['keyPair']['PrivateKey']) 53 | archive.writestr('root.ca.pem', rootCert.read()) 54 | archive.writestr('{}.txt'.format(certificates['certificateId']), "") 55 | 56 | mem_zip.seek(0) 57 | s3Client.upload_fileobj(mem_zip, BUCKET_NAME,'{0}/{0}_bootstraps.zip'.format(model)) 58 | 59 | return added_models -------------------------------------------------------------------------------- /SubTemplates/IoT/Lambdas/bootstrap_generator/requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.23.0 -------------------------------------------------------------------------------- /SubTemplates/IoT/Lambdas/cert_rotation_hook/app.py: -------------------------------------------------------------------------------- 1 | import json 2 | import boto3 3 | from datetime import date, datetime, timedelta 4 | 5 | client = boto3.client('iot') 6 | endpoint = boto3.client('iot-data') 7 | 8 | #used to validate device actually needs a new cert 9 | CERT_ROTATION_DAYS = 360 10 | 11 | #validation check date for registry query 12 | target_date = date.today()-timedelta(days=CERT_ROTATION_DAYS) 13 | target_date = target_date.strftime("%Y%m%d") 14 | 15 | 16 | #short hand date 17 | d = date.today() 18 | 19 | #Set up payload with new cert issuance date 20 | provision_response = {'allowProvisioning': False, "parameterOverrides": { 21 | "CertDate": date.today().strftime("%Y%m%d")}} 22 | 23 | 24 | def handler(event, context): 25 | 26 | # Future log Cloudwatch logs 27 | print("Received event: " + json.dumps(event, indent=2)) 28 | 29 | thing_name = event['parameters']['DeviceSerial'] 30 | response = client.describe_thing( 31 | thingName=thing_name) 32 | 33 | try: 34 | #Cross reference ID of requester with entry in registery to ensure device needs a rotation. 35 | if int(response['attributes']['cert_issuance']) < int(target_date): 36 | deactivate_cert(thing_name) 37 | provision_response["allowProvisioning"] = True 38 | except: 39 | provision_response["allowProvisioning"] = False 40 | 41 | return provision_response 42 | 43 | def deactivate_cert(thing_id): 44 | 45 | #Get all the certificates for a thing 46 | principals = client.list_thing_principals( 47 | thingName=thing_id 48 | ) 49 | 50 | #Describe each certificate 51 | for arn in principals['principals']: 52 | cert_id = strip_arn(arn) 53 | cert = client.describe_certificate( 54 | certificateId=cert_id 55 | ) 56 | 57 | #strip timezone awareness for date compare 58 | cert_date = cert['certificateDescription']['creationDate'].replace(tzinfo=None) 59 | 60 | #Deactivate old certificates 61 | if cert_date < datetime.now() - timedelta(minutes=5): 62 | print(cert['certificateDescription']['creationDate']) 63 | client.update_certificate(certificateId=cert['certificateDescription']['certificateId'],newStatus='INACTIVE') 64 | client.detach_thing_principal(thingName=thing_id, principal=arn) 65 | 66 | 67 | def strip_arn(arn): 68 | index = arn.index('/') + 1 69 | return arn[index:] -------------------------------------------------------------------------------- /SubTemplates/IoT/Lambdas/cert_rotation_hook/requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /SubTemplates/IoT/Lambdas/cert_rotation_monitor/app.py: -------------------------------------------------------------------------------- 1 | import json 2 | import boto3 3 | from datetime import date, datetime, timedelta 4 | 5 | client = boto3.client('iot') 6 | endpoint = boto3.client('iot-data') 7 | 8 | #Set Cert Rotation Interval 9 | CERT_ROTATION_DAYS = 360 10 | GRACE_PERIOD = 14 11 | 12 | #Thingname will be post-pended 13 | ALERT_TOPIC = 'cmd' 14 | 15 | #short hand date 16 | d = date.today() 17 | 18 | #Check for certificate expiry 19 | target_date = d-timedelta(days=CERT_ROTATION_DAYS) 20 | 21 | #Set up deactivation trigger 22 | trigger_date = target_date-timedelta(days=GRACE_PERIOD) 23 | 24 | #Convery to numeric format 25 | target_date = target_date.strftime("%Y%m%d") 26 | trigger_date = trigger_date.strftime("%Y%m%d") 27 | 28 | 29 | def handler(event, context): 30 | 31 | overdue_things = get_overdue_things(target_date) 32 | 33 | for thing in overdue_things['things']: 34 | print(thing) 35 | endpoint.publish( 36 | topic='{}/{}/alerts'.format(ALERT_TOPIC,thing['thingName']), 37 | payload='{"msg":"rotate_cert"}' 38 | ) 39 | 40 | if thing['attributes']["cert_issuance"] >= trigger_date: 41 | deactivate_cert(thing['thingName']) 42 | 43 | return { 44 | 'notified_things': overdue_things['things'] 45 | 46 | } 47 | 48 | 49 | def deactivate_cert(thing_id): 50 | 51 | #Get all the certificates for a thing 52 | principals = client.list_thing_principals( 53 | thingName=thing_id 54 | ) 55 | 56 | #Describe each certificate 57 | for arn in principals['principals']: 58 | cert_id = strip_arn(arn) 59 | cert = client.describe_certificate( 60 | certificateId=cert_id 61 | ) 62 | 63 | #strip timezone awareness for date compare 64 | cert_date = cert['certificateDescription']['creationDate'].replace(tzinfo=None) 65 | 66 | #Deactivate old certificates 67 | if cert_date < datetime.now() - timedelta(minutes=5): 68 | activation_response = client.update_certificate( 69 | certificateId=cert['certificateDescription']['certificateId'], 70 | newStatus='INACTIVE') 71 | client.detach_thing_principal(thingName=thing_id, principal=arn) 72 | 73 | 74 | def get_overdue_things(by_date): 75 | response = client.search_index( 76 | queryString='attributes.cert_issuance<{}'.format(by_date), 77 | maxResults=500) 78 | return response 79 | 80 | def strip_arn(arn): 81 | index = arn.index('/') + 1 82 | return arn[index:] -------------------------------------------------------------------------------- /SubTemplates/IoT/Lambdas/cert_rotation_monitor/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/automated-iot-fleet-provisioning-by-claim/6c09906b255a3ba3a96896921f638804e32265c9/SubTemplates/IoT/Lambdas/cert_rotation_monitor/requirements.txt -------------------------------------------------------------------------------- /SubTemplates/IoT/Lambdas/provision_device/app.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | import cfnresponse 5 | import boto3 6 | import os 7 | import sys 8 | import json 9 | from urllib.request import urlopen 10 | from zipfile import ZipFile, ZIP_DEFLATED 11 | import io 12 | from io import BytesIO 13 | 14 | iotClient = boto3.client('iot') 15 | s3Client = boto3.client('s3') 16 | 17 | resourceTag = os.environ['ResourceTag'] 18 | bucket = os.environ['BootstrapCertsBucket'] 19 | account = os.environ['Account'] 20 | region = os.environ['Region'] 21 | registrationRoleArn = os.environ['RegistrationRoleArn'] 22 | prodLambdaHookArn = os.environ['ProdLambdaHookArn'] 23 | rotateLambdaHookArn = os.environ['RotateLambdaHookArn'] 24 | 25 | bootstrapPolicyName = '{}_birth_policy'.format(resourceTag) 26 | productionPolicyName = '{}_prod_policy'.format(resourceTag) 27 | 28 | prodTemplateName = '{}_prod_template_CFN'.format(resourceTag) 29 | rotateTemplateName = '{}_rotation_template_CFN'.format(resourceTag) 30 | 31 | prodTemplateArn = 'arn:aws:iot:{}:{}:provisioningtemplate/{}'.format( 32 | region, account, prodTemplateName) 33 | rotateTemplateArn = 'arn:aws:iot:{}:{}:provisioningtemplate/{}'.format( 34 | region, account, rotateTemplateName) 35 | 36 | bootstrapPrefix = 'bootstrap' 37 | rootCertUrl = "https://www.amazontrust.com/repository/AmazonRootCA1.pem" 38 | certLocation = "certs/bootstrap-certificate.pem.crt" 39 | keyLocation = "certs/bootstrap-private.pem.key" 40 | scriptPath = os.path.dirname(__file__) 41 | 42 | 43 | def s3Put(bucket, key, body): 44 | s3Client.put_object( 45 | Body=body, 46 | Bucket=bucket, 47 | Key=key 48 | ) 49 | 50 | 51 | def s3UploadFileObject(data, key): 52 | s3Client.upload_fileobj(data, bucket, key) 53 | 54 | 55 | def s3List(): 56 | return s3Client.list_objects( 57 | Bucket=bucket 58 | ) 59 | 60 | 61 | def s3Delete(bucket, key): 62 | s3Client.delete_object( 63 | Bucket=bucket, 64 | Key=key 65 | ) 66 | 67 | 68 | def clearBootstrapPolicy(): 69 | items = s3List() 70 | for key in items['Contents']: 71 | if key['Key'].split('/')[-1].split('.')[1] == 'id': 72 | certId = key['Key'].split('/')[-1].split('.')[0] 73 | 74 | iotClient.update_certificate( 75 | certificateId=certId, 76 | newStatus='INACTIVE' 77 | ) 78 | iotClient.delete_certificate( 79 | certificateId=certId, 80 | forceDelete=True 81 | ) 82 | for fileobject in items['Contents']: 83 | s3Delete(bucket, fileobject['Key']) 84 | 85 | iotClient.delete_provisioning_template( 86 | templateName=prodTemplateName 87 | ) 88 | 89 | iotClient.delete_provisioning_template( 90 | templateName=rotateTemplateName 91 | ) 92 | iotClient.delete_policy( 93 | policyName=bootstrapPolicyName 94 | ) 95 | 96 | 97 | 98 | def getIoTEndpoint(): 99 | result = iotClient.describe_endpoint( 100 | endpointType='iot:Data-ATS' 101 | ) 102 | return result['endpointAddress'] 103 | 104 | 105 | def updateConfig(fullPath, filename, iotEndpoint): 106 | with open(fullPath, 'r') as config: 107 | data = config.read() 108 | if filename == 'config.ini': 109 | data = data.replace('$ENTER_ENDPOINT_HERE', iotEndpoint) 110 | data = data.replace('$ENTER_TEMPLATE_NAME_HERE', prodTemplateName) 111 | data = data.replace('$ENTER_CERT_ROTATION_TEMPLATE_HERE', rotateTemplateName) 112 | return data 113 | 114 | 115 | def createClient(certificates, iotEndpoint): 116 | mem_zip = BytesIO() 117 | clientDir = "{}/{}".format(scriptPath, 'client') 118 | 119 | with ZipFile(mem_zip, mode="w", compression=ZIP_DEFLATED) as client: 120 | for root, subFolder, files in os.walk(clientDir): 121 | for file in files: 122 | fullPath = root + '/' + file 123 | data = updateConfig(fullPath, file, iotEndpoint) 124 | client.writestr(fullPath.split('client/')[1], data) 125 | print('got to here') 126 | client.writestr(certLocation, certificates['certificatePem']) 127 | client.writestr(keyLocation, certificates['keyPair']['PrivateKey']) 128 | rootCert = urlopen(rootCertUrl) 129 | client.writestr("certs/root.ca.pem", rootCert.read()) 130 | mem_zip.seek(0) 131 | return mem_zip 132 | 133 | 134 | def createBootstrapPolicy(): 135 | with open('artifacts/bootstrapPolicy.json', 'r') as bsp: 136 | bootstrapPolicy = bsp.read().replace( 137 | '$REGION:$ACCOUNT', '{}:{}'.format(region, account)) 138 | bootstrapPolicy = bootstrapPolicy.replace( 139 | '$PROVTEMPLATE', prodTemplateName) 140 | 141 | bootstrapPolicy = json.loads(bootstrapPolicy) 142 | 143 | certificates = iotClient.create_keys_and_certificate( 144 | setAsActive=True 145 | ) 146 | iotClient.create_policy( 147 | policyName=bootstrapPolicyName, 148 | policyDocument=json.dumps(bootstrapPolicy) 149 | ) 150 | iotClient.attach_policy( 151 | policyName=bootstrapPolicyName, 152 | target=certificates['certificateArn'] 153 | ) 154 | 155 | return certificates 156 | 157 | 158 | def createProductionPolicy(): 159 | with open('artifacts/productionPolicy.json', 'r') as pp: 160 | productionPolicy = pp.read().replace( 161 | '$REGION:$ACCOUNT', '{}:{}'.format(region, account)) 162 | productionPolicy = productionPolicy.replace( 163 | '$PROVTEMPLATE', rotateTemplateName) 164 | productionPolicy = json.loads(productionPolicy) 165 | 166 | iotClient.create_policy( 167 | policyName=productionPolicyName, 168 | policyDocument=json.dumps(productionPolicy) 169 | ) 170 | 171 | 172 | def uploadClientToS3(certificates, client): 173 | Id = certificates['certificateId'] 174 | s3Put(bucket, "{}/{}.id".format(bootstrapPrefix, Id), Id) 175 | s3UploadFileObject(client, 'client.zip') 176 | 177 | 178 | def createTemplateBody(filePath): 179 | with open(filePath, 'r') as pt: 180 | provisioningTemplate = json.load(pt) 181 | provisioningTemplate['Resources']['policy']['Properties']['PolicyName'] = productionPolicyName 182 | print(provisioningTemplate) 183 | return provisioningTemplate 184 | 185 | 186 | def createTemplate(templateBody, templateName, lambdaHookArn): 187 | print(templateBody) 188 | print(templateName) 189 | print(lambdaHookArn) 190 | iotClient.create_provisioning_template( 191 | templateName=templateName, 192 | description=resourceTag + ' Provisioning Template', 193 | templateBody=json.dumps(templateBody), 194 | enabled=True, 195 | provisioningRoleArn=registrationRoleArn, 196 | preProvisioningHook={ 197 | 'targetArn': lambdaHookArn 198 | } 199 | ) 200 | 201 | def createModelBootstraps(): 202 | model_bucket = '{}-per-vendor-bootstraps'.format(resourceTag) 203 | 204 | with open('artifacts/models.txt', 'r') as rows: 205 | s3Client.create_bucket(Bucket=model_bucket, CreateBucketConfiguration={'LocationConstraint': region}) 206 | models = rows.read().splitlines() 207 | rootCert = urlopen(rootCertUrl) 208 | 209 | for model in models: 210 | #create certificates 211 | certificates = iotClient.create_keys_and_certificate(setAsActive=True) 212 | iotClient.attach_policy(policyName=bootstrapPolicyName,target=certificates['certificateArn']) 213 | mem_zip = BytesIO() 214 | 215 | with ZipFile(mem_zip, mode="w", compression=ZIP_DEFLATED) as archive: 216 | archive.writestr('bootstrap-certificate.pem.crt', certificates['certificatePem']) 217 | archive.writestr('bootstrap-private.pem.key', certificates['keyPair']['PrivateKey']) 218 | archive.writestr('root.ca.pem', rootCert.read()) 219 | mem_zip.seek(0) 220 | s3Client.upload_fileobj(mem_zip, model_bucket,'{0}/{0}_bootstraps.zip'.format(model)) 221 | 222 | def handler(event, context): 223 | responseData = {} 224 | print(event) 225 | try: 226 | 227 | result = cfnresponse.FAILED 228 | if event['RequestType'] == 'Create': 229 | certificates = createBootstrapPolicy() 230 | createProductionPolicy() 231 | iotEndpoint = getIoTEndpoint() 232 | print('iotendpoint') 233 | client = createClient(certificates, iotEndpoint) 234 | print('client created') 235 | uploadClientToS3(certificates, client) 236 | print('client uploaded') 237 | #create provisioning templates 238 | prodTemplateBody = createTemplateBody('artifacts/provisioningTemplate.json') 239 | createTemplate(prodTemplateBody, prodTemplateName, prodLambdaHookArn) 240 | rotateTemplateBody = createTemplateBody('artifacts/certRotationTemplate.json') 241 | createTemplate(rotateTemplateBody, rotateTemplateName, rotateLambdaHookArn) 242 | createModelBootstraps() 243 | result = cfnresponse.SUCCESS 244 | elif event['RequestType'] == 'Update': 245 | #create provisioning templates 246 | print('UPDATE FIRED!') 247 | createModelBootstraps() 248 | result = cfnresponse.SUCCESS 249 | else: 250 | clearBootstrapPolicy() 251 | result = cfnresponse.SUCCESS 252 | 253 | except Exception as e: 254 | print('error', e) 255 | result = cfnresponse.FAILED 256 | 257 | sys.stdout.flush() 258 | print(responseData) 259 | cfnresponse.send(event, context, result, responseData) 260 | -------------------------------------------------------------------------------- /SubTemplates/IoT/Lambdas/provision_device/artifacts/bootstrapPolicy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "iot:Connect" 8 | ], 9 | "Resource": [ 10 | "*" 11 | ] 12 | }, 13 | { 14 | "Effect": "Allow", 15 | "Action": [ 16 | "iot:Publish", 17 | "iot:Receive" 18 | ], 19 | "Resource": [ 20 | "arn:aws:iot:$REGION:$ACCOUNT:topic/$aws/certificates/create/*", 21 | "arn:aws:iot:$REGION:$ACCOUNT:topic/$aws/provisioning-templates/$PROVTEMPLATE/provision/*" 22 | ] 23 | }, 24 | { 25 | "Effect": "Allow", 26 | "Action": [ 27 | "iot:Subscribe" 28 | ], 29 | "Resource": [ 30 | "arn:aws:iot:$REGION:$ACCOUNT:topicfilter/$aws/certificates/create/*", 31 | "arn:aws:iot:$REGION:$ACCOUNT:topicfilter/$aws/provisioning-templates/$PROVTEMPLATE/provision/*" 32 | ] 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /SubTemplates/IoT/Lambdas/provision_device/artifacts/certRotationTemplate.json: -------------------------------------------------------------------------------- 1 | { 2 | "Parameters": { 3 | "DeviceSerial": { 4 | "Type": "String" 5 | }, 6 | "CertDate": { 7 | "Type": "String" 8 | }, 9 | "AWS::IoT::Certificate::Id": { 10 | "Type": "String" 11 | } 12 | }, 13 | "Resources": { 14 | "certificate": { 15 | "Properties": { 16 | "CertificateId": { 17 | "Ref": "AWS::IoT::Certificate::Id" 18 | }, 19 | "Status": "Active" 20 | }, 21 | "Type": "AWS::IoT::Certificate" 22 | }, 23 | "policy": { 24 | "Properties": { 25 | "PolicyName": "" 26 | }, 27 | "Type": "AWS::IoT::Policy" 28 | }, 29 | "thing": { 30 | "OverrideSettings": { 31 | "AttributePayload": "REPLACE", 32 | "ThingGroups": "REPLACE", 33 | "ThingTypeName": "REPLACE" 34 | }, 35 | "Properties": { 36 | "AttributePayload": { 37 | "cert_issuance": { 38 | "Ref": "CertDate" 39 | } 40 | }, 41 | "ThingGroups": [], 42 | "ThingName": { 43 | "Ref": "DeviceSerial" 44 | } 45 | }, 46 | "Type": "AWS::IoT::Thing" 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /SubTemplates/IoT/Lambdas/provision_device/artifacts/models.txt: -------------------------------------------------------------------------------- 1 | Model-A 2 | Model-B 3 | Model-C 4 | Model-D 5 | Model-E 6 | Model-F 7 | Model-G 8 | Model-H 9 | Model-I -------------------------------------------------------------------------------- /SubTemplates/IoT/Lambdas/provision_device/artifacts/productionPolicy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "iot:Connect" 8 | ], 9 | "Resource": [ 10 | "arn:aws:iot:$REGION:$ACCOUNT:client/${iot:Connection.Thing.ThingName}" 11 | ] 12 | }, 13 | { 14 | "Effect": "Allow", 15 | "Action": [ 16 | "iot:Publish", 17 | "iot:Receive" 18 | ], 19 | "Resource": [ 20 | "arn:aws:iot:$REGION:$ACCOUNT:topic/$aws/certificates/create/*", 21 | "arn:aws:iot:$REGION:$ACCOUNT:topic/$aws/provisioning-templates/$PROVTEMPLATE/provision/*", 22 | "arn:aws:iot:$REGION:$ACCOUNT:topic/*/${iot:Connection.Thing.ThingName}/*" 23 | 24 | ] 25 | }, 26 | { 27 | "Effect": "Allow", 28 | "Action": [ 29 | "iot:Subscribe" 30 | ], 31 | "Resource": [ 32 | "arn:aws:iot:$REGION:$ACCOUNT:topicfilter/$aws/certificates/create/*", 33 | "arn:aws:iot:$REGION:$ACCOUNT:topicfilter/$aws/provisioning-templates/$PROVTEMPLATE/provision/*", 34 | "arn:aws:iot:$REGION:$ACCOUNT:topicfilter/$aws/certificates/create/*", 35 | "arn:aws:iot:$REGION:$ACCOUNT:topicfilter/$aws/provisioning-templates/$PROVTEMPLATE/provision/*", 36 | "arn:aws:iot:$REGION:$ACCOUNT:topicfilter/*/${iot:Connection.Thing.ThingName}/*" 37 | 38 | ] 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /SubTemplates/IoT/Lambdas/provision_device/artifacts/provisioningTemplate.json: -------------------------------------------------------------------------------- 1 | { 2 | "Parameters": { 3 | "CertDate": { 4 | "Type": "String" 5 | }, 6 | "DeviceSerial": { 7 | "Type": "String" 8 | }, 9 | "AWS::IoT::Certificate::Id": { 10 | "Type": "String" 11 | } 12 | }, 13 | "Resources": { 14 | "certificate": { 15 | "Properties": { 16 | "CertificateId": { 17 | "Ref": "AWS::IoT::Certificate::Id" 18 | }, 19 | "Status": "Active" 20 | }, 21 | "Type": "AWS::IoT::Certificate" 22 | }, 23 | "policy": { 24 | "Properties": { 25 | "PolicyName": "" 26 | }, 27 | "Type": "AWS::IoT::Policy" 28 | }, 29 | "thing": { 30 | "OverrideSettings": { 31 | "AttributePayload": "MERGE", 32 | "ThingGroups": "DO_NOTHING", 33 | "ThingTypeName": "REPLACE" 34 | }, 35 | "Properties": { 36 | "AttributePayload": { 37 | "cert_issuance": { 38 | "Ref": "CertDate" 39 | } 40 | }, 41 | "ThingGroups": [], 42 | "ThingName": { 43 | "Ref": "DeviceSerial" 44 | } 45 | }, 46 | "Type": "AWS::IoT::Thing" 47 | } 48 | }, 49 | "DeviceConfiguration": { 50 | } 51 | } -------------------------------------------------------------------------------- /SubTemplates/IoT/Lambdas/provision_device/cfnresponse.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Amazon Web Services, Inc. or its affiliates. All Rights Reserved. 2 | # This file is licensed to you under the AWS Customer Agreement (the "License"). 3 | # You may not use this file except in compliance with the License. 4 | # A copy of the License is located at http://aws.amazon.com/agreement/ . 5 | # This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. 6 | # See the License for the specific language governing permissions and limitations under the License. 7 | 8 | import requests 9 | import json 10 | 11 | SUCCESS = "SUCCESS" 12 | FAILED = "FAILED" 13 | 14 | 15 | def send(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False): 16 | responseUrl = event['ResponseURL'] 17 | 18 | print(responseUrl) 19 | 20 | responseBody = {} 21 | responseBody['Status'] = responseStatus 22 | responseBody['Reason'] = 'See the details in CloudWatch Log Stream: ' + \ 23 | context.log_stream_name 24 | responseBody['PhysicalResourceId'] = physicalResourceId or context.log_stream_name 25 | responseBody['StackId'] = event['StackId'] 26 | responseBody['RequestId'] = event['RequestId'] 27 | responseBody['LogicalResourceId'] = event['LogicalResourceId'] 28 | responseBody['NoEcho'] = noEcho 29 | responseBody['Data'] = responseData 30 | 31 | json_responseBody = json.dumps(responseBody) 32 | 33 | print("Response body:\n" + json_responseBody) 34 | 35 | headers = { 36 | 'content-type': '', 37 | 'content-length': str(len(json_responseBody)) 38 | } 39 | 40 | try: 41 | response = requests.put(responseUrl, 42 | data=json_responseBody, 43 | headers=headers) 44 | print("Status code: " + response.reason) 45 | except Exception as e: 46 | print("send(..) failed executing requests.put(..): " + str(e)) 47 | -------------------------------------------------------------------------------- /SubTemplates/IoT/Lambdas/provision_device/client/__pycache__/provisioning_handler.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/automated-iot-fleet-provisioning-by-claim/6c09906b255a3ba3a96896921f638804e32265c9/SubTemplates/IoT/Lambdas/provision_device/client/__pycache__/provisioning_handler.cpython-37.pyc -------------------------------------------------------------------------------- /SubTemplates/IoT/Lambdas/provision_device/client/config.ini: -------------------------------------------------------------------------------- 1 | [SETTINGS] 2 | # Set the name of your IoT Endpoint 3 | IOT_ENDPOINT = $ENTER_ENDPOINT_HERE 4 | 5 | # Set the path to the location containing your certificates (root, private, claim certificate) 6 | SECURE_CERT_PATH = ./certs 7 | 8 | # Path to device specific parameters 9 | MACHINE_CONFIG_PATH = ./machine_config.json 10 | 11 | # Specify the names for the root cert, provisioning claim cert, and the private key. 12 | ROOT_CERT = root.ca.pem 13 | CLAIM_CERT = bootstrap-certificate.pem.crt 14 | SECURE_KEY = bootstrap-private.pem.key 15 | 16 | # Include the name for the provisioning templates that were created in IoT Core 17 | PRODUCTION_TEMPLATE = $ENTER_TEMPLATE_NAME_HERE 18 | CERT_ROTATION_TEMPLATE = $ENTER_CERT_ROTATION_TEMPLATE_HERE 19 | 20 | 21 | -------------------------------------------------------------------------------- /SubTemplates/IoT/Lambdas/provision_device/client/machine_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "serial_num": "12345ABCD", 3 | "model_type": "modelT" 4 | } -------------------------------------------------------------------------------- /SubTemplates/IoT/Lambdas/provision_device/client/main.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | # ----------------------------------------- 5 | # Consuming sample, demonstrating how a device process would leverage the provisioning class. 6 | # The handler makes use of the asycio library and therefore requires Python 3.7. 7 | # 8 | # Prereq's: 9 | # 1) A provisioning claim certificate has been cut from AWSIoT. 10 | # 2) A restrictive "birth" policy has been associated with the certificate. 11 | # 3) A provisioning template was created to manage the activities to be performed during new certificate activation. 12 | # 4) The claim certificate was placed securely on the device fleet and shipped to the field. (along with root/ca and private key) 13 | # 14 | # Execution: 15 | # 1) The paths to the certificates, and names of IoTCore endpoint and provisioning template are set in config.ini (this project) 16 | # 2) A device boots up and encounters it's "first run" experience and executes the process (main) below. 17 | # 3) The process instatiates a handler that uses the bootstrap certificate to connect to IoTCore. 18 | # 4) The connection only enables calls to the Foundry provisioning services, where a new certificate is requested. 19 | # 5) The certificate is assembled from the response payload, and a foundry service call is made to activate the certificate. 20 | # 6) The provisioning template executes the instructions provided and the process rotates to the new certificate. 21 | # 7) Using the new certificate, a pub/sub call is demonstrated on a previously forbidden topic to test the new certificate. 22 | # 8) New certificates are saved locally, and can be stored/consumed as the application deems necessary. 23 | # 24 | # 25 | # Initial version - Raleigh Murch, AWS 26 | # email: murchral@amazon.com 27 | # ------------------------------------------------------------------------------ 28 | 29 | from provisioning_handler import ProvisioningHandler 30 | from utils.config_loader import Config 31 | from pyfiglet import Figlet 32 | 33 | 34 | #Set Config path 35 | CONFIG_PATH = 'config.ini' 36 | 37 | config = Config(CONFIG_PATH) 38 | config_parameters = config.get_section('SETTINGS') 39 | secure_cert_path = config_parameters['SECURE_CERT_PATH'] 40 | bootstrap_cert = config_parameters['CLAIM_CERT'] 41 | 42 | # Demo Theater 43 | f = Figlet(font='slant') 44 | print(f.renderText(' F l e e t')) 45 | print(f.renderText('Provisioning')) 46 | print(f.renderText('----------')) 47 | 48 | # Provided callback for provisioning method feedback. 49 | def callback(payload): 50 | print(payload) 51 | 52 | # Used to kick off the provisioning lifecycle, exchanging the bootstrap cert for a 53 | # production certificate after being validated by a provisioning hook lambda. 54 | # 55 | # isRotation = True is used to rotate from one production certificate to a new production certificate. 56 | # Certificates signed by AWS IoT Root CA expire on 12/31/2049. Security best practices 57 | # urge frequent rotation of x.509 certificates and this method (used in conjunction with 58 | # a cloud cert management pattern) attempt to make cert exchange easy. 59 | def run_provisioning(isRotation=False): 60 | 61 | provisioner = ProvisioningHandler(CONFIG_PATH) 62 | 63 | if isRotation: 64 | provisioner.get_official_certs(callback, isRotation=True) 65 | else: 66 | #Check for availability of bootstrap cert 67 | try: 68 | with open("{}/{}".format(secure_cert_path, bootstrap_cert)) as f: 69 | # Call super-method to perform aquisition/activation 70 | # of certs, creation of thing, etc. Returns general 71 | # purpose callback at this point. 72 | # Instantiate provisioning handler, pass in path to config 73 | provisioner.get_official_certs(callback) 74 | 75 | except IOError: 76 | print("### Bootstrap cert non-existent. Official cert may already be in place.") 77 | 78 | if __name__ == "__main__": 79 | run_provisioning() 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /SubTemplates/IoT/Lambdas/provision_device/client/provisioning_handler.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | # ------------------------------------------------------------------------------ 5 | # Demonstrates how to call/orchestrate AWS fleet provisioning services 6 | # with a provided bootstrap certificate (aka - provisioning claim cert). 7 | # 8 | # Initial version - Raleigh Murch, AWS 9 | # email: murchral@amazon.com 10 | # ------------------------------------------------------------------------------ 11 | 12 | 13 | from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTClient 14 | import AWSIoTPythonSDK.exception 15 | from utils.config_loader import Config 16 | import time 17 | import logging 18 | import json 19 | import os 20 | import asyncio 21 | import glob 22 | import ssl 23 | import sys 24 | 25 | class ProvisioningHandler: 26 | 27 | def __init__(self, file_path): 28 | """Initializes the provisioning handler 29 | 30 | Arguments: 31 | file_path {string} -- path to your configuration file 32 | """ 33 | #Logging 34 | logging.basicConfig(level=logging.ERROR) 35 | self.logger = logging.getLogger(__name__) 36 | 37 | #Load configuration settings from config.ini 38 | config = Config(file_path) 39 | self.config_parameters = config.get_section('SETTINGS') 40 | self.secure_cert_path = self.config_parameters['SECURE_CERT_PATH'] 41 | self.iot_endpoint = self.config_parameters['IOT_ENDPOINT'] 42 | self.template_name = self.config_parameters['PRODUCTION_TEMPLATE'] 43 | self.rotation_template = self.config_parameters['CERT_ROTATION_TEMPLATE'] 44 | self.claim_cert = self.config_parameters['CLAIM_CERT'] 45 | self.secure_key = self.config_parameters['SECURE_KEY'] 46 | self.root_cert = self.config_parameters['ROOT_CERT'] 47 | self.machine_config = self.config_parameters['MACHINE_CONFIG_PATH'] 48 | 49 | with open(self.machine_config) as json_file: 50 | data = json.load(json_file) 51 | self.serial_num = data['serial_num'] 52 | self.model_type = data['model_type'] 53 | 54 | self.unique_id = self.serial_num 55 | 56 | # ------------------------------------------------------------------------------ 57 | # -- PROVISIONING HOOKS EXAMPLE -- 58 | # Provisioning Hooks are a powerful feature for fleet provisioning. Most of the 59 | # heavy lifting is performed within the cloud lambda. However, you can send 60 | # device attributes to be validated by the lambda. An example is show in the line 61 | # below (.hasValidAccount could be checked in the cloud against a database). 62 | # Alternatively, a serial number, geo-location, or any attribute could be sent. 63 | # 64 | # -- Note: This attribute is passed up as part of the register_thing method and 65 | # will be validated in your lambda's event data. 66 | # ------------------------------------------------------------------------------ 67 | 68 | self.primary_MQTTClient = AWSIoTMQTTClient(self.unique_id) 69 | self.test_MQTTClient = AWSIoTMQTTClient(self.unique_id) 70 | self.primary_MQTTClient.onMessage = self.on_message_callback 71 | self.callback_returned = False 72 | self.message_payload = {} 73 | self.isRotation = False 74 | 75 | 76 | def core_connect(self): 77 | """ Method used to connect to connect to AWS IoTCore Service. Endpoint collected from config. 78 | 79 | """ 80 | if self.isRotation: 81 | self.logger.info('##### CONNECTING WITH EXISTING CERT #####') 82 | print('##### CONNECTING WITH EXISTING CERT #####') 83 | self.get_current_certs() 84 | else: 85 | self.logger.info('##### CONNECTING WITH PROVISIONING CLAIM CERT #####') 86 | print('##### CONNECTING WITH PROVISIONING CLAIM CERT #####') 87 | 88 | self.primary_MQTTClient.configureEndpoint(self.iot_endpoint, 8883) 89 | self.primary_MQTTClient.configureCredentials("{}/{}".format(self.secure_cert_path, 90 | self.root_cert), "{}/{}".format(self.secure_cert_path, self.secure_key), 91 | "{}/{}".format(self.secure_cert_path, self.claim_cert)) 92 | self.primary_MQTTClient.configureOfflinePublishQueueing(-1) 93 | self.primary_MQTTClient.configureDrainingFrequency(2) 94 | self.primary_MQTTClient.configureConnectDisconnectTimeout(10) 95 | self.primary_MQTTClient.configureMQTTOperationTimeout(3) 96 | 97 | self.primary_MQTTClient.connect() 98 | 99 | def get_current_certs(self): 100 | """ Gets the currently active production certificates for a device. 101 | """ 102 | non_bootstrap_certs = glob.glob('{}/[!boot]*.crt'.format(self.secure_cert_path)) 103 | non_bootstrap_key = glob.glob('{}/[!boot]*.key'.format(self.secure_cert_path)) 104 | 105 | #Get the current cert 106 | if len(non_bootstrap_certs) > 0: 107 | self.claim_cert = os.path.basename(non_bootstrap_certs[0]) 108 | 109 | #Get the current key 110 | if len(non_bootstrap_key) > 0: 111 | self.secure_key = os.path.basename(non_bootstrap_key[0]) 112 | 113 | 114 | def enable_error_monitor(self): 115 | """ Subscribe to pertinent IoTCore topics that would emit errors 116 | """ 117 | self.primary_MQTTClient.subscribe("$aws/provisioning-templates/{}/provision/json/rejected".format(self.template_name), 1, callback=self.basic_callback) 118 | self.primary_MQTTClient.subscribe("$aws/certificates/create/json/rejected", 1, callback=self.basic_callback) 119 | 120 | 121 | def get_official_certs(self, callback, isRotation=False): 122 | """ Initiates an async loop/call to kick off the provisioning flow. 123 | 124 | Triggers: 125 | on_message_callback() providing the certificate payload 126 | """ 127 | if isRotation: 128 | self.template_name = self.rotation_template 129 | self.isRotation = True 130 | 131 | return asyncio.run(self.orchestrate_provisioning_flow(callback)) 132 | 133 | async def orchestrate_provisioning_flow(self,callback): 134 | # Connect to core with provision claim creds 135 | try: 136 | self.core_connect() 137 | except ssl.SSLError: 138 | print("Duplicate prod certs exist in your cert directory. Remove any certs not associated with device.") 139 | sys.exit() 140 | 141 | 142 | # Monitor topics for errors 143 | self.enable_error_monitor() 144 | 145 | # Make a publish call to topic to get official certs 146 | self.primary_MQTTClient.publish("$aws/certificates/create/json", "{}", 0) 147 | 148 | # Wait the function return until all callbacks have returned 149 | # Returned denoted when callback flag is set in this class. 150 | while not self.callback_returned: 151 | await asyncio.sleep(0) 152 | 153 | return callback(self.message_payload) 154 | 155 | 156 | 157 | def on_message_callback(self, message): 158 | """ Callback Message handler responsible for workflow routing of msg responses from provisioning services. 159 | 160 | Arguments: 161 | message {string} -- The response message payload. 162 | """ 163 | json_data = json.loads(message.payload) 164 | 165 | # A response has been recieved from the service that contains certificate data. 166 | if 'certificateId' in json_data: 167 | self.logger.info('##### SUCCESS. SAVING KEYS TO DEVICE! #####') 168 | print('##### SUCCESS. SAVING KEYS TO DEVICE! #####') 169 | self.assemble_certificates(json_data) 170 | 171 | # A response contains acknowledgement that the provisioning template has been acted upon. 172 | elif 'deviceConfiguration' in json_data: 173 | if self.isRotation: 174 | self.logger.info('##### ACTIVATION COMPLETE #####') 175 | print('##### ACTIVATION COMPLETE #####') 176 | else: 177 | self.logger.info('##### CERT ACTIVATED AND THING {} CREATED #####'.format(json_data['thingName'])) 178 | print('##### CERT ACTIVATED AND THING {} CREATED #####'.format(json_data['thingName'])) 179 | 180 | self.validate_certs() 181 | elif 'statusCode' in json_data: 182 | if json_data['statusCode'] == 403: 183 | os.remove("{}/{}".format(self.secure_cert_path,self.new_key_name)) 184 | os.remove("{}/{}".format(self.secure_cert_path,self.new_cert_name)) 185 | else: 186 | self.logger.info(json_data) 187 | 188 | def assemble_certificates(self, payload): 189 | """ Method takes the payload and constructs/saves the certificate and private key. Method uses 190 | existing AWS IoT Core naming convention. 191 | 192 | Arguments: 193 | payload {string} -- Certifiable certificate/key data. 194 | 195 | Returns: 196 | ownership_token {string} -- proof of ownership from certificate issuance activity. 197 | """ 198 | ### Cert ID 199 | cert_id = payload['certificateId'] 200 | self.new_key_root = cert_id[0:10] 201 | 202 | self.new_cert_name = '{}-certificate.pem.crt'.format(self.new_key_root) 203 | ### Create certificate 204 | f = open('{}/{}'.format(self.secure_cert_path, self.new_cert_name), 'w+') 205 | f.write(payload['certificatePem']) 206 | f.close() 207 | 208 | 209 | ### Create private key 210 | self.new_key_name = '{}-private.pem.key'.format(self.new_key_root) 211 | f = open('{}/{}'.format(self.secure_cert_path, self.new_key_name), 'w+') 212 | f.write(payload['privateKey']) 213 | f.close() 214 | 215 | ### Extract/return Ownership token 216 | self.ownership_token = payload['certificateOwnershipToken'] 217 | 218 | #register newly aquired cert 219 | self.register_thing(self.unique_id, self.ownership_token) 220 | 221 | 222 | 223 | def register_thing(self, serial, token): 224 | """Calls the fleet provisioning service responsible for acting upon instructions within device templates. 225 | 226 | Arguments: 227 | serial {string} -- unique identifer for the thing. Specified as a property in provisioning template. 228 | token {string} -- The token response from certificate creation to prove ownership/immediate possession of the certs. 229 | 230 | Triggers: 231 | on_message_callback() - providing acknowledgement that the provisioning template was processed. 232 | """ 233 | if self.isRotation: 234 | self.logger.info('##### VALIDATING EXPIRY & ACTIVATING CERT #####') 235 | print('##### VALIDATING EXPIRY & ACTIVATING CERT #####') 236 | else: 237 | self.logger.info('##### CREATING THING ACTIVATING CERT #####') 238 | print('##### CREATING THING ACTIVATING CERT #####') 239 | 240 | 241 | 242 | register_template = {"certificateOwnershipToken": token, "parameters": {"DeviceSerial": serial}} 243 | 244 | #Register thing / activate certificate 245 | self.primary_MQTTClient.publish("$aws/provisioning-templates/{}/provision/json".format(self.template_name), json.dumps(register_template), 0) 246 | 247 | 248 | def validate_certs(self): 249 | """Responsible for (re)connecting to IoTCore with the newly provisioned/activated certificate - (first class citizen cert) 250 | """ 251 | self.logger.info('##### CONNECTING WITH OFFICIAL CERT #####') 252 | print('##### CONNECTING WITH OFFICIAL CERT #####') 253 | self.cert_validation_test() 254 | self.new_cert_pub_sub() 255 | print("##### ACTIVATED AND TESTED CREDENTIALS ({}, {}). #####".format(self.new_key_name, self.new_cert_name)) 256 | print("##### FILES SAVED TO {} #####".format(self.secure_cert_path)) 257 | 258 | def cert_validation_test(self): 259 | self.primary_MQTTClient.disconnectAsync() 260 | self.test_MQTTClient.configureEndpoint(self.iot_endpoint, 8883) 261 | self.test_MQTTClient.configureCredentials("{}/{}".format(self.secure_cert_path, 262 | self.root_cert), "{}/{}".format(self.secure_cert_path, self.new_key_name), 263 | "{}/{}".format(self.secure_cert_path, self.new_cert_name)) 264 | self.test_MQTTClient.configureOfflinePublishQueueing(-1) # Infinite offline Publish queueing 265 | self.test_MQTTClient.configureDrainingFrequency(2) # Draining: 2 Hz 266 | self.test_MQTTClient.configureConnectDisconnectTimeout(10) # 10 sec 267 | self.test_MQTTClient.configureMQTTOperationTimeout(3) # 5 sec 268 | self.test_MQTTClient.connect() 269 | 270 | def basic_callback(self, client, userdata, msg): 271 | """Method responding to the openworld publish attempt. Demonstrating a successful pub/sub with new certificate. 272 | """ 273 | self.logger.info(msg.payload.decode()) 274 | self.message_payload = msg.payload.decode() 275 | self.callback_returned = True 276 | 277 | 278 | def new_cert_pub_sub(self): 279 | """Method testing a call to the 'openworld' topic (which was specified in the policy for the new certificate) 280 | """ 281 | self.test_MQTTClient.subscribe("cmd/{}/alerts".format(self.unique_id), 1, self.basic_callback) 282 | self.test_MQTTClient.publish("cmd/{}/alerts".format(self.unique_id), str({"service_response": "##### RESPONSE FROM PREVIOUSLY FORBIDDEN TOPIC #####"}), 0) 283 | -------------------------------------------------------------------------------- /SubTemplates/IoT/Lambdas/provision_device/client/requirements.txt: -------------------------------------------------------------------------------- 1 | asyncio==3.4.3 2 | pyfiglet==0.8.post1 3 | AWSIoTPythonSDK==1.4.7 4 | 5 | -------------------------------------------------------------------------------- /SubTemplates/IoT/Lambdas/provision_device/client/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/automated-iot-fleet-provisioning-by-claim/6c09906b255a3ba3a96896921f638804e32265c9/SubTemplates/IoT/Lambdas/provision_device/client/utils/__init__.py -------------------------------------------------------------------------------- /SubTemplates/IoT/Lambdas/provision_device/client/utils/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/automated-iot-fleet-provisioning-by-claim/6c09906b255a3ba3a96896921f638804e32265c9/SubTemplates/IoT/Lambdas/provision_device/client/utils/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /SubTemplates/IoT/Lambdas/provision_device/client/utils/__pycache__/config_loader.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/automated-iot-fleet-provisioning-by-claim/6c09906b255a3ba3a96896921f638804e32265c9/SubTemplates/IoT/Lambdas/provision_device/client/utils/__pycache__/config_loader.cpython-37.pyc -------------------------------------------------------------------------------- /SubTemplates/IoT/Lambdas/provision_device/client/utils/config_loader.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | # ----------------------------------------- 5 | 6 | from configparser import ConfigParser 7 | 8 | 9 | class Config: 10 | def __init__(self, config_file_path): 11 | self.cf = ConfigParser() 12 | self.cf.optionxform = str 13 | self.config_file_path = config_file_path 14 | self.cf.read(self.config_file_path) 15 | 16 | def get_section(self, section): 17 | return dict(self.cf.items(section)) 18 | -------------------------------------------------------------------------------- /SubTemplates/IoT/Lambdas/provision_device/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.13.2 2 | requests==2.23.0 -------------------------------------------------------------------------------- /SubTemplates/IoT/Lambdas/provision_hook/app.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import date 3 | 4 | provision_response = { 5 | 'allowProvisioning': False, 6 | "parameterOverrides": {"CertDate": date.today().strftime("%Y%m%d")} 7 | } 8 | 9 | 10 | def handler(event, context): 11 | 12 | # Future log Cloudwatch logs 13 | print("Received event: " + json.dumps(event, indent=2)) 14 | 15 | ### Validate the claim with extreme prejudice here against back-end logic and whitelisting. 16 | ### If good ... 17 | provision_response["allowProvisioning"] = True 18 | 19 | return provision_response -------------------------------------------------------------------------------- /SubTemplates/IoT/Lambdas/provision_hook/requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.23.0 2 | responses==0.10.14 3 | -------------------------------------------------------------------------------- /SubTemplates/IoT/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: "Sub template" 4 | Parameters: 5 | ResourceTag: 6 | Type: String 7 | Description: Tag applied to all resources 8 | Globals: 9 | Function: 10 | Timeout: 3 11 | Runtime: python3.7 12 | Tags: 13 | Project: !Ref ResourceTag 14 | Environment: 15 | Variables: 16 | ResourceTag: !Ref ResourceTag 17 | Resources: 18 | BootstrapCerts: 19 | Type: AWS::S3::Bucket 20 | Properties: 21 | BucketEncryption: 22 | ServerSideEncryptionConfiguration: 23 | - ServerSideEncryptionByDefault: 24 | SSEAlgorithm: AES256 25 | 26 | FleetProvisioningFunction: 27 | Type: AWS::Serverless::Function 28 | Properties: 29 | Description: Sets up fleet provisioning 30 | CodeUri: Lambdas/provision_device/ 31 | Handler: app.handler 32 | Timeout: 360 33 | MemorySize: 3008 34 | Environment: 35 | Variables: 36 | BootstrapCertsBucket: !Ref BootstrapCerts 37 | Account: !Ref AWS::AccountId 38 | Region: !Ref AWS::Region 39 | RegistrationRoleArn: !Sub ${ThingsRegistrationRole.Arn} 40 | ProdLambdaHookArn: !Sub ${FleetProvisioningHookFunction.Arn} 41 | RotateLambdaHookArn: !Sub ${CertRotationHookFunction.Arn} 42 | Policies: 43 | - AWSLambdaBasicExecutionRole 44 | - AdministratorAccess 45 | 46 | FleetProvisioningCustom: 47 | Type: Custom::FleetProvisioning 48 | Properties: 49 | ServiceToken: !GetAtt FleetProvisioningFunction.Arn 50 | 51 | ThingsRegistrationRole: 52 | Type: AWS::IAM::Role 53 | Properties: 54 | RoleName: !Sub ${ResourceTag}-ThingRegistration 55 | AssumeRolePolicyDocument: 56 | Version: 2012-10-17 57 | Statement: 58 | - Effect: Allow 59 | Principal: 60 | Service: iot.amazonaws.com 61 | Action: sts:AssumeRole 62 | ManagedPolicyArns: 63 | - arn:aws:iam::aws:policy/service-role/AWSIoTThingsRegistration 64 | 65 | FleetProvisioningHookFunction: 66 | Type: AWS::Serverless::Function 67 | Properties: 68 | Description: Lambda hook for provisioning acceptance logic 69 | CodeUri: Lambdas/provision_hook/ 70 | Handler: app.handler 71 | Timeout: 10 72 | Policies: 73 | - AWSLambdaBasicExecutionRole 74 | Environment: 75 | Variables: 76 | ENV: dev 77 | 78 | 79 | FleetProvisioningHookPermission: 80 | Type: AWS::Lambda::Permission 81 | Properties: 82 | Action: lambda:InvokeFunction 83 | Principal: iot.amazonaws.com 84 | FunctionName: !Ref FleetProvisioningHookFunction 85 | SourceAccount: !Ref AWS::AccountId 86 | 87 | CertMonitorFunction: 88 | Type: AWS::Serverless::Function 89 | Properties: 90 | Description: Lambda to monitor expired certs 91 | CodeUri: Lambdas/cert_rotation_monitor/ 92 | Handler: app.handler 93 | Timeout: 10 94 | Events: 95 | CheckCertExpiryEvent: 96 | Type: Schedule 97 | Properties: 98 | Schedule: rate(1 minute) 99 | Policies: 100 | - AWSLambdaBasicExecutionRole 101 | - Version: "2012-10-17" 102 | Statement: 103 | - Effect: Allow 104 | Action: 105 | - cloudwatch:PutMetricData 106 | - iot:Receive 107 | - logs:CreateLogStream 108 | - logs:CreateLogGroup 109 | - iot:DescribeThing 110 | - iot:SearchIndex 111 | - iot:ListThingsInThingGroup 112 | - iot:ListThingPrincipals 113 | - iot:DescribeCertificate 114 | - iot:UpdateCertificate 115 | - iot:DetachThingPrincipal 116 | - iot:PutLogEvents 117 | - iot:Publish 118 | Resource: "*" 119 | CertRotationHookFunction: 120 | Type: AWS::Serverless::Function 121 | Properties: 122 | Description: Lambda to manage provisioning of rotating certs 123 | CodeUri: Lambdas/cert_rotation_hook/ 124 | Handler: app.handler 125 | Timeout: 10 126 | Policies: 127 | - AWSLambdaBasicExecutionRole 128 | - Version: "2012-10-17" 129 | Statement: 130 | - Effect: Allow 131 | Action: 132 | - cloudwatch:PutMetricData 133 | - iot:Receive 134 | - logs:CreateLogStream 135 | - logs:CreateLogGroup 136 | - iot:DescribeThing 137 | - iot:SearchIndex 138 | - iot:ListThingsInThingGroup 139 | - iot:ListThingPrincipals 140 | - iot:DescribeCertificate 141 | - iot:UpdateCertificate 142 | - iot:DetachThingPrincipal 143 | - iot:PutLogEvents 144 | - iot:Publish 145 | Resource: "*" 146 | 147 | CertRotationHookPermission: 148 | Type: AWS::Lambda::Permission 149 | Properties: 150 | Action: lambda:InvokeFunction 151 | Principal: iot.amazonaws.com 152 | FunctionName: !Ref CertRotationHookFunction 153 | SourceAccount: !Ref AWS::AccountId 154 | 155 | BootstrapGeneratorFunction: 156 | Type: AWS::Serverless::Function 157 | Properties: 158 | Description: Creates bootstraps for each model type presented 159 | CodeUri: Lambdas/bootstrap_generator/ 160 | Handler: app.handler 161 | Timeout: 360 162 | MemorySize: 3008 163 | Environment: 164 | Variables: 165 | Region: !Ref AWS::Region 166 | Policies: 167 | - AWSLambdaBasicExecutionRole 168 | - Version: "2012-10-17" 169 | Statement: 170 | - Effect: Allow 171 | Action: 172 | - cloudwatch:PutMetricData 173 | - iot:CreateKeysAndCertificate 174 | - iot:AttachPolicy 175 | - logs:CreateLogStream 176 | - logs:CreateLogGroup 177 | - iot:ListThingPrincipals 178 | - iot:DescribeCertificate 179 | - iot:PutLogEvents 180 | Resource: "*" 181 | Outputs: 182 | BootstrapBucket: 183 | Description: 'Bucket with bootstrap certificates' 184 | Value: !Ref BootstrapCerts 185 | Export: 186 | Name: BootstrapBucket -------------------------------------------------------------------------------- /buildspec.yaml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | phases: 3 | install: 4 | runtime-versions: 5 | python: 3.7 6 | build: 7 | commands: 8 | - cd SubTemplates 9 | - | 10 | for template in */; do 11 | cd $template 12 | lambdaDir=$(pwd)"/Lambdas" 13 | echo $lambdaDir 14 | if [ -d "$lambdaDir" ]; then 15 | cd Lambdas 16 | echo $(pwd) 17 | for lambda in */; do 18 | cd $lambda 19 | pip install -r requirements.txt 20 | cd .. 21 | done 22 | cd .. 23 | fi 24 | sam build 25 | sam package --output-template-file packaged.yaml --s3-bucket $PipelineBucket 26 | cd .. 27 | done 28 | - cd .. 29 | - sam build 30 | post_build: 31 | commands: 32 | - sam package --output-template-file packaged.yaml --s3-bucket $PipelineBucket 33 | artifacts: 34 | files: 35 | - packaged.yaml 36 | -------------------------------------------------------------------------------- /pipeline.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Parameters: 3 | CodeRepositoryName: 4 | Type: String 5 | Default: fleet-provisioing 6 | CodeRepositoryBranch: 7 | Type: String 8 | Default: master 9 | ResourceTag: 10 | Type: String 11 | Default: fleet-prov 12 | 13 | Resources: 14 | CodePipeline: 15 | Type: AWS::CodePipeline::Pipeline 16 | Properties: 17 | Name: !Sub ${ResourceTag}-pipeline 18 | RoleArn: !GetAtt PipelineRole.Arn 19 | ArtifactStore: 20 | Location: !Ref PipelineBucket 21 | Type: S3 22 | Stages: 23 | - Name: Source 24 | Actions: 25 | - InputArtifacts: [] 26 | Name: Source 27 | ActionTypeId: 28 | Category: Source 29 | Owner: AWS 30 | Version: 1 31 | Provider: CodeCommit 32 | Configuration: 33 | RepositoryName: !Ref CodeRepositoryName 34 | BranchName: !Ref CodeRepositoryBranch 35 | OutputArtifacts: 36 | - Name: CFNSource 37 | RunOrder: 1 38 | - Name: Build 39 | Actions: 40 | - Name: PackageTemplate 41 | ActionTypeId: 42 | Category: Build 43 | Owner: AWS 44 | Version: 1 45 | Provider: CodeBuild 46 | Configuration: 47 | ProjectName: !Ref CodeBuild 48 | InputArtifacts: 49 | - Name: CFNSource 50 | OutputArtifacts: 51 | - Name: CFNBuild 52 | RunOrder: 1 53 | - Name: Deploy 54 | Actions: 55 | - Name: CreateChangeSet 56 | ActionTypeId: 57 | Category: Deploy 58 | Owner: AWS 59 | Provider: CloudFormation 60 | Version: 1 61 | Configuration: 62 | ActionMode: CHANGE_SET_REPLACE 63 | ChangeSetName: pipeline-changeset 64 | Capabilities: CAPABILITY_IAM,CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND 65 | StackName: !Sub ${ResourceTag} 66 | RoleArn: !GetAtt PipelineRole.Arn 67 | TemplatePath: CFNBuild::packaged.yaml 68 | ParameterOverrides: !Sub | 69 | { 70 | "ResourceTag": "${ResourceTag}", 71 | "PipelineBucket" : "${PipelineBucket}" 72 | } 73 | InputArtifacts: 74 | - Name: CFNBuild 75 | RunOrder: 2 76 | 77 | - Name: ExecuteChangeSet 78 | ActionTypeId: 79 | Category: Deploy 80 | Owner: AWS 81 | Provider: CloudFormation 82 | Version: 1 83 | Configuration: 84 | ActionMode: CHANGE_SET_EXECUTE 85 | ChangeSetName: pipeline-changeset 86 | StackName: !Sub ${ResourceTag} 87 | Capabilities: CAPABILITY_IAM,CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND 88 | RunOrder: 3 89 | 90 | CodeBuild: 91 | Type: AWS::CodeBuild::Project 92 | Properties: 93 | Artifacts: 94 | Type: CODEPIPELINE 95 | Description: !Sub "Sam build for ${ResourceTag} Project" 96 | Environment: 97 | ComputeType: BUILD_GENERAL1_LARGE 98 | EnvironmentVariables: 99 | - Name: PipelineBucket 100 | Value: !Ref PipelineBucket 101 | Image: aws/codebuild/amazonlinux2-x86_64-standard:1.0 102 | ImagePullCredentialsType: CODEBUILD 103 | Type: LINUX_CONTAINER 104 | Name: !Sub ${ResourceTag}Build 105 | ServiceRole: !Sub ${PipelineRole.Arn} 106 | Source: 107 | Type: CODEPIPELINE 108 | 109 | PipelineRole: 110 | Type: AWS::IAM::Role 111 | Properties: 112 | AssumeRolePolicyDocument: 113 | Version: 2012-10-17 114 | Statement: 115 | - Effect: Allow 116 | Principal: 117 | Service: codebuild.amazonaws.com 118 | Action: sts:AssumeRole 119 | - Effect: Allow 120 | Principal: 121 | Service: cloudformation.amazonaws.com 122 | Action: sts:AssumeRole 123 | - Effect: Allow 124 | Principal: 125 | Service: codepipeline.amazonaws.com 126 | Action: sts:AssumeRole 127 | ManagedPolicyArns: 128 | - arn:aws:iam::aws:policy/AdministratorAccess 129 | 130 | PipelineBucket: 131 | Type: AWS::S3::Bucket 132 | Properties: 133 | AccessControl: Private 134 | BucketEncryption: 135 | ServerSideEncryptionConfiguration: 136 | - ServerSideEncryptionByDefault: 137 | SSEAlgorithm: AES256 138 | 139 | cleanupBucketOnDelete: 140 | DependsOn: cleanupBucketOnDeleteLambda 141 | Type: Custom::cleanupbucket 142 | Properties: 143 | ServiceToken: 144 | Fn::GetAtt: 145 | - "cleanupBucketOnDeleteLambda" 146 | - "Arn" 147 | BucketName: !Ref PipelineBucket 148 | 149 | cleanupBucketOnDeleteLambda: 150 | DependsOn: PipelineBucket 151 | Type: "AWS::Lambda::Function" 152 | Properties: 153 | Code: 154 | ZipFile: !Sub | 155 | #!/usr/bin/env python 156 | # -*- coding: utf-8 -*- 157 | import json 158 | import boto3 159 | from botocore.vendored import requests 160 | def empty_delete_buckets(bucket_name): 161 | """ 162 | Empties and deletes the bucket 163 | :param bucket_name: 164 | :param region: 165 | :return: 166 | """ 167 | print "trying to delete the bucket {0}".format(bucket_name) 168 | # s3_client = SESSION.client('s3', region_name=region) 169 | s3_client = boto3.client('s3') 170 | # s3 = SESSION.resource('s3', region_name=region) 171 | s3 = boto3.resource('s3') 172 | try: 173 | bucket = s3.Bucket(bucket_name).load() 174 | except ClientError: 175 | print "bucket {0} does not exist".format(bucket_name) 176 | return 177 | # Check if versioning is enabled 178 | response = s3_client.get_bucket_versioning(Bucket=bucket_name) 179 | status = response.get('Status','') 180 | if status == 'Enabled': 181 | response = s3_client.put_bucket_versioning(Bucket=bucket_name, 182 | VersioningConfiguration={'Status': 'Suspended'}) 183 | paginator = s3_client.get_paginator('list_object_versions') 184 | page_iterator = paginator.paginate( 185 | Bucket=bucket_name 186 | ) 187 | for page in page_iterator: 188 | print page 189 | if 'DeleteMarkers' in page: 190 | delete_markers = page['DeleteMarkers'] 191 | if delete_markers is not None: 192 | for delete_marker in delete_markers: 193 | key = delete_marker['Key'] 194 | versionId = delete_marker['VersionId'] 195 | s3_client.delete_object(Bucket=bucket_name, Key=key, VersionId=versionId) 196 | if 'Versions' in page and page['Versions'] is not None: 197 | versions = page['Versions'] 198 | for version in versions: 199 | print version 200 | key = version['Key'] 201 | versionId = version['VersionId'] 202 | s3_client.delete_object(Bucket=bucket_name, Key=key, VersionId=versionId) 203 | object_paginator = s3_client.get_paginator('list_objects_v2') 204 | page_iterator = object_paginator.paginate( 205 | Bucket=bucket_name 206 | ) 207 | for page in page_iterator: 208 | if 'Contents' in page: 209 | for content in page['Contents']: 210 | key = content['Key'] 211 | s3_client.delete_object(Bucket=bucket_name, Key=content['Key']) 212 | #UNCOMMENT THE LINE BELOW TO MAKE LAMBDA DELETE THE BUCKET. 213 | # THIS WILL CAUSE AN FAILURE SINCE CLOUDFORMATION ALSO TRIES TO DELETE THE BUCKET 214 | #s3_client.delete_bucket(Bucket=bucket_name) 215 | #print "Successfully deleted the bucket {0}".format(bucket_name) 216 | print "Successfully emptied the bucket {0}".format(bucket_name) 217 | def lambda_handler(event, context): 218 | try: 219 | bucket = event['ResourceProperties']['BucketName'] 220 | if event['RequestType'] == 'Delete': 221 | empty_delete_buckets(bucket) 222 | #s3 = boto3.resource('s3') 223 | #bucket.objects.all().delete() 224 | #bucket = s3.Bucket(bucket) 225 | #for obj in bucket.objects.filter(): 226 | #s3.Object(bucket.name, obj.key).delete() 227 | sendResponseCfn(event, context, "SUCCESS") 228 | except Exception as e: 229 | print(e) 230 | sendResponseCfn(event, context, "FAILED") 231 | def sendResponseCfn(event, context, responseStatus): 232 | response_body = {'Status': responseStatus, 233 | 'Reason': 'Log stream name: ' + context.log_stream_name, 234 | 'PhysicalResourceId': context.log_stream_name, 235 | 'StackId': event['StackId'], 236 | 'RequestId': event['RequestId'], 237 | 'LogicalResourceId': event['LogicalResourceId'], 238 | 'Data': json.loads("{}")} 239 | requests.put(event['ResponseURL'], data=json.dumps(response_body)) 240 | Description: cleanup Bucket on Delete Lambda Lambda function. 241 | Handler: index.lambda_handler 242 | Role : !GetAtt cleanupBucketOnDeleteLambdaRole.Arn 243 | Runtime: python2.7 244 | Timeout: 60 245 | 246 | 247 | cleanupBucketOnDeleteLambdaRole: 248 | Type: AWS::IAM::Role 249 | Properties: 250 | AssumeRolePolicyDocument: 251 | Version: '2012-10-17' 252 | Statement: 253 | - Effect: Allow 254 | Principal: 255 | Service: 256 | - lambda.amazonaws.com 257 | Action: 258 | - sts:AssumeRole 259 | Path: "/" 260 | Policies: 261 | - PolicyName: !Join [ -, [!Ref 'AWS::StackName', 'cleanupBucketOnDeleteLambdaPolicy'] ] 262 | PolicyDocument: 263 | Version: '2012-10-17' 264 | Statement: 265 | - Effect: Allow 266 | Action: 267 | - logs:* 268 | - s3:* 269 | Resource: '*' 270 | - Effect: Deny 271 | Action: 272 | - s3:DeleteBucket 273 | Resource: '*' 274 | 275 | -------------------------------------------------------------------------------- /template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: template 4 | Parameters: 5 | ResourceTag: 6 | Type: String 7 | Description: Tag applied to all resources 8 | PipelineBucket: 9 | Type: String 10 | Description: S3 bucket with pipeline resources 11 | 12 | Resources: 13 | IoT: 14 | Type: AWS::Serverless::Application 15 | Properties: 16 | Location: SubTemplates/IoT/packaged.yaml 17 | Parameters: 18 | ResourceTag: !Ref ResourceTag 19 | Tags: 20 | Project: !Ref ResourceTag 21 | TimeoutInMinutes: 8 22 | 23 | Outputs: 24 | BootstrapCerts: 25 | Description: 'Bucket with bootstrap certificates' 26 | Value: !GetAtt IoT.Outputs.BootstrapBucket 27 | 28 | 29 | --------------------------------------------------------------------------------