Skip to content

Instantly share code, notes, and snippets.

@ryanjdillon
Last active September 9, 2024 19:26
Show Gist options
  • Save ryanjdillon/5e3077ffc320d93721090c37edec0fb9 to your computer and use it in GitHub Desktop.
Save ryanjdillon/5e3077ffc320d93721090c37edec0fb9 to your computer and use it in GitHub Desktop.
AWS EC2 with Nitro Enclave ACM SSL Certificate management - no load balancer

CDK implementation utilizing Nitro Enclave for ACM SSL cert handling

This synth's, but it wasn't tested as it turns out you need an instance class with vCPU==4+, which turns out to be quite expensive.

There is some additional nginx config to be done on the instance following provisioning, outlined in these AWS docs starting at point #7: https://docs.aws.amazon.com/enclaves/latest/user/nitro-enclave-refapp.html#install-acm

This was for a personal project, so I don't particularly see the usefulness of this at this price-point in comparison to using a load balancer, or Letsencrypt with a micro/nano instance.

I'd be interested to hear what applications this might have for those of you looking into this.

Pre-requirements

  • create EC2 key pair (named ec2-instance-keypair here)
  • create Route53 HostedZone for domainName with nameservers set appropriately
// Modified from:
// https://bobbyhadz.com/blog/aws-cdk-ec2-instance-example
import { readFileSync } from 'fs';

import * as cdk from 'aws-cdk-lib';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as route53 from 'aws-cdk-lib/aws-route53';
import { Construct } from 'constructs';

export class CdkStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const vpc = new ec2.Vpc(this, 'Vpc', {
      ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
      natGateways: 0,
      subnetConfiguration: [
        {name: 'public', cidrMask: 24, subnetType: ec2.SubnetType.PUBLIC},
      ],
    });

      // Create Security Group for the Instance
    const webserverSG = new ec2.SecurityGroup(this, 'webserver-sg', {
      vpc,
      allowAllOutbound: true,
    });

    webserverSG.addIngressRule(
      ec2.Peer.anyIpv4(),
      ec2.Port.tcp(22),
      'allow SSH access from anywhere',
    );

    webserverSG.addIngressRule(
      ec2.Peer.anyIpv4(),
      ec2.Port.tcp(80),
      'allow HTTP traffic from anywhere',
    );

    webserverSG.addIngressRule(
      ec2.Peer.anyIpv4(),
      ec2.Port.tcp(443),
      'allow HTTPS traffic from anywhere',
    );

    // Create a Role for the EC2 Instance
    const webserverRole = new iam.Role(this, 'webserver-role', {
      assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonS3ReadOnlyAccess'),
      ],
    });

    // Importing SSH key
    const keyPair = ec2.KeyPair.fromKeyPairName(
      this,
      'key-pair',
      'ec2-instance-keypair',
    );

    // Create the EC2 Instance
    const ec2Instance = new ec2.Instance(this, 'ec2-instance', {
      vpc,
      // Use ACM certificate using AWS Nitro Enclave
      enclaveEnabled: true,
      vpcSubnets: {
        subnetType: ec2.SubnetType.PUBLIC,
      },
      role: webserverRole,
      securityGroup: webserverSG,
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.BURSTABLE3,
        ec2.InstanceSize.MICRO,
      ),
      machineImage: new ec2.AmazonLinuxImage({
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2023,
      }),
      keyPair,
    });

    // Elastic IP
    // IPs are technically temporary for EC2 instance and not guarenteed.
    // Assigning an ElasticIP fixes this problem
    let elasticIp = new ec2.CfnEIP(this, "Ip", { instanceId: ec2Instance.instanceId });

    // Add userdata script that installs and configures server components
    const userDataScript = readFileSync('./lib/user-data.sh', 'utf8');

    // Add the User Data script to the Instance
    ec2Instance.addUserData(userDataScript);

    // Create A Record to route traffic for domain to instance IP
    const domainName = "example.com"
    const hostedZoneId = "Z038XZ42G2MM6EPOQS1RBSXE"
    const hostedZone = route53.HostedZone.fromHostedZoneAttributes(
      this,
      "HostedZone",
      {
        zoneName: domainName,
        hostedZoneId: hostedZoneId,
      }
    );
    new route53.ARecord(
      this,
      "AliasRecord",
      {
        zone: hostedZone,
        target: route53.RecordTarget.fromIpAddresses(elasticIp.ref)
      }
    )

    // SSL certificate handled by Nitro Enclave
    const certificate = new acm.Certificate(this, "siteCert", {
      domainName: `${domainName}`,
      validation: acm.CertificateValidation.fromDns(hostedZone)
    });

    // Create associates with cert role and certificate
    const enclaveCertRoleAssociation = new ec2.CfnEnclaveCertificateIamRoleAssociation(
      this,
      'CfnEnclaveCertificateIamRoleAssociation',
      {
        certificateArn: certificate.certificateArn,
        roleArn: webserverRole.roleArn
      }
    )

    // Grant role access permission to cert and encryption key
    webserverRole.attachInlinePolicy(new iam.Policy(this, "acmAccessPermissions", {
      statements: [
        new iam.PolicyStatement({
          effect: iam.Effect.ALLOW,
          actions: [
            's3:GetObject',
          ],
          resources: [ `arn:aws:s3:::${enclaveCertRoleAssociation.attrCertificateS3BucketName}/*` ],
	}),
        new iam.PolicyStatement({
          effect: iam.Effect.ALLOW,
          actions: [
            'kms:Decrypt',
          ],
          resources: [ `arn:aws:kms:region:*:key/${enclaveCertRoleAssociation.attrEncryptionKmsKeyId}` ],
	}),
        new iam.PolicyStatement({
          effect: iam.Effect.ALLOW,
          actions: [
            'iam:GetRole',
          ],
          resources: [ webserverRole.roleArn ],
	}),
      ]
    }));

  }
}

Deploy that, then setup the instance

From AWS docs on NitroEnclaves https://docs.aws.amazon.com/enclaves/latest/user/nitro-enclave-refapp.html#install-acm

Copy sample ACM

sudo mv /etc/nitro_enclaves/acm.example.yaml /etc/nitro_enclaves/acm.yaml

Replace /etc/nginx/nginx.conf

Append server block for domain

cat > "/etc/nginx/config.d/example.com.conf" << 'EOL'
server {
    listen 80;
    listen       [::]:80;
    server_name www.example.com example.com;

    location / {
       proxy_pass http://127.0.0.1:9000;
    }

    location /health {
      add_header Content-Type text/html;
      return 200 '<html><body>OK</html></body>';
    }
}
EOL

Update /etc/pki/tls/openssl.cnf

[openssl_init]
...
engines = engine_section

[engine_section]
pkcs11 = pkcs11_section

[ pkcs11_section ]
engine_id = pkcs11
init = 1
...

Start ACM for Nitro Enclave

sudo systemctl start nitro-enclaves-acm.services
sudo systemctl enable nitro-enclaves-acm
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment