如何通过AWS CloudFormation动态创建密钥对并复制PEM到EC2 Linux实例?
通过AWS CloudFormation动态创建密钥对并关联EC2实例
你完全可以实现这个需求——用CloudFormation动态生成密钥对,同时把对应的PEM私钥文件放到EC2 Linux实例里。不过要注意:CloudFormation原生的AWS::EC2::KeyPair资源不会自动把私钥传递给实例,所以需要借助一些额外的手段来完成私钥的注入。下面我给你两种可行的方案:
方案一:自定义资源生成密钥对+SSM存储私钥注入实例
这个方案通过Lambda自定义资源生成密钥对,把私钥加密存储到SSM参数仓库,再让EC2实例通过用户数据脚本拉取私钥并保存。
完整CloudFormation模板示例
AWSTemplateFormatVersion: '2010-09-09' Description: 动态创建密钥对并注入EC2实例 Resources: # Lambda执行角色:允许创建密钥对、操作SSM、响应CloudFormation LambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: KeyPairAndSSMAccess PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: [ec2:CreateKeyPair, ec2:DeleteKeyPair] Resource: "*" - Effect: Allow Action: [ssm:PutParameter, ssm:DeleteParameter] Resource: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/MyDynamicKeyPairPrivateKey" # 自定义Lambda:生成密钥对并存储私钥到SSM KeyPairCreatorLambda: Type: AWS::Lambda::Function Properties: Runtime: python3.11 Handler: index.lambda_handler Role: !GetAtt LambdaExecutionRole.Arn Code: ZipFile: | import boto3 import cfnresponse import os ec2 = boto3.client('ec2') ssm = boto3.client('ssm') PARAM_NAME = os.environ['PARAM_NAME'] def lambda_handler(event, context): response_data = {} try: key_name = event['ResourceProperties']['KeyName'] if event['RequestType'] == 'Create': # 创建密钥对并获取私钥 key_pair = ec2.create_key_pair(KeyName=key_name) private_key = key_pair['KeyMaterial'] # 加密存储私钥到SSM ssm.put_parameter( Name=PARAM_NAME, Value=private_key, Type='SecureString', Overwrite=True ) response_data['KeyName'] = key_name cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data) elif event['RequestType'] == 'Delete': # 清理资源:删除密钥对和SSM参数 try: ec2.delete_key_pair(KeyName=key_name) except Exception as e: print(f"删密钥对出错: {e}") try: ssm.delete_parameter(Name=PARAM_NAME) except Exception as e: print(f"删SSM参数出错: {e}") cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data) else: # 更新操作不处理(密钥对无法更新) cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data) except Exception as e: print(f"整体出错: {e}") cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': str(e)}) Environment: Variables: PARAM_NAME: MyDynamicKeyPairPrivateKey # 自定义资源:触发Lambda生成密钥对 DynamicKeyPair: Type: Custom::KeyPairCreator Properties: ServiceToken: !GetAtt KeyPairCreatorLambda.Arn KeyName: MyDynamicKeyPair # EC2实例角色:允许读取SSM中的私钥参数 EC2InstanceRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: ec2.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: SSMParameterAccess PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: ssm:GetParameter Resource: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/MyDynamicKeyPairPrivateKey" # 实例配置文件:关联EC2角色 EC2InstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Roles: [!Ref EC2InstanceRole] # 目标EC2实例:拉取私钥并保存 MyEC2Instance: Type: AWS::EC2::Instance Properties: InstanceType: t2.micro ImageId: ami-0c55b159cbfafe1f0 # 替换为你所在区域的Amazon Linux 2 AMI KeyName: !GetAtt DynamicKeyPair.KeyName IamInstanceProfile: !GetAtt EC2InstanceProfile.Arn UserData: Fn::Base64: | #!/bin/bash # 从SSM拉取解密后的私钥并保存 aws ssm get-parameter --name MyDynamicKeyPairPrivateKey --with-decryption --query Parameter.Value --output text > /home/ec2-user/my-key.pem # 设置PEM文件的安全权限(必须400才能用于SSH) chmod 400 /home/ec2-user/my-key.pem chown ec2-user:ec2-user /home/ec2-user/my-key.pem
方案说明
- 自定义资源Lambda:负责生成密钥对,把私钥加密存储到SSM参数仓库,同时处理栈删除时的资源清理。
- EC2实例角色:赋予实例读取SSM参数的权限,确保能安全拉取私钥。
- 用户数据脚本:实例启动后自动执行,从SSM获取私钥并保存到指定路径,同时设置正确的文件权限。
方案二:EC2实例本地生成密钥对并注册到AWS
如果不想用Lambda和SSM,也可以让EC2实例自己生成密钥对,再把公钥上传到AWS创建正式的密钥对。这样私钥直接保存在实例本地,流程更简洁。
模板示例
AWSTemplateFormatVersion: '2010-09-09' Description: EC2实例本地生成密钥对并注册到AWS Resources: # EC2实例角色:允许创建密钥对 EC2InstanceRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: ec2.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: EC2KeyPairAccess PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: ec2:CreateKeyPair Resource: "*" EC2InstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Roles: [!Ref EC2InstanceRole] MyEC2Instance: Type: AWS::EC2::Instance Properties: InstanceType: t2.micro ImageId: ami-0c55b159cbfafe1f0 # 替换为你所在区域的Amazon Linux 2 AMI IamInstanceProfile: !GetAtt EC2InstanceProfile.Arn UserData: Fn::Base64: | #!/bin/bash # 定义密钥对名称(可以改成固定值) KEY_NAME="EC2-Generated-Key-$(hostname)" # 在实例本地生成RSA密钥对(无密码) ssh-keygen -t rsa -b 2048 -f /home/ec2-user/${KEY_NAME}.pem -N "" # 设置私钥权限 chmod 400 /home/ec2-user/${KEY_NAME}.pem chown ec2-user:ec2-user /home/ec2-user/${KEY_NAME}.pem # 读取公钥内容并上传到AWS创建密钥对 PUBLIC_KEY=$(cat /home/ec2-user/${KEY_NAME}.pem.pub) aws ec2 create-key-pair --key-name ${KEY_NAME} --public-key-material "${PUBLIC_KEY}"
方案说明
- 实例初始化脚本:启动后自动生成本地密钥对,然后用AWS CLI把公钥上传到AWS,创建对应的密钥对。
- 优势:不需要额外的Lambda资源,流程简单;私钥直接保存在实例的
/home/ec2-user/目录下。 - 注意:如果实例终止,私钥会丢失,建议生成后备份到加密的S3桶等安全位置。
关键注意事项
- 私钥安全:不管用哪种方案,私钥都是敏感数据,一定要确保存储和传输过程加密。方案一中的SSM参数用
SecureString加密,方案二中要限制实例的访问权限。 - CloudFormation原生限制:原生的
AWS::EC2::KeyPair资源只会在栈创建时返回私钥(控制台或CLI输出),无法直接在模板内传递给实例,所以必须用上述两种方案来实现私钥注入。
内容的提问来源于stack exchange,提问作者Amit Chandra




