At binx.io we create immutable infrastructure. Using automation and desired state configuration, we leverage CloudFormation for creating infrastructure. It is not possible though to create Amazon EC2 instances with CloudFormation that are provisioned with a public/private key-pair. For this reason, Mark van Holsteijn, CTO binx.io has created the binxio/cfn-secret-provider. In the blog Deploying private key pairs with AWS CloudFormation, Mark introduces the secret provider and several use cases. The secret provider is a CloudFormation custom resource that creates RSA keys and KeyPairs that can be used for generating secrets. In this blog we’ll see one use of the secret provider, to generate a public/private keypair and use it to provision an EC2.
RSA and SSH
RSA Keys are used in public-key encryption. The algorithm uses a private key and a derived public key. The private key should be kept secret, but the public key can be shared with others. RSA keys are used when setting up Secure Shell or ‘SSH’. With Secure Shell, the public key is stored on the server to identify a client. Because with RSA, there is a one-to-one relationship with the public and private key. The reason is that public keys are derived from a single private key. The private key is used by the client to login to the server. The connection with the server is initiated by the client. The client encrypts the SSH connection with the private key. The server can decrypt the connection with the public key. The security is simple, only the public key can decrypt a connection initiated with the private key. If the decryption succeeds, the server automatically knows that the connection can be trusted.
Generating secrets
The secret provider can generate RSA keys. The private key is stored in the AWS parameter store. The public key is available by asking the custom provider. I have exported the public key with a CloudFormation output. The secret provider can also store the RSA keys as an EC2 KeyPair. Below we see how to setup such a configuration.
PrivateKey:
Type: Custom::RSAKey
Properties:
Name: /bastion/default/private-key
RefreshOnUpdate: false
ServiceToken: !GetAtt CFNSecretProvider.Arn
KeyPair:
Type: Custom::KeyPair
DependsOn: PrivateKey
Properties:
Name: BastionKeyPair
PublicKeyMaterial: !GetAtt 'PrivateKey.PublicKey'
ServiceToken: !GetAtt CFNSecretProvider.Arn
The secret provider is a CloudFormation custom resource. The implementation is a Lambda, and must be deployed to your AWS environment.
CFNSecretProvider:
Type: AWS::Lambda::Function
Properties:
Code:
S3Bucket: !Sub 'binxio-public-${AWS::Region}'
S3Key: lambdas/cfn-secret-provider-0.13.2.zip
Handler: secrets.handler
MemorySize: 128
Role: !GetAtt 'CFNSecretProviderRole.Arn'
Runtime: python3.6
Timeout: 300
CFNSecretProviderLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub '/aws/lambda/${CFNSecretProvider}'
RetentionInDays: 30
The lambda needs the appropriate permissions to generate and store the keys.
CFNSecretProviderRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
Condition: {}
Path: /
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: CFNCustomSecretProviderPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- iam:CreateAccessKey
- iam:UpdateAccessKey
- iam:DeleteAccessKey
- ssm:PutParameter
- ssm:GetParameter
- ssm:DeleteParameter
- ec2:ImportKeyPair
- ec2:DeleteKeyPair
Resource:
- '*'
- Effect: Allow
Action:
- kms:Encrypt
- kms:Decrypt
Resource:
- '*'
An EC2 instance can be configured with the KeyPair that has been created by the custom provider.
BastionHost:
Type: AWS::EC2::Instance
Properties:
ImageId: !FindInMap ['NatAMI', 'eu-west-1', 'ami']
KeyName: BastionKeyPair
InstanceType: 't3.micro'
SourceDestCheck: false
SubnetId: !Ref PublicSubnet
SecurityGroupIds:
- !Ref BastionHostSecurityGroup
IamInstanceProfile: !Ref BastionInstanceProfile
BlockDeviceMappings:
- DeviceName: /dev/xvdcz
Ebs:
VolumeType: gp2
VolumeSize: 10
DeleteOnTermination: true
Encrypted: true
UserData:
Fn::Base64: !Sub |
#!/bin/bash
export AWS_DEFAULT_REGION=eu-west-1
yum install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm
BastionInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Path: /
Roles:
- !Ref BastionRole
BastionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service:
- ec2.amazonaws.com
Action:
- sts:AssumeRole
Path: /
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM'
- 'arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser'
Policies:
- PolicyName: gitlab-runner
PolicyDocument:
Statement:
- Effect: Allow
Action:
- ssm:GetParameter
Resource:
- !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/bastion/default/*'
BastionHostSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
Groupcontent: 'Security group for the bastion host'
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: '-1'
CidrIp: '0.0.0.0/0'
SecurityGroupEgress:
- IpProtocol: '-1'
CidrIp: '0.0.0.0/0'
Accessing the private key
The private key can be downloaded by means of the AWS CLI. You need the private key to initiate a SSH connection from your computer to the EC2 instance.
# print the private key
aws ssm get-parameter --name /bastion/default/private-key --with-decryption | jq -r '.Parameter.Value'
# copy the private key to the clipboard
aws ssm get-parameter --name /bastion/default/private-key --with-decryption | jq -r '.Parameter.Value' | pbcopy
# writing the private key to 'bastion.pem'
aws ssm get-parameter --name /bastion/default/private-key --with-decryption | jq -r '.Parameter.Value' > bastion.pem
Setting up SSH
To setup an SSH connection you need access to the private key. The private key file needs 0600
permission. To login type make create && make ssh
or type:
DNSNAME=`sceptre --output json describe-stack-outputs example vpc | jq -r '.[] | select(.OutputKey=="BastionHostPublicDnsName") | .OutputValue'`
ssh -i bastion.pem ec2-user@$DNSNAME
Example
The example project shows how to configure a project to create KeyPairs and how to configure an EC2 instance with a KeyPair with CloudFormation. The example can be deployed with make deploy
and removed with make delete
. To login type make ssh
.
Conclusion
With the binxio/cfn-secret-provider it is possible to generate secrets with AWS CloudFormation. In this blog we’ve seen how to create RSA keys, how to create EC2 KeyPairs, and how to configure an EC2 instance to use that KeyPair. We have also seen how to get access to the private key and how to use that key to create a SSH connection with the EC2 instance.