AWSTemplateFormatVersion: 2010-09-09 Description: Template to listen to state change events. Parameters: JamfUrl: Type: String Description: The custom url for your Jamf instance JamfUserName: Type: String Description: The user name of that provides access to your Jamf instance JamfUserPassword: Type: String NoEcho: True Description: The user name of that provides access to your Jamf instance NotificationEmail: Type: String Description: The email address used for notification if the instance fails to enroll. Resources: jamfSecret: Type: AWS::SecretsManager::Secret Properties: Description: The Username and Password of the Jamf Account SecretString: !Sub | { "username": "${JamfUserName}", "password": "${JamfUserPassword}" } jamfInstallAutomation: Type: AWS::SSM::Document Properties: DocumentType: Automation Content: schemaVersion: "0.3" description: | # EC2 Mac Jamf Enrollment This automation will run when an instance enters the running state, it will then execute a set of commands on the instance to enroll the mac into Jamf. assumeRole: "{{AutomationAssumeRole}}" parameters: InstanceId: description: "ID of the Instance." type: "String" AutomationAssumeRole: default: "" description: "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf." type: "String" mainSteps: - name: getInstanceType action: 'aws:executeAwsApi' outputs: - Name: InstanceType Selector: '$.Reservations[0].Instances[0].InstanceType' Type: String - Name: IamInstanceProfile Selector: '$.Reservations[0].Instances[0].IamInstanceProfile.Arn' Type: String inputs: Service: ec2 Api: DescribeInstances InstanceIds: - '{{InstanceId}}' description: Runs the describe instance command to get the instance family - name: IfEc2Mac action: 'aws:branch' inputs: Choices: - NextStep: IfNoInstanceProfile Variable: '{{getInstanceType.InstanceType}}' StringEquals: mac1.metal isEnd: true - name: IfNoInstanceProfile action: 'aws:branch' inputs: Choices: - Variable: '{{getInstanceType.IamInstanceProfile}}' Contains: arn NextStep: waitForSSM Default: setInstanceProfile - name: setInstanceProfile action: 'aws:executeAwsApi' inputs: Service: ec2 Api: AssociateIamInstanceProfile InstanceId: '{{InstanceId}}' IamInstanceProfile: Arn: !GetAtt Ec2MacSSMInstanceProfile.Arn - name: waitForSSM action: 'aws:waitForAwsResourceProperty' timeoutSeconds: 1200 onFailure: 'step:notifyInstanceProfileError' inputs: Service: ssm Api: DescribeInstanceInformation Filters: - Key: InstanceIds Values: - '{{InstanceId}}' PropertySelector: '$.InstanceInformationList[0].PingStatus' DesiredValues: - Online - name: getInvitation action: 'aws:executeScript' description: Creates the invitation outputs: - Name: invitation Selector: $.Payload.invitation Type: String inputs: Runtime: python3.7 Handler: script_handler Script: !Sub |- import datetime import urllib3 import json import xml.etree.ElementTree as ET import boto3 http = urllib3.PoolManager() def script_handler(event, context): sm_client = boto3.client('secretsmanager') jamf_secret_resp = sm_client.get_secret_value(SecretId='${jamfSecret}') jamf_secret = json.loads(jamf_secret_resp['SecretString']) jamf_user = jamf_secret['username'] jamf_pwd = jamf_secret['password'] jamf_instance = "${JamfUrl}" headers = urllib3.make_headers(basic_auth=f'{jamf_user}:{jamf_pwd}') resp = http.request("post", f"https://{jamf_instance}/api/v1/auth/token", fields={}, headers=headers) if resp.status == 200: body = json.loads(resp.data.decode('utf-8')) token = body["token"] validate_token = http.request("get", f"https://{jamf_instance}/api/v1/auth", headers={"Authorization": f"Bearer {token}"}) if validate_token.status == 200: xmldata = f""" DEFAULT 2122-12-31 11:11:11 {jamf_user} {jamf_pwd} true false false false """ invitation = http.request("post", f"https://{jamf_instance}/JSSResource/computerinvitations/id/0", body=xmldata, headers={"content-type": "application/xml", "Authorization": f"Bearer {token}"}) print (f"Status Code: {invitation.status}") if invitation.status == 200: root = ET.fromstring(invitation.data) return {'invitation': root.find("invitation").text} return {'invitation': ''} - name: runEnrollment action: 'aws:runCommand' isEnd: True inputs: DocumentName: AWS-RunShellScript InstanceIds: - '{{InstanceId}}' Parameters: commands: - !Sub | computerInvitation={{getInvitation.invitation}} JAMFURL=${JamfUrl} TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -s -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") echo $JAMFURL echo $computerInvitation whoami #Function to download and installs the Jamf binary from the $JAMFURL Server. jamfinstall(){ /usr/bin/curl -ks "https://$JAMFURL/bin/jamf" -o /tmp/jamf /bin/mkdir -p /usr/local/jamf/bin /usr/local/bin /bin/mv /tmp/jamf /usr/local/jamf/bin /bin/chmod +x /usr/local/jamf/bin/jamf /bin/ln -s /usr/local/jamf/bin/jamf /usr/local/bin } # check if jamf is installed /usr/local/bin/jamf checkJSSConnection -retry 1 status=$? if [ $status -eq 0 ]; then # `jamf` command was able to connect to the server correctly so we are enrolled. echo 'Already Jamf enrolled.' exit 0 elif [ $status -eq 127 ]; then # `jamf` command not found so we are definitely not enrolled. echo 'Not already Jamf enrolled.' shouldEnroll=true else # `jamf` command exists, but had some other trouble contacting the server. echo 'Encountered a problem connecting to Jamf server.' if [[ "$jamfOutput" == *"Device Signature Error"* ]]; then echo 'Instance has likely moved to new physical hardware.' # Need to unenroll and then enroll as a new device. echo "Attempting to run 'jamf removeFramework'..." /usr/local/bin/jamf removeFramework removeStatus=${!}? if [ ${!removeStatus} -eq 0 ]; then echo 'Jamf enrollment removed.' shouldEnroll=true else echo "'jamf removeFramework' failed with exit code ${!removeStatus}." exit 1 fi else echo "Run '/usr/local/bin/jamf checkJSSConnection' manually to troubleshoot." exit 1 fi fi if $shouldEnroll ; then echo 'Attempting to enroll in Jamf Pro...' # Download binaries from public host jamfinstall #################################################### ## Create the configuration file at: ## /Library/Preferences/com.jamfsoftware.jamf.plist #################################################### jamfCLIPath=/usr/local/bin/jamf $jamfCLIPath createConf -url https://$JAMFURL/ -verifySSLCert always $jamfCLIPath setComputername --name $(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/instance-id) #################################################### ## Run enroll #################################################### $jamfCLIPath enroll -invitation $computerInvitation -noPolicy enrolled=$? if [ $enrolled -eq 0 ] then $jamfCLIPath update $jamfCLIPath policy -event enrollmentComplete enrolled=$? fi /bin/rm -rf /private/tmp/Binaries exit $enrolled fi description: Run - name: notifyInstanceProfileError isEnd: True action: 'aws:executeAwsApi' inputs: Service: sns Api: Publish TopicArn: !Ref EnrollmentFailureNotification Subject: 'Jamf enrollment failed for EC2 Mac Instance {{InstanceId}}' Message: 'Jamf enrollment failed for EC2 Mac Instance {{InstanceId}}. The instance was unable to connect to systems manager, make sure the AmazonSSMManagedInstanceCore managed policy is attached to the EC2 instance profile''s role' EnrollmentFailureNotification: Type: AWS::SNS::Topic Properties: Subscription: - Endpoint: !Ref NotificationEmail Protocol: email DLQ: Type: AWS::SQS::Queue Properties: Tags: - Key: IsDLQ Value: True DLQPolicy: Type: AWS::SQS::QueuePolicy Properties: PolicyDocument: Statement: - Effect: "Allow" Principal: Service: - events.amazonaws.com Action: - "SQS:SendMessage" Resource: !GetAtt DLQ.Arn Condition: ArnEquals: "aws:SourceArn": !GetAtt EC2Listener.Arn Queues: - !Ref DLQ EC2Listener: Type: AWS::Events::Rule Properties: Description: Listens for EC2 Mac lifecycle events EventPattern: source: - aws.ec2 detail-type: - EC2 Instance State-change Notification detail: state: - running RoleArn: !GetAtt EventRuleTargetIamRole.Arn State: ENABLED Targets: - Arn: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${jamfInstallAutomation}:$DEFAULT Id: "SSMAutomation" RoleArn: !GetAtt EventRuleTargetIamRole.Arn DeadLetterConfig: Arn: !GetAtt DLQ.Arn InputTransformer: InputPathsMap: "InstanceId" : "$.detail.instance-id" InputTemplate: !Sub | { "InstanceId": [], "AutomationAssumeRole": ["${SSMAutomationIamRole.Arn}"] } EventRuleTargetIamRole: Type: AWS::IAM::Role Properties: ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonSSMAutomationRole AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Sid: "" Effect: "Allow" Principal: Service: - "events.amazonaws.com" Action: - "sts:AssumeRole" Path: "/" Policies: - PolicyName: pass_role PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - iam:PassRole Resource: - !GetAtt SSMAutomationIamRole.Arn SSMAutomationIamRole: Type: AWS::IAM::Role Properties: ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonSSMAutomationRole - arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Sid: "" Effect: "Allow" Principal: Service: - "ssm.amazonaws.com" Action: - "sts:AssumeRole" Path: "/" Policies: - PolicyName: ssm_automation_permissions PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - sns:Publish Resource: !Ref EnrollmentFailureNotification - Effect: Allow Action: - ec2:AssociateIamInstanceProfile Resource: !Sub 'arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:instance/*' - Effect: Allow Action: - iam:PassRole Resource: - !GetAtt Ec2MacSSMRole.Arn - Effect: Allow Action: - secretsmanager:GetSecretValue Resource: - !Ref jamfSecret Ec2MacSSMRole: Type: "AWS::IAM::Role" Properties: ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "ec2.amazonaws.com" Action: - "sts:AssumeRole" Ec2MacSSMInstanceProfile: Type: "AWS::IAM::InstanceProfile" Properties: Roles: - !Ref Ec2MacSSMRole Outputs: jamfInstallAutomation: Description: SSMDocument Value: !Ref jamfInstallAutomation