AWS Secure Shell (SSH) setup with EC2 and CloudFormation

on
Nov 20, 2018
in
AWS

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.

Share this article: Tweet this post / Post on LinkedIn