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.
- 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 ],
}),
]
}));
}
}
From AWS docs on NitroEnclaves https://docs.aws.amazon.com/enclaves/latest/user/nitro-enclave-refapp.html#install-acm
sudo mv /etc/nitro_enclaves/acm.example.yaml /etc/nitro_enclaves/acm.yaml
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
[openssl_init]
...
engines = engine_section
[engine_section]
pkcs11 = pkcs11_section
[ pkcs11_section ]
engine_id = pkcs11
init = 1
...
sudo systemctl start nitro-enclaves-acm.services
sudo systemctl enable nitro-enclaves-acm